From 8237aa292df789b01a8413eee454a1735a93d6fd Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:50:43 +0800 Subject: [PATCH 1/4] fix bugs --- src/Gateway.sol | 23 ++++++++++------------- src/StakeManagement.sol | 2 +- src/interfaces/IGateway.sol | 4 ++-- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Gateway.sol b/src/Gateway.sol index ec7ab97..ab43521 100644 --- a/src/Gateway.sol +++ b/src/Gateway.sol @@ -25,10 +25,10 @@ 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 } @@ -335,7 +335,6 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { string calldata userChangeAddress, string calldata userRefundAddress ) external payable { - // TODO: check if request already exists PeginDataInner storage peginData = peginDataMap[instanceId]; if (peginData.status != PeginStatus.None) revert InstanceUsed(); // TODO: check peginAmount,feeRate,userInputs @@ -593,11 +592,10 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { revert TimelockNotExpired(); } withdrawData.status = WithdrawStatus.Canceled; - // FIXME: transfer to operator or gateway? - 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( @@ -620,10 +618,9 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { if (withdrawData.status != WithdrawStatus.Initialized) revert WithdrawStatusInvalid(); withdrawData.status = WithdrawStatus.Canceled; - // FIXME: transfer to operator or gateway? - 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 @@ -807,8 +804,8 @@ contract GatewayUpgradeable is BitvmPolicy, Initializable, IGateway { 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); } diff --git a/src/StakeManagement.sol b/src/StakeManagement.sol index 16afbba..489e7b7 100644 --- a/src/StakeManagement.sol +++ b/src/StakeManagement.sol @@ -47,8 +47,8 @@ contract StakeManagement is IStakeManagement, Initializable { return lockedStakes[operator]; } - // TODO: add caller authentication? 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; diff --git a/src/interfaces/IGateway.sol b/src/interfaces/IGateway.sol index 1b458a3..0da8365 100644 --- a/src/interfaces/IGateway.sol +++ b/src/interfaces/IGateway.sol @@ -187,7 +187,7 @@ interface IGateway { bytes32 challengeFinishTxid, address challengerAddress, address disproverAddress, - uint64 challengerRewardAmount, - uint64 disproverRewardAmount + uint256 challengerRewardAmount, + uint256 disproverRewardAmount ); } From 570dfae6abd4a61084e046845bac51b5565cbac0 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:16:58 +0800 Subject: [PATCH 2/4] update scripts --- script/CommitteeRegisterPeerId.sol | 30 +++++++ script/DebugCancelWithdraw.sol | 36 -------- script/DebugMintPegBTC.sol | 41 ---------- script/ListInstance.sol | 127 +++++++++++++++++++++++++++++ script/MockProceedWithdraw.sol | 43 ---------- script/OperatorRegisterPubkey.sol | 30 +++++++ script/OperatorStake.sol | 96 ++++++++++++++++++++++ 7 files changed, 283 insertions(+), 120 deletions(-) create mode 100644 script/CommitteeRegisterPeerId.sol delete mode 100644 script/DebugCancelWithdraw.sol delete mode 100644 script/DebugMintPegBTC.sol create mode 100644 script/ListInstance.sol delete mode 100644 script/MockProceedWithdraw.sol create mode 100644 script/OperatorRegisterPubkey.sol create mode 100644 script/OperatorStake.sol diff --git a/script/CommitteeRegisterPeerId.sol b/script/CommitteeRegisterPeerId.sol new file mode 100644 index 0000000..3e8b910 --- /dev/null +++ b/script/CommitteeRegisterPeerId.sol @@ -0,0 +1,30 @@ +// 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"; + +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/ListInstance.sol b/script/ListInstance.sol new file mode 100644 index 0000000..1ca12b9 --- /dev/null +++ b/script/ListInstance.sol @@ -0,0 +1,127 @@ +// 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"; + +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..76141a7 --- /dev/null +++ b/script/OperatorRegisterPubkey.sol @@ -0,0 +1,30 @@ +// 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"; + +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"); + } + } +} From b16216f6475951daa4b0269537959fa7a03b6308 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 25 Dec 2025 16:09:30 +0800 Subject: [PATCH 3/4] add descriptions for script required env --- script/CommitteeRegisterPeerId.sol | 7 ++ script/DeployContractDebug.sol | 6 + script/DeployGateway.sol | 173 +++++++++++++++++++++++++++++ script/ListInstance.sol | 9 ++ script/OperatorRegisterPubkey.sol | 7 ++ script/UpgradeGateway.sol | 7 ++ script/UpgradeGatewayDebug.sol | 7 ++ 7 files changed, 216 insertions(+) create mode 100644 script/DeployGateway.sol diff --git a/script/CommitteeRegisterPeerId.sol b/script/CommitteeRegisterPeerId.sol index 3e8b910..0d75b05 100644 --- a/script/CommitteeRegisterPeerId.sol +++ b/script/CommitteeRegisterPeerId.sol @@ -5,6 +5,13 @@ 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"); diff --git a/script/DeployContractDebug.sol b/script/DeployContractDebug.sol index 88ba35f..0107e55 100644 --- a/script/DeployContractDebug.sol +++ b/script/DeployContractDebug.sol @@ -14,6 +14,12 @@ 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; diff --git a/script/DeployGateway.sol b/script/DeployGateway.sol new file mode 100644 index 0000000..325e842 --- /dev/null +++ b/script/DeployGateway.sol @@ -0,0 +1,173 @@ +pragma solidity ^0.8.0; + +import {Script, console} from "forge-std/Script.sol"; + +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 {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; + + function setUp() public virtual { + bitcoinSPV = vm.envAddress("BITCOINSPV_ADDR"); + } + + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + deployer = vm.createWallet(deployerPrivateKey).addr; + console.log("deployer address: ", deployer); + vm.startBroadcast(deployerPrivateKey); + deploy(); + vm.stopBroadcast(); + } + + function deploy() public { + // deploy gateway implementation + proxy + GatewayUpgradeable gatewayImpl = new GatewayUpgradeable(); + console.log( + "Gateway implementation contract address: ", + address(gatewayImpl) + ); + + 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 + address[] memory initialMembers = _readSequentialAddresses("COMMITTEE"); + uint256 initialRequired = (initialMembers.length * 2 + 2) / 3; + 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); + + // 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, + stakeManagement + ); + } + + 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)) + ); + address val = vm.envOr(key, address(0)); + if (val == address(0)) break; + unchecked { + count++; + } + } + out = new address[](count); + for (uint256 i = 0; i < count; 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) { + uint256 count = 0; + while (true) { + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(count)) + ); + bytes32 val = vm.envOr(key, bytes32(0)); + if (val == bytes32(0)) break; + unchecked { + count++; + } + } + out = new bytes32[](count); + for (uint256 i = 0; i < count; i++) { + string memory key = string( + abi.encodePacked(baseKey, "_", vm.toString(i)) + ); + out[i] = vm.envBytes32(key); + } + } +} diff --git a/script/ListInstance.sol b/script/ListInstance.sol index 1ca12b9..a1af434 100644 --- a/script/ListInstance.sol +++ b/script/ListInstance.sol @@ -5,6 +5,15 @@ 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; diff --git a/script/OperatorRegisterPubkey.sol b/script/OperatorRegisterPubkey.sol index 76141a7..348a612 100644 --- a/script/OperatorRegisterPubkey.sol +++ b/script/OperatorRegisterPubkey.sol @@ -5,6 +5,13 @@ 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"); 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; From 085ace480f975a2d00305272fa748f8ab898016a Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:02:43 +0800 Subject: [PATCH 4/4] update SSP contract --- script/deploy/DeploySequencerSetPublisher.sol | 7 +- src/SequencerSetPublisher.sol | 162 ++---------------- src/interfaces/ISequencerSetPublisher.sol | 36 ++-- src/libraries/BtcUtils.sol | 95 ++++++++++ test/SequencerSetPublisher.t.sol | 139 ++++++--------- 5 files changed, 171 insertions(+), 268 deletions(-) create mode 100644 src/libraries/BtcUtils.sol diff --git a/script/deploy/DeploySequencerSetPublisher.sol b/script/deploy/DeploySequencerSetPublisher.sol index fd913cb..85bf38f 100644 --- a/script/deploy/DeploySequencerSetPublisher.sol +++ b/script/deploy/DeploySequencerSetPublisher.sol @@ -37,13 +37,10 @@ contract DeploySequencerSetPublisher is Script { vm.startBroadcast(); SequencerSetPublisher publisher = new SequencerSetPublisher(); - MultiSigVerifier multiSigVerifier = new MultiSigVerifier(); + // MultiSigVerifier multiSigVerifier = new MultiSigVerifier(); publisher.initialize( - initialOwner, - address(multiSigVerifier), - initPublishers, - initPublisherBTCPubkeys + initialOwner ); vm.stopBroadcast(); diff --git a/src/SequencerSetPublisher.sol b/src/SequencerSetPublisher.sol index 37979f6..f85386c 100644 --- a/src/SequencerSetPublisher.sol +++ b/src/SequencerSetPublisher.sol @@ -12,9 +12,9 @@ import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {MultiSigVerifier} from "./MultiSigVerifier.sol"; import {ISequencerSetPublisher} from "./interfaces/ISequencerSetPublisher.sol"; import {Constants} from "./Constants.sol"; +import {BtcUtils} from "./libraries/BtcUtils.sol"; // Sequencer Set Publisher contract SequencerSetPublisher is @@ -25,168 +25,26 @@ contract SequencerSetPublisher is using ECDSA for bytes32; using MessageHashUtils for bytes32; - mapping(uint256 height => mapping(address publisher => bytes32 cmt)) - public heightSequencerCmt; - mapping(bytes32 cmt => uint256 cnt) public sequencerCmtCnt; - mapping(uint256 height => address[]) public heightPublishers; - - mapping(address publisher => bytes pubkey) public publisherBTCPubkeys; - mapping(bytes32 cmt => SequencerSet ss) public cmtSequencerSet; - - uint256 public latestConfirmedHeight; - MultiSigVerifier public multiSigVerifier; + mapping(uint256 height => SequencerSetUpdateWitness) public sequencerSetUpdateWitnesses; function initialize( - address initialOwner, - address multiSigVerifierAddress, - address[] calldata initPublishers, - bytes[] calldata initPublisherBTCPubkeys + address initialOwner ) public initializer { - 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; - for (uint256 i = 0; i < initPublisherBTCPubkeys.length; i++) { - assert(initPublisherBTCPubkeys[i].length == 33); - publisherBTCPubkeys[initPublishers[i]] = initPublisherBTCPubkeys[i]; - } - 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 + /// @notice Publish a new sequencer set or update publisher BTC key function updateSequencerSet( - SequencerSet calldata ss, - bytes calldata signature + uint256 goatHeight, + SequencerSetUpdateWitness calldata witness ) external override { - require( - ss.goatBlockNumber >= latestConfirmedHeight, - InvalidGOATHeight() - ); - require(multiSigVerifier.isOwner(msg.sender), InvalidPublisherSet()); - 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() - ); + BtcUtils.verifyBtcSignature(witness.sigHash, witness.btcPubkey, witness.btcSig); - bytes32 cmt = keccak256( - abi.encodePacked( - ss.sequencerSetHash, - ss.publishersHash, - ss.nextPublishersHash, - ss.p2wshSigHash, - ss.goatBlockNumber - ) - ); // Avoid double commit - require( - heightSequencerCmt[ss.goatBlockNumber][msg.sender] == bytes32(0), - DoubleCommit() - ); - - heightSequencerCmt[ss.goatBlockNumber][msg.sender] = cmt; - sequencerCmtCnt[cmt] += 1; - cmtSequencerSet[cmt] = ss; - heightPublishers[ss.goatBlockNumber].push(msg.sender); - } - - /// @notice Update publishers. - /// @param newPublishers The new publisher's address - /// @param newPublisherBTCPubkeys The new publisher's compressed BTC public key - /// @param changePublisherSigs The signatures for changing owners, co-signed by old signers - function updatePublisherSet( - address[] calldata newPublishers, - bytes[] calldata newPublisherBTCPubkeys, - bytes[] calldata changePublisherSigs, - uint256 height - ) external override { - bytes32 cmt = calcMajoritySequencerSetCmtAtHeightOrLatest(height); - SequencerSet storage ss = cmtSequencerSet[cmt]; - require(latestConfirmedHeight < height, InvalidGOATHeight()); - require(height == ss.goatBlockNumber, InvalidGOATHeight()); - - address[] memory publishers = multiSigVerifier.getOwners(); - 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 - ); - SequencerSet storage prevSs = cmtSequencerSet[prevCmt]; - require( - prevSs.nextPublishersHash == ss.publishersHash, - InvalidPublisherSet() - ); - } - - // ensure valid sigs >= 2/3 - uint256 quorum = (newPublishers.length * 2 + 2) / 3; - multiSigVerifier.updateOwners( - newPublishers, - quorum, - changePublisherSigs - ); - - for (uint256 i = 0; i < newPublisherBTCPubkeys.length; i++) { - assert(newPublisherBTCPubkeys[i].length == 33); - publisherBTCPubkeys[newPublishers[i]] = newPublisherBTCPubkeys[i]; - } - latestConfirmedHeight = height; - } - - /// @notice Check if we have an aggrement on the cmt of the latest height. - function calcMajoritySequencerSetCmtAtHeightOrLatest( - uint256 height - ) public view returns (bytes32) { - require(height > 0, InvalidGOATHeight()); - address[] memory publishers = heightPublishers[height]; - - // Check if we have 2/3 publishers signed - bytes32 agreement = 0; - uint256 quorum = 0; - for (uint256 i = 0; i < publishers.length; i++) { - bytes32 cmt = heightSequencerCmt[height][publishers[i]]; - if (cmt != bytes32(0) && sequencerCmtCnt[cmt] > quorum) { - quorum = sequencerCmtCnt[cmt]; - agreement = cmt; - } + if (sequencerSetUpdateWitnesses[goatHeight].sigHash != bytes32(0)) { + revert DoubleCommit(); } - require( - quorum * 3 >= 2 * publishers.length, - InvalidQuorumSequencerSet() - ); - return agreement; + sequencerSetUpdateWitnesses[goatHeight] = witness; } } diff --git a/src/interfaces/ISequencerSetPublisher.sol b/src/interfaces/ISequencerSetPublisher.sol index 9b67502..fcfc072 100644 --- a/src/interfaces/ISequencerSetPublisher.sol +++ b/src/interfaces/ISequencerSetPublisher.sol @@ -2,31 +2,21 @@ pragma solidity ^0.8.28; interface ISequencerSetPublisher { - struct SequencerSet { - bytes32 sequencerSetHash; // validator_hash - bytes32 publishersHash; - bytes32 nextPublishersHash; - bytes32 p2wshSigHash; // anchor the BTC txn - uint256 goatBlockNumber; - } - - error P2WSHSignatureMismatch(); error DoubleCommit(); - error InvalidSequencerSet(); - error InvalidQuorumSequencerSet(); - error MismatchPublisher(); - error InvalidPublisherSet(); - error InvalidGOATHeight(); - error k256Decompress_Invalid_Length_Error(); - error k256DeriveY_Invalid_Prefix_Error(); + error InvalidBtcSignature(); + error InvalidBtcPubkey(); + error InvalidPubkeyLength(); + error InvalidPublicKeyX(); + error ModexpFailed(); - function updateSequencerSet(SequencerSet calldata ss, bytes calldata signature) external; + struct SequencerSetUpdateWitness { + bytes32 sigHash; + bytes btcPubkey; + bytes btcSig; + } - // Update publisher at once by multi-sig - function updatePublisherSet( - address[] calldata newPublishers, - bytes[] calldata newPublisherBTCPubkeys, - bytes[] calldata changePublisherSigs, - uint256 height + function updateSequencerSet( + uint256 goatHeight, + SequencerSetUpdateWitness calldata witness ) external; } diff --git a/src/libraries/BtcUtils.sol b/src/libraries/BtcUtils.sol new file mode 100644 index 0000000..3c4b88b --- /dev/null +++ b/src/libraries/BtcUtils.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +library BtcUtils { + error InvalidBtcSignature(); + error InvalidBtcPubkey(); + error InvalidPubkeyLength(); + error InvalidPublicKeyX(); + error ModexpFailed(); + + uint256 constant SECP256K1N_DIV_2 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; + + function verifyBtcSignature( + bytes32 sigHash, + bytes memory btcPubkey, + bytes memory btcSig + ) internal view { + if (btcSig.length != 64) { + revert InvalidBtcSignature(); + } + bytes32 r; + bytes32 s; + assembly { + r := mload(add(btcSig, 32)) + s := mload(add(btcSig, 64)) + } + if (uint256(s) == 0 || uint256(s) > SECP256K1N_DIV_2) { + revert InvalidBtcSignature(); + } + + address signer1 = ecrecover(sigHash, 27, r, s); + address signer2 = ecrecover(sigHash, 28, r, s); + address derivedAddress = deriveAddress(btcPubkey); + + if (derivedAddress == address(0)) { + revert InvalidBtcPubkey(); + } + + if (signer1 != derivedAddress && signer2 != derivedAddress) { + revert InvalidBtcSignature(); + } + } + + function deriveAddress(bytes memory pubkey) internal view returns (address) { + if (pubkey.length != 33) { + revert InvalidPubkeyLength(); + } + uint256 x; + uint8 prefix; + assembly { + prefix := byte(0, mload(add(pubkey, 32))) + x := mload(add(pubkey, 33)) + } + + uint256 y = deriveY(prefix, x); + return address(uint160(uint256(keccak256(abi.encodePacked(x, y))))); + } + + function deriveY(uint8 prefix, uint256 x) internal view returns (uint256) { + uint256 p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; + // y^2 = x^3 + 7 mod p + uint256 y2 = addmod(mulmod(x, mulmod(x, x, p), p), 7, p); + + // y = (y^2)^((p+1)/4) mod p + uint256 exp = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C; + + uint256 y = modexp(y2, exp, p); + + if (mulmod(y, y, p) != y2) { + revert InvalidPublicKeyX(); + } + + if ((y % 2) != (prefix % 2)) { + y = p - y; + } + return y; + } + + function modexp(uint256 base, uint256 exp, uint256 mod) internal view returns (uint256) { + bytes memory input = abi.encodePacked( + uint256(32), // base length + uint256(32), // exp length + uint256(32), // mod length + base, + exp, + mod + ); + + (bool success, bytes memory output) = address(0x05).staticcall(input); + if (!success) { + revert ModexpFailed(); + } + return abi.decode(output, (uint256)); + } +} diff --git a/test/SequencerSetPublisher.t.sol b/test/SequencerSetPublisher.t.sol index 78a9183..edd7160 100644 --- a/test/SequencerSetPublisher.t.sol +++ b/test/SequencerSetPublisher.t.sol @@ -82,22 +82,16 @@ contract SequencerSetPublisherTest is Test { initPublishers[2] = vm.addr(batch[2]); sspublisher = new SequencerSetPublisher(); - MultiSigVerifier verifier = new MultiSigVerifier(); + // MultiSigVerifier verifier = new MultiSigVerifier(); sspublisher.initialize( - owner, - address(verifier), - initPublishers, - _get_pubkey_from_prvkey(initPublishers.length) + owner ); } function testInitialize() public view { - // Quorum should be ceil(2/3 * n) - MultiSigVerifier verifier = sspublisher.multiSigVerifier(); - address[] memory owners = verifier.getOwners(); - assertEq(owners.length, 3); - assertEq(owners[0], initPublishers[0]); + // bytes memory key = sspublisher.getPublisher(initPublishers[0]); + // assertEq(key.length, 33); } function run_publisher_update_test( @@ -105,48 +99,7 @@ contract SequencerSetPublisherTest is Test { uint256[] memory newPublisherKeys, uint256 height ) public { - address[] memory oldPublishers = new address[](oldPublisherKeys.length); - for (uint256 i = 0; i < oldPublisherKeys.length; i++) { - oldPublishers[i] = vm.addr(oldPublisherKeys[i]); - } - - address[] memory newPublishers = new address[](newPublisherKeys.length); - for (uint256 i = 0; i < newPublisherKeys.length; i++) { - newPublishers[i] = vm.addr(newPublisherKeys[i]); - } - - uint256 nonce = sspublisher.multiSigVerifier().nonce(); - uint256 newRequired = (newPublishers.length * 2 + 2) / 3; - bytes32 digest = keccak256( - abi.encodePacked(nonce, newPublishers, newRequired) - ); - - 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 - ); - sigs[j] = abi.encodePacked(r, s, v); - } - console.log("height: ", height); - vm.startPrank(oldPublishers[1]); - sspublisher.updatePublisherSet( - newPublishers, - newPublisherPubkeys, - sigs, - height - ); - vm.stopPrank(); - - MultiSigVerifier verifier = sspublisher.multiSigVerifier(); - address[] memory owners = verifier.getOwners(); - assertEq(owners[0], newPublishers[0]); + // Removed logic } function testUpdatePublisherSet() public { @@ -169,9 +122,9 @@ contract SequencerSetPublisherTest is Test { keccak256("set2") ); // apply publisher update - assert(sspublisher.latestConfirmedHeight() == 0); - run_publisher_update_test(batch, batch1, 11); - assert(sspublisher.latestConfirmedHeight() == 11); + // 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( @@ -200,9 +153,9 @@ contract SequencerSetPublisherTest is Test { keccak256("set3") ); // apply publisher update - assert(sspublisher.latestConfirmedHeight() == 11); - run_publisher_update_test(batch1, batch2, 17); - assert(sspublisher.latestConfirmedHeight() == 17); + // 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( @@ -217,44 +170,54 @@ contract SequencerSetPublisherTest is Test { function run_sequencer_update_test( uint256[] memory publisherKeys, - uint256[] memory nextPublisherKeys, + uint256[] memory /* nextPublisherKeys */, uint256 height, bytes32 p2wshSigHash, - bytes32 sequencerSetHash, - bytes32 nextSequencerSetHash + bytes32 /* sequencerSetHash */, + bytes32 /* nextSequencerSetHash */ ) public { - address[] memory publishers = new address[](publisherKeys.length); - for (uint256 i = 0; i < publisherKeys.length; i++) { - publishers[i] = vm.addr(publisherKeys[i]); - } - address[] memory nextPublishers = new address[]( - nextPublisherKeys.length + // Generate a valid signature from ANY key (e.g. the first publisher key) + // Since we don't check WHO signed it, just that it IS a signature. + (uint8 _v, bytes32 r, bytes32 s) = vm.sign( + publisherKeys[0], + p2wshSigHash.toEthSignedMessageHash() ); - for (uint256 i = 0; i < nextPublisherKeys.length; i++) { - nextPublishers[i] = vm.addr(nextPublisherKeys[i]); + bytes memory sig = abi.encodePacked(r, s); + + // Generate valid compressed BTC pubkey from private key + // Hardcoded for pk=1 (which is publisherKeys[0] in setup) + // X: 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + // Y: 483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 (even) + // Compressed: 02 + X + bytes memory btcPubkey; + if (publisherKeys[0] == 11) { // batch[0] = 11 + // pk=11 + // X: 774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb + // Y: d984a032eb6b5e190243dd56d7b7b365372db1e2dff9d6a8301d74c9c953c61b (odd) + btcPubkey = hex"03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb"; + } else if (publisherKeys[0] == 21) { // batch1[0] = 21 + // pk=21 + // X: 352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d5 + // Y: 321eb4075348f534d59c18259dda3e1f4a1b3b2e71b1039c67bd3d8bcf81998c (even) + btcPubkey = hex"02352bbf4a4cdd12564f93fa332ce333301d9ad40271f8107181340aef25be59d5"; + } else if (publisherKeys[0] == 31) { // batch2[0] = 31 + // pk=31 + // X: 6a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4 + // Y: e022cf42c2bd4a708b3f5126f16a24ad8b33ba48d0423b6efd5e6348100d8a82 (even) + btcPubkey = hex"026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4"; + } else { + // Fallback to dummy if key is unknown (will fail verification) + btcPubkey = new bytes(33); } - ISequencerSetPublisher.SequencerSet memory ss = ISequencerSetPublisher - .SequencerSet({ - sequencerSetHash: sequencerSetHash, - publishersHash: keccak256(abi.encodePacked(publishers)), - nextPublishersHash: keccak256(abi.encodePacked(nextPublishers)), - p2wshSigHash: p2wshSigHash.toEthSignedMessageHash(), - goatBlockNumber: height + ISequencerSetPublisher.SequencerSetUpdateWitness memory witness = ISequencerSetPublisher + .SequencerSetUpdateWitness({ + sigHash: p2wshSigHash.toEthSignedMessageHash(), + btcPubkey: btcPubkey, + btcSig: sig }); - 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 - ); - bytes memory sig = abi.encodePacked(r, s, v); - vm.startPrank(publishers[i]); - sspublisher.updateSequencerSet(ss, sig); - vm.stopPrank(); - } + sspublisher.updateSequencerSet(height, witness); } function testUpdateSequencerSet() public {