diff --git a/contracts/contract-redesign.md b/contracts/contract-redesign.md new file mode 100644 index 000000000..6e3fae552 --- /dev/null +++ b/contracts/contract-redesign.md @@ -0,0 +1,287 @@ +# Context + +When a subnet is created, it has to register itself to a Gateway, submitting all the funds locked to the gateway. With the funds submitted to the gateway, the gateway will: + +1. Route the cross network messages for the subnet +2. Tracks the checkpointing +3. Handles fund deposit and release +4. … + +# Problems + +There are a few problems associated with the above approach or the current implementations. + +- First of all the funds of all subnets are locked in the gateway. If the gateway is misbehaving (it has an owner and can perform upgrades) or other subnets misbehaving, it might impact all subnets registered in the gateway. +- Any subnet can register with any gateway, there is no selection, no restriction and no checks. +- Currently federated power and collateral based permission modes are mixed together. It’s not easy to extend and modify. Also the initialisation of permission mode states in the child subnet is not fully synchronised. + +# Phases / scope + +- [ ] Milestone 1: Security-focused refactor of contracts + - Segregation of each `SubnetActor` + - Gateway performs routing of messages +- [ ] Milestone 2: Generalisation of permission modes +- [ ] Milestone 3: Generalisation of subnet genesis +- [ ] Milestone 4: Subnet and gateway upgrades + +Milestone 4 is not fully covered yet as the upgrade path is not fully clear to me (please help enlighten me). + +# Proposals + +To separate the subnet actor and gateway so that: + +1. Funds are managed by each subnet itself. Each subnet is segregated from another. +2. Gateway handles routing and high level subnet information display. + +To clarify the definitions of `Gateway` and `Subnet`. `Subnet`is basically a representation of a network. Each `Subnet` will have a parent, except for the `root` subnet. `Subnet` should have two parts: + +1. A smart contract, which is called `SubnetActor`, that lives in the parent network. +2. A blockchain that is running separately, such as as `fendermint`. `Gateway` is a smart contract that lives in the blockchain, that interfaces with `IPC`. + +For `SubnetActor`, as long as it implements the `SubnetActor` interface, it is considered as a potential `Subnet`. `IPC` should provide just templates and utils for `SubnetActor`'s actual implementation. The `Gateway` should be implemented mostly by `IPC`. + +`PowerAllocationMode` controls how power is allocated, which can be driven by collateral, explicitly-assigned weights, or both (hybrid). + +The `SubnetActor` interface is specified as (TO BE FILLED GRADUALLY): + +```solidity +interface SubnetActor { + // the token used + function supplySource() external view returns(SupplySource memory); + + // the genesis bytes, child blockchain should parse the bytes accordingly + function genesis() external view returns(bytes memory); + + function powerAllocationMode() external view + returns(PowerAllocationMode memory); + + function consensus() external view returns(Consensus memory); + + // deposit funds into the subnet + function deposit(FVMAddress to, uint256 amount) external emits IPCEnvolope; + + // route the cross network call from the gateway + function routeXnetCall(IpcEnvelope msg) external onlyGateway; +} + +enum PowerAllocationMode { + Collateral, + Federated, +} + +enum Consensus { + // proof of stake like consensus algorithm, could be stake or federated power + ProofOfPower, +} +``` + +The `Gateway` interface (logically) contains several parts: `GatewayChildRegistry`, `GatewayTopdownFacet`, `GatewayBottomUpFacet`. The `GatewayTopdownFacet` handles the requests from the parent. `GatewayBottomUpFacet` handles requests from the child to the parent. `GatewayChildRegistry` handles the registration of subnets in the parent. + +```solidity +interface GatewayChildRegistry { + // a subnet attempts to register itself to the gateway, only approved subnet + // can register + function register() onlyApproved external; + + // removes a subnet from the gateway + function revoke(SubnetId subnet) onlyRole(SubnetAdmin) external; + + function approveRegister(SubnetId subnet) onlyRole(SubnetAdmin) extenral; + + function rejectRegister(SubnetId subnet) onlyRole(SubnetAdmin) external; +} +``` + +```solidity +interface GatewayBottomUpFacet { + // withdraw the specified amount to the parent, amount is msg.value + function withdraw(FVMAddress to) onlyOwner external emits IPCEnvolope; + + // for registered subnet to route a message to the parent + function mail(IPCEnvelope envelope) onlyRegisterred external; + + // methods from existing `CheckpointingFacet` + ... +} +``` + +The `GatewayTopdownFacet` is the same as existing `TopDownFinalityFacet` + +The overall relationship is as follows: + +![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/75c9b610-402a-494d-9887-8258d6cc60b5/914d7a6e-5033-4c74-a2fd-f549141a9175/Untitled.png) + +## **Subnet Lifecycle** + +The lifecycle of a subnet happens both in the parent, i.e. through `SubnetActor`, and in the blockchain, i.e. `fendermint`. + +- Creation: Subnet creation is just contract deployment, which currently is handled by the subnet registry or by the subnet owner. The creation of the subnet is not a concern here. +- Bootstrap: When the subnet has reached `PowerAllocationMode` thresholds, such as min collateral and min validator count reached, for collateral based mode. The su + - Then each power allocation mode should have its own implementation. + +`IPC` will provide several template implementations of different permission modes. + +``` +interface CollateralSubnet is SubnetActor { + function consensus() external view override returns(Consensus memory) { + return Consensus.ProofOfPower; + } + + // ======= admin methods ======= + + // sample setter for configuration, what can be: minValidators, + // minColallateral, ... + function set(string what, uint256 value) onlyOwner external; + + // ======= open to public ======= + + // for join, stake, unstake, leave, kill handling of pre + function validatorJoin( + uint256 collateral, + bytes publicKey + ) external emits PowerChange[]; + + function validatorStake( + uint256 collateral + ) external emits PowerChange[]; + + function validatorUnstake( + uint256 collateral + ) external emits PowerChange[]; + + function validatorLeave() external emits PowerChange[]; + + function kill() external; + + // claim the collateral after collateral released + function claim() external; + + // ===== getters ===== + function isActiveValidator(address addr) external returns(bool); + + function isWaitingValidator(address addr) external returns(bool); + + ... +} +``` + +``` +interface FederatedSubnet is SubnetActor { + function setPower( + address[] calldata validators, + bytes[] calldata publicKeys, + uint256[] calldata powers + ) external onlyOwner; + + function kill() external; + + // ===== getters ===== + function getPower(address addr) external returns(uint256); +} +``` + +For the above implementations, it will call into the existing `LibStaking` (probably rename to `LibPower` or any other better names) that handles the validator tracking. + +For collateral based subnet, operations that deal with validator stakes will no longer send funds to the gateway contract. The funds will be managed by the subnet actor instead. + +## XNet Messaging (L2 only) + +The direct consequence of the change is cross messages execution. The biggest change is all cross message entrypoints are shifted to the `SubnetActor`, `Gateway` no longer plays a critical part in message execution. It exposes only a `mail` method to registered subnets and handles message routing. + + For topdown messages, the funds should be locked in the child subnet. For example, the implementation of `fund` will be: + +```jsx +contract SubnetActor { + + function fund(..., uint256 amount) { + SupplySource memory s = ...; + s.lock(amount); + + IPCEnvolope msg = ... + + // same as current implementation + commitTopdownMsg(msg); + } +} +``` + +The current `commitTopdownMsg` does not need to change. but only shift to the `SubnetActor`. The corresponding child gateway handling methods that executes the cross messages does not have to change. The execution of topdown messages in the child happens in `GatewayTopdownFacet`, if the message is targeting a grandchild subnet, then `GatewayTopdownFacet` will call the corresponding `SubnetActor` in the child network. + +For `sendXnetMsg`, there are still some questions to be clarified, see [link](https://filecoinproject.slack.com/archives/C06KWC57DRA/p1713411693578479). But the design should be mostly similar to `fund`. + +For bottom up checkpoint, there is no change to `CheckpointingFacet` as checkpoint creation and signature collection should still happen in the child gateway. When the relayer submits the checkpoint to the parent gateway, there is no need to call into gateway at `commitCheckpoint` , see [link](https://github.com/consensus-shipyard/ipc/blob/1b469edb840680297aa724683f481adfba529561/contracts/src/subnet/SubnetActorCheckpointingFacet.sol#L46). The execution of xnet messages should be shifted to `SubnetActor` , see [method](https://github.com/consensus-shipyard/ipc/blob/1b469edb840680297aa724683f481adfba529561/contracts/src/lib/LibGateway.sol#L357). Only when the target subnet is not the current subnet, then it call into the `Gateway` to route the message into the postbox. + +```jsx +contract GatewayRoutingFacet { + + function route(IPCEnvolop msg) external onlyRegisterred { + // design to be discussed, current system does not have this enabled. + ... + } +} +``` + +## Validator Changes Sync Simplification + +Currently the validator changes are emitted as operations, that can be replayed in both child and parent. It’s not really necessary for the child to take operations as inputs to power calculation, only the final weight is required. As such, the parent will still maintain the list of top validators, but only emits the final weight to the child. + +There are two approaches: + +- The power still consists of two parts: totalPower and confirmedPower. The parent records each validator change and applies them to the `totalPower`, the total power is emitted to the child as validator changes, the child picks up the validator changes and applies the batch validator changes to its state, updating the power of each validator in the batch. The child then sends back the final configuration number to the parent in the bottom up checkpoint. Once the parent receives the bottom up checkpoint, it updates the `confirmedPower` and propagates the changes to top validators. +- The power is just the a uint256. But with each validator change, the updated power is not immediately applied, but pushed to a queue. The queue is sequential respect to the configuration number. (It could be implemented with a circulation buffer, this could also be rated limited if the buffer is full). The change is actually propagated to the child sequentially according to the configuration number. The child applies the change just like the first approach. Once the parent receives the bottom up checkpoint, it pops the configuration changes from the queue and update the validator power accordingly and propagates the top validators. + +The first approach requires slightly less change to existing system, but the second approach might be cleaner. Feedbacks needed! + +## Generalisation of Subnet Genesis + +Currently the subnet genesis is manually constructed, i.e. with each new component or functionality change, one needs to update the code in `fendermint`, `GatewayManager` and `SubnetActor` to capture the changes. At the same time, one needs to make sure the parent genesis information is correctly propagated to the child subnet gateway, otherwise it’s a bug(this happened before). It’s very coupled. An automated way to track the genesis and propagate to the child subnet would be very helpful. + +The idea is as follows, instead of currently creating a variable that tracks the subnet state, i.e. `bootstrapped` field in the `SubnetStorage` struct, one can generalise the genesis formation as concatenation an array of `IGenesisComponent` interfaces. + +```solidity +/// @notice A interface that indicated the implementing facet contains a or multiple genesis settings. +interface IGenesisComponent { + /// @notice Returns the id of the component + function id() external view returns(bytes4); + + /// @notice Returns the actual bytes of the genesis component + function genesis() external view returns(bytes memory); + + /// @notice Checks if the component is bootstrapped + function bootstrapped() external view returns(bool); +} +``` + +Any `facet` that requires input to the genesis can just implement the `IGenesisComponent` interface. When the subnet is created and the facets are passed into the constructor, by simply checking if the facet support `IGenesisComponent`, one can automatically know which facets need to write data into the genesis. + +As an example, a subnet implementation has two facets implementing `IGenesisComponent`: + +- `SubnetActorFacet`: Tracks the information of subnet, i.e. bottom up checkpoint period. These metadata needs to propagate to the child subnet +- `FederatedPowerFacet`: Handles the power of the validators through federated mode. The initial validator information need to propagate to the child subnet. + +We can create a `SubnetBootstrapFacet` , that holds `[SubnetActorFacet, FederatedPowerFacet]` in its storage. It has the following methods: + +```solidity +contract SubnetBootstrapFacet { + /// @notice Checks if the subnet is bootstrapped + function bootstrapped() public view returns(bool) { + /// loops [SubnetActorFacet, FederatedPowerFacet], + /// to only returns true if the two facets are bootstrapped + } + + function genesis() external view returns(bytes memory) { + if (!bootstrapped()) { + revert SubnetNotBootstrapped(); + } + + return Bytes.concat( + SubnetActorFacet.genesis(), + FederatedPowerFacet.genesis(), + ); + } +} +``` + +The `SubnetActorFacet, FederatedPowerFacet` above can have their own bootstrap conditions. Caller of `SubnetBootstrapFacet.genesis` just need to parse the bytes to accordingly. + +The `SubnetBootstrapFacet.genesis()` should be passed to the child `Gateway` , so that the gateway can streamline the genesis syncing process, without manually customization. \ No newline at end of file diff --git a/contracts/src/enums/ConsensusType.sol b/contracts/src/enums/ConsensusType.sol index 383dfb3f1..03e2de867 100644 --- a/contracts/src/enums/ConsensusType.sol +++ b/contracts/src/enums/ConsensusType.sol @@ -6,3 +6,7 @@ pragma solidity ^0.8.23; enum ConsensusType { Fendermint } + +enum Consensus { + ProofOfPower +} \ No newline at end of file diff --git a/contracts/src/errors/IPCErrors.sol b/contracts/src/errors/IPCErrors.sol index ccb550514..6c6ee80c7 100644 --- a/contracts/src/errors/IPCErrors.sol +++ b/contracts/src/errors/IPCErrors.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; error AddressShouldBeValidator(); error AlreadyRegisteredSubnet(); error AlreadyInSet(); +error AlreadyInitialized(); error CannotConfirmFutureChanges(); error CannotReleaseZero(); error CannotSendCrossMsgToItself(); @@ -15,6 +16,7 @@ error CheckpointNotCreated(); error BottomUpCheckpointAlreadySubmitted(); error BatchNotCreated(); error CollateralIsZero(); +error DuplicatedGenesisValidator(); error EmptyAddress(); error FailedAddIncompleteQuorum(); error FailedAddSignatory(); @@ -73,12 +75,14 @@ error SubnetAlreadyBootstrapped(); error SubnetNotBootstrapped(); error FacetCannotBeZero(); error WrongGateway(); +error WrongSubnet(); error CannotFindSubnet(); error UnknownSubnet(); error MethodNotAllowed(string reason); error InvalidFederationPayload(); -error DuplicatedGenesisValidator(); error NotEnoughGenesisValidators(); +error NotBottomUpMessage(); +error L3NotSupportedYet(); enum InvalidXnetMessageReason { Sender, diff --git a/contracts/src/gateway/GatewayBottomUpFacet.sol b/contracts/src/gateway/GatewayBottomUpFacet.sol new file mode 100644 index 000000000..e3605acc0 --- /dev/null +++ b/contracts/src/gateway/GatewayBottomUpFacet.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {LibUtil} from "../lib/LibUtil.sol"; +import {FvmAddress} from "../structs/FvmAddress.sol"; + +import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; + +import {InvalidXnetMessage, InvalidXnetMessageReason, CheckpointAlreadyExists, CheckpointNotCreated} from "../errors/IPCErrors.sol"; + +import {LibGatewayChildQuery} from "./GatewayChildFacet.sol"; +import {BURNT_FUNDS_ACTOR} from "../constants/Constants.sol"; +import {IpcEnvelope, BottomUpMsgBatch, BottomUpCheckpoint} from "../structs/CrossNet.sol"; +import {LibQuorum} from "../lib/LibQuorum.sol"; +import {QuorumMap} from "../structs/Quorum.sol"; +import {FilAddress} from "fevmate/utils/FilAddress.sol"; + +/// @notice Handles the requests to the parent subnet in the child subnet. +contract GatewayBottomUpFacet { + using FilAddress for address payable; + + /// @notice release burns the received value locally in subnet and commits a bottom-up message to release the assets in the parent. + /// The local supply of a subnet is always the native coin, so this method doesn't have to deal with tokens. + function release(FvmAddress calldata to, uint256 amount) external payable { + if (amount == 0) { + // prevent spamming if there's no value to release. + revert InvalidXnetMessage(InvalidXnetMessageReason.Value); + } + IpcEnvelope memory crossMsg = CrossMsgHelper.createReleaseMsg({ + subnet: LibGatewayChildQuery.id(), + signer: msg.sender, + to: to, + value: amount + }); + + LibBottomUp.commitBottomUpMsg(crossMsg); + + // burn funds that are being released + // TODO: should only burn once the operation is successful + payable(BURNT_FUNDS_ACTOR).sendValue(msg.value); + } + + /// @notice creates a new bottom-up checkpoint + /// @param checkpoint - a bottom-up checkpoint + /// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight + /// @param membershipWeight - the total weight of the membership + function createBottomUpCheckpoint( + BottomUpCheckpoint calldata checkpoint, + bytes32 membershipRootHash, + uint256 membershipWeight + ) external { + LibUtil.enforceSystemActorOnly(); + LibBottomUp.createBottomUpCheckpoint(checkpoint, membershipRootHash, membershipWeight); + } + + /// @notice Set a new checkpoint retention height and garbage collect all checkpoints in range [`retentionHeight`, `newRetentionHeight`) + /// @dev `retentionHeight` is the height of the first incomplete checkpointswe must keep to implement checkpointing. + /// All checkpoints with a height less than `retentionHeight` are removed from the history, assuming they are committed to the parent. + /// @param newRetentionHeight - the height of the oldest checkpoint to keep + function pruneBottomUpCheckpoints(uint256 newRetentionHeight) external { + LibUtil.enforceSystemActorOnly(); + LibBottomUp.pruneBottomUpCheckpoints(newRetentionHeight); + } + + /// @notice checks whether the provided checkpoint signature for the block at height `height` is valid and accumulates that it + /// @dev If adding the signature leads to reaching the threshold, then the checkpoint is removed from `incompleteCheckpoints` + /// @param height - the height of the block in the checkpoint + /// @param membershipProof - a Merkle proof that the validator was in the membership at height `height` with weight `weight` + /// @param weight - the weight of the validator + /// @param signature - the signature of the checkpoint + function addCheckpointSignature( + uint256 height, + bytes32[] memory membershipProof, + uint256 weight, + bytes memory signature + ) external { + LibUtil.enforceSystemActorOnly(); + LibBottomUp.addCheckpointSignature(height, membershipProof, weight, signature); + } +} + +// ============ Internal Usage Only ============ +library LibBottomUp { + /// @notice checks if the bottom-up checkpoint already exists at the target epoch + function bottomUpCheckpointExists(uint256 epoch) internal view returns (bool) { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + return s.bottomUpCheckpoints[epoch].blockHeight != 0; + } + + function createBottomUpCheckpoint( + BottomUpCheckpoint calldata checkpoint, + bytes32 membershipRootHash, + uint256 membershipWeight + ) internal { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + if (LibBottomUp.bottomUpCheckpointExists(checkpoint.blockHeight)) { + revert CheckpointAlreadyExists(); + } + + LibQuorum.createQuorumInfo({ + self: s.checkpointQuorumMap, + objHeight: checkpoint.blockHeight, + objHash: keccak256(abi.encode(checkpoint)), + membershipRootHash: membershipRootHash, + membershipWeight: membershipWeight, + majorityPercentage: s.majorityPercentage + }); + + storeBottomUpCheckpoint(checkpoint); + } + + function pruneBottomUpCheckpoints(uint256 newRetentionHeight) internal { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + // we need to clean manually the checkpoints because Solidity does not support passing + // a storage variable as an interface (so we can iterate and remove directly inside pruneQuorums) + for (uint256 h = s.checkpointQuorumMap.retentionHeight; h < newRetentionHeight; ) { + delete s.bottomUpCheckpoints[h]; + delete s.bottomUpMsgBatches[h]; + unchecked { + ++h; + } + } + + LibQuorum.pruneQuorums(s.checkpointQuorumMap, newRetentionHeight); + } + + /// @notice checks whether the provided checkpoint signature for the block at height `height` is valid and accumulates that it + /// @dev If adding the signature leads to reaching the threshold, then the checkpoint is removed from `incompleteCheckpoints` + /// @param height - the height of the block in the checkpoint + /// @param membershipProof - a Merkle proof that the validator was in the membership at height `height` with weight `weight` + /// @param weight - the weight of the validator + /// @param signature - the signature of the checkpoint + function addCheckpointSignature( + uint256 height, + bytes32[] memory membershipProof, + uint256 weight, + bytes memory signature + ) external { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + // check if the checkpoint was already pruned before getting checkpoint + // and triggering the signature + LibQuorum.isHeightAlreadyProcessed(s.checkpointQuorumMap, height); + + if (!bottomUpCheckpointExists(height)) { + revert CheckpointNotCreated(); + } + LibQuorum.addQuorumSignature({ + self: s.checkpointQuorumMap, + height: height, + membershipProof: membershipProof, + weight: weight, + signature: signature + }); + } + + /// @notice Commits a new cross-net message to a message batch for execution + /// @param crossMessage - the cross message to be committed + function commitBottomUpMsg(IpcEnvelope memory crossMessage) internal { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + uint256 epoch = LibUtil.nextBottomUpCheckpointEpoch(block.number, s.bottomUpCheckPeriod); + + // assign nonce to the message. + crossMessage.nonce = s.bottomUpNonce; + s.bottomUpNonce += 1; + + // populate the batch for that epoch + (bool exists, BottomUpMsgBatch storage batch) = getBottomUpMsgBatch(epoch); + if (!exists) { + batch.subnetID = LibGatewayChildQuery.id(); + batch.blockHeight = epoch; + } + + batch.msgs.push(crossMessage); + + // TODO: the message batch logic should be removed and favor message batch proofs + } + + /// @notice returns the bottom-up batch + function getBottomUpMsgBatch(uint256 epoch) internal view returns (bool exists, BottomUpMsgBatch storage batch) { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + batch = s.bottomUpMsgBatches[epoch]; + exists = batch.blockHeight != 0; + } + + /// @notice stores checkpoint + function storeBottomUpCheckpoint(BottomUpCheckpoint memory checkpoint) internal { + BottomUpStorage storage s = LibBottomUpStorage.diamondStorage(); + + BottomUpCheckpoint storage b = s.bottomUpCheckpoints[checkpoint.blockHeight]; + b.blockHash = checkpoint.blockHash; + b.subnetID = checkpoint.subnetID; + b.nextConfigurationNumber = checkpoint.nextConfigurationNumber; + b.blockHeight = checkpoint.blockHeight; + + uint256 msgLength = checkpoint.msgs.length; + for (uint256 i; i < msgLength; ) { + // We need to push because initializing an array with a static + // length will cause a copy from memory to storage, making + // the compiler unhappy. + b.msgs.push(checkpoint.msgs[i]); + unchecked { + ++i; + } + } + } +} + +// ============ Private Usage Only ============ + +struct BottomUpStorage { + /// @notice bottom-up period in number of epochs for the subnet + uint256 bottomUpCheckPeriod; + /// @notice nonce for bottom-up messages + uint64 bottomUpNonce; + /// @notice Maximum number of messages per bottom up checkpoint + uint64 maxMsgsPerBottomUpBatch; + /// @notice majority percentage value (must be greater than or equal to 51) + uint8 majorityPercentage; + /// @notice A mapping of block numbers to bottom-up cross-messages + // slither-disable-next-line uninitialized-state + mapping(uint256 => BottomUpMsgBatch) bottomUpMsgBatches; + /// @notice A mapping of block numbers to bottom-up checkpoints + // slither-disable-next-line uninitialized-state + mapping(uint256 => BottomUpCheckpoint) bottomUpCheckpoints; + /// @notice Quorum information for checkpoints + QuorumMap checkpointQuorumMap; +} + +library LibBottomUpStorage { + function diamondStorage() internal pure returns (BottomUpStorage storage ds) { + bytes32 position = keccak256("ipc.gateway.bottomup.storage"); + assembly { + ds.slot := position + } + } +} \ No newline at end of file diff --git a/contracts/src/gateway/GatewayChildFacet.sol b/contracts/src/gateway/GatewayChildFacet.sol new file mode 100644 index 000000000..74d5368ec --- /dev/null +++ b/contracts/src/gateway/GatewayChildFacet.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; + +contract GatewayChildFacet { + function id() internal returns(SubnetID memory) { + return LibGatewayChildQuery.id(); + } +} + +library LibGatewayChildQuery { + function diamondStorage() internal pure returns (SubnetInfo storage ds) { + bytes32 position = keccak256("ipc.gateway.child.storage"); + assembly { + ds.slot := position + } + } + + function id() internal returns(SubnetID memory) { + return diamondStorage().id; + } +} + +// ============ Internal Usage Only ============ + +/// @notice Stores the child subnet information +struct SubnetInfo { + /// @notice The id of the subnet + SubnetID id; +} \ No newline at end of file diff --git a/contracts/src/gateway/GatewayRegistryFacet.sol b/contracts/src/gateway/GatewayRegistryFacet.sol new file mode 100644 index 000000000..725132b50 --- /dev/null +++ b/contracts/src/gateway/GatewayRegistryFacet.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +/// TODO: we might need this in the future, comment off first. + +// import {SubnetID} from "../structs/Subnet.sol"; +// import {InvalidXnetMessage} from "../errors/IPCErrors.sol"; +// import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; + +// /// @notice Handles the registration of subnet with the current gateway. The gateway will only route messages +// /// for registered subnet only. +// library LibGatewayRegistry { +// using SubnetIDHelper for SubnetID; + +// function enforceOnlyApproved(SubnetID calldata subnet) internal { +// require(false, "todo"); +// } + +// /// @notice Application to be a subnet of the current network +// function applyRegister(SubnetID calldata subnet) internal { +// require(false, "todo"); +// } + +// /// @notice Revoke the registration of a subnet from the current gateway +// function revokeRegister(SubnetID calldata subnet) internal { +// require(false, "todo"); +// } + +// /// @notice Approve the registration of a subnet +// function approveRegister(SubnetID calldata subnet) internal { +// require(false, "todo"); +// } +// } + +// contract GatewayRegistryFacet { +// modifier onlyOwner { +// require(false, "todo"); + +// _; +// } + +// /// @notice Application to be a subnet of the current network +// function applyRegister(SubnetID calldata subnet) onlyOwner external { +// LibGatewayRegistry.applyRegister(subnet); +// } + +// /// @notice Revoke the registration of a subnet from the current gateway +// function revokeRegister(SubnetID calldata subnet) onlyOwner external { +// LibGatewayRegistry.revokeRegister(subnet); +// } + +// /// @notice Approve the registration of a subnet +// function approveRegister(SubnetID calldata subnet) onlyOwner external { +// LibGatewayRegistry.approveRegister(subnet); +// } +// } diff --git a/contracts/src/gateway/GatewayTopDownFacet.sol b/contracts/src/gateway/GatewayTopDownFacet.sol new file mode 100644 index 000000000..d27e97bc2 --- /dev/null +++ b/contracts/src/gateway/GatewayTopDownFacet.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {ParentFinality} from "../structs/CrossNet.sol"; +import {LibUtil} from "../lib/LibUtil.sol"; + +import {ParentFinalityAlreadyCommitted} from "../errors/IPCErrors.sol"; +import {ProofOfPower, Validator, LibPowerQuery, LibPowerTracking, PowerChangeRequest} from "../lib/power/LibPower.sol"; + +contract GatewayTopDownFacet { + /// @notice commit the ipc parent finality into storage and returns the previous committed finality + /// This is useful to understand if the finalities are consistent or if there have been reorgs. + /// If there are no previous committed fainality, it will be default to zero values, i.e. zero height and block hash. + /// @param finality - the parent finality + /// @return hasCommittedBefore A flag that indicates if a finality record has been committed before. + /// @return previousFinality The previous finality information. + function commitParentFinality( + ParentFinality calldata finality + ) external returns (bool hasCommittedBefore, ParentFinality memory previousFinality) { + LibUtil.enforceSystemActorOnly(); + + previousFinality = LibTopDown.commitParentFinality(finality); + hasCommittedBefore = previousFinality.height != 0; + } + + /// @notice Store the validator change requests from parent. + /// @param changeRequests - the validator changes + function storeValidatorChanges(PowerChangeRequest[] calldata changeRequests) external { + LibUtil.enforceSystemActorOnly(); + LibTopDown.storeValidatorChanges(changeRequests); + } + + /// @notice Apply all changes committed through the commitment of parent finality. + /// @return configurationNumber The configuration number of the changes set that has been confirmed. + function applyFinalityChanges() external returns (uint64) { + LibUtil.enforceSystemActorOnly(); + return LibTopDown.applyFinalityChanges(); + } +} + +// ============ Internal Usage Only ============ + +/// @notice Membership information stored in the gateway. +struct Membership { + Validator[] validators; + uint64 configurationNumber; +} + +/// @notice Handles the request coming from the parent. This sits in the child network that handles topdown related +/// requests and updates. +library LibTopDown { + using LibPowerTracking for ProofOfPower; + using LibPowerQuery for ProofOfPower; + + /// @notice commit the ipc parent finality into storage + /// @param finality - the finality to be committed + function commitParentFinality( + ParentFinality calldata finality + ) internal returns (ParentFinality memory lastFinality) { + TopdownStorage storage s = LibTopDownStorage.diamondStorage(); + + uint256 lastHeight = s.latestParentHeight; + if (lastHeight >= finality.height) { + revert ParentFinalityAlreadyCommitted(); + } + lastFinality = s.finalitiesMap[lastHeight]; + + s.finalitiesMap[finality.height] = finality; + s.latestParentHeight = finality.height; + } + + /// @notice Store the validator change requests from parent. + /// @param changeRequests - the validator changes + function storeValidatorChanges(PowerChangeRequest[] calldata changeRequests) internal { + TopdownStorage storage s = LibTopDownStorage.diamondStorage(); + s.validatorPowers.batchStoreChange(changeRequests); + } + + /// @notice Apply all changes committed through the commitment of parent finality. + /// @return configurationNumber The configuration number of the changes set that has been confirmed. + function applyFinalityChanges() internal returns (uint64) { + TopdownStorage storage s = LibTopDownStorage.diamondStorage(); + + // get the latest configuration number for the change set + uint64 configurationNumber = s.validatorPowers.changeSet.nextConfigurationNumber - 1; + // return immediately if there are no changes to confirm by looking at next configNumber + if ( + // nextConfiguration == startConfiguration (i.e. no changes) + (configurationNumber + 1) == s.validatorPowers.changeSet.startConfigurationNumber + ) { + // 0 flags that there are no changes + return 0; + } + + // confirm the change + s.validatorPowers.confirmChange(configurationNumber); + + return configurationNumber; + } + + function currentMembership() internal returns(Membership memory membership) { + TopdownStorage storage s = LibTopDownStorage.diamondStorage(); + + // Get active validators and populate the new power table. + address[] memory validators = s.validatorPowers.listActiveValidators(); + uint256 vLength = validators.length; + Validator[] memory vs = new Validator[](vLength); + for (uint256 i; i < vLength; ) { + address addr = validators[i]; + vs[i] = s.validatorPowers.validators[addr]; + + unchecked { + ++i; + } + } + + uint64 configurationNumber = s.validatorPowers.changeSet.nextConfigurationNumber - 1; + return Membership({configurationNumber: configurationNumber, validators: vs}); + } + +} + +// ============ Private Usage Only ============ + +struct TopdownStorage { + /// @notice The latest parent height committed. + uint256 latestParentHeight; + /// @notice The parent finalities. Key is the block number, value is the finality struct. + mapping(uint256 => ParentFinality) finalitiesMap; + /// @notice Tracking the validator powers from the parent + ProofOfPower validatorPowers; +} + +library LibTopDownStorage { + function diamondStorage() internal pure returns (TopdownStorage storage ds) { + bytes32 position = keccak256("ipc.gateway.topdown.storage"); + assembly { + ds.slot := position + } + } +} \ No newline at end of file diff --git a/contracts/src/interfaces/IGenesis.sol b/contracts/src/interfaces/IGenesis.sol new file mode 100644 index 000000000..85b8543e4 --- /dev/null +++ b/contracts/src/interfaces/IGenesis.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +/// @notice A interface that indicated the implementing facet contains a or multiple genesis settings. +interface IGenesisComponent { + /// @notice Returns the id of the component + function id() external view returns(bytes4); + + /// @notice Returns the actual bytes of the genesis + function genesis() external view returns(bytes memory); + + /// @notice Checks if the component is bootstrapped + function bootstrapped() external view returns(bool); +} diff --git a/contracts/src/interfaces/IMsgRouting.sol b/contracts/src/interfaces/IMsgRouting.sol new file mode 100644 index 000000000..031e800b7 --- /dev/null +++ b/contracts/src/interfaces/IMsgRouting.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {BottomUpCheckpoint, BottomUpMsgBatch, IpcEnvelope, ParentFinality} from "../structs/CrossNet.sol"; +import {SubnetID} from "../structs/Subnet.sol"; +import {FvmAddress} from "../structs/FvmAddress.sol"; + +/// @title Gateway message routing interface +interface IMsgRouting { + /// @notice Route a topdown message to the target network + function routeTopdownMsg(IpcEnvelope calldata envelope) external payable; + + /// @notice Route a bottom up message to the target network + function routeBottomUpMsg(IpcEnvelope calldata envelope) external payable returns (IpcEnvelope memory committed); +} diff --git a/contracts/src/interfaces/ISubnet.sol b/contracts/src/interfaces/ISubnet.sol new file mode 100644 index 000000000..ff3cc7e17 --- /dev/null +++ b/contracts/src/interfaces/ISubnet.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; + +/// @title Subnet interface +interface ISubnet { + /// @notice Checks if the subnet is now bootstrapped + function bootstrapped() external view returns(bool); + + /// @notice Get the id of the subnet + function id() external view returns(SubnetID memory); +} diff --git a/contracts/src/lib/LibGenesis.sol b/contracts/src/lib/LibGenesis.sol new file mode 100644 index 000000000..b8e2e659f --- /dev/null +++ b/contracts/src/lib/LibGenesis.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {EnumerableMap} from "openzeppelin-contracts/utils/structs/EnumerableMap.sol"; +import {IGenesisComponent} from "../interfaces/IGenesis.sol"; + +struct SubnetGenesis { + /// @notice The total circulation supply of the subnet + uint256 circSupply; + /// @notice The genesis balances of the address + EnumerableMap.AddressToUintMap balances; +} + +/// @title Lib Subnet Genesis +/// @notice Handles the subnet genesis states and util functions +library LibSubnetGenesis { + using EnumerableMap for EnumerableMap.AddressToUintMap; + + /// @notice Deposit into the genesis balance of the address + function deposit(SubnetGenesis storage self, address addr, uint256 amount) internal { + (bool exists, uint256 existingAmount) = self.balances.tryGet(addr); + + if (exists) { + self.balances.set(addr, existingAmount + amount); + } else { + self.balances.set(addr, amount); + } + + self.circSupply += amount; + } +} diff --git a/contracts/src/lib/LibMsgExecution.sol b/contracts/src/lib/LibMsgExecution.sol new file mode 100644 index 000000000..cf332d1b0 --- /dev/null +++ b/contracts/src/lib/LibMsgExecution.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {IPCMsgType} from "../enums/IPCMsgType.sol"; +import {GatewayActorStorage, LibGatewayActorStorage} from "../lib/LibGatewayActorStorage.sol"; +import {BURNT_FUNDS_ACTOR} from "../constants/Constants.sol"; +import {SubnetID, Subnet, SupplyKind, SupplySource} from "../structs/Subnet.sol"; +import {SubnetActorGetterFacet} from "../subnet/SubnetActorGetterFacet.sol"; +import {CallMsg, IpcMsgKind, IpcEnvelope, OutcomeType, BottomUpMsgBatch, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality} from "../structs/CrossNet.sol"; +import {Membership} from "../structs/Subnet.sol"; +import {CannotSendCrossMsgToItself, NotBottomUpMessage, L3NotSupportedYet, MethodNotAllowed, MaxMsgsPerBatchExceeded, InvalidXnetMessage ,OldConfigurationNumber, NotRegisteredSubnet, InvalidActorAddress, ParentFinalityAlreadyCommitted, InvalidXnetMessageReason} from "../errors/IPCErrors.sol"; +import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; +import {FilAddress} from "fevmate/utils/FilAddress.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {SupplySourceHelper} from "../lib/SupplySourceHelper.sol"; +import {ISubnet} from "../interfaces/ISubnet.sol"; +import {IMsgRouting} from "../interfaces/IMsgRouting.sol"; +import {LibSubnetActor, LibSubnetActorQuery} from "../subnet/SubnetActorFacet.sol"; + +/// @notice The lib for bottom up message execution. Should be called from the Subnet Actor in the +/// parent network. +library LibBottomUpExecution { + using SubnetIDHelper for SubnetID; + using CrossMsgHelper for IpcEnvelope; + using SupplySourceHelper for address; + using SubnetIDHelper for SubnetID; + using FilAddress for address payable; + using SupplySourceHelper for SupplySource; + + /// @notice executes a cross message if its destination is the current network, otherwise adds it to the postbox to be propagated further + /// This function assumes that the relevant funds have been already minted or burnt + /// when the top-down or bottom-up messages have been queued for execution. + /// This function is not expected to revert. If a controlled failure happens, a new + /// cross-message receipt is propagated for execution to inform the sending contract. + /// `Call` cross-messages also trigger receipts if they are successful. + /// @param crossMsg - the cross message to be executed + function applyMsg(IpcEnvelope memory crossMsg) internal { + if (crossMsg.to.subnetId.isEmpty()) { + sendReceipt(crossMsg, OutcomeType.SystemErr, abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.DstSubnet)); + return; + } + + SubnetID memory id = ISubnet(address(this)).id(); + + if (crossMsg.applyType(id) != IPCMsgType.BottomUp) { + revert NotBottomUpMessage(); + } + + // If the crossnet destination is NOT the current network (network where the gateway is running), + // we add it to the postbox for further propagation. + // Even if we send for propagation, the execution of every message + // should increase the appliedNonce to allow the execution of the next message + // of the batch (this is way we have this after the nonce logic). + if (!crossMsg.to.subnetId.equals(id)) { + revert NotBottomUpMessage(); + } + + if (LibSubnetActor.getThenIncrAppliedBottomUpNonce() != crossMsg.nonce) { + sendReceipt(crossMsg, OutcomeType.SystemErr, abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.Nonce)); + return; + } + + // execute the message and get the receipt. + (bool success, bytes memory ret) = executeCrossMsg(crossMsg, LibSubnetActorQuery.supplySource()); + if (success) { + sendReceipt(crossMsg, OutcomeType.Ok, ret); + } else { + sendReceipt(crossMsg, OutcomeType.ActorErr, ret); + } + } + + /// @dev Execute the cross message using low level `call` method. This way ipc will + /// catch contract revert messages as well. We need this because in `CrossMsgHelper.execute` + /// there are `require` and `revert` calls, without reflexive call, the execution will + /// revert and block the checkpoint submission process. + function executeCrossMsg(IpcEnvelope memory crossMsg, SupplySource memory supplySource) internal returns (bool success, bytes memory result) { + (success, result) = address(CrossMsgHelper).delegatecall( // solhint-disable-line avoid-low-level-calls + abi.encodeWithSelector(CrossMsgHelper.execute.selector, crossMsg, supplySource) + ); + + if (success) { + return abi.decode(result, (bool, bytes)); + } + + return (success, result); + } + + /// @notice Sends a receipt from the execution of a cross-message. + /// Only `Call` messages trigger a receipt. Transfer messages should be directly + /// handled by the peer client to return the funds to the from address in the + /// failing network. + /// (we could optionally trigger a receipt from `Transfer`s to, but without + /// multi-level execution it would be adding unnecessary overhead). + function sendReceipt(IpcEnvelope memory original, OutcomeType outcomeType, bytes memory ret) internal { + if (original.isEmpty()) { + // This should not happen as previous validation should prevent empty messages arriving here. + // If it does, we simply ignore. + return; + } + + // if we get a `Receipt` do nothing, no need to send receipts. + // - And sending a `Receipt` to a `Receipt` could lead to amplification loops. + if (original.kind == IpcMsgKind.Result) { + return; + } + + // the result of a bottom up message is a top down message + LibSubnetActor.emitTopDownMsg(original.createResultMsg(outcomeType, ret)); + } +} + +// library LibTopdownExecution { +// using SubnetIDHelper for SubnetID; +// using CrossMsgHelper for IpcEnvelope; +// using SupplySourceHelper for address; +// using SubnetIDHelper for SubnetID; +// using FilAddress for address payable; +// using SupplySourceHelper for SupplySource; + +// function msgExecutionStorage() internal returns (MsgExecutionStorage storage) { +// bytes32 position = keccak256("ipc.msgExe.storage"); +// assembly { +// ds.slot := position +// } +// } + +// /// @notice applies a cross-net messages coming from some other subnet. +// /// The forwarder argument determines the previous subnet that submitted the checkpoint triggering the cross-net message execution. +// /// @param arrivingFrom - the immediate subnet from which this message is arriving +// /// @param crossMsgs - the cross-net messages to apply +// function applyMessages(SubnetID memory arrivingFrom, IpcEnvelope[] memory crossMsgs) internal { +// uint256 crossMsgsLength = crossMsgs.length; +// for (uint256 i; i < crossMsgsLength; ) { +// applyMsg(arrivingFrom, crossMsgs[i]); +// unchecked { +// ++i; +// } +// } +// } + +// /// @notice executes a cross message if its destination is the current network, otherwise adds it to the postbox to be propagated further +// /// This function assumes that the relevant funds have been already minted or burnt +// /// when the top-down or bottom-up messages have been queued for execution. +// /// This function is not expected to revert. If a controlled failure happens, a new +// /// cross-message receipt is propagated for execution to inform the sending contract. +// /// `Call` cross-messages also trigger receipts if they are successful. +// /// @param arrivingFrom - the immediate subnet from which this message is arriving +// /// @param crossMsg - the cross message to be executed +// function applyMsg(SubnetID memory arrivingFrom, IpcEnvelope memory crossMsg) internal { +// if (crossMsg.to.subnetId.isEmpty()) { +// sendReceipt(crossMsg, OutcomeType.SystemErr, abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.DstSubnet)); +// return; +// } + +// GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); + +// // The first thing we do is to find out the directionality of this message and act accordingly, +// // incrasing the applied nonces conveniently. +// // slither-disable-next-line uninitialized-local +// SupplySource memory supplySource; +// IPCMsgType applyType = crossMsg.applyType(s.networkName); +// if (applyType == IPCMsgType.BottomUp) { +// // Load the subnet this message is coming from. Ensure that it exists and that the nonce expectation is met. +// (bool registered, Subnet storage subnet) = LibGateway.getSubnet(arrivingFrom); +// if (!registered) { +// // this means the subnet that sent the bottom up message is not registered, +// // we cannot send the receipt back as top down because the subnet is not registered +// // we ignore this message for as it's not valid, and it may be someone trying to forge it. +// return; +// } +// if (subnet.appliedBottomUpNonce != crossMsg.nonce) { +// sendReceipt(crossMsg, OutcomeType.SystemErr, abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.Nonce)); +// return; +// } +// subnet.appliedBottomUpNonce += 1; + +// // The value carried in bottom-up messages needs to be treated according to the supply source +// // configuration of the subnet. +// supplySource = SubnetActorGetterFacet(subnet.id.getActor()).supplySource(); +// } else if (applyType == IPCMsgType.TopDown) { +// // Note: there is no need to load the subnet, as a top-down application means that _we_ are the subnet. +// if (s.appliedTopDownNonce != crossMsg.nonce) { +// sendReceipt(crossMsg, OutcomeType.SystemErr, abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.Nonce)); +// return; +// } +// s.appliedTopDownNonce += 1; + +// // The value carried in top-down messages locally maps to the native coin, so we pass over the +// // native supply source. +// supplySource = SupplySourceHelper.native(); +// } + +// // If the crossnet destination is NOT the current network (network where the gateway is running), +// // we add it to the postbox for further propagation. +// // Even if we send for propagation, the execution of every message +// // should increase the appliedNonce to allow the execution of the next message +// // of the batch (this is way we have this after the nonce logic). +// if (!crossMsg.to.subnetId.equals(s.networkName)) { +// bytes32 cid = crossMsg.toHash(); +// s.postbox[cid] = crossMsg; +// return; +// } + +// // execute the message and get the receipt. +// (bool success, bytes memory ret) = executeCrossMsg(crossMsg, supplySource); +// if (success) { +// sendReceipt(crossMsg, OutcomeType.Ok, ret); +// } else { +// sendReceipt(crossMsg, OutcomeType.ActorErr, ret); +// } +// } + +// /// @dev Execute the cross message using low level `call` method. This way ipc will +// /// catch contract revert messages as well. We need this because in `CrossMsgHelper.execute` +// /// there are `require` and `revert` calls, without reflexive call, the execution will +// /// revert and block the checkpoint submission process. +// function executeCrossMsg(IpcEnvelope memory crossMsg, SupplySource memory supplySource) internal returns (bool success, bytes memory result) { +// (success, result) = address(CrossMsgHelper).delegatecall( // solhint-disable-line avoid-low-level-calls +// abi.encodeWithSelector(CrossMsgHelper.execute.selector, crossMsg, supplySource) +// ); + +// if (success) { +// return abi.decode(result, (bool, bytes)); +// } + +// return (success, result); +// } + +// /// @notice Sends a receipt from the execution of a cross-message. +// /// Only `Call` messages trigger a receipt. Transfer messages should be directly +// /// handled by the peer client to return the funds to the from address in the +// /// failing network. +// /// (we could optionally trigger a receipt from `Transfer`s to, but without +// /// multi-level execution it would be adding unnecessary overhead). +// function sendReceipt(IpcEnvelope memory original, OutcomeType outcomeType, bytes memory ret) internal { +// if (original.isEmpty()) { +// // This should not happen as previous validation should prevent empty messages arriving here. +// // If it does, we simply ignore. +// return; +// } + +// // if we get a `Receipt` do nothing, no need to send receipts. +// // - And sending a `Receipt` to a `Receipt` could lead to amplification loops. +// if (original.kind == IpcMsgKind.Result) { +// return; +// } + +// // commmit the receipt for propagation +// // slither-disable-next-line unused-return +// commitCrossMessage(original.createResultMsg(outcomeType, ret)); +// } + +// /** +// * @notice Commit the cross message to storage. +// * +// * @dev It also validates that destination subnet ID is not empty +// * and not equal to the current network. +// * This function assumes that the funds inside `value` have been +// * conveniently minted or burnt already and the message is free to +// * use them (see execBottomUpMsgBatch for reference). +// * @param crossMessage The cross-network message to commit. +// * @return shouldBurn A Boolean that indicates if the input amount should be burned. +// */ +// function commitCrossMessage(IpcEnvelope memory crossMessage) internal returns (bool shouldBurn) { +// GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); +// SubnetID memory to = crossMessage.to.subnetId; +// if (to.isEmpty()) { +// revert InvalidXnetMessage(InvalidXnetMessageReason.DstSubnet); +// } +// // destination is the current network, you are better off with a good old message, no cross needed +// if (to.equals(s.networkName)) { +// revert CannotSendCrossMsgToItself(); +// } + +// SubnetID memory from = crossMessage.from.subnetId; +// IPCMsgType applyType = crossMessage.applyType(s.networkName); + +// // Are we the LCA? (Lowest Common Ancestor) +// bool isLCA = to.commonParent(from).equals(s.networkName); + +// // Even if multi-level messaging is enabled, we reject the xnet message +// // as soon as we learn that one of the networks involved use an ERC20 supply source. +// // This will block propagation on the first step, or the last step. +// // +// // TODO IPC does not implement fault handling yet, so if the message fails +// // to propagate, the user won't be able to reclaim funds. That's one of the +// // reasons xnet messages are disabled by default. + +// bool reject = false; +// if (applyType == IPCMsgType.BottomUp) { +// // We're traversing up, so if we're the first hop, we reject if the subnet was ERC20. +// // If we're not the first hop, a child propagated this to us, they made a mistake and +// // and we don't have enough info to evaluate. +// reject = from.getParentSubnet().equals(s.networkName) && from.getActor().hasSupplyOfKind(SupplyKind.ERC20); +// } else if (applyType == IPCMsgType.TopDown) { +// // We're traversing down. +// // Check the next subnet (which can may be the destination subnet). +// reject = to.down(s.networkName).getActor().hasSupplyOfKind(SupplyKind.ERC20); +// } +// if (reject) { +// if (crossMessage.kind == IpcMsgKind.Transfer) { +// revert MethodNotAllowed("propagation of `Transfer` messages not suppported for subnets with ERC20 supply"); +// } +// } + +// // If the directionality is top-down, or if we're inverting the direction +// // because we're the LCA, commit a top-down message. +// if (applyType == IPCMsgType.TopDown || isLCA) { +// ++s.appliedTopDownNonce; +// LibGateway.commitTopDownMsg(crossMessage); +// return (shouldBurn = false); +// } + +// // Else, commit a bottom up message. +// LibGateway.commitBottomUpMsg(crossMessage); +// // gas-opt: original check: value > 0 +// return (shouldBurn = crossMessage.value != 0); +// } + +// /** +// * @dev Performs transaction side-effects from the commitment of a cross-net message. Like +// * burning funds when bottom-up messages are propagated. +// * +// * @param v - the value of the committed cross-net message +// * @param shouldBurn - flag if the message should burn funds +// */ +// function crossMsgSideEffects(uint256 v, bool shouldBurn) internal { +// if (shouldBurn) { +// payable(BURNT_FUNDS_ACTOR).sendValue(v); +// } +// } + +// /// @notice Checks the length of a message batch, ensuring it is in (0, maxMsgsPerBottomUpBatch). +// /// @param msgs The batch of messages to check. +// function checkMsgLength(IpcEnvelope[] calldata msgs) internal view { +// GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); + +// if (msgs.length > s.maxMsgsPerBottomUpBatch) { +// revert MaxMsgsPerBatchExceeded(); +// } +// } +// } diff --git a/contracts/src/lib/LibUtil.sol b/contracts/src/lib/LibUtil.sol new file mode 100644 index 000000000..e9ecbc1aa --- /dev/null +++ b/contracts/src/lib/LibUtil.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {NotSystemActor} from "../errors/IPCErrors.sol"; +import {AccountHelper} from "../lib/AccountHelper.sol"; + +/// @notice A collection of util functions. +library LibUtil { + using AccountHelper for address; + + function enforceSystemActorOnly() internal view { + if (!msg.sender.isSystemActor()) { + revert NotSystemActor(); + } + } + + /// @notice Deduce the next expected bottom up checkpoint epoch given the target block number and checkpoint period + /// @param blockNumber - the given block number + /// @param checkPeriod - the checkpoint period + function nextBottomUpCheckpointEpoch(uint256 blockNumber, uint256 checkPeriod) internal pure returns (uint256) { + return ((uint64(blockNumber) / checkPeriod) + 1) * checkPeriod; + } +} \ No newline at end of file diff --git a/contracts/src/lib/power/LibMaxPQ.sol b/contracts/src/lib/power/LibMaxPQ.sol new file mode 100644 index 000000000..1aa02d199 --- /dev/null +++ b/contracts/src/lib/power/LibMaxPQ.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {ProofOfPower, LibPowerQuery} from "./LibPower.sol"; +import {PQ, LibPQ} from "./LibPQ.sol"; + +struct MaxPQ { + PQ inner; +} + +/// The max index priority queue for staking. The same implementation as LibMinPQ, just order compare +/// is reversed. +library LibMaxPQ { + using LibPQ for PQ; + using LibPowerQuery for ProofOfPower; + + function getSize(MaxPQ storage self) internal view returns (uint16) { + return self.inner.size; + } + + function getAddress(MaxPQ storage self, uint16 i) internal view returns (address) { + return self.inner.posToAddress[i]; + } + + function contains(MaxPQ storage self, address validator) internal view returns (bool) { + return self.inner.contains(validator); + } + + /// @notice Insert the validator address into this PQ. + /// NOTE that caller should ensure the valdiator is not already in the queue. + function insert(MaxPQ storage self, ProofOfPower storage proofOrPower, address validator) internal { + uint16 size = self.inner.size + 1; + + self.inner.addressToPos[validator] = size; + self.inner.posToAddress[size] = validator; + + self.inner.size = size; + + uint256 power = proofOrPower.getConfirmedPower(validator); + swim({self: self, proofOrPower: proofOrPower, pos: size, value: power}); + } + + /// @notice Pop the maximum value in the priority queue. + /// NOTE that caller should ensure the queue is not empty! + function pop(MaxPQ storage self, ProofOfPower storage proofOrPower) internal { + self.inner.requireNotEmpty(); + + uint16 size = self.inner.size; + + self.inner.exchange(1, size); + + self.inner.size = size - 1; + self.inner.del(size); + + uint256 power = self.inner.getPower(proofOrPower, 1); + sink({self: self, proofOrPower: proofOrPower, pos: 1, value: power}); + } + + /// @notice Reheapify the heap when the validator is deleted. + /// NOTE that caller should ensure the queue is not empty. + function deleteReheapify(MaxPQ storage self, ProofOfPower storage proofOrPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint16 size = self.inner.size; + + self.inner.exchange(pos, size); + + // remove the item + self.inner.size = size - 1; + self.inner.del(size); + + if (size == pos) { + return; + } + + // swim pos up in case exchanged index is smaller + uint256 power = self.inner.getPower(proofOrPower, pos); + swim({self: self, proofOrPower: proofOrPower, pos: pos, value: power}); + + // sink pos down in case updated pos is larger + power = self.inner.getPower(proofOrPower, pos); + sink({self: self, proofOrPower: proofOrPower, pos: pos, value: power}); + } + + /// @notice Reheapify the heap when the collateral of a key has increased. + /// NOTE that caller should ensure the queue is not empty. + function increaseReheapify(MaxPQ storage self, ProofOfPower storage proofOrPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint256 power = proofOrPower.getConfirmedPower(validator); + swim({self: self, proofOrPower: proofOrPower, pos: pos, value: power}); + } + + /// @notice Reheapify the heap when the collateral of a key has decreased. + /// NOTE that caller should ensure the queue is not empty. + function decreaseReheapify(MaxPQ storage self, ProofOfPower storage proofOrPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint256 power = proofOrPower.getConfirmedPower(validator); + sink({self: self, proofOrPower: proofOrPower, pos: pos, value: power}); + } + + /// @notice Get the maximum value in the priority queue. + /// NOTE that caller should ensure the queue is not empty! + function max(MaxPQ storage self, ProofOfPower storage proofOrPower) internal view returns (address, uint256) { + self.inner.requireNotEmpty(); + + address addr = self.inner.posToAddress[1]; + uint256 power = proofOrPower.getConfirmedPower(addr); + return (addr, power); + } + + /*************************************************************************** + * Heap internal helper functions, should not be called by external functions + ****************************************************************************/ + function swim(MaxPQ storage self, ProofOfPower storage proofOrPower, uint16 pos, uint256 value) internal { + uint16 parentPos; + uint256 parentPower; + + while (pos > 1) { + parentPos = pos >> 1; // parentPos = pos / 2 + parentPower = self.inner.getPower(proofOrPower, parentPos); + + // Parent power is not smaller than that of the current child, and the heap condition met. + if (!firstValueSmaller(parentPower, value)) { + break; + } + + self.inner.exchange(parentPos, pos); + pos = parentPos; + } + } + + function sink(MaxPQ storage self, ProofOfPower storage proofOrPower, uint16 pos, uint256 value) internal { + uint16 childPos = pos << 1; // childPos = pos * 2 + uint256 childPower; + + uint16 size = self.inner.size; + + while (childPos <= size) { + if (childPos < size) { + // select the max of the two children + (childPos, childPower) = largerPosition({ + self: self, + proofOrPower: proofOrPower, + pos1: childPos, + pos2: childPos + 1 + }); + } else { + childPower = self.inner.getPower(proofOrPower, childPos); + } + + // parent, current idx, is not more than its two children, min heap condition is met. + if (!firstValueSmaller(value, childPower)) { + break; + } + + self.inner.exchange(childPos, pos); + pos = childPos; + childPos = pos << 1; + } + } + + /// @notice Get the larger index of pos1 and pos2. + function largerPosition( + MaxPQ storage self, + ProofOfPower storage proofOrPower, + uint16 pos1, + uint16 pos2 + ) internal view returns (uint16, uint256) { + uint256 power1 = self.inner.getPower(proofOrPower, pos1); + uint256 power2 = self.inner.getPower(proofOrPower, pos2); + + if (firstValueSmaller(power1, power2)) { + return (pos2, power2); + } + return (pos1, power1); + } + + function firstValueSmaller(uint256 v1, uint256 v2) internal pure returns (bool) { + return v1 < v2; + } +} \ No newline at end of file diff --git a/contracts/src/lib/power/LibMinPQ.sol b/contracts/src/lib/power/LibMinPQ.sol new file mode 100644 index 000000000..35426ec2c --- /dev/null +++ b/contracts/src/lib/power/LibMinPQ.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {ProofOfPower, LibPowerQuery} from "./LibPower.sol"; +import {PQ, LibPQ} from "./LibPQ.sol"; + +struct MinPQ { + PQ inner; +} + +/// The min index priority queue for staking +library LibMinPQ { + using LibPQ for PQ; + using LibPowerQuery for ProofOfPower; + + function getSize(MinPQ storage self) internal view returns (uint16) { + return self.inner.size; + } + + function getAddress(MinPQ storage self, uint16 i) internal view returns (address) { + return self.inner.posToAddress[i]; + } + + function contains(MinPQ storage self, address validator) internal view returns (bool) { + return self.inner.contains(validator); + } + + /// @notice Insert the validator address into this PQ. + /// NOTE that caller should ensure the validator is not already in the queue. + function insert(MinPQ storage self, ProofOfPower storage proofOfPower, address validator) internal { + uint16 size = self.inner.size + 1; + + self.inner.addressToPos[validator] = size; + self.inner.posToAddress[size] = validator; + + self.inner.size = size; + + uint256 power = proofOfPower.getConfirmedPower(validator); + swim({self: self, proofOfPower: proofOfPower, pos: size, value: power}); + } + + /// @notice Pop the minimal value in the priority queue. + function pop(MinPQ storage self, ProofOfPower storage proofOfPower) internal { + self.inner.requireNotEmpty(); + + uint16 size = self.inner.size; + + self.inner.exchange(1, size); + + self.inner.size = size - 1; + self.inner.del(size); + + uint256 power = self.inner.getPower(proofOfPower, 1); + sink({self: self, proofOfPower: proofOfPower, pos: 1, value: power}); + } + + /// @notice Reheapify the heap when the validator is deleted. + function deleteReheapify(MinPQ storage self, ProofOfPower storage proofOfPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint16 size = self.inner.size; + + self.inner.exchange(pos, size); + + // remove the item + self.inner.size = size - 1; + self.inner.del(size); + + if (size == pos) { + return; + } + + // swim pos up in case exchanged index is smaller + uint256 val = self.inner.getPower(proofOfPower, pos); + swim({self: self, proofOfPower: proofOfPower, pos: pos, value: val}); + + // sink pos down in case updated pos is larger + val = self.inner.getPower(proofOfPower, pos); + sink({self: self, proofOfPower: proofOfPower, pos: pos, value: val}); + } + + /// @notice Reheapify the heap when the collateral of a key has increased. + function increaseReheapify(MinPQ storage self, ProofOfPower storage proofOfPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint256 val = proofOfPower.getConfirmedPower(validator); + sink({self: self, proofOfPower: proofOfPower, pos: pos, value: val}); + } + + /// @notice Reheapify the heap when the collateral of a key has decreased. + function decreaseReheapify(MinPQ storage self, ProofOfPower storage proofOfPower, address validator) internal { + uint16 pos = self.inner.getPosOrRevert(validator); + uint256 val = proofOfPower.getConfirmedPower(validator); + swim({self: self, proofOfPower: proofOfPower, pos: pos, value: val}); + } + + /// @notice Get the minimal value in the priority queue. + /// NOTE that caller should ensure the queue is not empty! + function min(MinPQ storage self, ProofOfPower storage proofOfPower) internal view returns (address, uint256) { + self.inner.requireNotEmpty(); + + address addr = self.inner.posToAddress[1]; + uint256 power = proofOfPower.getConfirmedPower(addr); + return (addr, power); + } + + /*************************************************************************** + * Heap internal helper functions, should not be called by external functions + ****************************************************************************/ + function swim(MinPQ storage self, ProofOfPower storage proofOfPower, uint16 pos, uint256 value) internal { + uint16 parentPos; + uint256 parentPower; + + while (pos > 1) { + // parentPos = pos / 2; + parentPos = pos >> 1; + parentPower = self.inner.getPower(proofOfPower, parentPos); + + // parent power is not more than that of the current child, heap condition met. + if (!firstValueLarger(parentPower, value)) { + break; + } + + self.inner.exchange(parentPos, pos); + pos = parentPos; + } + } + + function sink(MinPQ storage self, ProofOfPower storage proofOfPower, uint16 pos, uint256 value) internal { + uint16 childPos = pos * 2; + uint256 childPower; + + uint16 size = self.inner.size; + + while (childPos <= size) { + if (childPos < size) { + // select the min of the two children + (childPos, childPower) = smallerPosition({ + self: self, + proofOfPower: proofOfPower, + pos1: childPos, + pos2: childPos + 1 + }); + } else { + childPower = self.inner.getPower(proofOfPower, childPos); + } + + // parent, current idx, is not more than its two children, min heap condition is met. + if (!firstValueLarger(value, childPower)) { + break; + } + + self.inner.exchange(childPos, pos); + pos = childPos; + childPos = pos * 2; + } + } + + /// @notice Get the smaller index of pos1 and pos2. + function smallerPosition( + MinPQ storage self, + ProofOfPower storage proofOfPower, + uint16 pos1, + uint16 pos2 + ) internal view returns (uint16, uint256) { + uint256 value1 = self.inner.getPower(proofOfPower, pos1); + uint256 value2 = self.inner.getPower(proofOfPower, pos2); + + if (!firstValueLarger(value1, value2)) { + return (pos1, value1); + } + return (pos2, value2); + } + + function firstValueLarger(uint256 v1, uint256 v2) internal pure returns (bool) { + return v1 > v2; + } +} \ No newline at end of file diff --git a/contracts/src/lib/power/LibPQ.sol b/contracts/src/lib/power/LibPQ.sol new file mode 100644 index 000000000..669a23e42 --- /dev/null +++ b/contracts/src/lib/power/LibPQ.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {ProofOfPower, LibPowerQuery} from "./LibPower.sol"; +import {PQEmpty, PQDoesNotContainAddress} from "../../errors/IPCErrors.sol"; + +/// The implementation that mimics the Java impl in https://algs4.cs.princeton.edu/24pq/IndexMinPQ.java.html. + +/// The inner data structure for both min and max priority queue +struct PQ { + /// The size of the priority queue + uint16 size; + /// @notice The mapping from validator address to the position on the priority queue. Position is 1-based indexing. + mapping(address => uint16) addressToPos; + /// @notice The mapping from the position on the priority queue to validator address. + mapping(uint16 => address) posToAddress; +} + +library LibPQ { + using LibPowerQuery for ProofOfPower; + + function isEmpty(PQ storage self) internal view returns (bool) { + return self.size == 0; + } + + function requireNotEmpty(PQ storage self) internal view { + if (self.size == 0) { + revert PQEmpty(); + } + } + + function getSize(PQ storage self) internal view returns (uint16) { + return self.size; + } + + function contains(PQ storage self, address validator) internal view returns (bool) { + return self.addressToPos[validator] != 0; + } + + function getPosOrRevert(PQ storage self, address validator) internal view returns (uint16 pos) { + pos = self.addressToPos[validator]; + if (pos == 0) { + revert PQDoesNotContainAddress(); + } + } + + function del(PQ storage self, uint16 pos) internal { + address addr = self.posToAddress[pos]; + delete self.posToAddress[pos]; + delete self.addressToPos[addr]; + } + + function getPower( + PQ storage self, + ProofOfPower storage proofOfPower, + uint16 pos + ) internal view returns (uint256) { + address addr = self.posToAddress[pos]; + return proofOfPower.getConfirmedPower(addr); + } + + function exchange(PQ storage self, uint16 pos1, uint16 pos2) internal { + assert(pos1 <= self.size); + assert(pos2 <= self.size); + + address addr1 = self.posToAddress[pos1]; + address addr2 = self.posToAddress[pos2]; + + self.addressToPos[addr1] = pos2; + self.addressToPos[addr2] = pos1; + + self.posToAddress[pos2] = addr1; + self.posToAddress[pos1] = addr2; + } +} \ No newline at end of file diff --git a/contracts/src/lib/power/LibPower.sol b/contracts/src/lib/power/LibPower.sol new file mode 100644 index 000000000..e9feca054 --- /dev/null +++ b/contracts/src/lib/power/LibPower.sol @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {MinPQ, LibMinPQ} from "./LibMinPQ.sol"; +import {MaxPQ, LibMaxPQ} from "./LibMaxPQ.sol"; +import {NotValidator, InvalidConfigurationNumber, NotOwnerOfPublicKey, WithdrawExceedingCollateral, AddressShouldBeValidator, CannotConfirmFutureChanges} from "../../errors/IPCErrors.sol"; +import {VALIDATOR_SECP256K1_PUBLIC_KEY_LENGTH} from "../../constants/Constants.sol"; + +/// @notice Subnet power change operations. +enum PowerOperation { + NewPower, + SetMetadata +} + +/// @notice The change request to validator staking. +struct PowerChange { + PowerOperation op; + bytes payload; + address validator; +} + +/// @notice The change associated with its corresponding configuration number. +struct PowerChangeRequest { + PowerChange change; + uint64 configurationNumber; +} + +/// @notice The collection of staking changes. +struct PowerChangeLog { + /// @notice The next configuration number to assign to new changes. + uint64 nextConfigurationNumber; + /// @notice The starting configuration number stored. + uint64 startConfigurationNumber; + /// The details of the changes, mapping of configuration number to changes. + mapping(uint64 => PowerChange) changes; +} + +struct Validator { + uint256 confirmedPower; + uint256 unconfirmedPower; + /// The metadata associated with the validator, i.e. off-chain network address. + /// This information is not important to the protocol, off-chain should know how + /// to parse or decode the bytes. + bytes metadata; +} + +/// @notice Proof of power is a generalisation of POS, which is using a generic power to rank the validators +/// @notice Keeping track of the list of validators. +/// @dev There are two types of validators: +/// - Active +/// - Waiting +/// Active validators are those that are producing blocks in the child subnet. +/// Waiting validators are those that do no have as high powers as Active validators. +/// +/// The max number of active validators is limited by `activeLimit` and the size of waiting +/// validators is not bounded. +/// +/// With each validator staking change, waiting validators can be promoted to active validators +/// and active validators can be knocked off. +struct ProofOfPower { + /// The total number of active validators allowed. + uint16 activeLimit; + /// The total power confirmed. + uint256 totalConfirmedPower; + /// The mapping of each validator address to its information. + mapping(address => Validator) validators; + /// @notice The active validators tracked using min priority queue. + MinPQ activeValidators; + /// @notice The waiting validators tracked using max priority queue. + MaxPQ waitingValidators; + + /// @notice Contains the list of changes to validator set. Configuration number is associated at each change. + PowerChangeLog changeSet; +} + +/// The util library for `PowerChangeLog` +library LibPowerChangeLog { + event NewPowerChangeRequest(PowerOperation op, address validator, bytes payload, uint64 configurationNumber); + + /// @notice Validator request to update its metadata + function metadataRequest(PowerChangeLog storage changes, address validator, bytes calldata metadata) internal { + uint64 configurationNumber = recordChange({ + changes: changes, + validator: validator, + op: PowerOperation.SetMetadata, + payload: metadata + }); + + emit NewPowerChangeRequest({ + op: PowerOperation.SetMetadata, + validator: validator, + payload: metadata, + configurationNumber: configurationNumber + }); + } + + /// @notice Updates the power of the validator + function setPowerRequest(PowerChangeLog storage changes, address validator, uint256 power) internal { + bytes memory payload = abi.encode(power); + + uint64 configurationNumber = recordChange({ + changes: changes, + validator: validator, + op: PowerOperation.NewPower, + payload: payload + }); + + emit NewPowerChangeRequest({ + op: PowerOperation.NewPower, + validator: validator, + payload: payload, + configurationNumber: configurationNumber + }); + } + + /// @notice Perform upsert operation to the deposit changes + function recordChange( + PowerChangeLog storage changes, + address validator, + PowerOperation op, + bytes memory payload + ) internal returns (uint64 configurationNumber) { + configurationNumber = changes.nextConfigurationNumber; + + changes.changes[configurationNumber] = PowerChange({op: op, validator: validator, payload: payload}); + + changes.nextConfigurationNumber = configurationNumber + 1; + } + + /// @notice Get the change at configuration number + function getChange( + PowerChangeLog storage changes, + uint64 configurationNumber + ) internal view returns (PowerChange storage) { + return changes.changes[configurationNumber]; + } + + function purgeChange(PowerChangeLog storage changes, uint64 configurationNumber) internal { + delete changes.changes[configurationNumber]; + } +} + +library LibPowerQuery { + using LibMinPQ for MinPQ; + using LibMaxPQ for MaxPQ; + + // =============== Getters ============= + function getConfirmedPower( + ProofOfPower storage self, + address validator + ) internal view returns(uint256) { + return self.validators[validator].confirmedPower; + } + + function getUnconfirmedPower( + ProofOfPower storage self, + address validator + ) internal view returns(uint256) { + return self.validators[validator].unconfirmedPower; + } + + /// @notice Checks if the validator is an active validator + function isActiveValidator(ProofOfPower storage self, address validator) internal view returns (bool) { + return self.activeValidators.contains(validator); + } + + /// @notice Checks if the validator is a waiting validator + function isWaitingValidator(ProofOfPower storage self, address validator) internal view returns (bool) { + return self.waitingValidators.contains(validator); + } + + /// @notice Checks if the validator has power. + /// @param validator The address to check for power. + /// @return A boolean indicating whether the validator has power. + function hasPower(ProofOfPower storage self, address validator) internal view returns (bool) { + return self.validators[validator].unconfirmedPower != 0; + } + + function listActiveValidators(ProofOfPower storage self) internal view returns (address[] memory addresses) { + uint16 size = self.activeValidators.getSize(); + addresses = new address[](size); + for (uint16 i = 1; i <= size; ) { + addresses[i - 1] = self.activeValidators.getAddress(i); + unchecked { + ++i; + } + } + return addresses; + } + + /// @notice Get the total power of *active* validators. + function confirmedPowerOfAllActiveValidators(ProofOfPower storage self) internal view returns (uint256 power) { + uint16 size = self.activeValidators.getSize(); + for (uint16 i = 1; i <= size; ) { + address validator = self.activeValidators.getAddress(i); + power += getConfirmedPower(self, validator); + unchecked { + ++i; + } + } + } + + /// @notice Get the total power of *active* validators. + function confirmedPowerOfAllActiveValidators() internal view returns (uint256 power) { + ProofOfPower storage s = LibPowerChangeStorage.diamondStorage(); + return confirmedPowerOfAllActiveValidators(s); + } + + /// @notice Get the total confirmed power of the active validators. + /// The function reverts if at least one validator is not in the active validator set. + function confirmedPowerOfActiveValidators( + address[] memory addresses + ) internal view returns (uint256[] memory) { + ProofOfPower storage s = LibPowerChangeStorage.diamondStorage(); + return confirmedPowerOfActiveValidators(s, addresses); + } + + /// @notice Get the total confirmed power of the active validators. + /// The function reverts if at least one validator is not in the active validator set. + function confirmedPowerOfActiveValidators( + ProofOfPower storage self, + address[] memory addresses + ) internal view returns (uint256[] memory) { + uint256 size = addresses.length; + uint256[] memory activePowerTable = new uint256[](size); + + for (uint256 i; i < size; ) { + if (!isActiveValidator(self, addresses[i])) { + revert NotValidator(addresses[i]); + } + activePowerTable[i] = getConfirmedPower(self, addresses[i]); + unchecked { + ++i; + } + } + return activePowerTable; + } + + function totalActiveValidators(ProofOfPower storage self) internal view returns (uint16) { + return self.activeValidators.getSize(); + } + + /// @notice Gets the total number of validators, including active and waiting + function totalValidators(ProofOfPower storage self) internal view returns (uint16) { + return self.waitingValidators.getSize() + self.activeValidators.getSize(); + } + + function getTotalConfirmedPower(ProofOfPower storage self) internal view returns (uint256) { + return self.totalConfirmedPower; + } + + function getConfigurationNumbers(ProofOfPower storage self) internal view returns(uint64, uint64) { + return (self.changeSet.nextConfigurationNumber, self.changeSet.startConfigurationNumber); + } +} + +/// @notice Handles the proof of power with child subnet. +/// @dev This is a contract instead of a library so that hooks can be added for downstream use cases. +library LibPowerChange { + using LibPowerChangeLog for PowerChangeLog; + using LibMaxPQ for MaxPQ; + using LibMinPQ for MinPQ; + + event ActiveValidatorPowerUpdated(address validator, uint256 newPower); + event WaitingValidatorPowerUpdated(address validator, uint256 newPower); + event NewActiveValidator(address validator, uint256 power); + event NewWaitingValidator(address validator, uint256 power); + event ActiveValidatorReplaced(address oldValidator, address newValidator); + event ActiveValidatorLeft(address validator); + event WaitingValidatorLeft(address validator); + event ConfigurationNumberConfirmed(uint64 number); + + uint64 internal constant INITIAL_CONFIGURATION_NUMBER = 1; + + /// @notice Set the metadata of a validator + function setValidatorMetadata(ProofOfPower storage self, address validator, bytes calldata metadata) internal { + self.changeSet.metadataRequest(validator, metadata); + } + + /// @notice Increase the power of the validator + function setNewPower(ProofOfPower storage self, address validator, uint256 power) internal { + self.validators[validator].unconfirmedPower = power; + self.changeSet.setPowerRequest(validator, power); + } + + /// @notice Confirm the changes in bottom up checkpoint submission, only call this in bottom up checkpoint execution. + /// TODO: take this to an external facing library + function confirmChange(uint64 configurationNumber) internal { + ProofOfPower storage s = LibPowerChangeStorage.diamondStorage(); + confirmChange(s, configurationNumber); + } + + /// @notice Confirm the changes in bottom up checkpoint submission, only call this in bottom up checkpoint execution. + function confirmChange(ProofOfPower storage self, uint64 configurationNumber) internal { + PowerChangeLog storage changeSet = self.changeSet; + + if (configurationNumber >= changeSet.nextConfigurationNumber) { + revert CannotConfirmFutureChanges(); + } else if (configurationNumber < changeSet.startConfigurationNumber) { + return; + } + + uint64 start = changeSet.startConfigurationNumber; + for (uint64 i = start; i <= configurationNumber; ) { + PowerChange storage change = changeSet.getChange(i); + address validator = change.validator; + + if (change.op == PowerOperation.SetMetadata) { + confirmMetadata(self, validator, change.payload); + } else { + uint256 newPower = abi.decode(change.payload, (uint256)); + confirmNewPower(self, validator, newPower); + } + + changeSet.purgeChange(i); + unchecked { + ++i; + } + } + + changeSet.startConfigurationNumber = configurationNumber + 1; + + emit ConfigurationNumberConfirmed(configurationNumber); + } + + /// @notice Confirm the metadata of a validator + function confirmMetadata(ProofOfPower storage self, address validator, bytes memory metadata) internal { + self.validators[validator].metadata = metadata; + } + + function confirmNewPower(ProofOfPower storage self, address validator, uint256 newPower) internal { + uint256 oldPower = self.validators[validator].confirmedPower; + + if (oldPower == newPower) { + return; + } + + self.validators[validator].confirmedPower = newPower; + self.totalConfirmedPower = self.totalConfirmedPower - oldPower + newPower; + + if (newPower > oldPower) { + increaseReshuffle({self: self, maybeActive: validator, newPower: newPower}); + } else { + reduceReshuffle({self: self, validator: validator, newPower: newPower}); + } + } + + function validatePublicKeys( + address[] calldata validators, + bytes[] calldata publicKeys + ) internal pure { + uint256 length = validators.length; + for (uint256 i; i < length; ) { + // check addresses + address convertedAddress = publicKeyToAddress(publicKeys[i]); + if (convertedAddress != validators[i]) { + revert NotOwnerOfPublicKey(); + } + + unchecked { + ++i; + } + } + } + + /// @notice Converts a 65-byte public key to its corresponding address. + /// @param publicKey The 65-byte public key to be converted. + /// @return The address derived from the given public key. + function publicKeyToAddress(bytes calldata publicKey) internal pure returns (address) { + assert(publicKey.length == VALIDATOR_SECP256K1_PUBLIC_KEY_LENGTH); + bytes32 hashed = keccak256(publicKey[1:]); + return address(uint160(uint256(hashed))); + } + + // ================ DO NOT CALL THESE METHODS OUTSIDE OF THIS LIB =================== + + /// @notice Reshuffles the active and waiting validators when an increase in power is confirmed + function increaseReshuffle(ProofOfPower storage self, address maybeActive, uint256 newPower) internal { + if (self.activeValidators.contains(maybeActive)) { + self.activeValidators.increaseReheapify(self, maybeActive); + emit ActiveValidatorPowerUpdated(maybeActive, newPower); + return; + } + + // incoming address is not active validator + uint16 activeLimit = self.activeLimit; + uint16 activeSize = self.activeValidators.getSize(); + if (activeLimit > activeSize) { + // we can still take more active validators, just insert to the pq. + self.activeValidators.insert(self, maybeActive); + emit NewActiveValidator(maybeActive, newPower); + return; + } + + // now we have enough active validators, we need to check: + // - if the incoming new collateral is more than the min active collateral, + // - yes: + // - pop the min active validator + // - remove the incoming validator from waiting validators + // - insert incoming validator into active validators + // - insert popped validator into waiting validators + // - no: + // - insert the incoming validator into waiting validators + (address minAddress, uint256 minActivePower) = self.activeValidators.min(self); + if (minActivePower < newPower) { + self.activeValidators.pop(self); + + if (self.waitingValidators.contains(maybeActive)) { + self.waitingValidators.deleteReheapify(self, maybeActive); + } + + self.activeValidators.insert(self, maybeActive); + self.waitingValidators.insert(self, minAddress); + + emit ActiveValidatorReplaced(minAddress, maybeActive); + return; + } + + if (self.waitingValidators.contains(maybeActive)) { + self.waitingValidators.increaseReheapify(self, maybeActive); + emit WaitingValidatorPowerUpdated(maybeActive, newPower); + return; + } + + self.waitingValidators.insert(self, maybeActive); + emit NewWaitingValidator(maybeActive, newPower); + } + + /// @notice Reshuffles the active and waiting validators when a power reduction is confirmed + function reduceReshuffle(ProofOfPower storage self, address validator, uint256 newPower) internal { + if (self.waitingValidators.contains(validator)) { + if (newPower == 0) { + self.waitingValidators.deleteReheapify(self, validator); + emit WaitingValidatorLeft(validator); + return; + } + self.waitingValidators.decreaseReheapify(self, validator); + emit WaitingValidatorPowerUpdated(validator, newPower); + return; + } + + // sanity check + if (!self.activeValidators.contains(validator)) { + revert AddressShouldBeValidator(); + } + + // the validator is an active validator! + + if (newPower == 0) { + self.activeValidators.deleteReheapify(self, validator); + emit ActiveValidatorLeft(validator); + + if (self.waitingValidators.getSize() != 0) { + (address toBePromoted, uint256 power) = self.waitingValidators.max(self); + self.waitingValidators.pop(self); + self.activeValidators.insert(self, toBePromoted); + emit NewActiveValidator(toBePromoted, power); + } + + return; + } + + self.activeValidators.decreaseReheapify(self, validator); + + if (self.waitingValidators.getSize() == 0) { + return; + } + + (address mayBeDemoted, uint256 minActivePower) = self.activeValidators.min(self); + (address mayBePromoted, uint256 maxWaitingPower) = self.waitingValidators.max(self); + if (minActivePower < maxWaitingPower) { + self.activeValidators.pop(self); + self.waitingValidators.pop(self); + self.activeValidators.insert(self, mayBePromoted); + self.waitingValidators.insert(self, mayBeDemoted); + + emit ActiveValidatorReplaced(mayBeDemoted, mayBePromoted); + return; + } + + emit ActiveValidatorPowerUpdated(validator, newPower); + } +} + +/// The library for tracking validator power changes coming from the parent. +/// Should be used in the child gateway to store changes until they can be applied. +library LibPowerTracking { + using LibPowerChangeLog for PowerChangeLog; + + function storeChange(ProofOfPower storage s, PowerChangeRequest calldata changeRequest) internal { + uint64 configurationNumber = s.changeSet.recordChange({ + validator: changeRequest.change.validator, + op: changeRequest.change.op, + payload: changeRequest.change.payload + }); + + if (configurationNumber != changeRequest.configurationNumber) { + revert InvalidConfigurationNumber(); + } + } + + function batchStoreChange( + ProofOfPower storage s, + PowerChangeRequest[] calldata changeRequests + ) internal { + uint256 length = changeRequests.length; + if (length == 0) { + return; + } + + for (uint256 i; i < length; ) { + storeChange(s, changeRequests[i]); + unchecked { + ++i; + } + } + } + + /// @notice Confirm the changes in for a finality commitment + function confirmChange(ProofOfPower storage s, uint64 configurationNumber) internal { + LibPowerChange.confirmChange(s, configurationNumber); + } +} + +library LibPowerChangeStorage { + function diamondStorage() internal pure returns (ProofOfPower storage ds) { + bytes32 position = keccak256("ipc.lib.power.change.storage"); + assembly { + ds.slot := position + } + } +} + +library LibPowerTrackingStorage { + function diamondStorage() internal pure returns (ProofOfPower storage ds) { + bytes32 position = keccak256("ipc.lib.power.tracking.storage"); + assembly { + ds.slot := position + } + } +} \ No newline at end of file diff --git a/contracts/src/structs/Subnet.sol b/contracts/src/structs/Subnet.sol index bf7776097..dd727d554 100644 --- a/contracts/src/structs/Subnet.sol +++ b/contracts/src/structs/Subnet.sol @@ -177,3 +177,13 @@ enum SupplyKind { Native, ERC20 } + +// ============= Redesign structs ========== + +/// @notice Determines the how validators obtain their power. +enum PowerAllocationMode { + /// Validator power is determined by the collateral staked + Collateral, + /// Validator power is assigned by the owner of the subnet + Federated +} \ No newline at end of file diff --git a/contracts/src/subnet/FederatedSubnetFacet.sol b/contracts/src/subnet/FederatedSubnetFacet.sol new file mode 100644 index 000000000..64a6b6706 --- /dev/null +++ b/contracts/src/subnet/FederatedSubnetFacet.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {InvalidXnetMessage, InvalidXnetMessageReason, DuplicatedGenesisValidator, WrongSubnet, InvalidFederationPayload, NotEnoughGenesisValidators} from "../errors/IPCErrors.sol"; +import {LibPowerChange, LibPowerChangeStorage, ProofOfPower, LibPowerQuery} from "../lib/power/LibPower.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {Pausable} from "../lib/LibPausable.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {ISubnet} from "../interfaces/ISubnet.sol"; +import {LibSubnetActor} from "./SubnetActorFacet.sol"; +import {IGenesisComponent} from "../interfaces/IGenesis.sol"; + +library LibFederatedPower { + // The federated power storage + struct FederatedPowerStorage { + uint64 minValidators; + /// @notice If the federated power mode is bootstrapped + bool bootstrapped; + } + + function diamondStorage() internal pure returns (FederatedPowerStorage storage ds) { + bytes32 position = keccak256("ipc.subnet.federated.storage"); + assembly { + ds.slot := position + } + } +} + +contract FederatedSubnetFacet is IGenesisComponent, ReentrancyGuard, Pausable { + using LibPowerQuery for ProofOfPower; + using LibPowerChange for ProofOfPower; + + event FederatedPowerBootstrapped(); + + // ============== Genesis related ============= + /// @notice Returns the id of the component + function id() external view returns(bytes4) { + return bytes4(keccak256("federated-power")); + } + + /// @notice Returns the actual bytes of the genesis + function genesis() external view returns(bytes memory) { + require(false, "todo"); + } + + /// @notice Checks if the component is bootstrapped + function bootstrapped() external view returns(bool) { + return LibFederatedPower.diamondStorage().bootstrapped; + } + + // ============== Federated power related =========== + function setPower( + address[] calldata validators, + bytes[] calldata publicKeys, + uint256[] calldata powers + ) external { + if (validators.length != powers.length) { + revert InvalidFederationPayload(); + } + + if (validators.length != publicKeys.length) { + revert InvalidFederationPayload(); + } + + LibPowerChange.validatePublicKeys(validators, publicKeys); + + // only subnet owner is allowed to set powers + LibDiamond.enforceIsContractOwner(); + + LibFederatedPower.FederatedPowerStorage storage fps = LibFederatedPower.diamondStorage(); + if (!fps.bootstrapped) { + preBootstrap(validators, publicKeys, powers); + } else { + postBootstrap(validators, publicKeys, powers); + } + } + + // ===== Getters ===== + function confimedPower(address addr) external view returns(uint256) { + return LibPowerChangeStorage.diamondStorage().getConfirmedPower(addr); + } + + function unconfirmedPower(address addr) external view returns(uint256) { + return LibPowerChangeStorage.diamondStorage().getUnconfirmedPower(addr); + } + + // ======= Internal functions ====== + function preBootstrap( + address[] calldata validators, + bytes[] calldata publicKeys, + uint256[] calldata powers + ) internal { + uint256 length = validators.length; + + LibFederatedPower.FederatedPowerStorage storage fps = LibFederatedPower.diamondStorage(); + ProofOfPower storage proofS = LibPowerChangeStorage.diamondStorage(); + + if (length <= fps.minValidators) { + revert NotEnoughGenesisValidators(); + } + + for (uint256 i; i < length; ) { + // performing deduplication + // validator should have no power when first added + if (proofS.getConfirmedPower(validators[i]) > 0) { + revert DuplicatedGenesisValidator(); + } + + proofS.confirmMetadata(validators[i], publicKeys[i]); + proofS.confirmNewPower(validators[i], powers[i]); + + // s.genesisValidators.push(Validator({addr: validators[i], weight: powers[i], metadata: publicKeys[i]})); + + unchecked { + ++i; + } + } + + fps.bootstrapped = true; + // emit FederatedPowerBootstrapped(s.genesisValidators); + + // TODO: register with the gateway + } + + function postBootstrap( + address[] calldata validators, + bytes[] calldata publicKeys, + uint256[] calldata powers + ) internal { + uint256 length = validators.length; + ProofOfPower storage proofS = LibPowerChangeStorage.diamondStorage(); + + for (uint256 i; i < length; ) { + proofS.setValidatorMetadata(validators[i], publicKeys[i]); + proofS.setNewPower(validators[i], powers[i]); + + unchecked { + ++i; + } + } + } +} diff --git a/contracts/src/subnet/SubnetActorFacet.sol b/contracts/src/subnet/SubnetActorFacet.sol new file mode 100644 index 000000000..09494db74 --- /dev/null +++ b/contracts/src/subnet/SubnetActorFacet.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +/** + * Subnet actor facet that tracks the general state of the subnet. It's required for any subnet actor. + */ + +import {InvalidXnetMessage, InvalidXnetMessageReason, NotGateway, WrongSubnet} from "../errors/IPCErrors.sol"; +// import {IGateway} from "../interfaces/IGateway.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {LibSubnetGenesis, SubnetGenesis} from "../lib/LibGenesis.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {Pausable} from "../lib/LibPausable.sol"; +import {SupplySource, SubnetID, PowerAllocationMode} from "../structs/Subnet.sol"; +import {SupplySourceHelper} from "../lib/SupplySourceHelper.sol"; +import {FvmAddressHelper} from "../lib/FvmAddressHelper.sol"; +import {FvmAddress} from "../structs/FvmAddress.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {IpcEnvelope} from "../structs/CrossNet.sol"; +import {Consensus} from "../enums/ConsensusType.sol"; +import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; +import {ISubnet} from "../interfaces/ISubnet.sol"; + +/// @notice The getters for querying subnet state +library LibSubnetActorQuery { + function ipcGatewayAddr() internal view returns(address) { + return LibSubnetActorStorage.diamondStorage().ipcGatewayAddr; + } + + function id() internal view returns(SubnetID memory) { + return LibSubnetActorStorage.diamondStorage().id; + } + + function supplySource() internal view returns(SupplySource memory) { + return LibSubnetActorStorage.diamondStorage().supplySource; + } + + function powerAllocationMode() external view returns(PowerAllocationMode) { + return LibSubnetActorStorage.diamondStorage().powerAllocMode; + } + + function consensus() external view returns(Consensus) { + return LibSubnetActorStorage.diamondStorage().consensus; + } +} + +contract SubnetActorFacet is ReentrancyGuard, Pausable { + using FvmAddressHelper for FvmAddress; + using LibSubnetGenesis for SubnetGenesis; + using SubnetIDHelper for SubnetID; + + /// @notice The supplying token + function supplySource() external view returns(SupplySource memory) { + return LibSubnetActorQuery.supplySource(); + } + + function powerAllocationMode() external view returns(PowerAllocationMode) { + return LibSubnetActorQuery.powerAllocationMode(); + } + + function consensus() external view returns(Consensus) { + return LibSubnetActorQuery.consensus(); + } + + /// @notice Handles a specific cross network messages from the gateway. + function handleXnetCall(IpcEnvelope calldata envelope) external { + LibSubnetActor.onlyGateway(); + revert("todo"); + } + + /// @notice credits the received value to the specified address in the specified child subnet. + /// + /// @dev There may be an associated fee that gets distributed to validators in the subnet. Currently this fee is zero, + /// i.e. funding a subnet is free. + /// + /// @param to: the address to which to credit funds in the subnet. + /// @param amount: the amount to send + function fund(FvmAddress calldata to, uint256 amount) external payable { + if (amount == 0) { + // prevent spamming if there's no value to fund. + revert InvalidXnetMessage(InvalidXnetMessageReason.Value); + } + + // Locks a specified amount into custody, adjusting for tokens with transfer fees. This operation + // accommodates inflationary tokens, potentially reflecting a higher effective locked amount. + // Operation reverts if the effective transferred amount is zero. + uint256 transferAmount = LibSubnetActor.lockFund(amount); + + SubnetActorStorage storage s = LibSubnetActorStorage.diamondStorage(); + + if (!ISubnet(address(this)).bootstrapped()) { + /// TODO: convert to to evm is actually a hack. `to` is much more general. + s.genesis.deposit(to.extractEvmAddress(), transferAmount); + return; + } + + LibSubnetActor.emitTopDownMsg( + CrossMsgHelper.createFundMsg({ + subnet: s.id, + signer: msg.sender, + to: to, + value: transferAmount + }) + ); + } +} + +/// @notice Metadata handling and fund management for the subnet actor +library LibSubnetActor { + using SupplySourceHelper for SupplySource; + using SubnetIDHelper for SubnetID; + + event NewTopDownMessage(IpcEnvelope message); + + function onlyGateway() internal view { + SubnetActorStorage storage s = LibSubnetActorStorage.diamondStorage(); + if (msg.sender != s.ipcGatewayAddr) { + revert NotGateway(); + } + } + + /// @notice Lock certain amount of fund in the subnet + function lockFund(uint256 amount) internal returns (uint256) { + SubnetActorStorage storage s = LibSubnetActorStorage.diamondStorage(); + return s.supplySource.lock({value: amount}); + } + + /// @notice Obtains the applied bottom up nonce and increment by one. Returns the old value + function getThenIncrAppliedBottomUpNonce() internal returns (uint64 appliedBottomUpNonce) { + SubnetActorStorage storage s = LibSubnetActorStorage.diamondStorage(); + appliedBottomUpNonce = s.appliedBottomUpNonce; + s.appliedBottomUpNonce = appliedBottomUpNonce + 1; + } + + /// @notice Obtains the topdown nonce and increment by one. Returns the old value. + function getThenIncrTopdownNonce() internal returns (uint64 topDownNonce) { + SubnetActorStorage storage s = LibSubnetActorStorage.diamondStorage(); + topDownNonce = s.topDownNonce; + s.topDownNonce = topDownNonce + 1; + } + + /// @notice commit topdown messages for their execution in the subnet. Adds the message to the subnet struct for future execution + /// @param crossMessage - the cross message to be committed + function emitTopDownMsg(IpcEnvelope memory crossMessage) internal { + SubnetID memory self = LibSubnetActorStorage.diamondStorage().id; + + SubnetID memory commonParent = crossMessage.to.subnetId.commonParent(self); + if (!commonParent.equals(self)) { + revert WrongSubnet(); + } + + crossMessage.nonce = getThenIncrTopdownNonce(); + + emit NewTopDownMessage({message: crossMessage}); + } +} + +/// ============== Internal ============== + +// The subnet actor storage +struct SubnetActorStorage { + /// @notice Address of the IPC gateway for the subnet + address ipcGatewayAddr; + /// @notice The topdown nonce + uint64 topDownNonce; + /// @notice The genesis state tracked by this contract + SubnetGenesis genesis; + /// immutable params + Consensus consensus; + /// @notice ID of the self + SubnetID id; + /// @notice The power allocation mode + PowerAllocationMode powerAllocMode; + /// @notice subnet supply strategy. + SupplySource supplySource; + /// @notice ID of the parent subnet + SubnetID parentId; + /// @notice The applied bottom up nonce, i.e. number of bottom up messages executed + uint64 appliedBottomUpNonce; +} + +library LibSubnetActorStorage { + function diamondStorage() internal pure returns (SubnetActorStorage storage ds) { + bytes32 position = keccak256("ipc.subnet.actor.storage"); + assembly { + ds.slot := position + } + } +} \ No newline at end of file diff --git a/contracts/src/subnet/SubnetBootstrapFacet.sol b/contracts/src/subnet/SubnetBootstrapFacet.sol new file mode 100644 index 000000000..ee704c941 --- /dev/null +++ b/contracts/src/subnet/SubnetBootstrapFacet.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetNotBootstrapped, AlreadyInitialized} from "../errors/IPCErrors.sol"; +import {IGenesisComponent} from "../interfaces/IGenesis.sol"; + +struct BootstrapTrackerStorage { + /// @notice Tracks the number of component in the genesis + IGenesisComponent[] components; + /// @notice If the bootstrap tracker is initialized + bool initialized; +} + +/// @notice Tracks the facet bootstrap progress. Once all the facets that need to be bootstrapped are bootstrapped, the subnet +/// is then bootstrapped and `SubnetGenesis` will be finalized. +contract SubnetBootstrapFacet { + function setup(IGenesisComponent[] calldata components) external { + BootstrapTrackerStorage storage s = diamondStorage(); + + if (s.initialized) { + revert AlreadyInitialized(); + } + + uint256 length = components.length; + for (uint256 i = 0; i < length; ) { + s.components.push(components[i]); + } + } + + /// @notice Checks if the subnet is bootstrapped + function bootstrapped() public view returns(bool) { + BootstrapTrackerStorage storage s = diamondStorage(); + + uint256 length = s.components.length; + for (uint256 i = 0; i < length; ) { + IGenesisComponent c = s.components[i]; + + if (!c.bootstrapped()) { + return false; + } + + unchecked { + i++; + } + } + + return true; + } + + function genesis() external view returns(bytes memory) { + if (!bootstrapped()) { + revert SubnetNotBootstrapped(); + } + + // constructs the genesis bytes by scanning all the interfaces that supports `IGenesisComponent` + revert("todo"); + } + + function diamondStorage() internal pure returns (BootstrapTrackerStorage storage ds) { + bytes32 position = keccak256("ipc.subnet.genesis.storage"); + assembly { + ds.slot := position + } + } +} diff --git a/contracts/src/subnet/SubnetCheckpointingFacet.sol b/contracts/src/subnet/SubnetCheckpointingFacet.sol new file mode 100644 index 000000000..bb71a2071 --- /dev/null +++ b/contracts/src/subnet/SubnetCheckpointingFacet.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {MaxMsgsPerBatchExceeded, InvalidSubnet, InvalidXnetMessage, InvalidSignatureErr, InvalidXnetMessageReason, BottomUpCheckpointAlreadySubmitted, CannotSubmitFutureCheckpoint, InvalidCheckpointEpoch} from "../errors/IPCErrors.sol"; +import {CallMsg, IpcMsgKind, IpcEnvelope, OutcomeType, BottomUpMsgBatch, BottomUpCheckpoint} from "../structs/CrossNet.sol"; +import {MultisignatureChecker} from "../lib/LibMultisignatureChecker.sol"; +import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; +import {Pausable} from "../lib/LibPausable.sol"; +import {LibUtil} from "../lib/LibUtil.sol"; +import {LibPowerChange, ProofOfPower, LibPowerQuery} from "../lib/power/LibPower.sol"; +import {LibSubnetActorQuery} from "./SubnetActorFacet.sol"; +import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; +import {SubnetID} from "../structs/CrossNet.sol"; +import {LibBottomUpExecution} from "../lib/LibMsgExecution.sol"; + +struct SubnetCheckpointStorage { + /// @notice The committed bottom up checkpoint hashes + mapping(uint256 => bytes32) pastCheckpointHashes; + /// @notice The previously committed bottom up checkpoint + uint256 lastBottomUpCheckpointHeight; + + // ========= List of configuration params ======= + /// @notice The majority percentage that forms a quorum + uint256 majorityPercentage; + /// @notice The bottom up checkpoint period + uint64 bottomUpCheckPeriod; + /// @notice The max number of bottom up messages a checkpoint can store + uint64 maxMsgsPerBottomUpBatch; +} + +/// @notice This facet interfaces with the child subnet in the parent network. It receives the bottom up checkpoints +/// sent from the child to the parent. +contract SubnetCheckpointingFacet is ReentrancyGuard, Pausable { + using LibSubnetCheckpoint for SubnetCheckpointStorage; + + /// @notice Submits a checkpoint commitment for execution. + /// @dev It triggers the commitment of the checkpoint and any other side-effects that + /// need to be triggered by the checkpoint such as relayer reward book keeping. + /// @param checkpoint The executed bottom-up checkpoint. + /// @param signatories The addresses of validators signing the checkpoint. + /// @param signatures The signatures of validators on the checkpoint. + function submitCheckpoint( + BottomUpCheckpoint calldata checkpoint, + address[] calldata signatories, + bytes[] calldata signatures + ) external whenNotPaused { + SubnetCheckpointStorage storage s = LibSubnetCheckpoint.checkpointStorage(); + + s.ensureValidCheckpoint(checkpoint); + + bytes32 checkpointHash = keccak256(abi.encode(checkpoint)); + + // validate signatures and quorum threshold, revert if validation fails + s.validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures}); + + // If the checkpoint height is the next expected height then this is a new checkpoint which must be executed + // in the Gateway Actor, the checkpoint and the relayer must be stored, last bottom-up checkpoint updated. + s.newCheckpointSubmitted(checkpointHash, checkpoint.blockHeight); + + LibSubnetCheckpoint.execBottomUpMsgs(checkpoint.msgs); + + // confirming the changes in membership in the child + LibPowerChange.confirmChange(checkpoint.nextConfigurationNumber); + } +} + +library LibSubnetCheckpoint { + using SubnetIDHelper for SubnetID; + + function checkpointStorage() internal pure returns (SubnetCheckpointStorage storage ds) { + bytes32 position = keccak256("ipc.subnet.bottomup.checkpoint.storage"); + assembly { + ds.slot := position + } + } + + function newCheckpointSubmitted( + SubnetCheckpointStorage storage s, + bytes32 checkpointHash, + uint256 blockHeight + ) internal { + s.pastCheckpointHashes[blockHeight] = checkpointHash; + s.lastBottomUpCheckpointHeight = blockHeight; + } + + /// @notice Checks whether the signatures are valid for the provided signatories and hash within the current validator set. + /// Reverts otherwise. + /// @dev Signatories in `signatories` and their signatures in `signatures` must be provided in the same order. + /// Having it public allows external users to perform sanity-check verification if needed. + /// @param signatories The addresses of the signatories. + /// @param hash The hash of the checkpoint. + /// @param signatures The packed signatures of the checkpoint. + function validateActiveQuorumSignatures( + SubnetCheckpointStorage storage s, + address[] memory signatories, + bytes32 hash, + bytes[] memory signatures + ) public view { + // This call reverts if at least one of the signatories (validator) is not in the active validator set. + uint256[] memory collaterals = LibPowerQuery.confirmedPowerOfActiveValidators(signatories); + uint256 activeCollateral = LibPowerQuery.confirmedPowerOfAllActiveValidators(); + + uint256 threshold = (activeCollateral * s.majorityPercentage) / 100; + + (bool valid, MultisignatureChecker.Error err) = MultisignatureChecker.isValidWeightedMultiSignature({ + signatories: signatories, + weights: collaterals, + threshold: threshold, + hash: hash, + signatures: signatures + }); + + if (!valid) { + revert InvalidSignatureErr(uint8(err)); + } + } + + /// @notice Ensures the checkpoint is valid. + /// @dev The checkpoint block height must be equal to the last bottom-up checkpoint height or + /// @dev the next one or the number of bottom up messages exceeds the max batch size. + function ensureValidCheckpoint( + SubnetCheckpointStorage storage s, + BottomUpCheckpoint calldata checkpoint + ) internal view { + if (!LibSubnetActorQuery.id().equals(checkpoint.subnetID)) { + revert InvalidSubnet(); + } + + uint64 maxMsgsPerBottomUpBatch = s.maxMsgsPerBottomUpBatch; + + if (checkpoint.msgs.length > maxMsgsPerBottomUpBatch) { + revert MaxMsgsPerBatchExceeded(); + } + + uint256 lastBottomUpCheckpointHeight = s.lastBottomUpCheckpointHeight; + uint256 bottomUpCheckPeriod = s.bottomUpCheckPeriod; + + // cannot submit past bottom up checkpoint + if (checkpoint.blockHeight <= lastBottomUpCheckpointHeight) { + revert BottomUpCheckpointAlreadySubmitted(); + } + + uint256 nextCheckpointHeight = LibUtil.nextBottomUpCheckpointEpoch(lastBottomUpCheckpointHeight, bottomUpCheckPeriod); + + if (checkpoint.blockHeight > nextCheckpointHeight) { + revert CannotSubmitFutureCheckpoint(); + } + + // the expected bottom up checkpoint height, valid height + if (checkpoint.blockHeight == nextCheckpointHeight) { + return; + } + + // if the bottom up messages' length is max, we consider that epoch valid, allow early submission + if (checkpoint.msgs.length == maxMsgsPerBottomUpBatch) { + return; + } + + revert InvalidCheckpointEpoch(); + } + + /// @notice submit a batch of cross-net messages for execution. + /// @param msgs The batch of bottom-up cross-network messages to be executed. + function execBottomUpMsgs(IpcEnvelope[] calldata msgs) internal { + uint256 totalValue; + uint256 crossMsgLength = msgs.length; + + for (uint256 i; i < crossMsgLength; ) { + // TODO: validate it is indeed bottom up messages + + totalValue += msgs[i].value; + unchecked { + ++i; + } + + LibBottomUpExecution.applyMsg(msgs[i]); + } + + // TODO: udpate subnet circulation supply + + // if (subnet.circSupply < totalAmount) { + // revert NotEnoughSubnetCircSupply(); + // } + + // subnet.circSupply -= totalAmount; + } +} \ No newline at end of file diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 0f2dd6683..37c973c7d 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -86,3 +86,17 @@ pub struct BottomUpCheckpoint { /// The list of messages for execution pub msgs: Vec, } + +impl Display for BottomUpCheckpoint { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "BottomUpCheckpoint(subnet_id = {}, height = {}, hash = {}, next_config_number = {}, msgs = {})", + self.subnet_id, + self.block_height, + hex::encode(&self.block_hash), + self.next_configuration_number, + self.msgs.iter().map(|a| a.to_string()).collect::>().join(",") + ) + } +} diff --git a/ipc/api/src/cross.rs b/ipc/api/src/cross.rs index ee6eaccf5..9146e5198 100644 --- a/ipc/api/src/cross.rs +++ b/ipc/api/src/cross.rs @@ -5,10 +5,12 @@ use crate::address::IPCAddress; use crate::subnet_id::SubnetID; use anyhow::anyhow; +use ethers::utils::hex; use fvm_shared::address::Address; use fvm_shared::econ::TokenAmount; use serde::{Deserialize, Serialize}; use serde_tuple::{Deserialize_tuple, Serialize_tuple}; +use std::fmt::{Display, Formatter}; #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct IpcEnvelope { @@ -104,7 +106,7 @@ impl IpcEnvelope { } /// Type of cross-net messages currently supported -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, strum::Display)] #[repr(u8)] pub enum IpcMsgKind { /// for cross-net messages that move native token, i.e. fund/release. @@ -190,6 +192,21 @@ impl IpcEnvelope { } } +impl Display for IpcEnvelope { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "IpcEnvelope(kind = {}, from = {}, to = {}, value = {}, message = {}, nonce = {})", + self.kind, + self.from, + self.to, + self.value, + hex::encode(&self.message), + self.nonce, + ) + } +} + #[cfg(test)] mod tests { use crate::cross::*; diff --git a/ipc/cli/src/commands/checkpoint/bottomup_bundles.rs b/ipc/cli/src/commands/checkpoint/bottomup_bundles.rs index 66fdc4dc6..0f9009a81 100644 --- a/ipc/cli/src/commands/checkpoint/bottomup_bundles.rs +++ b/ipc/cli/src/commands/checkpoint/bottomup_bundles.rs @@ -32,6 +32,7 @@ impl CommandLineHandler for GetBottomUpBundles { "checkpoint: {:?}, signatures: {:?}, signatories: {:?}", bundle.checkpoint, bundle.signatures, bundle.signatories, ); + println!("{bundle:?}"); }