diff --git a/Cargo.lock b/Cargo.lock index 844a2ceaf8..6d8b6cf60b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2459,7 +2459,7 @@ dependencies = [ "serde", "serde_json", "syn 2.0.72", - "toml 0.8.16", + "toml 0.8.19", "walkdir", ] @@ -2838,11 +2838,12 @@ dependencies = [ ] [[package]] -name = "fendermint_actor_gas_market" +name = "fendermint_actor_gas_market_eip1559" version = "0.1.0" dependencies = [ "anyhow", "cid", + "fendermint_actors_api", "fil_actors_evm_shared", "fil_actors_runtime", "frc42_dispatch", @@ -2865,11 +2866,33 @@ dependencies = [ "cid", "fendermint_actor_chainmetadata", "fendermint_actor_eam", + "fendermint_actor_gas_market_eip1559", "fil_actor_bundler", "fil_actors_runtime", "fvm_ipld_blockstore", "fvm_ipld_encoding", "num-traits", + "toml 0.8.19", +] + +[[package]] +name = "fendermint_actors_api" +version = "0.1.0" +dependencies = [ + "anyhow", + "cid", + "fil_actors_evm_shared", + "fil_actors_runtime", + "frc42_dispatch", + "fvm_ipld_blockstore", + "fvm_ipld_encoding", + "fvm_shared", + "hex-literal 0.4.1", + "log", + "multihash 0.18.1", + "num-derive 0.3.3", + "num-traits", + "serde", ] [[package]] @@ -2882,7 +2905,7 @@ dependencies = [ "bytes", "cid", "fendermint_abci", - "fendermint_actor_gas_market", + "fendermint_actor_gas_market_eip1559", "fendermint_app_options", "fendermint_app_settings", "fendermint_crypto", @@ -3007,7 +3030,8 @@ dependencies = [ "bytes", "cid", "ethers", - "fendermint_actor_gas_market", + "fendermint_actor_gas_market_eip1559", + "fendermint_actors_api", "fendermint_crypto", "fendermint_rpc", "fendermint_testing", @@ -3140,7 +3164,7 @@ dependencies = [ "tempfile", "tendermint-rpc", "tokio", - "toml 0.8.16", + "toml 0.8.19", "tracing", "url", ] @@ -3232,7 +3256,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "toml 0.8.16", + "toml 0.8.19", ] [[package]] @@ -3324,6 +3348,7 @@ dependencies = [ "fvm_shared", "hex", "ipc-api", + "ipc-types", "multihash 0.18.1", "num-traits", "quickcheck", @@ -3349,8 +3374,9 @@ dependencies = [ "fendermint_actor_activity_tracker", "fendermint_actor_chainmetadata", "fendermint_actor_eam", - "fendermint_actor_gas_market", + "fendermint_actor_gas_market_eip1559", "fendermint_actors", + "fendermint_actors_api", "fendermint_crypto", "fendermint_eth_hardhat", "fendermint_rpc", @@ -4394,7 +4420,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.6", + "indexmap 2.6.0", "slab", "tokio", "tokio-util 0.7.11", @@ -4429,6 +4455,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "hashers" version = "1.0.1" @@ -4970,12 +5002,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "serde", ] @@ -5149,6 +5181,7 @@ dependencies = [ "ipc_actors_abis", "libsecp256k1", "log", + "merkle-tree-rs", "num-derive 0.3.3", "num-traits", "prometheus", @@ -5163,7 +5196,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite 0.18.0", - "toml 0.8.16", + "toml 0.8.19", "tower-http", "tracing", "url", @@ -7162,7 +7195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap 2.6.0", ] [[package]] @@ -8604,7 +8637,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -8629,7 +8662,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -9723,21 +9756,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.17", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -9748,7 +9781,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -9761,22 +9794,22 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.17" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.16", + "winnow 0.6.20", ] [[package]] @@ -10345,7 +10378,7 @@ version = "0.110.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dfcdb72d96f01e6c85b6bf20102e7423bdbaad5c337301bab2bbf253d26413c" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.6.0", "semver", ] @@ -10356,7 +10389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ "bitflags 2.6.0", - "indexmap 2.2.6", + "indexmap 2.6.0", "semver", ] @@ -10381,7 +10414,7 @@ dependencies = [ "bumpalo", "cfg-if", "fxprof-processed-profile", - "indexmap 2.2.6", + "indexmap 2.6.0", "libc", "log", "object 0.31.1", @@ -10459,7 +10492,7 @@ dependencies = [ "anyhow", "cranelift-entity", "gimli 0.27.3", - "indexmap 2.2.6", + "indexmap 2.6.0", "log", "object 0.31.1", "serde", @@ -10523,7 +10556,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", - "indexmap 2.2.6", + "indexmap 2.6.0", "libc", "log", "mach", @@ -10834,9 +10867,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.16" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 5c74d67a11..cfe34ad095 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,11 @@ members = [ "fendermint/tracing", "fendermint/vm/*", "fendermint/actors", + "fendermint/actors/api", "fendermint/actors/chainmetadata", - "fendermint/actors/gas_market", "fendermint/actors/activity-tracker", + "fendermint/actors/eam", + "fendermint/actors/gas_market/eip1559", ] [workspace.package] @@ -181,6 +183,7 @@ ipc_ipld_resolver = { path = "ipld/resolver" } ipc-types = { path = "ipc/types" } ipc-observability = { path = "ipc/observability" } ipc_actors_abis = { path = "contracts/binding" } +fendermint_actors_api = { path = "fendermint/actors/api" } # Vendored for cross-compilation, see https://github.com/cross-rs/cross/wiki/Recipes#openssl # Make sure every top level build target actually imports this dependency, and don't end up diff --git a/contracts/binding/build.rs b/contracts/binding/build.rs index 75d56a51a9..1c45e66a46 100644 --- a/contracts/binding/build.rs +++ b/contracts/binding/build.rs @@ -60,6 +60,7 @@ fn main() { "LibStakingChangeLog", "LibGateway", "LibQuorum", + "ValidatorRewardFacet", ] { let module_name = camel_to_snake(contract_name); let input_path = @@ -91,6 +92,7 @@ fn main() { "SubnetActorCheckpointingFacet", "SubnetActorGetterFacet", "LibGateway", + "CheckpointingFacet", ]; let modules = fvm_address_conversion.into_iter().map(camel_to_snake); diff --git a/contracts/binding/src/lib.rs b/contracts/binding/src/lib.rs index 81be776be5..a617bead0e 100644 --- a/contracts/binding/src/lib.rs +++ b/contracts/binding/src/lib.rs @@ -48,17 +48,19 @@ pub mod subnet_registry_diamond; #[allow(clippy::all)] pub mod top_down_finality_facet; #[allow(clippy::all)] +pub mod validator_reward_facet; +#[allow(clippy::all)] pub mod xnet_messaging_facet; // The list of contracts need to convert FvmAddress to fvm_shared::Address fvm_address_conversion!(gateway_manager_facet); fvm_address_conversion!(gateway_getter_facet); -fvm_address_conversion!(checkpointing_facet); fvm_address_conversion!(xnet_messaging_facet); fvm_address_conversion!(gateway_messenger_facet); fvm_address_conversion!(subnet_actor_checkpointing_facet); fvm_address_conversion!(subnet_actor_getter_facet); fvm_address_conversion!(lib_gateway); +fvm_address_conversion!(checkpointing_facet); // The list of contracts that need to convert common types between each other common_type_conversion!(subnet_actor_getter_facet, checkpointing_facet); diff --git a/contracts/contracts/SubnetActorDiamond.sol b/contracts/contracts/SubnetActorDiamond.sol index 5abb6cfb32..dc36e7ba84 100644 --- a/contracts/contracts/SubnetActorDiamond.sol +++ b/contracts/contracts/SubnetActorDiamond.sol @@ -15,6 +15,8 @@ import {SubnetIDHelper} from "./lib/SubnetIDHelper.sol"; import {LibStaking} from "./lib/LibStaking.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {AssetHelper} from "./lib/AssetHelper.sol"; +import {LibValidatorReward} from "./activities/ValidatorRewardFacet.sol"; + error FunctionNotFound(bytes4 _functionSelector); contract SubnetActorDiamond { @@ -37,6 +39,7 @@ contract SubnetActorDiamond { Asset collateralSource; SubnetID parentId; address validatorGater; + address validatorRewarder; } constructor(IDiamond.FacetCut[] memory _diamondCut, ConstructorParams memory params, address owner) { @@ -102,6 +105,10 @@ contract SubnetActorDiamond { if (params.validatorGater != address(0)) { s.validatorGater = params.validatorGater; } + + if (params.validatorRewarder != address(0)) { + LibValidatorReward.updateRewarder(params.validatorRewarder); + } } function _fallback() internal { diff --git a/contracts/contracts/SubnetRegistryDiamond.sol b/contracts/contracts/SubnetRegistryDiamond.sol index bd57a65b90..250dfffb61 100644 --- a/contracts/contracts/SubnetRegistryDiamond.sol +++ b/contracts/contracts/SubnetRegistryDiamond.sol @@ -25,6 +25,7 @@ contract SubnetRegistryDiamond { address diamondCutFacet; address diamondLoupeFacet; address ownershipFacet; + address validatorRewardFacet; bytes4[] subnetActorGetterSelectors; bytes4[] subnetActorManagerSelectors; bytes4[] subnetActorRewarderSelectors; @@ -33,6 +34,7 @@ contract SubnetRegistryDiamond { bytes4[] subnetActorDiamondCutSelectors; bytes4[] subnetActorDiamondLoupeSelectors; bytes4[] subnetActorOwnershipSelectors; + bytes4[] validatorRewardSelectors; SubnetCreationPrivileges creationPrivileges; } @@ -64,6 +66,9 @@ contract SubnetRegistryDiamond { if (params.ownershipFacet == address(0)) { revert FacetCannotBeZero(); } + if (params.validatorRewardFacet == address(0)) { + revert FacetCannotBeZero(); + } LibDiamond.setContractOwner(msg.sender); LibDiamond.diamondCut({_diamondCut: _diamondCut, _init: address(0), _calldata: new bytes(0)}); @@ -83,6 +88,7 @@ contract SubnetRegistryDiamond { s.SUBNET_ACTOR_DIAMOND_CUT_FACET = params.diamondCutFacet; s.SUBNET_ACTOR_LOUPE_FACET = params.diamondLoupeFacet; s.SUBNET_ACTOR_OWNERSHIP_FACET = params.ownershipFacet; + s.VALIDATOR_REWARD_FACET = params.validatorRewardFacet; s.subnetActorGetterSelectors = params.subnetActorGetterSelectors; s.subnetActorManagerSelectors = params.subnetActorManagerSelectors; @@ -92,6 +98,7 @@ contract SubnetRegistryDiamond { s.subnetActorDiamondCutSelectors = params.subnetActorDiamondCutSelectors; s.subnetActorDiamondLoupeSelectors = params.subnetActorDiamondLoupeSelectors; s.subnetActorOwnershipSelectors = params.subnetActorOwnershipSelectors; + s.validatorRewardSelectors = params.validatorRewardSelectors; s.creationPrivileges = params.creationPrivileges; } diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index a9792598e7..36f37e684b 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -1,17 +1,40 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -/// The commitments for the child subnet activities that should be submitted to the parent subnet +import {SubnetID} from "../structs/Subnet.sol"; + +event ActivityReportCreated(uint64 checkpointHeight, ActivityReport report); + +/// The full validator activities report +struct ActivityReport { + ValidatorActivityReport[] validators; +} + +struct ValidatorActivityReport { + /// @dev The validator whose activity we're reporting about. + address validator; + /// @dev The number of blocks committed by each validator in the position they appear in the validators array. + /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. + uint64 blocksCommitted; + /// @dev Other metadata + bytes metadata; +} + +/// The summary for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint -struct ActivityCommitment { - /// The activity summary for validators - bytes32 summary; +struct ActivitySummary { + /// The total number of distintive validators that have mined + uint64 totalActiveValidators; + /// The activity commitment for validators + bytes32 commitment; // TODO: add relayed rewarder commitment } -/// The summary for a single validator +/// The summary for a single validator struct ValidatorSummary { + /// @dev The child subnet checkpoint height associated with this summary + uint64 checkpointHeight; /// @dev The validator whose activity we're reporting about. address validator; /// @dev The number of blocks committed by each validator in the position they appear in the validators array. @@ -21,34 +44,14 @@ struct ValidatorSummary { bytes metadata; } -/// A summary of validator's activity in the child subnet. This is submitted to the parent for reward distribution. -struct ActivitySummary { - /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. - uint256[2] blockRange; - ValidatorSummary[] activities; +/// The proof required for validators to claim rewards +struct ValidatorClaimProof { + ValidatorSummary summary; + bytes32[] proof; } -library LibActivitySummary { - function numValidators(ActivitySummary calldata self) internal pure returns(uint64) { - return uint64(self.activities.length); - } - - function commitment(ActivitySummary calldata self) internal pure returns(bytes32) { - return keccak256(abi.encode(self)); - } - - function containsValidator(ActivitySummary calldata self, address validator) internal pure returns(bool) { - uint256 len = self.activities.length; - for (uint256 i = 0; i < len; ) { - if (self.activities[i].validator == validator) { - return true; - } - - unchecked { - i++; - } - } - - return false; - } -} \ No newline at end of file +/// The proofs to batch claim validator rewards +struct BatchClaimProofs { + SubnetID subnetId; + ValidatorClaimProof[] proofs; +} diff --git a/contracts/contracts/activities/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol index 8468ea3040..eb47b7fedb 100644 --- a/contracts/contracts/activities/IValidatorRewarder.sol +++ b/contracts/contracts/activities/IValidatorRewarder.sol @@ -16,3 +16,15 @@ interface IValidatorRewarder { /// @dev This method should revert if the summary is invalid; this will cause the function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external; } + +/// @title Validator reward setup interface +/// +/// @dev This is used to initialize a reward distribution +interface IValidatorRewardSetup { + function initDistribution( + SubnetID calldata subnetId, + uint64 checkpointHeight, + bytes32 commitment, + uint64 totalActiveValidators + ) external; +} diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 4c98c085b6..3ff06a4ae5 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -8,9 +8,19 @@ import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProo /// Verifies the proof to the commitment in subnet activity summary library LibActivityMerkleVerifier { - function ensureValidProof(bytes32 commitment, ValidatorSummary calldata summary, bytes32[] calldata proof) internal pure { + function ensureValidProof( + bytes32 commitment, + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) internal pure { // Constructing leaf: https://github.com/OpenZeppelin/merkle-tree#leaf-hash - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(summary.validator, summary.blocksCommitted, summary.metadata)))); + bytes32 leaf = keccak256( + bytes.concat( + keccak256( + abi.encode(summary.checkpointHeight, summary.validator, summary.blocksCommitted, summary.metadata) + ) + ) + ); bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); if (!valid) { revert InvalidProof(); diff --git a/contracts/contracts/activities/ValidatorActivityTracker.sol b/contracts/contracts/activities/ValidatorActivityTracker.sol deleted file mode 100644 index e0bd032288..0000000000 --- a/contracts/contracts/activities/ValidatorActivityTracker.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.23; - -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; - -import {ValidatorSummary, ActivitySummary} from "./Activity.sol"; -import {SystemContract} from "../lib/LibGatewayActorStorage.sol"; - - -/// The validator reward facet for the child subnet, i.e. for child subnet to track validators's activies -/// and create the commitment. -contract ValidatorActivityTracker is SystemContract { - using EnumerableSet for EnumerableSet.AddressSet; - - /// @dev The starting height of validator's mining activities since the last purged block - uint64 startHeight; - /// @dev The list of validator who have participated in mining since `startHeight` - EnumerableSet.AddressSet validators; - /// Tracks the number of blocks a validator has committed since `startHeight` - mapping(address => uint64) blocksCommitted; - - /// Validators claim their reward for doing work in the child subnet - function recordValidatorActivity(address validator) external systemActorOnly { - blocksCommitted[validator] += 1; - - if (!validators.contains(validator)) { - validators.add(validator); - } - } - - /// Reads the current validator summary - function getSummary() external view returns(ActivitySummary memory summary) { - summary.blockRange = [startHeight, block.number]; - - // prepare the activities - uint256 num_validators = validators.length(); - - summary.activities = new ValidatorSummary[](num_validators); - for (uint256 i = 0; i < num_validators; ) { - address validator = validators.at(i); - bytes memory metadata = new bytes(0); - - summary.activities[i] = ValidatorSummary({ - validator: validator, - blocksCommitted: blocksCommitted[validator], - metadata: metadata - }); - - unchecked { - i++; - } - } - } - - /// Reads the current validator summary and purge the data accordingly - /// @dev Call this method only when bottom up checkpoint needs to be created - function purge_activities() external systemActorOnly { - // prepare the activities - uint256 num_validators = validators.length(); - - for (uint256 i = num_validators - 1; i >= 0; ) { - address validator = validators.at(i); - - delete blocksCommitted[validator]; - validators.remove(validator); - - unchecked { - if (i == 0) { break; } - i--; - } - } - - startHeight = uint64(block.number); - } -} diff --git a/contracts/contracts/activities/ValidatorRewardParentFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol similarity index 62% rename from contracts/contracts/activities/ValidatorRewardParentFacet.sol rename to contracts/contracts/activities/ValidatorRewardFacet.sol index e7e7b9ebc4..5af82f0103 100644 --- a/contracts/contracts/activities/ValidatorRewardParentFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -6,26 +6,43 @@ import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; -import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed} from "../errors/IPCErrors.sol"; -import {ValidatorSummary, ActivitySummary, LibActivitySummary} from "./Activity.sol"; -import {IValidatorRewarder} from "./IValidatorRewarder.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; +import {ValidatorSummary, BatchClaimProofs} from "./Activity.sol"; +import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {SubnetID} from "../structs/Subnet.sol"; import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; /// The validator reward facet for the parent subnet, i.e. for validators in the child subnet /// to claim their reward in the parent subnet, which should be the current subnet this facet /// is deployed. -contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { - using LibActivitySummary for ActivitySummary; +contract ValidatorRewardFacet is ReentrancyGuard, Pausable { + function batchClaim(BatchClaimProofs calldata payload) external nonReentrant whenNotPaused { + uint256 len = payload.proofs.length; + for (uint256 i = 0; i < len; ) { + _claim(payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); + unchecked { + i++; + } + } + } /// Validators claim their reward for doing work in the child subnet function claim( SubnetID calldata subnetId, - uint64 checkpointHeight, ValidatorSummary calldata summary, bytes32[] calldata proof ) external nonReentrant whenNotPaused { + _claim(subnetId, summary, proof); + } + + function handleRelay() internal pure { + // no opt for now + return; + } + + function _claim(SubnetID calldata subnetId, ValidatorSummary calldata summary, bytes32[] calldata proof) internal { // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight // note: will never exist. @@ -33,40 +50,18 @@ contract ValidatorRewardParentFacet is ReentrancyGuard, Pausable { revert NotValidator(msg.sender); } - ValidatorRewardParentStorage storage s = LibValidatorRewardParent.facetStorage(); + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); if (s.validatorRewarder == address(0)) { return handleRelay(); } - bytes32 commitment = LibValidatorRewardParent.ensureValidCommitment(s, subnetId, checkpointHeight); - LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); - - handleDistribution(s, subnetId, commitment, summary); - - } - - function handleRelay() internal pure { - revert("not implemented yet"); - } - - function handleDistribution( - ValidatorRewardParentStorage storage s, - SubnetID calldata subnetId, - bytes32 commitment, - ValidatorSummary calldata summary - ) internal { - LibValidatorRewardParent.validatorTryClaim(s, commitment, summary.validator); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); - - // LibValidatorRewardParent.tryPurgeCommitment(s, subnetId, commitment, summary.numValidators()); + LibValidatorReward.handleDistribution(s, subnetId, summary, proof); } } /// The activity summary commiment that is currently under reward distribution struct RewardDistribution { - /// The checkpoint height that this distribution - uint64 checkpointHeight; /// Total number of valdators to claim the distribution uint64 totalValidators; /// The list of validators that have claimed the reward @@ -74,7 +69,7 @@ struct RewardDistribution { } /// Used by the SubnetActor to track the rewards for each validator -struct ValidatorRewardParentStorage { +struct ValidatorRewardStorage { /// @notice The contract address for validator rewarder address validatorRewarder; /// @notice Summaries look up pending to be processed. @@ -84,10 +79,11 @@ struct ValidatorRewardParentStorage { mapping(bytes32 => EnumerableMap.Bytes32ToBytes32Map) commitments; /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. /// Only used if the validator rewarder is non-zero. - mapping(bytes32 => RewardDistribution) distributions; + /// Partitioned by subnet ID (hash) then by checkpoint block height in the child subnet to the commitment + mapping(bytes32 => mapping(uint64 => RewardDistribution)) distributions; } -/// The payload for list commitments query +/// The payload for list commitments query struct ListCommimentDetail { /// The child subnet checkpoint height uint64 checkpointHeight; @@ -95,7 +91,7 @@ struct ListCommimentDetail { bytes32 commitment; } -library LibValidatorRewardParent { +library LibValidatorReward { bytes32 private constant NAMESPACE = keccak256("validator.reward.parent"); using SubnetIDHelper for SubnetID; @@ -104,8 +100,28 @@ library LibValidatorRewardParent { // =========== External library functions ============= - function listCommitments(SubnetID calldata subnetId) internal view returns(ListCommimentDetail[] memory listDetails) { - ValidatorRewardParentStorage storage ds = facetStorage(); + function initNewDistribution( + SubnetID calldata subnetId, + uint64 checkpointHeight, + bytes32 commitment, + uint64 totalActiveValidators + ) internal { + ValidatorRewardStorage storage ds = facetStorage(); + + bytes32 subnetKey = subnetId.toHash(); + + if (ds.distributions[subnetKey][checkpointHeight].totalValidators != 0) { + revert CommitmentAlreadyInitialized(); + } + + ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); + ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; + } + + function listCommitments( + SubnetID calldata subnetId + ) internal view returns (ListCommimentDetail[] memory listDetails) { + ValidatorRewardStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); @@ -128,22 +144,14 @@ library LibValidatorRewardParent { return listDetails; } - function initNewDistribution(uint64 checkpointHeight, bytes32 commitment, SubnetID calldata subnetId) internal { - ValidatorRewardParentStorage storage ds = facetStorage(); - - bytes32 subnetKey = subnetId.toHash(); - - if (ds.distributions[commitment].checkpointHeight != 0) { - revert CommitmentAlreadyInitialized(); - } - - ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); - ds.distributions[commitment].checkpointHeight = checkpointHeight; + function updateRewarder(address rewarder) internal { + ValidatorRewardStorage storage ds = facetStorage(); + ds.validatorRewarder = rewarder; } // ============ Internal library functions ============ - function facetStorage() internal pure returns (ValidatorRewardParentStorage storage ds) { + function facetStorage() internal pure returns (ValidatorRewardStorage storage ds) { bytes32 position = NAMESPACE; assembly { ds.slot := position @@ -151,13 +159,26 @@ library LibValidatorRewardParent { return ds; } - function ensureValidCommitment( - ValidatorRewardParentStorage storage ds, + function handleDistribution( + ValidatorRewardStorage storage s, SubnetID calldata subnetId, - uint64 checkpointHeight - ) internal view returns(bytes32) { + ValidatorSummary calldata summary, + bytes32[] calldata proof + ) internal { bytes32 subnetKey = subnetId.toHash(); + bytes32 commitment = ensureValidCommitment(s, subnetKey, summary.checkpointHeight); + LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); + + validatorTryClaim(s, subnetKey, summary.checkpointHeight, summary.validator); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); + } + + function ensureValidCommitment( + ValidatorRewardStorage storage ds, + bytes32 subnetKey, + uint64 checkpointHeight + ) internal view returns (bytes32) { (bool exists, bytes32 commitment) = ds.commitments[subnetKey].tryGet(bytes32(uint256(checkpointHeight))); if (!exists) { revert SubnetNoTargetCommitment(); @@ -175,28 +196,35 @@ library LibValidatorRewardParent { /// Validator tries to claim the reward. The validator can only claim the reward if the validator /// has not claimed before - function validatorTryClaim(ValidatorRewardParentStorage storage ds, bytes32 commitment, address validator) internal { - if(ds.distributions[commitment].claimed.contains(validator)) { + function validatorTryClaim( + ValidatorRewardStorage storage ds, + bytes32 subnetKey, + uint64 checkpointHeight, + address validator + ) internal { + if (ds.distributions[subnetKey][checkpointHeight].claimed.contains(validator)) { revert ValidatorAlreadyClaimed(); } - ds.distributions[commitment].claimed.add(validator); + ds.distributions[subnetKey][checkpointHeight].claimed.add(validator); } - /// Try to remove the commiment in the target subnet when ALL VALIDATORS HAVE CLAIMED. - function tryPurgeCommitment( - ValidatorRewardParentStorage storage ds, + /// Try to remove the distribution in the target subnet when ALL VALIDATORS HAVE CLAIMED. + function tryPurgeDistribution( + ValidatorRewardStorage storage ds, SubnetID calldata subnetId, - bytes32 commitment, - uint64 totalValidators + uint64 checkpointHeight ) internal { bytes32 subnetKey = subnetId.toHash(); - if (ds.distributions[commitment].claimed.length() < totalValidators) { + uint256 total = uint256(ds.distributions[subnetKey][checkpointHeight].totalValidators); + uint256 claimed = ds.distributions[subnetKey][checkpointHeight].claimed.length(); + + if (claimed < total) { return; } delete ds.commitments[subnetKey]; - delete ds.distributions[commitment]; + delete ds.distributions[subnetKey][checkpointHeight]; } -} \ No newline at end of file +} diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index f65028d14c..2cdc7ba969 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -84,7 +84,7 @@ error CommitmentAlreadyInitialized(); error SubnetNoTargetCommitment(); error ValidatorAlreadyClaimed(); error InvalidProof(); - +error NotOwner(); enum InvalidXnetMessageReason { Sender, diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol new file mode 100644 index 0000000000..1dfa33df5a --- /dev/null +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; +import {ValidatorSummary} from "../activities/Activity.sol"; +import {SubnetID} from "../structs/Subnet.sol"; + +/// An example validator rewarder implementation that tracks the accumulated +/// reward for each valdiator only. +contract ValidatorRewarderMap is IValidatorRewarder { + SubnetID public subnetId; + address public owner; + + mapping(address => uint64) public blocksCommitted; + + constructor() { + owner = msg.sender; + } + + function setSubnet(SubnetID calldata id) external { + require(msg.sender == owner, "not owner"); + require(id.route.length > 0, "root not allowed"); + + subnetId = id; + } + + function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external { + require(keccak256(abi.encode(id)) == keccak256(abi.encode(subnetId)), "not my subnet"); + + address actor = id.route[id.route.length - 1]; + require(actor == msg.sender, "not from subnet"); + + blocksCommitted[summary.validator] += summary.blocksCommitted; + } +} diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index b4df277572..a88cf10266 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol"; -import {BottomUpCheckpoint, ActivitySummary, ActivitySummaryCommitted} from "../../structs/CrossNet.sol"; +import {BottomUpCheckpoint} from "../../structs/CrossNet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; import {LibQuorum} from "../../lib/LibQuorum.sol"; import {Subnet} from "../../structs/Subnet.sol"; @@ -16,7 +16,8 @@ import {BatchNotCreated, InvalidBatchEpoch, BatchAlreadyExists, NotEnoughSubnetC import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {LibValidatorRewardParent} from "../../activities/ValidatorRewardParentFacet.sol"; + +import {ActivityReportCreated, ActivityReport} from "../../activities/Activity.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -42,12 +43,35 @@ contract CheckpointingFacet is GatewayActorModifiers { LibGateway.checkMsgLength(checkpoint.msgs); execBottomUpMsgs(checkpoint.msgs, subnet); + } + + /// @notice creates a new bottom-up checkpoint with activity report + /// @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 + /// @param activityReport - the validator validator report + function createBUChptWithActivities( + BottomUpCheckpoint calldata checkpoint, + bytes32 membershipRootHash, + uint256 membershipWeight, + ActivityReport calldata activityReport + ) external systemActorOnly { + if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { + revert CheckpointAlreadyExists(); + } - LibValidatorRewardParent.initNewDistribution( - uint64(checkpoint.blockHeight), - checkpoint.activities.summary, - checkpoint.subnetID - ); + LibQuorum.createQuorumInfo({ + self: s.checkpointQuorumMap, + objHeight: checkpoint.blockHeight, + objHash: keccak256(abi.encode(checkpoint)), + membershipRootHash: membershipRootHash, + membershipWeight: membershipWeight, + majorityPercentage: s.majorityPercentage + }); + + LibGateway.storeBottomUpCheckpoint(checkpoint); + + emit ActivityReportCreated(uint64(checkpoint.blockHeight), activityReport); } /// @notice creates a new bottom-up checkpoint @@ -56,7 +80,6 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @param membershipWeight - the total weight of the membership function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, - // TODO(rewarder) ActivitySummary calldata summary, bytes32 membershipRootHash, uint256 membershipWeight ) external systemActorOnly { @@ -64,8 +87,11 @@ contract CheckpointingFacet is GatewayActorModifiers { revert CheckpointAlreadyExists(); } - // TODO(rewarder): compute the commitment to the summary and set it in the checkpoint. - // Collect summaries to relay and put them in the checkpoint. Reset the pending summaries map. + // TODO(rewarder): step 1. call fvm ActivityTrackerActor::get_summary to generate the summary + // TODO(rewarder): step 2. update checkpoint.activities with that in step 1 + // TODO: (if there is more time, should wrap param checkpoint with another data structure) + // TODO(rewarder): step 3. call fvm ActivityTrackerActor::purge_activities to purge the activities + // TODO(rewarder): step 4. emit validator details as event LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, @@ -76,8 +102,6 @@ contract CheckpointingFacet is GatewayActorModifiers { majorityPercentage: s.majorityPercentage }); - // TODO(rewarder): emit an ActivitySummaryCommittedevent so relayers can pick it up. - LibGateway.storeBottomUpCheckpoint(checkpoint); } diff --git a/contracts/contracts/lib/LibDiamond.sol b/contracts/contracts/lib/LibDiamond.sol index e60f520c20..476086d12b 100644 --- a/contracts/contracts/lib/LibDiamond.sol +++ b/contracts/contracts/lib/LibDiamond.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.23; import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; import {IDiamond} from "../interfaces/IDiamond.sol"; +import {NotOwner} from "../errors/IPCErrors.sol"; library LibDiamond { bytes32 public constant DIAMOND_STORAGE_POSITION = keccak256("libdiamond.lib.diamond.storage"); error InvalidAddress(); - error NotOwner(); error NoBytecodeAtAddress(address _contractAddress, string _message); error IncorrectFacetCutAction(IDiamondCut.FacetCutAction _action); error NoSelectorsProvidedForFacetForCut(address _facetAddress); diff --git a/contracts/contracts/lib/LibGateway.sol b/contracts/contracts/lib/LibGateway.sol index f7c3e84ada..eb96ea800a 100644 --- a/contracts/contracts/lib/LibGateway.sol +++ b/contracts/contracts/lib/LibGateway.sol @@ -82,6 +82,7 @@ library LibGateway { b.subnetID = checkpoint.subnetID; b.nextConfigurationNumber = checkpoint.nextConfigurationNumber; b.blockHeight = checkpoint.blockHeight; + b.activities = checkpoint.activities; uint256 msgLength = checkpoint.msgs.length; for (uint256 i; i < msgLength; ) { diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index fed633a025..3983c76eea 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -12,6 +12,7 @@ struct SubnetRegistryActorStorage { // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_MANAGER_FACET; // solhint-disable-next-line var-name-mixedcase + /// TODO: this should be removed as it's for collateral withdraw only, not rewarder address SUBNET_ACTOR_REWARD_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_CHECKPOINTING_FACET; @@ -23,11 +24,14 @@ struct SubnetRegistryActorStorage { address SUBNET_ACTOR_LOUPE_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_OWNERSHIP_FACET; + // solhint-disable-next-line var-name-mixedcase + address VALIDATOR_REWARD_FACET; /// The subnet actor getter facet functions selectors bytes4[] subnetActorGetterSelectors; /// The subnet actor manager facet functions selectors bytes4[] subnetActorManagerSelectors; /// The subnet actor reward facet functions selectors + /// TODO: this should be removed as it's for collateral withdraw only, not rewarder bytes4[] subnetActorRewarderSelectors; /// The subnet actor checkpointing facet functions selectors bytes4[] subnetActorCheckpointerSelectors; @@ -39,6 +43,8 @@ struct SubnetRegistryActorStorage { bytes4[] subnetActorDiamondLoupeSelectors; /// The subnet actor ownership facet functions selectors bytes4[] subnetActorOwnershipSelectors; + /// The validator reward facet functions selectors + bytes4[] validatorRewardSelectors; /// @notice Mapping that tracks the deployed subnet actors per user. /// Key is the hash of Subnet ID, values are addresses. /// mapping owner => nonce => subnet diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index c11d21236b..1dc78be10d 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.23; import {SubnetID, IPCAddress} from "./Subnet.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {ActivityCommitment} from "../activities/Activity.sol"; +import {ActivitySummary} from "../activities/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -30,22 +30,10 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; - /// @dev The activity commitment from child subnet to parent subnet - ActivityCommitment activities; + /// @dev The activity summary from child subnet to parent subnet + ActivitySummary activities; } -struct ActivitySummary { - /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. - uint256[2] blockRange; - /// @dev The validators whose activity we're reporting about. - address[] validators; - /// @dev The number of blocks committed by each validator in the position they appear in the validators array. - /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. - uint64[] blocksCommitted; -} - -event ActivitySummaryCommitted(bytes32 indexed commitment, ActivitySummary summary); - struct RelayedSummary { /// @dev The subnet IDs whose activity is being relayed. SubnetID subnet; diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index 84aefa1464..c130b2ad5d 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -13,6 +13,7 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet import {LibSubnetActor} from "../lib/LibSubnetActor.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {LibGateway} from "../lib/LibGateway.sol"; +import {LibValidatorReward} from "../activities/ValidatorRewardFacet.sol"; contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using EnumerableSet for EnumerableSet.AddressSet; @@ -49,6 +50,13 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); + LibValidatorReward.initNewDistribution( + checkpoint.subnetID, + uint64(checkpoint.blockHeight), + checkpoint.activities.commitment, + checkpoint.activities.totalActiveValidators + ); + // confirming the changes in membership in the child LibStaking.confirmChange(checkpoint.nextConfigurationNumber); } diff --git a/contracts/contracts/subnet/SubnetActorRewardFacet.sol b/contracts/contracts/subnet/SubnetActorRewardFacet.sol index f34ed6f1ff..1b8b84b7db 100644 --- a/contracts/contracts/subnet/SubnetActorRewardFacet.sol +++ b/contracts/contracts/subnet/SubnetActorRewardFacet.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {ActivitySummary} from "../structs/CrossNet.sol"; import {QuorumObjKind} from "../structs/Quorum.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; diff --git a/contracts/sdk/IpcContractUpgradeable.sol b/contracts/sdk/IpcContractUpgradeable.sol index 45133c73fd..1c4be0e2a4 100644 --- a/contracts/sdk/IpcContractUpgradeable.sol +++ b/contracts/sdk/IpcContractUpgradeable.sol @@ -27,6 +27,7 @@ abstract contract IpcExchangeUpgradeable is Initializable, IIpcHandler, OwnableU _disableInitializers(); } + // solhint-disable-next-line func-name-mixedcase function __IpcExchangeUpgradeable_init(address gatewayAddr_) public onlyInitializing { gatewayAddr = gatewayAddr_; __Ownable_init(msg.sender); diff --git a/contracts/tasks/deploy-gateway.ts b/contracts/tasks/deploy-gateway.ts index d8971e82d4..de2226d1b0 100644 --- a/contracts/tasks/deploy-gateway.ts +++ b/contracts/tasks/deploy-gateway.ts @@ -80,6 +80,8 @@ async function deployGatewayDiamond( gatewayConstructorParams.networkName.root = await hre.getChainId() gatewayConstructorParams.commitSha = hre.ethers.utils.formatBytes32String(gitCommitSha()) + console.log('deploy gateway with params', gatewayConstructorParams) + const deployments = await Deployments.deploy(hre, deployer, { name: 'GatewayDiamond', args: [facets.asFacetCuts(), gatewayConstructorParams], diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index f087bd8b04..854e1c61da 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -37,10 +37,17 @@ task('deploy-registry') }, { name: 'SubnetActorPauseFacet' }, { name: 'SubnetActorRewardFacet' }, - { name: 'SubnetActorCheckpointingFacet' }, + { + name: 'SubnetActorCheckpointingFacet', + libraries: ['SubnetIDHelper'], + }, { name: 'DiamondCutFacet' }, { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, + { + name: 'ValidatorRewardFacet', + libraries: ['SubnetIDHelper'], + }, ) const registryFacets = await Deployments.deploy( @@ -85,6 +92,7 @@ task('deploy-registry') diamondCutFacet: subnetActorFacets.addresses['DiamondCutFacet'], diamondLoupeFacet: subnetActorFacets.addresses['DiamondLoupeFacet'], ownershipFacet: subnetActorFacets.addresses['OwnershipFacet'], + validatorRewardFacet: subnetActorFacets.addresses['ValidatorRewardFacet'], subnetActorGetterSelectors: selectors(subnetActorFacets.contracts['SubnetActorGetterFacet']), subnetActorManagerSelectors: selectors(subnetActorFacets.contracts['SubnetActorManagerFacet']), @@ -94,6 +102,8 @@ task('deploy-registry') subnetActorDiamondCutSelectors: selectors(subnetActorFacets.contracts['DiamondCutFacet']), subnetActorDiamondLoupeSelectors: selectors(subnetActorFacets.contracts['DiamondLoupeFacet']), subnetActorOwnershipSelectors: selectors(subnetActorFacets.contracts['OwnershipFacet']), + validatorRewardSelectors: selectors(subnetActorFacets.contracts['ValidatorRewardFacet']), + creationPrivileges: Number(mode), } diff --git a/contracts/tasks/deploy.ts b/contracts/tasks/deploy.ts index ee102c043e..fdedcf5385 100644 --- a/contracts/tasks/deploy.ts +++ b/contracts/tasks/deploy.ts @@ -7,18 +7,18 @@ task( async (args, hre: HardhatRuntimeEnvironment) => { await hre.run('compile') - console.log() - console.log( - '==== LIBRARY DEPLOYMENT ===========================================================================', - ) - await hre.run('deploy-libraries') - console.log() + // console.log() + // console.log( + // '==== LIBRARY DEPLOYMENT ===========================================================================', + // ) + // await hre.run('deploy-libraries') + // console.log() - console.log( - '==== GATEWAY DEPLOYMENT ===========================================================================', - ) - await hre.run('deploy-gateway') - console.log() + // console.log( + // '==== GATEWAY DEPLOYMENT ===========================================================================', + // ) + // await hre.run('deploy-gateway') + // console.log() console.log( '==== REGISTRY DEPLOYMENT ==========================================================================', diff --git a/contracts/tasks/gen-selector-library.ts b/contracts/tasks/gen-selector-library.ts index 31a4e045b7..512060afba 100644 --- a/contracts/tasks/gen-selector-library.ts +++ b/contracts/tasks/gen-selector-library.ts @@ -32,6 +32,7 @@ task('gen-selector-library', 'Generates a Solidity library with contract selecto 'RegisterSubnetFacet', 'SubnetGetterFacet', 'SubnetActorMock', + 'ValidatorRewardFacet', ] const resolveSelectors = async (contractName: string) => { diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 901e611bc5..d0ab1c6928 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -5,4 +5,5 @@ import './deploy-registry' import './deploy' import './upgrade' import './validator-gater' +import './validator-rewarder' import './gen-selector-library' diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts new file mode 100644 index 0000000000..86e37e4d3c --- /dev/null +++ b/contracts/tasks/validator-rewarder.ts @@ -0,0 +1,46 @@ +import { task } from 'hardhat/config' +import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types' +import { Deployments } from './lib' + +// step 1. deploy the validator rewarder +// sample command: pnpm exec hardhat validator-rewarder-deploy --network calibrationnet +task('validator-rewarder-deploy') + .setDescription('Deploy example subnet validator rewarder contract') + .setAction(async (_: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + console.log( + `Deploying validator rewarder contract with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + await Deployments.deploy(hre, deployer, { + name: 'ValidatorRewarderMap', + libraries: [], + }) + }) + +// step 2. set the subnet for the rewarder +// sample command: pnpm exec hardhat validator-rewarder-set-subnet --network calibrationnet 314159 +task('validator-rewarder-set-subnet') + .setDescription('Deploy example subnet validator rewarder contract') + .addPositionalParam('root', 'the chain id of parent subnet') + .addPositionalParam('address', 'the address of the subnet actor contract, L2 only') + .setAction(async (args: TaskArguments, hre: HardhatRuntimeEnvironment) => { + await hre.run('compile') + + const [deployer] = await hre.getUnnamedAccounts() + const balance = await hre.ethers.provider.getBalance(deployer) + console.log( + `Set validator rewarder subnet with account: ${deployer} and balance: ${hre.ethers.utils.formatEther(balance.toString())}`, + ) + + // only L2 for now + const subnetId = { root: args.root, route: [args.address] } + console.log('pointing to', subnetId) + + const contracts = await Deployments.resolve(hre, 'ValidatorRewarderMap') + const contract = contracts.contracts.ValidatorRewarderMap + await contract.setSubnet(subnetId) + }) \ No newline at end of file diff --git a/contracts/test/IntegrationTestBase.sol b/contracts/test/IntegrationTestBase.sol index 967319f1a3..53d17edbfe 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,8 +46,9 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; - +import {ActivitySummary} from "../contracts/activities/Activity.sol"; +import {ValidatorRewarderMap} from "../contracts/examples/ValidatorRewarderMap.sol"; +import {ValidatorRewardFacet} from "../contracts/activities/ValidatorRewardFacet.sol"; struct TestSubnetDefinition { GatewayDiamond gateway; @@ -166,6 +167,7 @@ contract TestSubnetActor is Test, TestParams { bytes4[] saCutterSelectors; bytes4[] saLouperSelectors; bytes4[] saOwnershipSelectors; + bytes4[] validatorRewardSelectors; SubnetActorDiamond saDiamond; SubnetActorMock saMock; @@ -180,6 +182,7 @@ contract TestSubnetActor is Test, TestParams { saCutterSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); saLouperSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); saOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); } function defaultSubnetActorParamsWith( @@ -208,7 +211,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: source, collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -232,7 +236,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: source, collateralSource: collateral, - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -266,7 +271,8 @@ contract TestSubnetActor is Test, TestParams { permissionMode: PermissionMode.Collateral, supplySource: Asset({kind: AssetKind.ERC20, tokenAddress: tokenAddress}), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }); return params; } @@ -415,6 +421,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, functionSelectors: gwOwnershipSelectors }) ); + gatewayDiamond = new GatewayDiamond(gwDiamondCut, params); return gatewayDiamond; @@ -492,8 +499,9 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, DiamondLoupeFacet louper = new DiamondLoupeFacet(); DiamondCutFacet cutter = new DiamondCutFacet(); OwnershipFacet ownership = new OwnershipFacet(); + ValidatorRewardFacet validatorReward = new ValidatorRewardFacet(); - IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](8); + IDiamond.FacetCut[] memory diamondCut = new IDiamond.FacetCut[](9); diamondCut[0] = ( IDiamond.FacetCut({ @@ -559,6 +567,13 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, }) ); + diamondCut[8] = ( + IDiamond.FacetCut({ + facetAddress: address(validatorReward), + action: IDiamond.FacetCutAction.Add, + functionSelectors: validatorRewardSelectors + }) + ); SubnetActorDiamond diamond = new SubnetActorDiamond(diamondCut, params, address(this)); return diamond; @@ -609,7 +624,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, permissionMode: _permissionMode, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); saDiamond = createSubnetActor(params); } @@ -640,7 +656,8 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, permissionMode: _permissionMode, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: _validatorGater + validatorGater: _validatorGater, + validatorRewarder: address(new ValidatorRewarderMap()) }); saDiamond = createSubnetActor(params); } @@ -917,7 +934,45 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(nextConfigNum))}) + activities: ActivitySummary({ + totalActiveValidators: uint64(validators.length), + commitment: bytes32(uint256(nextConfigNum)) + }) + }); + + vm.deal(address(saDiamond), 100 ether); + + bytes32 hash = keccak256(abi.encode(checkpoint)); + + for (uint256 i = 0; i < n; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKeys[i], hash); + signatures[i] = abi.encodePacked(r, s, v); + } + + vm.prank(validators[0]); + saDiamond.checkpointer().submitCheckpoint(checkpoint, validators, signatures); + } + + function confirmChange( + address[] memory validators, + uint256[] memory privKeys, + ActivitySummary memory activities + ) internal { + uint256 n = validators.length; + + bytes[] memory signatures = new bytes[](n); + + (uint64 nextConfigNum, ) = saDiamond.getter().getConfigurationNumbers(); + + uint256 h = saDiamond.getter().lastBottomUpCheckpointHeight() + saDiamond.getter().bottomUpCheckPeriod(); + + BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ + subnetID: saDiamond.getter().getParent().createSubnetId(address(saDiamond)), + blockHeight: h, + blockHash: keccak256(abi.encode(h)), + nextConfigurationNumber: nextConfigNum - 1, + msgs: new IpcEnvelope[](0), + activities: activities }); vm.deal(address(saDiamond), 100 ether); diff --git a/contracts/test/helpers/GatewayFacetsHelper.sol b/contracts/test/helpers/GatewayFacetsHelper.sol index d358891340..5034bb3502 100644 --- a/contracts/test/helpers/GatewayFacetsHelper.sol +++ b/contracts/test/helpers/GatewayFacetsHelper.sol @@ -48,7 +48,6 @@ library GatewayFacetsHelper { return facet; } - // function ownership(GatewayDiamond gw) internal pure returns (OwnershipFacet) { OwnershipFacet facet = OwnershipFacet(address(gw)); return facet; diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index 0307d7090a..e3b7ed21f1 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -31,4 +31,42 @@ library MerkleTreeHelper { return (root, proofs); } + + function createMerkleProofsForActivities( + address[] memory addrs, + uint64[] memory blocksMined, + uint64[] memory checkpointHeights, + bytes[] memory metadatas + ) internal returns (bytes32, bytes32[][] memory) { + Merkle merkleTree = new Merkle(); + + if (addrs.length != blocksMined.length) { + revert("different array lengths btw blocks mined and addrs"); + } + if (addrs.length != metadatas.length) { + revert("different array lengths btw metadatas and addrs"); + } + if (addrs.length != checkpointHeights.length) { + revert("different array lengths btw checkpointHeights and addrs"); + } + uint256 len = addrs.length; + + bytes32 root; + bytes32[][] memory proofs = new bytes32[][](len); + bytes32[] memory data = new bytes32[](len); + for (uint256 i = 0; i < len; i++) { + data[i] = keccak256( + bytes.concat(keccak256(abi.encode(checkpointHeights[i], addrs[i], blocksMined[i], metadatas[i]))) + ); + } + + root = merkleTree.getRoot(data); + // get proof + for (uint256 i = 0; i < len; i++) { + bytes32[] memory proof = merkleTree.getProof(data, i); + proofs[i] = proof; + } + + return (root, proofs); + } } diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 4d3b853a69..0aca743467 100644 --- a/contracts/test/helpers/SelectorLibrary.sol +++ b/contracts/test/helpers/SelectorLibrary.sol @@ -48,7 +48,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("CheckpointingFacet"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000453b4e7bf00000000000000000000000000000000000000000000000000000000024ad232000000000000000000000000000000000000000000000000000000005e6de63200000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff200000000000000000000000000000000000000000000000000000000ed915e7d000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -97,14 +97,14 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { return abi.decode( - hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002e72f09ca00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021b6bda5d00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("RegisterSubnetFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000018471614600000000000000000000000000000000000000000000000000000000", + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001611941f900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -118,7 +118,14 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorMock"))) { return abi.decode( - hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001210fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000350a14bf00000000000000000000000000000000000000000000000000000000c7ebdaef000000000000000000000000000000000000000000000000000000003ae247130000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000008456cb59000000000000000000000000000000000000000000000000000000005c975abb000000000000000000000000000000000000000000000000000000004d9013a10000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee00000000000000000000000000000000000000000000000000000000dcda897300000000000000000000000000000000000000000000000000000000a694fc3a0000000000000000000000000000000000000000000000000000000079979f57000000000000000000000000000000000000000000000000000000003f4ba83a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d10fd4261000000000000000000000000000000000000000000000000000000004e71d92d00000000000000000000000000000000000000000000000000000000350a14bf00000000000000000000000000000000000000000000000000000000c7ebdaef000000000000000000000000000000000000000000000000000000003ae247130000000000000000000000000000000000000000000000000000000041c0e1b500000000000000000000000000000000000000000000000000000000d66d9e19000000000000000000000000000000000000000000000000000000004d9013a10000000000000000000000000000000000000000000000000000000066783c9b00000000000000000000000000000000000000000000000000000000da5d09ee00000000000000000000000000000000000000000000000000000000dcda897300000000000000000000000000000000000000000000000000000000a694fc3a000000000000000000000000000000000000000000000000000000002e17de7800000000000000000000000000000000000000000000000000000000", + (bytes4[]) + ); + } + if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { + return + abi.decode( + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000221dc19cc000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/helpers/SubnetActorFacetsHelper.sol b/contracts/test/helpers/SubnetActorFacetsHelper.sol index 4d7a240028..fcea95a4fc 100644 --- a/contracts/test/helpers/SubnetActorFacetsHelper.sol +++ b/contracts/test/helpers/SubnetActorFacetsHelper.sol @@ -9,6 +9,7 @@ import {SubnetActorGetterFacet} from "../../contracts/subnet/SubnetActorGetterFa import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; library SubnetActorFacetsHelper { function manager(address sa) internal pure returns (SubnetActorManagerFacet) { @@ -48,6 +49,11 @@ library SubnetActorFacetsHelper { // + function validatorReward(SubnetActorDiamond sa) internal pure returns (ValidatorRewardFacet) { + ValidatorRewardFacet facet = ValidatorRewardFacet(address(sa)); + return facet; + } + function manager(SubnetActorDiamond sa) internal pure returns (SubnetActorManagerFacet) { SubnetActorManagerFacet facet = SubnetActorManagerFacet(address(sa)); return facet; diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index d9e45d0598..db2f720ab5 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,7 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -64,7 +64,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT require(owner != newOwner, "ownership should be updated"); require(newOwner == address(1), "new owner not address 1"); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gatewayDiamond.ownership().transferOwnership(address(1)); } @@ -142,7 +142,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT ); //test that other user cannot call diamondcut to add function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); @@ -163,7 +163,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT //test that other user cannot call diamondcut to replace function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); @@ -181,7 +181,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT //test that other user cannot call diamondcut to remove function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); gwDiamondCutter.diamondCut(gwDiamondCut, address(0), new bytes(0)); @@ -1070,7 +1070,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1079,7 +1079,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // failed to create a checkpoint with zero membership weight @@ -1121,7 +1121,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1145,7 +1145,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1167,7 +1167,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); @@ -1214,7 +1214,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); @@ -1235,7 +1235,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1244,7 +1244,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1309,7 +1309,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1371,7 +1371,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1455,7 +1455,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1490,7 +1490,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1535,7 +1535,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // create a checkpoint @@ -1584,7 +1584,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1648,7 +1648,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index f191f152b5..9b1b4e50bc 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,8 +33,7 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; - +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -167,7 +166,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.prank(address(saDiamond)); @@ -226,7 +225,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); // Verify that we received the call and that the recipient has the tokens. diff --git a/contracts/test/integration/MultiSubnet.t.sol b/contracts/test/integration/MultiSubnet.t.sol index 5fdee611f8..7313e35c68 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,7 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary} from "../../contracts/activities/Activity.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -1351,7 +1351,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1381,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index 64ada4509b..6225847716 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,7 +43,9 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivityCommitment} from "../../contracts/activities/Activity.sol"; +import {ActivitySummary, ValidatorSummary} from "../../contracts/activities/Activity.sol"; +import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; +import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -80,7 +82,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { } function testSubnetActorDiamondReal_LoupeFunction() public view { - require(saDiamond.diamondLouper().facets().length == 8, "unexpected length"); + require(saDiamond.diamondLouper().facets().length == 9, "unexpected length"); require( saDiamond.diamondLouper().supportsInterface(type(IERC165).interfaceId) == true, "IERC165 not supported" @@ -342,7 +344,8 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { permissionMode: PermissionMode.Collateral, supplySource: native, collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(0) }), address(saDupGetterFaucet), address(saDupMangerFaucet), @@ -691,7 +694,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -700,7 +703,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -801,7 +804,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -810,7 +813,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(1))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); vm.deal(address(saDiamond), 100 ether); @@ -839,7 +842,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivityCommitment({ summary: bytes32(uint256(2))}); + checkpoint.activities = ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -896,7 +899,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(1) )}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -909,7 +912,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(2))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -921,7 +924,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(3))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(3))}) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -933,7 +936,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(4))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(4))}) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -944,7 +947,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(5))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(5))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -958,7 +961,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(6))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(6))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -972,7 +975,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(7))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(7))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -986,7 +989,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(8))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(8))}) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -997,7 +1000,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(9))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(9))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1011,7 +1014,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivityCommitment({ summary: bytes32(uint256(10))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(10))}) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1053,7 +1056,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(0)}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) }); vm.deal(address(saDiamond), 100 ether); @@ -1097,7 +1100,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivityCommitment({ summary: bytes32(uint256(1))}) + activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) }); hash = keccak256(abi.encode(checkpoint)); @@ -1135,7 +1138,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { ); //test that other user cannot call diamondcut to add function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1155,7 +1158,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { //test that other user cannot call diamondcut to replace function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1173,7 +1176,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { //test that other user cannot call diamondcut to remove function vm.prank(0x1234567890123456789012345678901234567890); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); saDiamondCutter.diamondCut(saDiamondCut, address(0), new bytes(0)); @@ -1792,14 +1795,14 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { function testSubnetActorDiamond_PauseUnpause_NotOwner() public { vm.prank(vm.addr(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamond.pauser().pause(); saDiamond.pauser().pause(); require(saDiamond.pauser().paused(), "not paused"); vm.prank(vm.addr(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); saDiamond.pauser().unpause(); saDiamond.pauser().unpause(); @@ -2329,6 +2332,118 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { require(address(gatewayAddress).balance == DEFAULT_MIN_VALIDATOR_STAKE, "gateway post claim balance wrong"); } + // ============== Test Activities =============== + function testGatewayDiamond_ValidatorClaimMiningReward_Works() public { + gatewayAddress = address(gatewayDiamond); + + Asset memory source = Asset({kind: AssetKind.Native, tokenAddress: address(0)}); + + SubnetActorDiamond.ConstructorParams memory params = defaultSubnetActorParamsWith( + gatewayAddress, + SubnetID(ROOTNET_CHAINID, new address[](0)), + source, + AssetHelper.native() + ); + ValidatorRewarderMap m = new ValidatorRewarderMap(); + params.validatorRewarder = address(m); + params.minValidators = 2; + params.permissionMode = PermissionMode.Federated; + + saDiamond = createSubnetActor(params); + + SubnetID memory subnetId = SubnetID(ROOTNET_CHAINID, new address[](1)); + subnetId.route[0] = address(saDiamond); + m.setSubnet(subnetId); + + (address[] memory addrs, uint256[] memory privKeys, bytes[] memory pubkeys) = TestUtils.newValidators(4); + + uint256[] memory powers = new uint256[](4); + powers[0] = 10000; + powers[1] = 10000; + powers[2] = 10000; + powers[3] = 10000; + saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); + + bytes[] memory metadata = new bytes[](addrs.length); + uint64[] memory blocksMined = new uint64[](addrs.length); + uint64[] memory checkpointHeights = new uint64[](addrs.length); + + blocksMined[0] = 1; + blocksMined[1] = 2; + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[2] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[3] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( + addrs, + blocksMined, + checkpointHeights, + metadata + ); + + confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot})); + + vm.startPrank(addrs[0]); + vm.deal(addrs[0], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[0], + blocksCommitted: blocksMined[0], + metadata: metadata[0] + }), + proofs[0] + ); + + vm.startPrank(addrs[1]); + vm.deal(addrs[1], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[1], + blocksCommitted: blocksMined[1], + metadata: metadata[1] + }), + proofs[1] + ); + + vm.startPrank(addrs[2]); + vm.deal(addrs[2], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[2], + blocksCommitted: blocksMined[2], + metadata: metadata[2] + }), + proofs[2] + ); + + vm.startPrank(addrs[3]); + vm.deal(addrs[3], 1 ether); + saDiamond.validatorReward().claim( + subnetId, + ValidatorSummary({ + checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + validator: addrs[3], + blocksCommitted: blocksMined[3], + metadata: metadata[3] + }), + proofs[3] + ); + + // check + assert(m.blocksCommitted(addrs[0]) == 1); + assert(m.blocksCommitted(addrs[1]) == 2); + assert(m.blocksCommitted(addrs[2]) == 0); + assert(m.blocksCommitted(addrs[3]) == 0); + } + // ----------------------------------------------------------------------------------------------------------------- // Tests for validator gater // ----------------------------------------------------------------------------------------------------------------- diff --git a/contracts/test/integration/SubnetRegistry.t.sol b/contracts/test/integration/SubnetRegistry.t.sol index 95c7d8b875..68e99f550c 100644 --- a/contracts/test/integration/SubnetRegistry.t.sol +++ b/contracts/test/integration/SubnetRegistry.t.sol @@ -29,6 +29,8 @@ import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; import {AssetHelper} from "../../contracts/lib/AssetHelper.sol"; import {RegistryFacetsHelper} from "../helpers/RegistryFacetsHelper.sol"; import {DiamondFacetsHelper} from "../helpers/DiamondFacetsHelper.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; +import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -66,6 +68,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); + params.validatorRewardFacet = address(new ValidatorRewardFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -75,6 +78,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); params.creationPrivileges = SubnetCreationPrivileges.Unrestricted; @@ -101,7 +105,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { params.permissionMode = PermissionMode.Collateral; vm.prank(address(1)); - vm.expectRevert(LibDiamond.NotOwner.selector); + vm.expectRevert(NotOwner.selector); s.register().newSubnetActor(params); } @@ -170,6 +174,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { new SubnetRegistryDiamond(diamondCut, params); params.ownershipFacet = address(8); + params.validatorRewardFacet = address(9); new SubnetRegistryDiamond(diamondCut, params); } @@ -257,7 +262,8 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { permissionMode: PermissionMode.Collateral, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); registrySubnetFacet.newSubnetActor(params); @@ -308,7 +314,7 @@ contract SubnetRegistryTest is Test, TestRegistry, IntegrationTestBase { // Test only owner can update vm.prank(address(1)); // Set a different address as the sender - vm.expectRevert(abi.encodeWithSelector(LibDiamond.NotOwner.selector)); // Expected revert message + vm.expectRevert(abi.encodeWithSelector(NotOwner.selector)); // Expected revert message registrySubnetGetterFacet.updateReferenceSubnetContract( newGetterFacet, newManagerFacet, diff --git a/contracts/test/invariants/SubnetRegistryInvariants.t.sol b/contracts/test/invariants/SubnetRegistryInvariants.t.sol index 4dd42e2472..e26c32a2e6 100644 --- a/contracts/test/invariants/SubnetRegistryInvariants.t.sol +++ b/contracts/test/invariants/SubnetRegistryInvariants.t.sol @@ -18,6 +18,7 @@ import {SubnetGetterFacet} from "../../contracts/subnetregistry/SubnetGetterFace import {DiamondLoupeFacet} from "../../contracts/diamond/DiamondLoupeFacet.sol"; import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; import {OwnershipFacet} from "../../contracts/OwnershipFacet.sol"; +import {ValidatorRewardFacet} from "../../contracts/activities/ValidatorRewardFacet.sol"; import {IntegrationTestBase, TestRegistry} from "../IntegrationTestBase.sol"; import {SelectorLibrary} from "../helpers/SelectorLibrary.sol"; @@ -51,6 +52,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.diamondCutFacet = address(new DiamondCutFacet()); params.diamondLoupeFacet = address(new DiamondLoupeFacet()); params.ownershipFacet = address(new OwnershipFacet()); + params.validatorRewardFacet = address(new ValidatorRewardFacet()); params.subnetActorGetterSelectors = mockedSelectors; params.subnetActorManagerSelectors = mockedSelectors2; @@ -60,6 +62,7 @@ contract SubnetRegistryInvariants is StdInvariant, Test, TestRegistry, Integrati params.subnetActorDiamondCutSelectors = SelectorLibrary.resolveSelectors("DiamondCutFacet"); params.subnetActorDiamondLoupeSelectors = SelectorLibrary.resolveSelectors("DiamondLoupeFacet"); params.subnetActorOwnershipSelectors = SelectorLibrary.resolveSelectors("OwnershipFacet"); + params.validatorRewardSelectors = SelectorLibrary.resolveSelectors("ValidatorRewardFacet"); registryDiamond = createSubnetRegistry(params); registryHandler = new SubnetRegistryHandler(registryDiamond); diff --git a/contracts/test/invariants/handlers/SubnetRegistryHandler.sol b/contracts/test/invariants/handlers/SubnetRegistryHandler.sol index 6883831979..a842c2106e 100644 --- a/contracts/test/invariants/handlers/SubnetRegistryHandler.sol +++ b/contracts/test/invariants/handlers/SubnetRegistryHandler.sol @@ -11,6 +11,8 @@ import {SubnetRegistryDiamond} from "../../../contracts/SubnetRegistryDiamond.so import {ConsensusType} from "../../../contracts/enums/ConsensusType.sol"; import {SubnetID, PermissionMode} from "../../../contracts/structs/Subnet.sol"; import {AssetHelper} from "../../../contracts/lib/AssetHelper.sol"; +import {ValidatorRewarderMap} from "../../../contracts/examples/ValidatorRewarderMap.sol"; + import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {RegistryFacetsHelper} from "../../helpers/RegistryFacetsHelper.sol"; @@ -124,7 +126,8 @@ contract SubnetRegistryHandler is CommonBase, StdCheats, StdUtils { permissionMode: PermissionMode.Collateral, supplySource: AssetHelper.native(), collateralSource: AssetHelper.native(), - validatorGater: address(0) + validatorGater: address(0), + validatorRewarder: address(new ValidatorRewarderMap()) }); address owner = getRandomOldAddressOrNewOne(seed); diff --git a/contracts/test/mocks/SubnetActorMock.sol b/contracts/test/mocks/SubnetActorMock.sol index b5ff8a1235..152901b1c7 100644 --- a/contracts/test/mocks/SubnetActorMock.sol +++ b/contracts/test/mocks/SubnetActorMock.sol @@ -3,16 +3,9 @@ pragma solidity ^0.8.23; import {SubnetActorManagerFacet} from "../../contracts/subnet/SubnetActorManagerFacet.sol"; import {LibStaking} from "../../contracts/lib/LibStaking.sol"; -import {SubnetActorPauseFacet} from "../../contracts/subnet/SubnetActorPauseFacet.sol"; import {SubnetActorRewardFacet} from "../../contracts/subnet/SubnetActorRewardFacet.sol"; -import {SubnetActorCheckpointingFacet} from "../../contracts/subnet/SubnetActorCheckpointingFacet.sol"; -contract SubnetActorMock is - SubnetActorPauseFacet, - SubnetActorManagerFacet, - SubnetActorRewardFacet, - SubnetActorCheckpointingFacet -{ +contract SubnetActorMock is SubnetActorManagerFacet, SubnetActorRewardFacet { function confirmChange(uint64 _configurationNumber) external { LibStaking.confirmChange(_configurationNumber); } diff --git a/docs/fendermint/observability.md b/docs/fendermint/observability.md index 255c3cf849..c962124695 100644 --- a/docs/fendermint/observability.md +++ b/docs/fendermint/observability.md @@ -9,47 +9,47 @@ This is achieved through the use of the `ipc-observability` crate/library, which ### How it works 1. **Events**: Specific events are defined and triggered throughout the codebase to capture significant occurrences or actions. -These events encapsulate relevant data and context about what is happening within the system. + These events encapsulate relevant data and context about what is happening within the system. 2. **Journal**: Events are recorded in a journal, which is a rotational ledger that records chronologically ordered, timestamped trace objects to log files on disk. -The journal can also be emitted to console. + The journal can also be emitted to console. 3. **Metrics**: Each event is associated with one or more Prometheus metrics. -When an event is triggered, the corresponding metrics are updated to reflect the event's occurrence. -This allows for real-time tracking and monitoring of various system activities and states through dashboards and alerts. + When an event is triggered, the corresponding metrics are updated to reflect the event's occurrence. + This allows for real-time tracking and monitoring of various system activities and states through dashboards and alerts. 4. **Prometheus integration**: The metrics collected are designed to integrate seamlessly with Prometheus, a powerful monitoring and alerting toolkit. -Prometheus collects and stores these metrics, enabling detailed analysis and visualization through its query language and dashboarding capabilities. + Prometheus collects and stores these metrics, enabling detailed analysis and visualization through its query language and dashboarding capabilities. 5. **ipc-observability crate**: This custom library encapsulates the logic and functionality required to define, trigger, and record events and metrics. -It simplifies the process of adding observability to the codebase by providing ready-to-use macros, structs, and functions. + It simplifies the process of adding observability to the codebase by providing ready-to-use macros, structs, and functions. ## Metrics -- `consensus_block_proposal_received_height` (IntGauge): Incremented when a block proposal is received. -- `consensus_block_proposal_sent_height` (IntGauge): Incremented when a block proposal is sent. -- `consensus_block_proposal_accepted_height` (IntGauge): Incremented if the block proposal is accepted. -- `consensus_block_proposal_rejected_height` (IntGauge): Incremented if the block proposal is rejected. -- `consensus_block_committed_height` (IntGauge): Incremented when a block is committed. -- `exec_fvm_check_execution_time_secs` (Histogram): Records the execution time of FVM check in seconds. -- `exec_fvm_estimate_execution_time_secs` (Histogram): Records the execution time of FVM estimate in seconds. -- `exec_fvm_apply_execution_time_secs` (Histogram): Records the execution time of FVM apply in seconds. -- `exec_fvm_call_execution_time_secs` (Histogram): Records the execution time of FVM call in seconds. -- `bottomup_checkpoint_created_total` (IntCounter): Incremented when a bottom-up checkpoint is created. -- `bottomup_checkpoint_created_height` (IntGauge): Sets the height of the created checkpoint. -- `bottomup_checkpoint_created_msgcount` (IntGauge): Sets the number of messages in the created checkpoint. -- `bottomup_checkpoint_created_confignum` (IntGauge): Sets the configuration number of the created checkpoint. -- `bottomup_checkpoint_signed_height` (IntGaugeVec): Sets the height of the signed checkpoint, labeled by validator. -- `bottomup_checkpoint_finalized_height` (IntGauge): Sets the height of the finalized checkpoint. -- `topdown_parent_rpc_call_total` (IntCounterVec): Incremented when a parent RPC call is made. -- `topdown_parent_rpc_call_latency_secs` (HistogramVec): Records the latency of parent RPC calls. -- `topdown_parent_finality_latest_acquired_height` (IntGaugeVec): Sets the height of the latest locally acquired parent finality. -- `topdown_parent_finality_voting_latest_received_height` (IntGaugeVec): Sets the height of the received parent finality peer vote. -- `topdown_parent_finality_voting_latest_sent_height` (IntGauge): Sets the height of the sent parent finality peer vote. -- `topdown_parent_finality_voting_quorum_height` (IntGauge): Sets the height of the parent finality quorum. -- `topdown_parent_finality_voting_quorum_weight` (IntGauge): Sets the weight of the parent finality quorum. -- `topdown_parent_finality_committed_height` (IntGauge): Sets the height of the committed parent finality. -- `tracing_errors` (IntCounterVec): Increments the count of tracing errors for the affected event. +- `ipc_consensus_block_proposal_received_height` (IntGauge): Incremented when a block proposal is received. +- `ipc_consensus_block_proposal_sent_height` (IntGauge): Incremented when a block proposal is sent. +- `ipc_consensus_block_proposal_accepted_height` (IntGauge): Incremented if the block proposal is accepted. +- `ipc_consensus_block_proposal_rejected_height` (IntGauge): Incremented if the block proposal is rejected. +- `ipc_consensus_block_committed_height` (IntGauge): Incremented when a block is committed. +- `ipc_exec_fvm_check_execution_time_secs` (Histogram): Records the execution time of FVM check in seconds. +- `ipc_exec_fvm_estimate_execution_time_secs` (Histogram): Records the execution time of FVM estimate in seconds. +- `ipc_exec_fvm_apply_execution_time_secs` (Histogram): Records the execution time of FVM apply in seconds. +- `ipc_exec_fvm_call_execution_time_secs` (Histogram): Records the execution time of FVM call in seconds. +- `ipc_bottomup_checkpoint_created_total` (IntCounter): Incremented when a bottom-up checkpoint is created. +- `ipc_bottomup_checkpoint_created_height` (IntGauge): Sets the height of the created checkpoint. +- `ipc_bottomup_checkpoint_created_msgcount` (IntGauge): Sets the number of messages in the created checkpoint. +- `ipc_bottomup_checkpoint_created_confignum` (IntGauge): Sets the configuration number of the created checkpoint. +- `ipc_bottomup_checkpoint_signed_height` (IntGaugeVec): Sets the height of the signed checkpoint, labeled by validator. +- `ipc_bottomup_checkpoint_finalized_height` (IntGauge): Sets the height of the finalized checkpoint. +- `ipc_topdown_parent_rpc_call_total` (IntCounterVec): Incremented when a parent RPC call is made. +- `ipc_topdown_parent_rpc_call_latency_secs` (HistogramVec): Records the latency of parent RPC calls. +- `ipc_topdown_parent_finality_latest_acquired_height` (IntGaugeVec): Sets the height of the latest locally acquired parent finality. +- `ipc_topdown_parent_finality_voting_latest_received_height` (IntGaugeVec): Sets the height of the received parent finality peer vote. +- `ipc_topdown_parent_finality_voting_latest_sent_height` (IntGauge): Sets the height of the sent parent finality peer vote. +- `ipc_topdown_parent_finality_voting_quorum_height` (IntGauge): Sets the height of the parent finality quorum. +- `ipc_topdown_parent_finality_voting_quorum_weight` (IntGauge): Sets the weight of the parent finality quorum. +- `ipc_topdown_parent_finality_committed_height` (IntGauge): Sets the height of the committed parent finality. +- `ipc_tracing_errors` (IntCounterVec): Increments the count of tracing errors for the affected event. ## Events and corresponding metrics @@ -68,7 +68,7 @@ Represents a block proposal received event. **Affects metrics:** -- `consensus_block_proposal_received_height` +- `ipc_consensus_block_proposal_received_height` ### BlockProposalSent @@ -84,7 +84,7 @@ Represents a block proposal sent event. **Affects metrics:** -- `consensus_block_proposal_sent_height` +- `ipc_consensus_block_proposal_sent_height` ### BlockProposalEvaluated @@ -103,8 +103,8 @@ Represents the evaluation of a block proposal. **Affects metrics:** -- `consensus_block_proposal_accepted_height` -- `consensus_block_proposal_rejected_height` +- `ipc_consensus_block_proposal_accepted_height` +- `ipc_consensus_block_proposal_rejected_height` ### BlockCommitted @@ -118,7 +118,7 @@ Represents a block committed event. **Affects metrics:** -- `consensus_block_committed_height` +- `ipc_consensus_block_committed_height` ### MsgExec @@ -135,10 +135,10 @@ Represents an execution message for different purposes. **Affects metrics:** -- `exec_fvm_check_execution_time_secs` -- `exec_fvm_estimate_execution_time_secs` -- `exec_fvm_apply_execution_time_secs` -- `exec_fvm_call_execution_time_secs` +- `ipc_exec_fvm_check_execution_time_secs` +- `ipc_exec_fvm_estimate_execution_time_secs` +- `ipc_exec_fvm_apply_execution_time_secs` +- `ipc_exec_fvm_call_execution_time_secs` ### CheckpointCreated @@ -154,10 +154,10 @@ Represents the creation of a bottom-up checkpoint. **Affects metrics:** -- `bottomup_checkpoint_created_total` -- `bottomup_checkpoint_created_height` -- `bottomup_checkpoint_created_msgcount` -- `bottomup_checkpoint_created_confignum` +- `ipc_bottomup_checkpoint_created_total` +- `ipc_bottomup_checkpoint_created_height` +- `ipc_bottomup_checkpoint_created_msgcount` +- `ipc_bottomup_checkpoint_created_confignum` ### CheckpointSigned @@ -187,7 +187,7 @@ Represents the finalization of a bottom-up checkpoint. **Affects metrics:** -- `bottomup_checkpoint_finalized_height` +- `ipc_bottomup_checkpoint_finalized_height` ### ParentRpcCalled @@ -204,8 +204,8 @@ Represents a parent RPC call. **Affects metrics:** -- `topdown_parent_rpc_call_total` -- `topdown_parent_rpc_call_latency_secs` +- `ipc_topdown_parent_rpc_call_total` +- `ipc_topdown_parent_rpc_call_latency_secs` ### ParentFinalityAcquired @@ -224,7 +224,7 @@ Represents the acquisition of parent finality. **Affects metrics:** -- `topdown_parent_finality_latest_acquired_height` +- `ipc_topdown_parent_finality_latest_acquired_height` ### ParentFinalityPeerVoteReceived @@ -240,7 +240,7 @@ Represents the reception of a parent finality peer vote. **Affects metrics:** -- `topdown_parent_finality_voting_latest_received_height` +- `ipc_topdown_parent_finality_voting_latest_received_height` ### ParentFinalityPeerVoteSent @@ -255,7 +255,7 @@ Represents the sending of a parent finality peer vote. **Affects metrics:** -- `topdown_parent_finality_voting_latest_sent_height` +- `ipc_topdown_parent_finality_voting_latest_sent_height` ### ParentFinalityPeerQuorumReached @@ -271,8 +271,8 @@ Represents the reaching of a parent finality quorum. **Affects metrics:** -- `topdown_parent_finality_voting_quorum_height` -- `topdown_parent_finality_voting_quorum_weight` +- `ipc_topdown_parent_finality_voting_quorum_height` +- `ipc_topdown_parent_finality_voting_quorum_weight` ### ParentFinalityCommitted @@ -288,7 +288,7 @@ Represents the commitment of parent finality. **Affects metrics:** -- `topdown_parent_finality_committed_height` +- `ipc_topdown_parent_finality_committed_height` ### TracingError @@ -302,7 +302,7 @@ Represents an error that occurs during tracing. **Affects metrics:** -- `tracing_errors` +- `ipc_tracing_errors` ## Configuration @@ -330,7 +330,7 @@ enabled = true > 🚧 Note: the event journal and general logs are currently output to the same file. > We plan to segregate in the near future so that the event journal has its dedicated file. -> See this issue: https://github.com/consensus-shipyard/ipc/issues/1084. +> See this issue: https://github.com/consensus-shipyard/ipc/issues/1084. Tracing can also be configured via the configuration file for Fendermint. You can set the tracing level and specify whether to log to console or file. @@ -342,7 +342,7 @@ Example config: [tracing] [tracing.console] -level = "trace" # Options: off, error, warn, info, debug, trace (default: trace) +level = "trace" # Eg. "info,my_crate::module=trace" - https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives ``` ### File tracing @@ -352,7 +352,7 @@ Example config: ```toml [tracing.file] enabled = true # Options: true, false -level = "trace" # Options: off, error, warn, info, debug, trace (default: trace) +level = "trace" # Eg. "info,my_crate::module=trace" - https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives directory = "/path/to/log/directory" max_log_files = 5 # Number of files to keep after rotation rotation = "daily" # Options: minutely, hourly, daily, never diff --git a/fendermint/actors/Cargo.toml b/fendermint/actors/Cargo.toml index 60413997be..117ad73ea4 100644 --- a/fendermint/actors/Cargo.toml +++ b/fendermint/actors/Cargo.toml @@ -6,9 +6,8 @@ edition.workspace = true license.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] -fendermint_actor_chainmetadata = { path = "chainmetadata", features = [ - "fil-actor", -] } +fendermint_actor_chainmetadata = { path = "chainmetadata", features = ["fil-actor"] } +fendermint_actor_gas_market_eip1559 = { path = "gas_market/eip1559", features = ["fil-actor"] } fendermint_actor_eam = { path = "eam", features = ["fil-actor"] } [dependencies] @@ -18,8 +17,11 @@ fvm_ipld_blockstore = { workspace = true } fvm_ipld_encoding = { workspace = true } fendermint_actor_chainmetadata = { path = "chainmetadata" } fendermint_actor_eam = { path = "eam" } +fendermint_actor_gas_market_eip1559 = { path = "gas_market/eip1559" } [build-dependencies] +anyhow = { workspace = true } fil_actors_runtime = { workspace = true, features = ["test_utils"] } fil_actor_bundler = "6.1.0" num-traits = { workspace = true } +toml = "0.8.19" \ No newline at end of file diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 4679d15ee3..ec075cbca1 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -1,25 +1,26 @@ // Copyright 2021-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use fil_actors_runtime::actor_error; +use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fil_actors_runtime::runtime::{ActorCode, Runtime}; use fil_actors_runtime::{actor_dispatch, ActorError}; -use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::tuple::*; use fvm_shared::address::Address; -use fvm_shared::METHOD_CONSTRUCTOR; use fvm_shared::clock::ChainEpoch; +use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; -use fil_actors_runtime::actor_error; +use serde::{Deserialize, Serialize}; -pub use crate::state::{ValidatorSummary}; -use crate::state::State; +pub use crate::state::State; +pub use crate::state::ValidatorSummary; mod state; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(ActivityTrackerActor); -pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity"; +pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity_tracker"; pub struct ActivityTrackerActor; @@ -28,12 +29,21 @@ pub struct BlockedMinedParams { pub validator: Address, } -#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct GetActivitiesResult { pub activities: Vec, pub start_height: ChainEpoch, } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct GetActivitySummaryResult { + pub commitment: [u8; 32], + /// Total number validators that have mined blocks + pub total_active_validators: u64, + /// The validator details + pub activities: Vec, +} + #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { @@ -41,6 +51,7 @@ pub enum Method { BlockMined = frc42_dispatch::method_hash!("BlockMined"), GetActivities = frc42_dispatch::method_hash!("GetActivities"), PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), + GetSummary = frc42_dispatch::method_hash!("GetSummary"), } impl ActivityTrackerActor { @@ -72,14 +83,26 @@ impl ActivityTrackerActor { Ok(()) } + pub fn get_summary(_rt: &impl Runtime) -> Result { + // todo + let dummy = GetActivitySummaryResult { + commitment: [0; 32], + total_active_validators: 10, + activities: vec![], + }; + Ok(dummy) + } + pub fn get_activities(rt: &impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + let state: State = rt.state()?; let activities = state.validator_activities(rt)?; Ok(GetActivitiesResult { - activities, start_height: state.start_height + activities, + start_height: state.start_height, }) } - } impl ActorCode for ActivityTrackerActor { @@ -94,5 +117,6 @@ impl ActorCode for ActivityTrackerActor { BlockMined => block_mined, GetActivities => get_activities, PurgeActivities => purge_activities, + GetSummary => get_summary, } } diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs index d5ca0d1744..7377b78b37 100644 --- a/fendermint/actors/activity-tracker/src/state.rs +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -26,11 +26,12 @@ pub struct State { } impl State { - pub fn new( - store: &BS, - ) -> Result { + pub fn new(store: &BS) -> Result { let mut deployers_map = BlockCommittedMap::empty(store, DEFAULT_HAMT_CONFIG, "empty"); - Ok(State { start_height: 0, blocks_committed: deployers_map.flush()? }) + Ok(State { + start_height: 0, + blocks_committed: deployers_map.flush()?, + }) } pub fn reset_start_height(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { @@ -40,7 +41,12 @@ impl State { pub fn purge_validator_block_committed(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { let all_validators = self.validator_activities(rt)?; - let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + let mut validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; for v in all_validators { validators.delete(&v.validator)?; @@ -51,8 +57,17 @@ impl State { Ok(()) } - pub fn incr_validator_block_committed(&self, rt: &impl Runtime, validator: &Address) -> Result<(), ActorError> { - let mut validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + pub fn incr_validator_block_committed( + &mut self, + rt: &impl Runtime, + validator: &Address, + ) -> Result<(), ActorError> { + let mut validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; let v = if let Some(v) = validators.get(validator)? { *v + 1 @@ -62,19 +77,32 @@ impl State { validators.set(validator, v)?; + self.blocks_committed = validators.flush()?; + Ok(()) } - pub fn validator_activities(&self, rt: &impl Runtime) -> Result, ActorError> { + pub fn validator_activities( + &self, + rt: &impl Runtime, + ) -> Result, ActorError> { let mut result = vec![]; - let validators = BlockCommittedMap::load(rt.store(), &self.blocks_committed, DEFAULT_HAMT_CONFIG, "verifiers")?; + let validators = BlockCommittedMap::load( + rt.store(), + &self.blocks_committed, + DEFAULT_HAMT_CONFIG, + "verifiers", + )?; validators.for_each(|k, v| { - result.push(ValidatorSummary{ validator: k, block_committed: *v, metadata: vec![]}); + result.push(ValidatorSummary { + validator: k, + block_committed: *v, + metadata: vec![], + }); Ok(()) })?; Ok(result) } } - diff --git a/fendermint/actors/api/Cargo.toml b/fendermint/actors/api/Cargo.toml new file mode 100644 index 0000000000..19ba4fcc86 --- /dev/null +++ b/fendermint/actors/api/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "fendermint_actors_api" +description = "API and interface types for pluggable actors." +license.workspace = true +edition.workspace = true +authors.workspace = true +version = "0.1.0" + +[lib] +## lib is necessary for integration tests +## cdylib is necessary for Wasm build +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = { workspace = true } +cid = { workspace = true } +fil_actors_runtime = { workspace = true } +fvm_ipld_blockstore = { workspace = true } +fvm_ipld_encoding = { workspace = true } +fvm_shared = { workspace = true } +log = { workspace = true } +multihash = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true } +hex-literal = { workspace = true } +frc42_dispatch = { workspace = true } + +[dev-dependencies] +fil_actors_evm_shared = { workspace = true } +fil_actors_runtime = { workspace = true, features = ["test_utils"] } + +[features] +fil-actor = ["fil_actors_runtime/fil-actor"] diff --git a/fendermint/actors/api/src/gas_market.rs b/fendermint/actors/api/src/gas_market.rs new file mode 100644 index 0000000000..7f55f4433b --- /dev/null +++ b/fendermint/actors/api/src/gas_market.rs @@ -0,0 +1,47 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fil_actors_runtime::runtime::Runtime; +use fil_actors_runtime::ActorError; +use fvm_ipld_encoding::tuple::*; +use fvm_shared::econ::TokenAmount; +use num_derive::FromPrimitive; + +pub type Gas = u64; + +/// A reading of the current gas market state for use by consensus. +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct Reading { + /// The current gas limit for the block. + pub block_gas_limit: Gas, + /// The current base fee for the block. + pub base_fee: TokenAmount, +} + +/// The current utilization for the client to report to the gas market. +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct Utilization { + /// The gas used by the current block, at the end of the block. To be invoked as an implicit + /// message, so that gas metering for this message is disabled. + pub block_gas_used: Gas, +} + +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum Method { + CurrentReading = frc42_dispatch::method_hash!("CurrentReading"), + UpdateUtilization = frc42_dispatch::method_hash!("UpdateUtilization"), +} + +/// The trait to be implemented by a gas market actor, provided here for convenience, +/// using the standard Runtime libraries. Ready to be implemented as-is by an actor. +pub trait GasMarket { + /// Returns the current gas market reading. + fn current_reading(rt: &impl Runtime) -> Result; + + /// Updates the current utilization in the gas market, returning the reading after the update. + fn update_utilization( + rt: &impl Runtime, + utilization: Utilization, + ) -> Result; +} diff --git a/fendermint/actors/api/src/lib.rs b/fendermint/actors/api/src/lib.rs new file mode 100644 index 0000000000..e021eb5573 --- /dev/null +++ b/fendermint/actors/api/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +pub mod gas_market; diff --git a/fendermint/actors/build.rs b/fendermint/actors/build.rs index fc1a180e9a..543c1a4fec 100644 --- a/fendermint/actors/build.rs +++ b/fendermint/actors/build.rs @@ -1,14 +1,37 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use anyhow::anyhow; use fil_actor_bundler::Bundler; use std::error::Error; use std::io::{BufRead, BufReader}; use std::path::Path; use std::process::{Command, Stdio}; use std::thread; +use toml::Value; + +fn parse_dependencies_for_wasm32() -> anyhow::Result> { + let manifest = std::fs::read_to_string("Cargo.toml")?; + let document = manifest.parse::()?; + + let dependencies = document + .get("target") + .and_then(|t| t.get(r#"cfg(target_arch = "wasm32")"#)) + .and_then(|t| t.get("dependencies")) + .and_then(Value::as_table) + .ok_or_else(|| anyhow!("could not find wasm32 dependencies"))?; + + let mut ret = Vec::with_capacity(dependencies.len()); + for (name, details) in dependencies.iter() { + let path = details + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("could not find path for a wasm32 dependency"))?; + ret.push((name.clone(), path.to_string())); + } -const ACTORS: &[&str] = &["chainmetadata", "eam", "gas_market"]; + Ok(ret) +} const FILES_TO_WATCH: &[&str] = &["Cargo.toml", "src"]; @@ -27,18 +50,20 @@ fn main() -> Result<(), Box> { Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR unset")) .join("Cargo.toml"); - for file in [FILES_TO_WATCH, ACTORS].concat() { + let actors = parse_dependencies_for_wasm32()?; + let actor_files = actors + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(); + + for file in [FILES_TO_WATCH, actor_files.as_slice()].concat() { println!("cargo:rerun-if-changed={}", file); } // Cargo build command for all test_actors at once. let mut cmd = Command::new(cargo); cmd.arg("build") - .args( - ACTORS - .iter() - .map(|pkg| "-p=fendermint_actor_".to_owned() + pkg), - ) + .args(actors.iter().map(|(pkg, _)| "-p=".to_owned() + pkg)) .arg("--target=wasm32-unknown-unknown") .arg("--profile=wasm") .arg("--features=fil-actor") @@ -88,17 +113,25 @@ fn main() -> Result<(), Box> { let dst = Path::new("output/custom_actors_bundle.car"); let mut bundler = Bundler::new(dst); - for (&pkg, id) in ACTORS.iter().zip(1u32..) { + for (pkg, id) in actors.iter().map(|(pkg, _)| pkg).zip(1u32..) { let bytecode_path = Path::new(&out_dir) .join("wasm32-unknown-unknown/wasm") - .join(format!("fendermint_actor_{}.wasm", pkg)); + .join(format!("{}.wasm", pkg)); // This actor version doesn't force synthetic CIDs; it uses genuine // content-addressed CIDs. let forced_cid = None; + let actor_name = pkg + .to_owned() + .strip_prefix("fendermint_actor_") + .ok_or_else(|| { + format!("expected fendermint_actor_ prefix in actor package name; got: {pkg}") + })? + .to_owned(); + let cid = bundler - .add_from_file(id, pkg.to_owned(), forced_cid, &bytecode_path) + .add_from_file(id, actor_name, forced_cid, &bytecode_path) .unwrap_or_else(|err| { panic!( "failed to add file {:?} to bundle for actor {}: {}", diff --git a/fendermint/actors/gas_market/eip1559/Cargo.toml b/fendermint/actors/gas_market/eip1559/Cargo.toml new file mode 100644 index 0000000000..12865aef3f --- /dev/null +++ b/fendermint/actors/gas_market/eip1559/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "fendermint_actor_gas_market_eip1559" +description = "EIP-1559 gas market actor for IPC. Singleton actor to be deployed at ID 98." +license.workspace = true +edition.workspace = true +authors.workspace = true +version = "0.1.0" + +[lib] +## lib is necessary for integration tests +## cdylib is necessary for Wasm build +crate-type = ["cdylib", "lib"] + +[dependencies] +anyhow = { workspace = true } +cid = { workspace = true } +fendermint_actors_api = { workspace = true } +fil_actors_runtime = { workspace = true } +fvm_ipld_blockstore = { workspace = true } +fvm_ipld_encoding = { workspace = true } +fvm_shared = { workspace = true } +log = { workspace = true } +multihash = { workspace = true } +num-derive = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true } +hex-literal = { workspace = true } +frc42_dispatch = { workspace = true } + +[dev-dependencies] +fil_actors_evm_shared = { workspace = true } +fil_actors_runtime = { workspace = true, features = ["test_utils"] } + +[features] +fil-actor = ["fil_actors_runtime/fil-actor"] diff --git a/fendermint/actors/gas_market/eip1559/src/lib.rs b/fendermint/actors/gas_market/eip1559/src/lib.rs new file mode 100644 index 0000000000..3f92736bc2 --- /dev/null +++ b/fendermint/actors/gas_market/eip1559/src/lib.rs @@ -0,0 +1,344 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fendermint_actors_api::gas_market::Gas; +use fil_actors_runtime::actor_error; +use fil_actors_runtime::runtime::{ActorCode, Runtime}; +use fil_actors_runtime::SYSTEM_ACTOR_ADDR; +use fil_actors_runtime::{actor_dispatch, ActorError}; +use fvm_ipld_encoding::tuple::*; +use fvm_shared::econ::TokenAmount; +use fvm_shared::METHOD_CONSTRUCTOR; +use num_derive::FromPrimitive; +use std::cmp::Ordering; + +#[cfg(feature = "fil-actor")] +fil_actors_runtime::wasm_trampoline!(Actor); + +pub const ACTOR_NAME: &str = "gas_market_eip1559"; + +pub type SetConstants = Constants; + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct State { + pub base_fee: TokenAmount, + pub constants: Constants, +} + +/// Constant params used by EIP-1559. +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct Constants { + pub block_gas_limit: Gas, + /// The minimal base fee floor when gas utilization is low. + pub minimal_base_fee: TokenAmount, + /// Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559). + pub elasticity_multiplier: u64, + /// Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559). + pub base_fee_max_change_denominator: u64, +} + +#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] +pub struct ConstructorParams { + initial_base_fee: TokenAmount, + constants: Constants, +} + +pub struct Actor {} + +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum Method { + Constructor = METHOD_CONSTRUCTOR, + GetConstants = frc42_dispatch::method_hash!("GetConstants"), + SetConstants = frc42_dispatch::method_hash!("SetConstants"), + + // Standard methods. + CurrentReading = fendermint_actors_api::gas_market::Method::CurrentReading as u64, + UpdateUtilization = fendermint_actors_api::gas_market::Method::UpdateUtilization as u64, +} + +impl Actor { + /// Creates the actor + pub fn constructor(rt: &impl Runtime, params: ConstructorParams) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + let st = State { + base_fee: params.initial_base_fee, + constants: params.constants, + }; + + rt.create(&st) + } + + fn set_constants(rt: &impl Runtime, constants: SetConstants) -> Result<(), ActorError> { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut State, _rt| { + st.constants = constants; + Ok(()) + })?; + + Ok(()) + } + + fn get_constants(rt: &impl Runtime) -> Result { + rt.validate_immediate_caller_accept_any()?; + rt.state::().map(|s| s.constants) + } +} + +impl fendermint_actors_api::gas_market::GasMarket for Actor { + fn current_reading( + rt: &impl Runtime, + ) -> Result { + rt.validate_immediate_caller_accept_any()?; + + let st = rt.state::()?; + Ok(fendermint_actors_api::gas_market::Reading { + block_gas_limit: st.constants.block_gas_limit, + base_fee: st.base_fee, + }) + } + + fn update_utilization( + rt: &impl Runtime, + utilization: fendermint_actors_api::gas_market::Utilization, + ) -> Result { + rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + + rt.transaction(|st: &mut State, _rt| { + st.base_fee = st.next_base_fee(utilization.block_gas_used); + Ok(fendermint_actors_api::gas_market::Reading { + block_gas_limit: st.constants.block_gas_limit, + base_fee: st.base_fee.clone(), + }) + }) + } +} + +impl Default for Constants { + fn default() -> Self { + Self { + // Matching the Filecoin block gas limit. Note that IPC consensus != Filecoin Expected Consensus, + // TODO + block_gas_limit: 10_000_000_000, + // Matching Filecoin's minimal base fee. + minimal_base_fee: TokenAmount::from_atto(100), + // Elasticity multiplier as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + elasticity_multiplier: 2, + // Base fee max change denominator as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) + base_fee_max_change_denominator: 8, + } + } +} + +impl State { + fn next_base_fee(&self, gas_used: Gas) -> TokenAmount { + let base_fee = self.base_fee.clone(); + let gas_target = self.constants.block_gas_limit / self.constants.elasticity_multiplier; + + match gas_used.cmp(&gas_target) { + Ordering::Equal => base_fee, + Ordering::Less => { + let base_fee_delta = base_fee.atto() * (gas_target - gas_used) + / gas_target + / self.constants.base_fee_max_change_denominator; + let base_fee_delta = TokenAmount::from_atto(base_fee_delta); + if base_fee_delta >= base_fee { + self.constants.minimal_base_fee.clone() + } else { + base_fee - base_fee_delta + } + } + Ordering::Greater => { + let gas_used_delta = gas_used - gas_target; + let delta = base_fee.atto() * gas_used_delta + / gas_target + / self.constants.base_fee_max_change_denominator; + base_fee + TokenAmount::from_atto(delta).max(TokenAmount::from_atto(1)) + } + } + } +} + +// This import is necessary so that the actor_dispatch macro can find the methods on the GasMarket +// trait, implemented by Self. +use fendermint_actors_api::gas_market::GasMarket; + +impl ActorCode for Actor { + type Methods = Method; + + fn name() -> &'static str { + ACTOR_NAME + } + + actor_dispatch! { + Constructor => constructor, + SetConstants => set_constants, + GetConstants => get_constants, + + CurrentReading => current_reading, + UpdateUtilization => update_utilization, + } +} + +#[cfg(test)] +mod tests { + use crate::{Actor, Constants, ConstructorParams, Method, State}; + use fendermint_actors_api::gas_market::{Reading, Utilization}; + use fil_actors_runtime::test_utils::{expect_empty, MockRuntime, SYSTEM_ACTOR_CODE_ID}; + use fil_actors_runtime::SYSTEM_ACTOR_ADDR; + use fvm_ipld_encoding::ipld_block::IpldBlock; + use fvm_shared::address::Address; + use fvm_shared::econ::TokenAmount; + use fvm_shared::error::ExitCode; + + pub fn construct_and_verify() -> MockRuntime { + let rt = MockRuntime { + receiver: Address::new_id(10), + ..Default::default() + }; + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let result = rt + .call::( + Method::Constructor as u64, + IpldBlock::serialize_cbor(&ConstructorParams { + initial_base_fee: TokenAmount::from_atto(100), + constants: Constants::default(), + }) + .unwrap(), + ) + .unwrap(); + expect_empty(result); + rt.verify(); + rt.reset(); + + rt + } + + #[test] + fn test_set_ok() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&Constants { + minimal_base_fee: Default::default(), + elasticity_multiplier: 0, + base_fee_max_change_denominator: 0, + block_gas_limit: 20, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + let s = rt.get_state::(); + assert_eq!(s.constants.block_gas_limit, 20); + } + + #[test] + fn test_update_utilization_full_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&Utilization { + // full block usage + block_gas_used: 10_000_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(112)); + } + + #[test] + fn test_update_utilization_equal_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&Utilization { + // full block usage + block_gas_used: 5_000_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(100)); + } + + #[test] + fn test_update_utilization_under_usage() { + let rt = construct_and_verify(); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let r = rt.call::( + Method::UpdateUtilization as u64, + IpldBlock::serialize_cbor(&Utilization { + // full block usage + block_gas_used: 100_000_000, + }) + .unwrap(), + ); + assert!(r.is_ok()); + + rt.expect_validate_caller_any(); + let r = rt + .call::(Method::CurrentReading as u64, None) + .unwrap() + .unwrap(); + let reading = r.deserialize::().unwrap(); + assert_eq!(reading.base_fee, TokenAmount::from_atto(88)); + } + + #[test] + fn test_not_allowed() { + let rt = construct_and_verify(); + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, Address::new_id(1000)); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + let code = rt + .call::( + Method::SetConstants as u64, + IpldBlock::serialize_cbor(&Constants { + minimal_base_fee: TokenAmount::from_atto(10000), + elasticity_multiplier: 0, + base_fee_max_change_denominator: 0, + block_gas_limit: 20, + }) + .unwrap(), + ) + .unwrap_err() + .exit_code(); + assert_eq!(code, ExitCode::USR_FORBIDDEN) + } +} diff --git a/fendermint/actors/src/manifest.rs b/fendermint/actors/src/manifest.rs index 1b3030ddf1..0577516d6c 100644 --- a/fendermint/actors/src/manifest.rs +++ b/fendermint/actors/src/manifest.rs @@ -4,12 +4,17 @@ use anyhow::{anyhow, Context}; use cid::Cid; use fendermint_actor_chainmetadata::CHAINMETADATA_ACTOR_NAME; use fendermint_actor_eam::IPC_EAM_ACTOR_NAME; +use fendermint_actor_gas_market_eip1559::ACTOR_NAME as GAS_MARKET_EIP1559_ACTOR_NAME; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::CborStore; use std::collections::HashMap; // array of required actors -pub const REQUIRED_ACTORS: &[&str] = &[CHAINMETADATA_ACTOR_NAME, IPC_EAM_ACTOR_NAME]; +pub const REQUIRED_ACTORS: &[&str] = &[ + CHAINMETADATA_ACTOR_NAME, + IPC_EAM_ACTOR_NAME, + GAS_MARKET_EIP1559_ACTOR_NAME, +]; /// A mapping of internal actor CIDs to their respective types. pub struct Manifest { diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index d2b275e1b1..27bf1db702 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -51,7 +51,7 @@ fendermint_rocksdb = { path = "../rocksdb" } fendermint_rpc = { path = "../rpc" } fendermint_storage = { path = "../storage" } fendermint_tracing = { path = "../tracing" } -fendermint_actor_gas_market = { path = "../actors/gas_market" } +fendermint_actor_gas_market_eip1559 = { path = "../actors/gas_market/eip1559" } fendermint_vm_actor_interface = { path = "../vm/actor_interface" } fendermint_vm_core = { path = "../vm/core" } fendermint_vm_encoding = { path = "../vm/encoding" } diff --git a/fendermint/app/config/test.toml b/fendermint/app/config/test.toml index 87698a0b41..6a6174c813 100644 --- a/fendermint/app/config/test.toml +++ b/fendermint/app/config/test.toml @@ -9,9 +9,7 @@ kind = "ethereum" subnet_id = "/r31415926" [resolver.membership] -static_subnets = [ - "/r31415926/f2xwzbdu7z5sam6hc57xxwkctciuaz7oe5omipwbq", -] +static_subnets = ["/r31415926/f2xwzbdu7z5sam6hc57xxwkctciuaz7oe5omipwbq"] [resolver.discovery] static_addresses = [ @@ -30,3 +28,10 @@ external_addresses = [ [testing] push_chain_meta = false + +[tracing.file] +enabled = true +level = "debug" +directory = "logs" +max_log_files = 4 +rotation = "minutely" diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 6d2095267f..86e2313dab 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -22,7 +22,7 @@ use fendermint_vm_interpreter::fvm::state::{ FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; -use fendermint_vm_interpreter::fvm::{FvmApplyRet, PowerUpdates}; +use fendermint_vm_interpreter::fvm::{BlockGasLimit, FvmApplyRet, PowerUpdates}; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; use fendermint_vm_interpreter::signed::InvalidSignature; use fendermint_vm_interpreter::{ @@ -324,34 +324,35 @@ where Ok(ret) } - /// Get a read only fvm execution state. This is useful to perform query commands targeting - /// the latest state. - pub fn new_read_only_exec_state( + /// Get a read-only view from the current FVM execution state, optionally passing a new BlockContext. + /// This is useful to perform query commands targeting the latest state. Mutations from transactions + /// will not be persisted. + pub fn read_only_view( &self, + height: Option, ) -> Result>>>> { - let maybe_app_state = self.get_committed_state()?; + let app_state = match self.get_committed_state()? { + Some(app_state) => app_state, + None => return Ok(None), + }; - Ok(if let Some(app_state) = maybe_app_state { - let block_height = app_state.block_height; - let state_params = app_state.state_params; + let block_height = height.unwrap_or(app_state.block_height); + let state_params = app_state.state_params; - // wait for block production - if !Self::can_query_state(block_height, &state_params) { - return Ok(None); - } + // wait for block production + if !Self::can_query_state(block_height, &state_params) { + return Ok(None); + } - let exec_state = FvmExecState::new( - ReadOnlyBlockstore::new(self.state_store.clone()), - self.multi_engine.as_ref(), - block_height as ChainEpoch, - state_params, - ) - .context("error creating execution state")?; + let exec_state = FvmExecState::new( + ReadOnlyBlockstore::new(self.state_store.clone()), + self.multi_engine.as_ref(), + block_height as ChainEpoch, + state_params, + ) + .context("error creating execution state")?; - Some(exec_state) - } else { - None - }) + Ok(Some(exec_state)) } /// Look up a past state at a particular height Tendermint Core is looking for. @@ -423,7 +424,7 @@ where Message = Vec, BeginOutput = FvmApplyRet, DeliverOutput = BytesMessageApplyRes, - EndOutput = PowerUpdates, + EndOutput = (PowerUpdates, BlockGasLimit), >, I: CheckInterpreter< State = FvmExecState>, @@ -627,7 +628,7 @@ where let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect(); let state = self - .new_read_only_exec_state()? + .read_only_view(Some(request.height.value()))? .ok_or_else(|| anyhow!("exec state should be present"))?; let txs = self @@ -666,7 +667,7 @@ where let num_txs = txs.len(); let state = self - .new_read_only_exec_state()? + .read_only_view(Some(request.height.value()))? .ok_or_else(|| anyhow!("exec state should be present"))?; let accept = self @@ -734,10 +735,11 @@ where .validators .get_validator(&request.header.proposer_address, block_height) .await?; + let state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) .context("error creating new state")? .with_block_hash(block_hash) - .with_validator(validator); + .with_block_producer(validator); tracing::debug!("initialized exec state"); @@ -791,22 +793,40 @@ where async fn end_block(&self, request: request::EndBlock) -> AbciResult { tracing::debug!(height = request.height, "end block"); - // TODO: Return events from epoch transitions. - let ret = self - .modify_exec_state(|s| async { - let ((chain_env, mut state), update) = self.interpreter.end(s).await?; - - let mut end_block = EndBlockUpdate::new(update); - if let Some(gas) = state.gas_market_mut().take_constant_update() { - end_block.update_gas(gas) - } - - Ok(((chain_env, state), end_block)) - }) + // End the interpreter for this block. + let (power_updates, new_block_gas_limit) = self + .modify_exec_state(|s| self.interpreter.end(s)) .await .context("end failed")?; - Ok(to_end_block(&self.client, request.height, ret).await?) + // Convert the incoming power updates to Tendermint validator updates. + let validator_updates = + to_validator_updates(power_updates.0).context("failed to convert validator updates")?; + + // If the block gas limit has changed, we need to update the consensus layer so it can + // pack subsequent blocks against the new limit. + let consensus_param_updates = { + let mut consensus_params = self + .client + .consensus_params(tendermint::block::Height::try_from(request.height)?) + .await? + .consensus_params; + + if consensus_params.block.max_gas != new_block_gas_limit as i64 { + consensus_params.block.max_gas = new_block_gas_limit as i64; + Some(consensus_params) + } else { + None + } + }; + + let ret = response::EndBlock { + validator_updates, + consensus_param_updates, + events: Vec::new(), // TODO: Return events from epoch transitions. + }; + + Ok(ret) } /// Commit the current state at the current height. diff --git a/fendermint/app/src/cmd/run.rs b/fendermint/app/src/cmd/run.rs index 9d8e685169..81357ae5df 100644 --- a/fendermint/app/src/cmd/run.rs +++ b/fendermint/app/src/cmd/run.rs @@ -71,7 +71,8 @@ async fn run(settings: Settings) -> anyhow::Result<()> { // Prometheus metrics let metrics_registry = if settings.metrics.enabled { - let registry = prometheus::Registry::new(); + let registry = prometheus::Registry::new_custom(Some("ipc".to_string()), None) + .context("failed to create Prometheus registry")?; register_default_metrics(®istry).context("failed to register default metrics")?; register_topdown_metrics(®istry).context("failed to register topdown metrics")?; diff --git a/fendermint/app/src/ipc.rs b/fendermint/app/src/ipc.rs index 5415a0efe7..8f187a7873 100644 --- a/fendermint/app/src/ipc.rs +++ b/fendermint/app/src/ipc.rs @@ -58,7 +58,7 @@ where where F: FnOnce(FvmExecState>>) -> anyhow::Result, { - match self.app.new_read_only_exec_state()? { + match self.app.read_only_view(None)? { Some(s) => f(s).map(Some), None => Ok(None), } diff --git a/fendermint/app/src/tmconv.rs b/fendermint/app/src/tmconv.rs index 6c031072ec..cd1e1e7865 100644 --- a/fendermint/app/src/tmconv.rs +++ b/fendermint/app/src/tmconv.rs @@ -2,22 +2,19 @@ // SPDX-License-Identifier: Apache-2.0, MIT //! Conversions to Tendermint data types. use anyhow::{anyhow, bail, Context}; -use fendermint_actor_gas_market::SetConstants; use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Power, Validator}; use fendermint_vm_interpreter::fvm::{ state::{BlockHash, FvmStateParams}, - FvmApplyRet, FvmCheckRet, FvmQueryRet, PowerUpdates, + FvmApplyRet, FvmCheckRet, FvmQueryRet, }; use fendermint_vm_message::signed::DomainHash; use fendermint_vm_snapshot::{SnapshotItem, SnapshotManifest}; -use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, error::ExitCode, event::StampedEvent, ActorID}; use prost::Message; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, num::NonZeroU32}; use tendermint::abci::{response, Code, Event, EventAttribute}; -use tendermint_rpc::Client; use crate::{app::AppError, BlockHeight}; @@ -27,25 +24,6 @@ struct SnapshotMetadata { state_params: FvmStateParams, } -/// The end block update for cometbft -pub struct EndBlockUpdate { - pub max_gas: Option, - pub validators: PowerUpdates, -} - -impl EndBlockUpdate { - pub fn new(power: PowerUpdates) -> Self { - Self { - max_gas: None, - validators: power, - } - } - - pub fn update_gas(&mut self, constants: SetConstants) { - self.max_gas = Some(constants.block_gas_limit); - } -} - /// IPLD encoding of data types we know we must be able to encode. macro_rules! ipld_encode { ($var:ident) => { @@ -54,31 +32,6 @@ macro_rules! ipld_encode { }; } -pub(crate) async fn to_end_block( - client: &C, - height: ChainEpoch, - value: EndBlockUpdate, -) -> anyhow::Result { - let validator_updates = - to_validator_updates(value.validators.0).context("failed to convert validator updates")?; - - let mut consensus_param_updates = None; - if let Some(max_gas) = value.max_gas { - let mut consensus_params = client - .consensus_params(tendermint::block::Height::try_from(height)?) - .await? - .consensus_params; - consensus_params.block.max_gas = max_gas as i64; - consensus_param_updates = Some(consensus_params); - } - - Ok(response::EndBlock { - validator_updates, - consensus_param_updates, - events: Vec::new(), // TODO: Events from epoch transitions? - }) -} - /// Response to delivery where the input was blatantly invalid. /// This indicates that the validator who made the block was Byzantine. pub fn invalid_deliver_tx(err: AppError, description: String) -> response::DeliverTx { @@ -111,22 +64,6 @@ pub fn invalid_query(err: AppError, description: String) -> response::Query { } } -/// Convert validator power to tendermint validator update. -/// TODO: the import is quite strange, `Validator` and `Power` are imported from `genesis` crate, -/// TODO: which should be from a `type` or `validator` crate. -pub fn to_validator_updates( - validators: Vec>, -) -> anyhow::Result> { - let mut updates = vec![]; - for v in validators { - updates.push(tendermint::validator::Update { - pub_key: tendermint::PublicKey::try_from(v.public_key)?, - power: tendermint::vote::Power::try_from(v.power.0)?, - }); - } - Ok(updates) -} - pub fn to_deliver_tx( ret: FvmApplyRet, domain_hash: Option, @@ -370,6 +307,22 @@ pub fn to_query(ret: FvmQueryRet, block_height: BlockHeight) -> anyhow::Result>, +) -> anyhow::Result> { + let mut updates = vec![]; + for v in validators { + updates.push(tendermint::validator::Update { + pub_key: tendermint::PublicKey::try_from(v.public_key)?, + power: tendermint::vote::Power::try_from(v.power.0)?, + }); + } + Ok(updates) +} + pub fn to_timestamp(time: tendermint::time::Time) -> Timestamp { Timestamp( time.unix_timestamp() diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 643bc26534..405b8c22fc 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -24,6 +24,7 @@ byteorder = { workspace = true } ipc-api = { workspace = true } ipc_actors_abis = { workspace = true } +fendermint_actors_api = { workspace = true } fendermint_testing = { path = "..", features = ["smt", "arb"] } fendermint_crypto = { path = "../../crypto" } fendermint_vm_actor_interface = { path = "../../vm/actor_interface" } @@ -45,4 +46,4 @@ bytes = { workspace = true } fvm_ipld_encoding = { workspace = true } multihash = { workspace = true } fvm = { workspace = true, features = ["testing"] } -fendermint_actor_gas_market = { path = "../../actors/gas_market" } \ No newline at end of file +fendermint_actor_gas_market_eip1559 = { path = "../../actors/gas_market/eip1559" } \ No newline at end of file diff --git a/fendermint/testing/contract-test/src/lib.rs b/fendermint/testing/contract-test/src/lib.rs index fb9d5236c5..fb7b8841e5 100644 --- a/fendermint/testing/contract-test/src/lib.rs +++ b/fendermint/testing/contract-test/src/lib.rs @@ -9,7 +9,7 @@ use std::{future::Future, sync::Arc}; use fendermint_crypto::PublicKey; use fendermint_vm_genesis::Genesis; -use fendermint_vm_interpreter::fvm::PowerUpdates; +use fendermint_vm_interpreter::fvm::{BlockGasLimit, PowerUpdates}; use fendermint_vm_interpreter::genesis::{create_test_genesis_state, GenesisOutput}; use fendermint_vm_interpreter::{ fvm::{ @@ -67,7 +67,7 @@ where Message = FvmMessage, BeginOutput = FvmApplyRet, DeliverOutput = FvmApplyRet, - EndOutput = PowerUpdates, + EndOutput = (PowerUpdates, BlockGasLimit), >, { pub async fn new(interpreter: I, genesis: Genesis) -> anyhow::Result { @@ -125,15 +125,7 @@ where guard.take().expect("exec state empty") } - pub async fn begin_block(&self, block_height: ChainEpoch) -> Result<()> { - self.begin_block_with_validator(block_height, None).await - } - - pub async fn begin_block_with_validator( - &self, - block_height: ChainEpoch, - maybe_validator: Option, - ) -> Result<()> { + pub async fn begin_block(&self, block_height: ChainEpoch, producer: PublicKey) -> Result<()> { let mut block_hash: [u8; 32] = [0; 32]; let _ = block_hash.as_mut().write_i64::(block_height); @@ -141,13 +133,10 @@ where let mut state_params = self.state_params.clone(); state_params.timestamp = Timestamp(block_height as u64); - let mut state = - FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) - .context("error creating new state")? - .with_block_hash(block_hash); - if let Some(validator) = maybe_validator { - state = state.with_validator(validator); - } + let state = FvmExecState::new(db, self.multi_engine.as_ref(), block_height, state_params) + .context("error creating new state")? + .with_block_hash(block_hash) + .with_block_producer(producer); self.put_exec_state(state).await; diff --git a/fendermint/testing/contract-test/tests/run_upgrades.rs b/fendermint/testing/contract-test/tests/run_upgrades.rs index 532c2f71f9..2885037f61 100644 --- a/fendermint/testing/contract-test/tests/run_upgrades.rs +++ b/fendermint/testing/contract-test/tests/run_upgrades.rs @@ -219,9 +219,11 @@ async fn test_applying_upgrades() { // check that the app version is 0 assert_eq!(tester.state_params().app_version, 0); + let producer = my_secret_key().public_key(); + // iterate over all the upgrades for block_height in 1..=3 { - tester.begin_block(block_height).await.unwrap(); + tester.begin_block(block_height, producer).await.unwrap(); tester.end_block(block_height).await.unwrap(); tester.commit().await.unwrap(); diff --git a/fendermint/vm/actor_interface/src/activity.rs b/fendermint/vm/actor_interface/src/activity.rs index b0191ee4dc..51353cacd0 100644 --- a/fendermint/vm/actor_interface/src/activity.rs +++ b/fendermint/vm/actor_interface/src/activity.rs @@ -1,4 +1,4 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -define_id!(ACTIVITY_TRACKER { id: 99 }); \ No newline at end of file +define_id!(ACTIVITY_TRACKER { id: 99 }); diff --git a/fendermint/vm/actor_interface/src/gas_market.rs b/fendermint/vm/actor_interface/src/gas_market.rs new file mode 100644 index 0000000000..ccc6c1f18a --- /dev/null +++ b/fendermint/vm/actor_interface/src/gas_market.rs @@ -0,0 +1,4 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +define_id!(GAS_MARKET { id: 98 }); diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index f57b3c36de..2ffc78a1eb 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -129,6 +129,10 @@ lazy_static! { name: "SubnetGetterFacet", abi: ia::subnet_getter_facet::SUBNETGETTERFACET_ABI.to_owned(), }, + EthFacet { + name: "ValidatorRewardFacet", + abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), + }, ], }, ), @@ -470,6 +474,7 @@ pub mod registry { pub diamond_cut_facet: Address, pub diamond_loupe_facet: Address, pub ownership_facet: Address, + pub validator_reward_facet: Address, pub subnet_getter_selectors: Vec, pub subnet_manager_selectors: Vec, pub subnet_rewarder_selectors: Vec, @@ -478,6 +483,7 @@ pub mod registry { pub subnet_actor_diamond_cut_selectors: Vec, pub subnet_actor_diamond_loupe_selectors: Vec, pub subnet_actor_ownership_selectors: Vec, + pub validator_reward_selectors: Vec, pub creation_privileges: u8, // 0 = Unrestricted, 1 = Owner. } } diff --git a/fendermint/vm/actor_interface/src/lib.rs b/fendermint/vm/actor_interface/src/lib.rs index 81f0b15e3a..5e6b1c77e8 100644 --- a/fendermint/vm/actor_interface/src/lib.rs +++ b/fendermint/vm/actor_interface/src/lib.rs @@ -43,6 +43,7 @@ macro_rules! define_singleton { } pub mod account; +pub mod activity; pub mod burntfunds; pub mod chainmetadata; pub mod cron; @@ -50,11 +51,10 @@ pub mod diamond; pub mod eam; pub mod ethaccount; pub mod evm; -pub mod gas; +pub mod gas_market; pub mod init; pub mod ipc; pub mod multisig; pub mod placeholder; pub mod reward; pub mod system; -pub mod activity; diff --git a/fendermint/vm/genesis/Cargo.toml b/fendermint/vm/genesis/Cargo.toml index c44bfbe969..27214281c7 100644 --- a/fendermint/vm/genesis/Cargo.toml +++ b/fendermint/vm/genesis/Cargo.toml @@ -34,6 +34,7 @@ quickcheck = { workspace = true } quickcheck_macros = { workspace = true } hex = { workspace = true } serde_json = { workspace = true } +ipc-types = {workspace = true} # Enable arb on self for tests. fendermint_vm_genesis = { path = ".", features = ["arb"] } diff --git a/fendermint/vm/genesis/src/arb.rs b/fendermint/vm/genesis/src/arb.rs index 99128cd0de..491872716d 100644 --- a/fendermint/vm/genesis/src/arb.rs +++ b/fendermint/vm/genesis/src/arb.rs @@ -9,6 +9,7 @@ use fendermint_crypto::SecretKey; use fendermint_testing::arb::{ArbSubnetID, ArbTokenAmount}; use fendermint_vm_core::Timestamp; use fvm_shared::{address::Address, version::NetworkVersion}; +use ipc_types::EthAddress; use quickcheck::{Arbitrary, Gen}; use rand::{rngs::StdRng, SeedableRng}; diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index c0b7ee33ab..39c6f2e345 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +fendermint_actors_api = { workspace = true } fendermint_vm_actor_interface = { path = "../actor_interface" } fendermint_vm_core = { path = "../core" } fendermint_vm_event = { path = "../event" } @@ -23,8 +24,8 @@ fendermint_rpc = { path = "../../rpc" } fendermint_tracing = { path = "../../tracing" } fendermint_actors = { path = "../../actors" } fendermint_actor_chainmetadata = { path = "../../actors/chainmetadata" } -fendermint_actor_gas_market = { path = "../../actors/gas_market" } fendermint_actor_activity_tracker = { path = "../../actors/activity-tracker" } +fendermint_actor_gas_market_eip1559 = { path = "../../actors/gas_market/eip1559" } fendermint_actor_eam = { workspace = true } fendermint_testing = { path = "../../testing", optional = true } ipc_actors_abis = { workspace = true } diff --git a/fendermint/vm/interpreter/src/chain.rs b/fendermint/vm/interpreter/src/chain.rs index 2a62dfe373..034090a302 100644 --- a/fendermint/vm/interpreter/src/chain.rs +++ b/fendermint/vm/interpreter/src/chain.rs @@ -1,9 +1,8 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::fvm::gas::GasMarket; use crate::fvm::state::ipc::GatewayCaller; use crate::fvm::store::ReadOnlyBlockstore; -use crate::fvm::{topdown, FvmApplyRet, PowerUpdates}; +use crate::fvm::{topdown, BlockGasLimit, FvmApplyRet, PowerUpdates}; use crate::selector::{GasLimitSelector, MessageSelector}; use crate::{ fvm::state::FvmExecState, @@ -235,7 +234,7 @@ where }; } - Ok(block_gas_usage <= state.gas_market().available().block_gas) + Ok(block_gas_usage <= state.block_gas_tracker().available()) } } @@ -247,7 +246,7 @@ where Message = VerifiableMessage, DeliverOutput = SignedMessageApplyRes, State = FvmExecState, - EndOutput = PowerUpdates, + EndOutput = (PowerUpdates, BlockGasLimit), >, { // The state consists of the resolver pool, which this interpreter needs, and the rest of the @@ -390,7 +389,7 @@ where let local_block_height = state.block_height() as u64; let proposer = state - .validator_pubkey() + .block_producer() .map(|id| hex::encode(id.serialize_compressed())); let proposer_ref = proposer.as_deref(); @@ -435,9 +434,10 @@ where let (state, out) = self.inner.end(state).await?; // Update any component that needs to know about changes in the power table. - if !out.0.is_empty() { + if !out.0 .0.is_empty() { let power_updates = out .0 + .0 .iter() .map(|v| { let vk = ValidatorKey::from(v.public_key.0); @@ -555,11 +555,16 @@ fn relayed_bottom_up_ckpt_to_fvm( Ok(msg) } +/// Selects messages to be executed. Currently, this is a static function whose main purpose is to +/// coordinate various selectors. However, it does not have formal semantics for doing so, e.g. +/// do we daisy-chain selectors, do we parallelize, how do we treat rejections and acceptances? +/// It hasn't been well thought out yet. When we refactor the whole *Interpreter stack, we will +/// revisit this and make the selection function properly pluggable. fn messages_selection( msgs: Vec, state: &FvmExecState, ) -> anyhow::Result> { - let mut signed = msgs + let mut user_msgs = msgs .into_iter() .map(|msg| match msg { ChainMessage::Signed(inner) => Ok(inner), @@ -567,11 +572,13 @@ fn messages_selection( }) .collect::>>()?; - // currently only one selector, we can potentially extend to more selectors + // Currently only one selector, we can potentially extend to more selectors + // This selector enforces that the total cumulative gas limit of all messages is less than the + // currently active block gas limit. let selectors = vec![GasLimitSelector {}]; for s in selectors { - signed = s.select_messages(state, signed) + user_msgs = s.select_messages(state, user_msgs) } - Ok(signed.into_iter().map(ChainMessage::Signed).collect()) + Ok(user_msgs.into_iter().map(ChainMessage::Signed).collect()) } diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs index 56e6680b41..87ddad1cb4 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/actor.rs @@ -1,21 +1,26 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use fvm::executor::{ApplyKind, ApplyRet, Executor}; -use fvm_shared::clock::ChainEpoch; -use fendermint_actor_activity_tracker::{ValidatorSummary}; -use fendermint_vm_actor_interface::system; -use crate::fvm::activities::{ActivitySummary, BlockMined, ValidatorActivityTracker}; +use crate::fvm::activities::{ActivityDetails, BlockMined, ValidatorActivityTracker}; +use crate::fvm::state::FvmExecState; use crate::fvm::FvmMessage; +use anyhow::Context; +use fendermint_actor_activity_tracker::{GetActivitiesResult, ValidatorSummary}; use fendermint_vm_actor_interface::activity::ACTIVITY_TRACKER_ACTOR_ADDR; use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_actor_interface::system; +use fvm::executor::ApplyRet; +use fvm_ipld_blockstore::Blockstore; +use fvm_shared::clock::ChainEpoch; -pub struct ActorActivityTracker<'a, E> { - pub(crate) executor: &'a mut E, +pub struct ActorActivityTracker<'a, DB: Blockstore + Clone + 'static> { + pub(crate) executor: &'a mut FvmExecState, pub(crate) epoch: ChainEpoch, } -impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> { +impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker + for ActorActivityTracker<'a, DB> +{ type ValidatorSummaryDetail = ValidatorSummary; fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { @@ -41,30 +46,31 @@ impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> Ok(()) } - fn get_activities_summary(&self) -> anyhow::Result> { - // let msg = FvmMessage { - // from: system::SYSTEM_ACTOR_ADDR, - // to: ACTIVITY_TRACKER_ACTOR_ADDR, - // sequence: self.epoch as u64, - // // exclude this from gas restriction - // gas_limit: i64::MAX as u64, - // method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, - // params: fvm_ipld_encoding::RawBytes::default(), - // value: Default::default(), - // version: Default::default(), - // gas_fee_cap: Default::default(), - // gas_premium: Default::default(), - // }; - // - // let apply_ret = self.apply_implicit_message(msg)?; - // let r = - // fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) - // .context("failed to parse validator activities")?; - // Ok(ActivitySummary { - // block_range: (r.start_height, self.epoch), - // details: r.activities, - // }) - todo!() + fn get_activities_summary( + &self, + ) -> anyhow::Result> { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: self.epoch as u64, + // exclude this from gas restriction + gas_limit: i64::MAX as u64, + method_num: fendermint_actor_activity_tracker::Method::GetActivities as u64, + params: fvm_ipld_encoding::RawBytes::default(), + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let apply_ret = self.executor.call_state()?.call(msg)?; + let r = fvm_ipld_encoding::from_slice::( + &apply_ret.msg_receipt.return_data, + ) + .context("failed to parse validator activities")?; + Ok(ActivityDetails { + details: r.activities, + }) } fn purge_activities(&mut self) -> anyhow::Result<()> { @@ -87,14 +93,9 @@ impl <'a, E: Executor> ValidatorActivityTracker for ActorActivityTracker<'a, E> } } -impl <'a, E: Executor> ActorActivityTracker<'a, E> { - fn apply_implicit_message<>( - &mut self, - msg: FvmMessage, - ) -> anyhow::Result { - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = self.executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; - +impl<'a, DB: Blockstore + Clone + 'static> ActorActivityTracker<'a, DB> { + fn apply_implicit_message(&mut self, msg: FvmMessage) -> anyhow::Result { + let (apply_ret, _) = self.executor.execute_implicit(msg)?; if let Some(err) = apply_ret.failure_info { anyhow::bail!("failed to apply activity tracker messages: {}", err) } else { diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs index db79ff40e6..0aa2ff0b0c 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/merkle.rs @@ -3,6 +3,7 @@ use anyhow::Context; use fendermint_actor_activity_tracker::ValidatorSummary; +use fvm_shared::clock::ChainEpoch; use ipc_api::evm::payload_to_evm_address; use ipc_observability::lazy_static; use merkle_tree_rs::format::Raw; @@ -12,7 +13,7 @@ pub type Hash = ethers::types::H256; lazy_static!( /// ABI types of the Merkle tree which contains validator addresses and their voting power. - pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; ); /// The merkle tree based proof verification to interact with solidity contracts @@ -26,15 +27,19 @@ impl MerkleProofGen { } } -impl TryFrom<&[ValidatorSummary]> for MerkleProofGen { - type Error = anyhow::Error; - - fn try_from(values: &[ValidatorSummary]) -> Result { +impl MerkleProofGen { + pub fn new(values: &[ValidatorSummary], checkpoint_height: ChainEpoch) -> anyhow::Result { let values = values .iter() .map(|t| { - payload_to_evm_address(t.validator.payload()) - .map(|addr| vec![format!("{addr:?}"), t.block_committed.to_string(), hex::encode(&t.metadata)]) + payload_to_evm_address(t.validator.payload()).map(|addr| { + vec![ + checkpoint_height.to_string(), + format!("{addr:?}"), + t.block_committed.to_string(), + hex::encode(&t.metadata), + ] + }) }) .collect::>>()?; @@ -42,4 +47,4 @@ impl TryFrom<&[ValidatorSummary]> for MerkleProofGen { .context("failed to construct Merkle tree")?; Ok(MerkleProofGen { tree }) } -} \ No newline at end of file +} diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs index 99ab166744..251c47bd0b 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/activities/mod.rs @@ -7,19 +7,20 @@ pub mod actor; mod merkle; -use std::fmt::Debug; +use crate::fvm::activities::merkle::MerkleProofGen; use fendermint_actor_activity_tracker::ValidatorSummary; use fendermint_crypto::PublicKey; -use ipc_api::checkpoint::ActivityCommitment; -use crate::fvm::activities::merkle::MerkleProofGen; +use fvm_shared::clock::ChainEpoch; +use ipc_api::checkpoint::ActivitySummary; +use std::fmt::Debug; pub struct BlockMined { pub(crate) validator: PublicKey, } #[derive(Debug, Clone)] -pub struct ActivitySummary { - pub details: Vec +pub struct ActivityDetails { + pub details: Vec, } /// Tracks the validator activities in the current blockchain @@ -30,17 +31,20 @@ pub trait ValidatorActivityTracker { fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()>; /// Get the validators activities summary since the checkpoint height - fn get_activities_summary(&self) -> anyhow::Result>; + fn get_activities_summary( + &self, + ) -> anyhow::Result>; /// Purge the current validator activities summary fn purge_activities(&mut self) -> anyhow::Result<()>; } -impl ActivitySummary { - pub fn commitment(&self) -> anyhow::Result { - let gen = MerkleProofGen::try_from(self.details.as_slice())?; - Ok(ActivityCommitment{ - summary: gen.root().to_fixed_bytes().to_vec() +impl ActivityDetails { + pub fn commitment(&self, checkpoint_height: ChainEpoch) -> anyhow::Result { + let gen = MerkleProofGen::new(self.details.as_slice(), checkpoint_height)?; + Ok(ActivitySummary { + total_active_validators: self.details.len() as u64, + commitment: gen.root().to_fixed_bytes().to_vec(), }) } -} \ No newline at end of file +} diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 38dea88465..aa56f50863 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -19,6 +19,8 @@ use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::BottomUpCheckpoint; use fendermint_vm_genesis::{Power, Validator, ValidatorKey}; +use ipc_api::evm::payload_to_evm_address; + use ipc_actors_abis::checkpointing_facet as checkpoint; use ipc_actors_abis::gateway_getter_facet as getter; use ipc_api::staking::ConfigurationNumber; @@ -97,6 +99,8 @@ where let num_msgs = msgs.len(); + let activities = state.activities_tracker().get_activities_summary()?; + // Construct checkpoint. let checkpoint = BottomUpCheckpoint { subnet_id, @@ -104,13 +108,31 @@ where block_hash, next_configuration_number, msgs, - activities: state.activities_tracker().get_activities_summary()?.commitment()?.try_into()?, + activities: activities.commitment(height.value() as i64)?.try_into()?, }; // Save the checkpoint in the ledger. // Pass in the current power table, because these are the validators who can sign this checkpoint. + + // gateway + // .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) + // .context("failed to store checkpoint")?; + + let report = checkpoint::ActivityReport { + validators: activities + .details + .into_iter() + .map(|v| { + Ok(checkpoint::ValidatorActivityReport { + validator: payload_to_evm_address(v.validator.payload())?, + blocks_committed: v.block_committed, + metadata: ethers::types::Bytes::from(v.metadata), + }) + }) + .collect::>>()?, + }; gateway - .create_bottom_up_checkpoint(state, checkpoint.clone(), &curr_power_table.0) + .create_bu_ckpt_with_activities(state, checkpoint.clone(), &curr_power_table.0, report) .context("failed to store checkpoint")?; state.activities_tracker().purge_activities()?; @@ -246,9 +268,10 @@ where block_hash: cp.block_hash, next_configuration_number: cp.next_configuration_number, msgs: convert_tokenizables(cp.msgs)?, - activities: checkpoint::ActivityCommitment { - summary: cp.activities.summary, - } + activities: checkpoint::ActivitySummary { + total_active_validators: cp.activities.total_active_validators, + commitment: cp.activities.commitment, + }, }; // We mustn't do these in parallel because of how nonces are fetched. diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 930fda16da..cd139f1174 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -5,6 +5,8 @@ use anyhow::Context; use async_trait::async_trait; use std::collections::HashMap; +use crate::fvm::activities::{BlockMined, ValidatorActivityTracker}; +use crate::ExecInterpreter; use fendermint_vm_actor_interface::{chainmetadata, cron, system}; use fvm::executor::ApplyRet; use fvm_ipld_blockstore::Blockstore; @@ -12,17 +14,11 @@ use fvm_shared::{address::Address, ActorID, MethodNum, BLOCK_GAS_LIMIT}; use ipc_observability::{emit, measure_time, observe::TracingError, Traceable}; use tendermint_rpc::Client; -use crate::fvm::gas::{GasMarket, GasUtilization}; -use crate::ExecInterpreter; -use crate::fvm::activities::BlockMined; - -use crate::fvm::activities::ValidatorActivityTracker; - use super::{ checkpoint::{self, PowerUpdates}, observe::{CheckpointFinalized, MsgExec, MsgExecPurpose}, state::FvmExecState, - FvmMessage, FvmMessageInterpreter, + BlockGasLimit, FvmMessage, FvmMessageInterpreter, }; /// The return value extended with some things from the message that @@ -49,10 +45,10 @@ where type Message = FvmMessage; type BeginOutput = FvmApplyRet; type DeliverOutput = FvmApplyRet; - /// Return validator power updates. + /// Return validator power updates and the next base fee. /// Currently ignoring events as there aren't any emitted by the smart contract, /// but keep in mind that if there were, those would have to be propagated. - type EndOutput = PowerUpdates; + type EndOutput = (PowerUpdates, BlockGasLimit); async fn begin( &self, @@ -161,24 +157,15 @@ where (apply_ret, emitters, latency) } else { - let available_gas = state.gas_market().available().block_gas; - if msg.gas_limit > available_gas { + if let Err(err) = state.block_gas_tracker().ensure_sufficient_gas(&msg) { // This is panic-worthy, but we suppress it to avoid liveness issues. // Consider maybe record as evidence for the validator slashing? - tracing::warn!( - txn_gas_limit = msg.gas_limit, - block_gas_available = available_gas, - "[ASSERTION FAILED] message gas limit exceed available block gas limit; consensus engine is misbehaving" - ); + tracing::warn!("insufficient block gas; continuing to avoid halt, but this should've not happened: {}", err); } let (execution_result, latency) = measure_time(|| state.execute_explicit(msg.clone())); let (apply_ret, emitters) = execution_result?; - state - .gas_market_mut() - .record_utilization(GasUtilization::from(&apply_ret)); - (apply_ret, emitters, latency) }; @@ -205,11 +192,13 @@ where } async fn end(&self, mut state: Self::State) -> anyhow::Result<(Self::State, Self::EndOutput)> { - if let Some(pubkey) = state.validator_pubkey() { - state.activities_tracker().track_block_mined(BlockMined { validator: pubkey })?; - } + let next_gas_market = state.finalize_gas_market()?; - state.update_gas_market()?; + if let Some(pubkey) = state.block_producer() { + state + .activities_tracker() + .track_block_mined(BlockMined { validator: pubkey })?; + } // TODO: Consider doing this async, since it's purely informational and not consensus-critical. let _ = checkpoint::emit_trace_if_check_checkpoint_finalized(&self.gateway, &mut state) @@ -269,6 +258,7 @@ where PowerUpdates::default() }; - Ok((state, updates)) + let ret = (updates, next_gas_market.block_gas_limit); + Ok((state, ret)) } } diff --git a/fendermint/vm/interpreter/src/fvm/externs.rs b/fendermint/vm/interpreter/src/fvm/externs.rs index f17e03f687..7f02b06118 100644 --- a/fendermint/vm/interpreter/src/fvm/externs.rs +++ b/fendermint/vm/interpreter/src/fvm/externs.rs @@ -36,6 +36,18 @@ where } } +impl FendermintExterns +where + DB: Blockstore + 'static + Clone, +{ + pub fn read_only_clone(&self) -> FendermintExterns> { + FendermintExterns { + blockstore: ReadOnlyBlockstore::new(self.blockstore.clone()), + state_root: self.state_root, + } + } +} + impl Rand for FendermintExterns where DB: Blockstore + 'static, diff --git a/fendermint/vm/interpreter/src/fvm/gas.rs b/fendermint/vm/interpreter/src/fvm/gas.rs new file mode 100644 index 0000000000..a0c5289b41 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/gas.rs @@ -0,0 +1,164 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::FvmMessage; +use anyhow::{bail, Context}; + +use fendermint_actors_api::gas_market::{Gas, Reading, Utilization}; +use fendermint_vm_actor_interface::gas_market::GAS_MARKET_ACTOR_ADDR; +use fendermint_vm_actor_interface::{reward, system}; +use fvm::executor::{ApplyKind, ApplyRet, Executor}; +use fvm_shared::address::Address; +use fvm_shared::econ::TokenAmount; +use fvm_shared::METHOD_SEND; +use num_traits::Zero; + +#[derive(Debug, Clone)] +pub struct BlockGasTracker { + /// The current base fee. + base_fee: TokenAmount, + /// The current block gas limit. + block_gas_limit: Gas, + /// The cumulative gas premiums claimable by the block producer. + cumul_gas_premium: TokenAmount, + /// The accumulated gas usage throughout the block. + cumul_gas_used: Gas, +} + +impl BlockGasTracker { + pub fn create(executor: &mut E) -> anyhow::Result { + let mut ret = Self { + base_fee: Zero::zero(), + block_gas_limit: Zero::zero(), + cumul_gas_premium: Zero::zero(), + cumul_gas_used: Zero::zero(), + }; + + let reading = Self::read_gas_market(executor)?; + + ret.base_fee = reading.base_fee; + ret.block_gas_limit = reading.block_gas_limit; + + Ok(ret) + } + + pub fn available(&self) -> Gas { + self.block_gas_limit.saturating_sub(self.cumul_gas_used) + } + + pub fn ensure_sufficient_gas(&self, msg: &FvmMessage) -> anyhow::Result<()> { + let available_gas = self.available(); + if msg.gas_limit > available_gas { + bail!("message gas limit exceed available block gas limit; consensus engine may be misbehaving; txn gas limit: {}, block gas available: {}", + msg.gas_limit, + available_gas + ); + } + Ok(()) + } + + pub fn record_utilization(&mut self, ret: &ApplyRet) { + self.cumul_gas_premium += ret.miner_tip.clone(); + self.cumul_gas_used = self.cumul_gas_used.saturating_add(ret.msg_receipt.gas_used); + + // sanity check, should not happen; only trace if it does so we can debug later. + if self.cumul_gas_used >= self.block_gas_limit { + tracing::warn!("out of block gas; cumulative gas used exceeds block gas limit!"); + } + } + + pub fn finalize( + &self, + executor: &mut E, + premium_recipient: Option
, + ) -> anyhow::Result { + if let Some(premium_recipient) = premium_recipient { + self.distribute_premiums(executor, premium_recipient)? + } + self.commit_utilization(executor) + } + + pub fn read_gas_market(executor: &mut E) -> anyhow::Result { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, + sequence: 0, // irrelevant for implicit executions. + gas_limit: i64::MAX as u64, + method_num: fendermint_actors_api::gas_market::Method::CurrentReading as u64, + params: fvm_ipld_encoding::RawBytes::default(), + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let apply_ret = Self::apply_implicit_message(executor, msg)?; + + if let Some(err) = apply_ret.failure_info { + bail!("failed to acquire gas market reading: {}", err); + } + + fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse gas market reading") + } + + fn commit_utilization(&self, executor: &mut E) -> anyhow::Result { + let params = fvm_ipld_encoding::RawBytes::serialize(Utilization { + block_gas_used: self.cumul_gas_used, + })?; + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: GAS_MARKET_ACTOR_ADDR, + sequence: 0, // irrelevant for implicit executions. + gas_limit: i64::MAX as u64, + method_num: fendermint_actors_api::gas_market::Method::UpdateUtilization as u64, + params, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + let apply_ret = Self::apply_implicit_message(executor, msg)?; + fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse gas utilization result") + } + + fn distribute_premiums( + &self, + executor: &mut E, + premium_recipient: Address, + ) -> anyhow::Result<()> { + if self.cumul_gas_premium.is_zero() { + return Ok(()); + } + + let msg = FvmMessage { + from: reward::REWARD_ACTOR_ADDR, + to: premium_recipient, + sequence: 0, // irrelevant for implicit executions. + gas_limit: i64::MAX as u64, + method_num: METHOD_SEND, + params: fvm_ipld_encoding::RawBytes::default(), + value: self.cumul_gas_premium.clone(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + Self::apply_implicit_message(executor, msg)?; + + Ok(()) + } + + fn apply_implicit_message( + executor: &mut E, + msg: FvmMessage, + ) -> anyhow::Result { + let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, 0)?; + if let Some(err) = apply_ret.failure_info { + bail!("failed to apply message: {}", err) + } + Ok(apply_ret) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/gas/actor.rs b/fendermint/vm/interpreter/src/fvm/gas/actor.rs deleted file mode 100644 index 6efe355882..0000000000 --- a/fendermint/vm/interpreter/src/fvm/gas/actor.rs +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::fvm::gas::{Available, CommitRet, Gas, GasMarket, GasUtilization}; -use crate::fvm::FvmMessage; -use anyhow::{anyhow, Context}; - -use fendermint_actor_gas_market::{BlockGasUtilizationRet, GasMarketReading, SetConstants}; -use fendermint_crypto::PublicKey; -use fendermint_vm_actor_interface::eam::EthAddress; -use fendermint_vm_actor_interface::gas::GAS_MARKET_ACTOR_ADDR; -use fendermint_vm_actor_interface::{reward, system}; -use fvm::executor::{ApplyKind, ApplyRet, Executor}; -use fvm_shared::address::Address; -use fvm_shared::clock::ChainEpoch; -use fvm_shared::econ::TokenAmount; -use fvm_shared::METHOD_SEND; - -#[derive(Default)] -pub struct ActorGasMarket { - /// The base fee for fvm - base_fee: TokenAmount, - /// The total gas premium for the miner - gas_premium: TokenAmount, - /// The block gas limit - block_gas_limit: Gas, - /// The accumulated gas usage so far - block_gas_used: Gas, - /// Pending update to the underlying gas actor - constant_update: Option, -} - -impl GasMarket for ActorGasMarket { - type Constant = SetConstants; - - fn set_constants(&mut self, constants: Self::Constant) { - self.constant_update = Some(constants); - } - - fn available(&self) -> Available { - Available { - block_gas: self.block_gas_limit - self.block_gas_used.min(self.block_gas_limit), - } - } - - fn record_utilization(&mut self, utilization: GasUtilization) { - self.gas_premium += utilization.gas_premium; - self.block_gas_used += utilization.gas_used; - - // sanity check - if self.block_gas_used >= self.block_gas_limit { - tracing::warn!("out of block gas, vm execution more than available gas limit"); - } - } -} - -impl ActorGasMarket { - pub fn current_reading( - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::CurrentReading as u64, - params: fvm_ipld_encoding::RawBytes::default(), - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to read gas market state: {}", err); - } - - let r = - fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) - .context("failed to parse gas market readying")?; - Ok(r) - } - - pub fn create( - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let reading = Self::current_reading(executor, block_height)?; - Ok(Self { - base_fee: reading.base_fee, - gas_premium: TokenAmount::from_atto(0), - block_gas_limit: reading.block_gas_limit, - block_gas_used: 0, - constant_update: None, - }) - } - - pub fn take_constant_update(&mut self) -> Option { - self.constant_update.take() - } - - pub fn commit( - &self, - executor: &mut E, - block_height: ChainEpoch, - validator: Option, - ) -> anyhow::Result { - self.distribute_reward(executor, block_height, validator)?; - self.commit_constants(executor, block_height)?; - self.commit_utilization(executor, block_height) - } - - fn distribute_reward( - &self, - executor: &mut E, - block_height: ChainEpoch, - validator: Option, - ) -> anyhow::Result<()> { - if validator.is_none() || self.gas_premium.is_zero() { - return Ok(()); - } - - let validator = validator.unwrap(); - let validator = Address::from(EthAddress::new_secp256k1(&validator.serialize())?); - - let msg = FvmMessage { - from: reward::REWARD_ACTOR_ADDR, - to: validator, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: METHOD_SEND, - params: fvm_ipld_encoding::RawBytes::default(), - value: self.gas_premium.clone(), - - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - self.apply_implicit_message(msg, executor)?; - - Ok(()) - } - - fn commit_constants( - &self, - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result<()> { - let Some(ref constants) = self.constant_update else { - return Ok(()); - }; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::SetConstants as u64, - params: fvm_ipld_encoding::RawBytes::serialize(constants)?, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - self.apply_implicit_message(msg, executor)?; - - Ok(()) - } - - fn commit_utilization( - &self, - executor: &mut E, - block_height: ChainEpoch, - ) -> anyhow::Result { - let block_gas_used = self.block_gas_used.min(self.block_gas_limit); - let params = fvm_ipld_encoding::RawBytes::serialize( - fendermint_actor_gas_market::BlockGasUtilization { block_gas_used }, - )?; - - let msg = FvmMessage { - from: system::SYSTEM_ACTOR_ADDR, - to: GAS_MARKET_ACTOR_ADDR, - sequence: block_height as u64, - // exclude this from gas restriction - gas_limit: i64::MAX as u64, - method_num: fendermint_actor_gas_market::Method::UpdateUtilization as u64, - params, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - let apply_ret = self.apply_implicit_message(msg, executor)?; - let r = fvm_ipld_encoding::from_slice::( - &apply_ret.msg_receipt.return_data, - ) - .context("failed to parse gas utilization result")?; - Ok(CommitRet { - base_fee: r.base_fee, - }) - } - - fn apply_implicit_message( - &self, - msg: FvmMessage, - executor: &mut E, - ) -> anyhow::Result { - let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; - let apply_ret = executor.execute_message(msg, ApplyKind::Implicit, raw_length)?; - - if let Some(err) = apply_ret.failure_info { - anyhow::bail!("failed to update EIP1559 gas state: {}", err) - } else { - Ok(apply_ret) - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/gas/mod.rs b/fendermint/vm/interpreter/src/fvm/gas/mod.rs deleted file mode 100644 index 3d189276f1..0000000000 --- a/fendermint/vm/interpreter/src/fvm/gas/mod.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use fvm::executor::ApplyRet; -use fvm_shared::econ::TokenAmount; - -pub mod actor; - -pub type Gas = u64; - -pub struct Available { - pub block_gas: Gas, -} - -pub struct CommitRet { - pub base_fee: TokenAmount, -} - -pub struct GasUtilization { - gas_used: Gas, - gas_premium: TokenAmount, -} - -/// The gas market for fendermint. This should be backed by an fvm actor. -pub trait GasMarket { - /// The constant parameters that determines the readings of gas market, such as block gas limit. - type Constant; - - /// Update the constants of the gas market. If the gas market is actor based, then it's recommended - /// to flush at EndBlock. - #[allow(dead_code)] - fn set_constants(&mut self, constants: Self::Constant); - - /// Obtain the current block gas available for execution - fn available(&self) -> Available; - - /// Tracks the amount of gas consumed by a transaction - fn record_utilization(&mut self, gas: GasUtilization); -} - -impl From<&ApplyRet> for GasUtilization { - fn from(ret: &ApplyRet) -> Self { - Self { - gas_used: ret.msg_receipt.gas_used, - gas_premium: ret.miner_tip.clone(), - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 0f979036a8..98ab043b82 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -12,11 +12,12 @@ pub mod state; pub mod store; pub mod upgrades; +pub mod activities; #[cfg(any(test, feature = "bundle"))] pub mod bundle; -pub mod gas; + +pub(crate) mod gas; pub(crate) mod topdown; -pub mod activities; pub use check::FvmCheckRet; pub use checkpoint::PowerUpdates; @@ -31,6 +32,8 @@ pub use self::broadcast::Broadcaster; use self::{state::ipc::GatewayCaller, upgrades::UpgradeScheduler}; pub type FvmMessage = fvm_shared::message::Message; +pub type BaseFee = fvm_shared::econ::TokenAmount; +pub type BlockGasLimit = u64; #[derive(Clone)] pub struct ValidatorContext { diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index 525adffda4..f26f634bf7 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -1,12 +1,22 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; +use crate::fvm::activities::actor::ActorActivityTracker; +use crate::fvm::externs::FendermintExterns; +use crate::fvm::gas::BlockGasTracker; +use crate::fvm::store::ReadOnlyBlockstore; use anyhow::Ok; use cid::Cid; +use fendermint_actors_api::gas_market::Reading; use fendermint_crypto::PublicKey; +use fendermint_vm_actor_interface::eam::EthAddress; +use fendermint_vm_core::{chainid::HasChainID, Timestamp}; +use fendermint_vm_encoding::IsHumanReadable; use fendermint_vm_genesis::PowerScale; +use fvm::engine::EnginePool; use fvm::{ call_manager::DefaultCallManager, engine::MultiEngine, @@ -24,12 +34,6 @@ use fvm_shared::{ use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::fvm::externs::FendermintExterns; -use crate::fvm::gas::actor::ActorGasMarket; -use fendermint_vm_core::{chainid::HasChainID, Timestamp}; -use fendermint_vm_encoding::IsHumanReadable; -use crate::fvm::activities::actor::ActorActivityTracker; - pub type BlockHash = [u8; 32]; pub type ActorAddressMap = HashMap; @@ -37,8 +41,6 @@ pub type ActorAddressMap = HashMap; /// The result of the message application bundled with any delegated addresses of event emitters. pub type ExecResult = anyhow::Result<(ApplyRet, ActorAddressMap)>; -pub type StateExecutor = DefaultExecutor>>>>; - /// Parts of the state which evolve during the lifetime of the chain. #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] @@ -101,25 +103,23 @@ where executor: DefaultExecutor< DefaultKernel>>>, >, - /// Hash of the block currently being executed. For queries and checks this is empty. /// /// The main motivation to add it here was to make it easier to pass in data to the /// execution interpreter without having to add yet another piece to track at the app level. block_hash: Option, - - /// Public key of the validator who created this block. For queries and checks this is empty. - validator_pubkey: Option, + /// Public key of the validator who created this block. For queries, checks, and proposal + /// validations this is None. + block_producer: Option, + /// Keeps track of block gas usage during execution, and takes care of updating + /// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). + block_gas_tracker: BlockGasTracker, /// State of parameters that are outside the control of the FVM but can change and need to be persisted. params: FvmUpdatableParams, - /// Indicate whether the parameters have been updated. params_dirty: bool, - /// Keeps track of block gas usage during execution, and takes care of updating - /// the chosen gas market strategy (by default an on-chain actor delivering EIP-1559 behaviour). - gas_market: ActorGasMarket, - // chain_info: (NetworkVersion, ChainID, EnginePool), + executor_info: ExecutorInfo, } impl FvmExecState @@ -152,14 +152,16 @@ where let engine = multi_engine.get(&nc)?; let externs = FendermintExterns::new(blockstore.clone(), params.state_root); - let machine = DefaultMachine::new(&mc, blockstore, externs)?; + let machine = DefaultMachine::new(&mc, blockstore.clone(), externs)?; let mut executor = DefaultExecutor::new(engine.clone(), machine)?; - let gas_market = ActorGasMarket::create(&mut executor, block_height)?; + + let block_gas_tracker = BlockGasTracker::create(&mut executor)?; Ok(Self { executor, block_hash: None, - validator_pubkey: None, + block_producer: None, + block_gas_tracker, params: FvmUpdatableParams { app_version: params.app_version, base_fee: params.base_fee, @@ -167,8 +169,11 @@ where power_scale: params.power_scale, }, params_dirty: false, - gas_market, - // chain_info: (params.network_version, ChainID::from(params.chain_id), engine), + + executor_info: ExecutorInfo { + engine_pool: engine, + store: blockstore.clone(), + }, }) } @@ -179,17 +184,21 @@ where } /// Set the validator during execution. - pub fn with_validator(mut self, key: PublicKey) -> Self { - self.validator_pubkey = Some(key); + pub fn with_block_producer(mut self, pubkey: PublicKey) -> Self { + self.block_producer = Some(pubkey); self } - pub fn gas_market_mut(&mut self) -> &mut ActorGasMarket { - &mut self.gas_market + pub fn block_gas_tracker(&self) -> &BlockGasTracker { + &self.block_gas_tracker + } + + pub fn block_gas_tracker_mut(&mut self) -> &mut BlockGasTracker { + &mut self.block_gas_tracker } - pub fn gas_market(&self) -> &ActorGasMarket { - &self.gas_market + pub fn read_gas_market(&mut self) -> anyhow::Result { + BlockGasTracker::read_gas_market(&mut self.executor) } /// Execute message implicitly. @@ -211,9 +220,27 @@ where let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; let ret = self.executor.execute_message(msg, kind, raw_length)?; let addrs = self.emitter_delegated_addresses(&ret)?; + + // Record the utilization of this message if the apply type was Explicit. + if kind == ApplyKind::Explicit { + self.block_gas_tracker.record_utilization(&ret); + } + Ok((ret, addrs)) } + /// Execute a function with the internal executor and return an arbitrary result. + pub fn execute_with_executor(&mut self, exec_func: F) -> anyhow::Result + where + F: FnOnce( + &mut DefaultExecutor< + DefaultKernel>>>, + >, + ) -> anyhow::Result, + { + exec_func(&mut self.executor) + } + /// Commit the state. It must not fail, but we're returning a result so that error /// handling can be done in the application root. /// @@ -236,9 +263,9 @@ where self.block_hash } - /// Identity of the block creator, if we are indeed executing any blocks. - pub fn validator_pubkey(&self) -> Option { - self.validator_pubkey + /// Identity of the block producer, if we are indeed executing any blocks. + pub fn block_producer(&self) -> Option { + self.block_producer } /// The timestamp of the currently executing block. @@ -275,8 +302,11 @@ where self.executor.context().network.chain_id } - pub fn activities_tracker(&mut self) -> ActorActivityTracker> { - ActorActivityTracker { epoch: self.block_height(), executor: &mut self.executor } + pub fn activities_tracker(&mut self) -> ActorActivityTracker { + ActorActivityTracker { + epoch: self.block_height(), + executor: self, + } } /// Collect all the event emitters' delegated addresses, for those who have any. @@ -308,13 +338,19 @@ where self.update_params(|p| f(&mut p.app_version)) } - pub fn update_gas_market(&mut self) -> anyhow::Result<()> { - let height = self.block_height(); - let ret = self - .gas_market - .commit(&mut self.executor, height, self.validator_pubkey)?; - self.params.base_fee = ret.base_fee; - Ok(()) + /// Finalizes updates to the gas market based on the transactions processed by this instance. + /// Returns the new base fee for the next height. + pub fn finalize_gas_market(&mut self) -> anyhow::Result { + let premium_recipient = match self.block_producer { + Some(pubkey) => Some(Address::from(EthAddress::new_secp256k1( + &pubkey.serialize(), + )?)), + None => None, + }; + + self.block_gas_tracker + .finalize(&mut self.executor, premium_recipient) + .inspect(|reading| self.update_params(|p| p.base_fee = reading.base_fee.clone())) } /// Update the circulating supply, effective from the next block. @@ -334,18 +370,21 @@ where self.params_dirty = true; } - // pub fn call_only(&self) -> FvmCallState { - // let mut nc = NetworkConfig::new(self.chain_info.0); - // nc.chain_id = self.chain_info.1; - // - // let engine = self.chain_info.2.clone(); - // - // self.executor.blockstore().clone() - // let externs = FendermintExterns::new(blockstore.clone(), params.state_root); - // let machine = DefaultMachine::new(&mc, blockstore, externs)?; - // let mut executor = DefaultExecutor::new(engine, machine)?; - // - // } + pub fn call_state(&self) -> anyhow::Result> { + let externs = self.executor.externs().read_only_clone(); + let machine = DefaultMachine::new( + self.executor.context(), + ReadOnlyBlockstore::new(self.executor_info.store.clone()), + externs, + )?; + + Ok(FvmCallState { + executor: RefCell::new(DefaultExecutor::new( + self.executor_info.engine_pool.clone(), + machine, + )?), + }) + } } impl HasChainID for FvmExecState @@ -387,19 +426,32 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { (ret, Default::default()) } -// /// Compared to FvmExecState, this is used mostly for getters -// pub struct FvmCallState { -// #[allow(clippy::type_complexity)] -// executor: RefCell, FendermintExterns>>>>, -// >>, -// } -// -// impl FvmExecState -// where -// DB: Blockstore + Clone + 'static, -// { -// pub fn call(&self, message: Message) -> anyhow::Result { -// -// } -// } \ No newline at end of file +/// Tracks the metadata about the executor, so that it can be used to clone itself or create call state +struct ExecutorInfo { + engine_pool: EnginePool, + store: DB, +} + +type CallExecutor = DefaultExecutor< + DefaultKernel< + DefaultCallManager< + DefaultMachine, FendermintExterns>>, + >, + >, +>; + +/// A state we create for the calling the getters through fvm +pub struct FvmCallState +where + DB: Blockstore + Clone + 'static, +{ + executor: RefCell>, +} + +impl FvmCallState { + pub fn call(&self, msg: Message) -> anyhow::Result { + let mut inner = self.executor.borrow_mut(); + let raw_length = fvm_ipld_encoding::to_vec(&msg).map(|bz| bz.len())?; + Ok(inner.execute_message(msg, ApplyKind::Implicit, raw_length)?) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 12caa26c6f..0cbd973134 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -138,6 +138,33 @@ impl GatewayCaller { }) } + /// Insert a new checkpoint at the period boundary. + pub fn create_bu_ckpt_with_activities( + &self, + state: &mut FvmExecState, + checkpoint: checkpointing_facet::BottomUpCheckpoint, + power_table: &[Validator], + activities: checkpointing_facet::ActivityReport, + ) -> anyhow::Result<()> { + // Construct a Merkle tree from the power table, which we can use to validate validator set membership + // when the signatures are submitted in transactions for accumulation. + let tree = + ValidatorMerkleTree::new(power_table).context("failed to create validator tree")?; + + let total_power = power_table.iter().fold(et::U256::zero(), |p, v| { + p.saturating_add(et::U256::from(v.power.0)) + }); + + self.checkpointing.call(state, |c| { + c.create_bu_chpt_with_activities( + checkpoint, + tree.root_hash().0, + total_power, + activities, + ) + }) + } + /// Retrieve checkpoints which have not reached a quorum. pub fn incomplete_checkpoints( &self, diff --git a/fendermint/vm/interpreter/src/fvm/state/mod.rs b/fendermint/vm/interpreter/src/fvm/state/mod.rs index 22a1504e4c..ab3d9e7f6f 100644 --- a/fendermint/vm/interpreter/src/fvm/state/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/state/mod.rs @@ -1,13 +1,14 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +pub mod fevm; +pub mod ipc; +pub mod snapshot; + mod check; mod exec; -pub mod fevm; mod genesis; -pub mod ipc; mod query; -pub mod snapshot; use std::sync::Arc; diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 8334272363..03331ec0ce 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -18,9 +18,7 @@ use fendermint_eth_hardhat::{ContractSourceAndName, Hardhat, FQN}; use fendermint_vm_actor_interface::diamond::{EthContract, EthContractMap}; use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_actor_interface::ipc::IPC_CONTRACTS; -use fendermint_vm_actor_interface::{ - account, burntfunds, chainmetadata, cron, eam, gas, init, ipc, reward, system, EMPTY_ARR, -}; +use fendermint_vm_actor_interface::{account, burntfunds, chainmetadata, cron, eam, gas_market, init, ipc, reward, system, EMPTY_ARR, activity}; use fendermint_vm_core::{chainid, Timestamp}; use fendermint_vm_genesis::{ActorMeta, Collateral, Genesis, Power, PowerScale, Validator}; use futures_util::io::Cursor; @@ -430,22 +428,38 @@ impl GenesisBuilder { ) .context("failed to replace built in eam actor")?; - // currently hard code them for now, once genesis V2 is implemented, should be taken - // from genesis. - // initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) - let initial_base_fee = TokenAmount::from_atto(1_000_000_000); - let gas_market_state = fendermint_actor_gas_market::EIP1559GasState::from( - fendermint_actor_gas_market::GasActorConstructorParams::new(initial_base_fee), - ); + // Currently hardcoded for now, once genesis V2 is implemented, should be taken + // from genesis parameters. + // + // Default initial base fee equals minimum base fee in Filecoin. + let initial_base_fee = TokenAmount::from_atto(100); + // We construct the actor state here for simplicity, but for better decoupling we should + // be invoking the constructor instead. + let gas_market_state = fendermint_actor_gas_market_eip1559::State { + base_fee: initial_base_fee, + // If you need to customize the gas market constants, you can do so here. + constants: fendermint_actor_gas_market_eip1559::Constants::default(), + }; state .create_custom_actor( - fendermint_actor_gas_market::IPC_GAS_MARKET_ACTOR_NAME, - gas::GAS_MARKET_ACTOR_ID, + fendermint_actor_gas_market_eip1559::ACTOR_NAME, + gas_market::GAS_MARKET_ACTOR_ID, &gas_market_state, TokenAmount::zero(), None, ) - .context("failed to create gas market actor")?; + .context("failed to create default eip1559 gas market actor")?; + + let tracker_state = fendermint_actor_activity_tracker::State::new(state.store())?; + state + .create_custom_actor( + fendermint_actor_activity_tracker::IPC_ACTIVITY_TRACKER_ACTOR_NAME, + activity::ACTIVITY_TRACKER_ACTOR_ID, + &tracker_state, + TokenAmount::zero(), + None, + ) + .context("failed to create activity tracker actor")?; // STAGE 2: Create non-builtin accounts which do not have a fixed ID. @@ -577,6 +591,7 @@ fn deploy_contracts( let diamond_loupe_facet = facets.remove(0); let diamond_cut_facet = facets.remove(0); let ownership_facet = facets.remove(0); + let validator_reward_facet = facets.remove(0); debug_assert_eq!(facets.len(), 2, "SubnetRegistry has 2 facets of its own"); @@ -590,6 +605,7 @@ fn deploy_contracts( diamond_cut_facet: diamond_cut_facet.facet_address, diamond_loupe_facet: diamond_loupe_facet.facet_address, ownership_facet: ownership_facet.facet_address, + validator_reward_facet: validator_reward_facet.facet_address, subnet_getter_selectors: getter_facet.function_selectors, subnet_manager_selectors: manager_facet.function_selectors, subnet_rewarder_selectors: rewarder_facet.function_selectors, @@ -598,6 +614,7 @@ fn deploy_contracts( subnet_actor_diamond_cut_selectors: diamond_cut_facet.function_selectors, subnet_actor_diamond_loupe_selectors: diamond_loupe_facet.function_selectors, subnet_actor_ownership_selectors: ownership_facet.function_selectors, + validator_reward_selectors: validator_reward_facet.function_selectors, creation_privileges: 0, }; diff --git a/fendermint/vm/interpreter/src/selector.rs b/fendermint/vm/interpreter/src/selector.rs index e3ab7197cb..e9282140f9 100644 --- a/fendermint/vm/interpreter/src/selector.rs +++ b/fendermint/vm/interpreter/src/selector.rs @@ -3,7 +3,6 @@ //! Gas related message selection -use crate::fvm::gas::GasMarket; use crate::fvm::state::FvmExecState; use fendermint_vm_message::signed::SignedMessage; use fvm_ipld_blockstore::Blockstore; @@ -25,22 +24,21 @@ impl MessageSelector for GasLimitSelector { state: &FvmExecState, mut msgs: Vec, ) -> Vec { - let total_gas_limit = state.gas_market().available().block_gas; + let total_gas_limit = state.block_gas_tracker().available(); - // sort by gas limit descending + // Sort by gas limit descending msgs.sort_by(|a, b| b.message.gas_limit.cmp(&a.message.gas_limit)); let mut total_gas_limit_consumed = 0; - let mut selected = vec![]; - for msg in msgs { - if total_gas_limit_consumed + msg.message.gas_limit <= total_gas_limit { - total_gas_limit_consumed += msg.message.gas_limit; - selected.push(msg); - } else { - break; - } - } - - selected + msgs.into_iter() + .take_while(|msg| { + let gas_limit = msg.message.gas_limit; + let accepted = total_gas_limit_consumed + gas_limit <= total_gas_limit; + if accepted { + total_gas_limit_consumed += gas_limit; + } + accepted + }) + .collect() } } diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 4efc04b7f5..d67e648f43 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -76,14 +76,33 @@ pub struct BottomUpMsgBatch { pub msgs: Vec, } -#[serde_as] -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] /// The commitments for the child subnet activities that should be submitted to the parent subnet /// together with a bottom up checkpoint -pub struct ActivityCommitment { +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ValidatorSummary { + /// The checkpoint height in the child subnet + pub checkpoint_height: u64, + /// The validator address + pub validator: Address, + /// The number of blocks mined + pub blocks_committed: u64, + /// The extra metadata attached to the validator + pub metadata: Vec, +} + +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ValidatorClaimProof { + pub summary: ValidatorSummary, + pub proof: Vec<[u8; 32]>, +} + +#[serde_as] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] +pub struct ActivitySummary { + pub total_active_validators: u64, /// The activity summary for validators #[serde_as(as = "HumanReadable")] - pub summary: Vec, + pub commitment: Vec, // TODO: add relayed activity commitment } @@ -106,7 +125,7 @@ pub struct BottomUpCheckpoint { /// The list of messages for execution pub msgs: Vec, /// The activity commitment from child subnet to parent subnet - pub activities: ActivityCommitment, + pub activities: ActivitySummary, } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 72f8fdf137..3000591da7 100644 --- a/ipc/api/src/evm.rs +++ b/ipc/api/src/evm.rs @@ -4,8 +4,8 @@ //! Type conversion for IPC Agent struct with solidity contract struct use crate::address::IPCAddress; -use crate::checkpoint::{BottomUpCheckpoint, ActivityCommitment}; -use crate::checkpoint::BottomUpMsgBatch; +use crate::checkpoint::{ActivitySummary, BottomUpCheckpoint}; +use crate::checkpoint::{BottomUpMsgBatch, ValidatorClaimProof}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -18,10 +18,10 @@ use fvm_shared::address::{Address, Payload}; use fvm_shared::clock::ChainEpoch; use fvm_shared::econ::TokenAmount; use ipc_actors_abis::{ - gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, - register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, - subnet_actor_getter_facet, top_down_finality_facet, xnet_messaging_facet, - checkpointing_facet + checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, + lib_gateway, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_diamond, + subnet_actor_getter_facet, top_down_finality_facet, validator_reward_facet, + xnet_messaging_facet, }; /// The type conversion for IPC structs to evm solidity contracts. We need this convenient macro because @@ -122,15 +122,17 @@ macro_rules! cross_msg_types { /// The type conversion between different bottom up checkpoint definition in ethers and sdk macro_rules! bottom_up_checkpoint_conversion { ($module:ident) => { - impl TryFrom for $module::ActivityCommitment { + impl TryFrom for $module::ActivitySummary { type Error = anyhow::Error; - fn try_from(c: ActivityCommitment) -> Result { - Ok( - $module::ActivityCommitment { - summary: c.summary.try_into().map_err(|_| anyhow!("cannot convert bytes32"))?, - } - ) + fn try_from(c: ActivitySummary) -> Result { + Ok($module::ActivitySummary { + total_active_validators: c.total_active_validators, + commitment: c + .commitment + .try_into() + .map_err(|_| anyhow!("cannot convert bytes32"))?, + }) } } @@ -167,8 +169,9 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map(IpcEnvelope::try_from) .collect::, _>>()?, - activities: ActivityCommitment { - summary: value.activities.summary.to_vec(), + activities: ActivitySummary { + total_active_validators: value.activities.total_active_validators, + commitment: value.activities.commitment.to_vec(), }, }) } @@ -244,6 +247,7 @@ base_type_conversion!(gateway_getter_facet); base_type_conversion!(gateway_messenger_facet); base_type_conversion!(lib_gateway); base_type_conversion!(checkpointing_facet); +base_type_conversion!(validator_reward_facet); cross_msg_types!(checkpointing_facet); cross_msg_types!(gateway_getter_facet); @@ -273,6 +277,22 @@ impl TryFrom for AssetKind { } } +impl TryFrom for validator_reward_facet::ValidatorClaimProof { + type Error = anyhow::Error; + + fn try_from(v: ValidatorClaimProof) -> Result { + Ok(Self { + proof: v.proof, + summary: validator_reward_facet::ValidatorSummary { + checkpoint_height: v.summary.checkpoint_height, + validator: payload_to_evm_address(v.summary.validator.payload())?, + blocks_committed: v.summary.blocks_committed, + metadata: ethers::types::Bytes::from(v.summary.metadata), + }, + }) + } +} + /// Convert the ipc SubnetID type to a vec of evm addresses. It extracts all the children addresses /// in the subnet id and turns them as a vec of evm addresses. pub fn subnet_id_to_evm_addresses( diff --git a/ipc/api/src/subnet.rs b/ipc/api/src/subnet.rs index 386df79a3e..3213d35025 100644 --- a/ipc/api/src/subnet.rs +++ b/ipc/api/src/subnet.rs @@ -88,6 +88,7 @@ pub struct ConstructParams { pub supply_source: Asset, pub collateral_source: Asset, pub validator_gater: Address, + pub validator_rewarder: Address, } /// Consensus types supported by hierarchical consensus diff --git a/ipc/cli/src/commands/mod.rs b/ipc/cli/src/commands/mod.rs index 782c497fdb..54c803fa2d 100644 --- a/ipc/cli/src/commands/mod.rs +++ b/ipc/cli/src/commands/mod.rs @@ -8,6 +8,7 @@ mod crossmsg; // mod daemon; mod subnet; mod util; +mod validator; mod wallet; use crate::commands::checkpoint::CheckpointCommandsArgs; @@ -30,6 +31,7 @@ use std::path::Path; use std::str::FromStr; use crate::commands::config::ConfigCommandsArgs; +use crate::commands::validator::ValidatorCommandsArgs; use crate::commands::wallet::WalletCommandsArgs; use subnet::SubnetCommandsArgs; @@ -47,6 +49,7 @@ enum Commands { CrossMsg(CrossMsgsCommandsArgs), Checkpoint(CheckpointCommandsArgs), Util(UtilCommandsArgs), + Validator(ValidatorCommandsArgs), } #[derive(Debug, Parser)] @@ -133,6 +136,7 @@ pub async fn cli() -> anyhow::Result<()> { Commands::Wallet(args) => args.handle(global).await, Commands::Checkpoint(args) => args.handle(global).await, Commands::Util(args) => args.handle(global).await, + Commands::Validator(args) => args.handle(global).await, }; r.with_context(|| format!("error processing command {:?}", args.command)) diff --git a/ipc/cli/src/commands/subnet/create.rs b/ipc/cli/src/commands/subnet/create.rs index f170827f0a..fb1769d46e 100644 --- a/ipc/cli/src/commands/subnet/create.rs +++ b/ipc/cli/src/commands/subnet/create.rs @@ -42,6 +42,12 @@ impl CreateSubnet { .clone() .unwrap_or(ZERO_ADDRESS.to_string()); let validator_gater = require_fil_addr_from_str(&raw_addr)?; + + let raw_addr = arguments + .validator_rewarder + .clone() + .unwrap_or(ZERO_ADDRESS.to_string()); + let validator_rewarder = require_fil_addr_from_str(&raw_addr)?; let addr = provider .create_subnet( from, @@ -57,6 +63,7 @@ impl CreateSubnet { supply_source, collateral_source, validator_gater, + validator_rewarder, ) .await?; @@ -165,6 +172,8 @@ pub struct CreateSubnetArgs { help = "The address of validator gating contract. None if validator gating is disabled" )] pub validator_gater: Option, + #[arg(long, help = "The address of validator rewarder contract.")] + pub validator_rewarder: Option, #[arg( long, help = "The kind of collateral source of a subnet on its parent subnet: native or erc20", diff --git a/ipc/cli/src/commands/util/eth.rs b/ipc/cli/src/commands/util/eth.rs new file mode 100644 index 0000000000..0e0fa1208c --- /dev/null +++ b/ipc/cli/src/commands/util/eth.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT +//! Eth address util + +use async_trait::async_trait; +use clap::Args; +use fvm_shared::address::Address; +use ipc_api::evm::payload_to_evm_address; +use std::fmt::Debug; +use std::str::FromStr; + +use crate::{CommandLineHandler, GlobalArguments}; + +pub(crate) struct F4ToEthAddr; + +#[async_trait] +impl CommandLineHandler for F4ToEthAddr { + type Arguments = F4ToEthAddrArgs; + + async fn handle(_global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + let addr = Address::from_str(&arguments.addr)?; + let eth_addr = payload_to_evm_address(addr.payload())?; + log::info!("eth address: {:?}", eth_addr); + Ok(()) + } +} + +#[derive(Debug, Args)] +#[command(about = "Get Ethereum address for an F4")] +pub(crate) struct F4ToEthAddrArgs { + #[arg(long, help = "F4 address to get the underlying Ethereum addr from")] + pub addr: String, +} diff --git a/ipc/cli/src/commands/util/f4.rs b/ipc/cli/src/commands/util/f4.rs index 71b435e99d..175371853b 100644 --- a/ipc/cli/src/commands/util/f4.rs +++ b/ipc/cli/src/commands/util/f4.rs @@ -19,7 +19,7 @@ impl CommandLineHandler for EthToF4Addr { async fn handle(_global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { let eth_addr = EthAddress::from_str(&arguments.addr)?; - log::info!("f4 address: {:}", Address::from(eth_addr)); + log::info!("f4 address: {}", Address::from(eth_addr)); Ok(()) } } diff --git a/ipc/cli/src/commands/util/mod.rs b/ipc/cli/src/commands/util/mod.rs index f01102cbdd..698ffccbce 100644 --- a/ipc/cli/src/commands/util/mod.rs +++ b/ipc/cli/src/commands/util/mod.rs @@ -4,8 +4,10 @@ use crate::{CommandLineHandler, GlobalArguments}; use clap::{Args, Subcommand}; +use self::eth::{F4ToEthAddr, F4ToEthAddrArgs}; use self::f4::{EthToF4Addr, EthToF4AddrArgs}; +mod eth; mod f4; #[derive(Debug, Args)] @@ -20,6 +22,7 @@ impl UtilCommandsArgs { pub async fn handle(&self, global: &GlobalArguments) -> anyhow::Result<()> { match &self.command { Commands::EthToF4Addr(args) => EthToF4Addr::handle(global, args).await, + Commands::F4ToEthAddr(args) => F4ToEthAddr::handle(global, args).await, } } } @@ -27,4 +30,5 @@ impl UtilCommandsArgs { #[derive(Debug, Subcommand)] pub(crate) enum Commands { EthToF4Addr(EthToF4AddrArgs), + F4ToEthAddr(F4ToEthAddrArgs), } diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs new file mode 100644 index 0000000000..3a7c5da9a9 --- /dev/null +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -0,0 +1,55 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +use crate::commands::get_ipc_provider; +use crate::{CommandLineHandler, GlobalArguments}; +use async_trait::async_trait; +use clap::Args; +use fvm_shared::{address::Address, clock::ChainEpoch}; +use ipc_api::subnet_id::SubnetID; +use std::str::FromStr; + +#[derive(Debug, Args)] +#[command(about = "validator batch claim rewards for a target subnet")] +pub(crate) struct BatchClaimArgs { + #[arg(long, help = "The JSON RPC server url for ipc agent")] + pub validator: String, + #[arg(long, help = "The checkpoint height to claim from")] + pub from: ChainEpoch, + #[arg(long, help = "The checkpoint height to claim to")] + pub to: ChainEpoch, + #[arg(long, help = "The source subnet that generated the reward")] + pub reward_source_subnet: String, + #[arg(long, help = "The subnet to claim reward from")] + pub reward_claim_subnet: String, +} + +pub(crate) struct BatchClaim; + +#[async_trait] +impl CommandLineHandler for BatchClaim { + type Arguments = BatchClaimArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("batch claim operation with args: {:?}", arguments); + + let provider = get_ipc_provider(global)?; + let reward_source_subnet = SubnetID::from_str(&arguments.reward_source_subnet)?; + let reward_claim_subnet = SubnetID::from_str(&arguments.reward_claim_subnet)?; + let validator = Address::from_str(&arguments.validator)?; + + provider + .batch_claim( + &reward_claim_subnet, + &reward_source_subnet, + arguments.from, + arguments.to, + &validator, + ) + .await?; + + println!("rewards claimed"); + + Ok(()) + } +} diff --git a/ipc/cli/src/commands/validator/list.rs b/ipc/cli/src/commands/validator/list.rs new file mode 100644 index 0000000000..7209de34d1 --- /dev/null +++ b/ipc/cli/src/commands/validator/list.rs @@ -0,0 +1,52 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +use crate::commands::get_ipc_provider; +use crate::{CommandLineHandler, GlobalArguments}; +use async_trait::async_trait; +use clap::Args; +use fvm_shared::{address::Address, clock::ChainEpoch}; +use ipc_api::subnet_id::SubnetID; +use std::str::FromStr; + +#[derive(Debug, Args)] +#[command(about = "validator list activities in a subnet")] +pub(crate) struct ListActivitiesArgs { + #[arg(long, help = "The JSON RPC server url for ipc agent")] + pub validator: String, + #[arg(long, help = "The checkpoint height to claim from")] + pub from: ChainEpoch, + #[arg(long, help = "The checkpoint height to claim to")] + pub to: ChainEpoch, + #[arg(long, help = "The subnet to list activities from")] + pub subnet: String, +} + +pub(crate) struct ListActivities; + +#[async_trait] +impl CommandLineHandler for ListActivities { + type Arguments = ListActivitiesArgs; + + async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { + log::debug!("list validator activities with args: {:?}", arguments); + + let provider = get_ipc_provider(global)?; + let subnet = SubnetID::from_str(&arguments.subnet)?; + let validator = Address::from_str(&arguments.validator)?; + + let r = provider + .list_validator_activities(&subnet, &validator, arguments.from, arguments.to) + .await?; + + println!("found total {} entries", r.len()); + for v in r { + println!("checkpoint height: {}", v.checkpoint_height); + println!(" addr: {}", v.validator); + println!(" metadata: {}", hex::encode(v.metadata)); + println!(" locks_committed: {}", v.blocks_committed); + } + + Ok(()) + } +} diff --git a/ipc/cli/src/commands/validator/mod.rs b/ipc/cli/src/commands/validator/mod.rs new file mode 100644 index 0000000000..2845904c95 --- /dev/null +++ b/ipc/cli/src/commands/validator/mod.rs @@ -0,0 +1,33 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: MIT + +mod batch_claim; +mod list; + +use crate::commands::validator::batch_claim::{BatchClaim, BatchClaimArgs}; +use crate::commands::validator::list::{ListActivities, ListActivitiesArgs}; +use crate::{CommandLineHandler, GlobalArguments}; +use clap::{Args, Subcommand}; + +#[derive(Debug, Args)] +#[command(name = "validator", about = "validator reward related commands")] +#[command(args_conflicts_with_subcommands = true)] +pub(crate) struct ValidatorCommandsArgs { + #[command(subcommand)] + command: Commands, +} + +impl ValidatorCommandsArgs { + pub async fn handle(&self, global: &GlobalArguments) -> anyhow::Result<()> { + match &self.command { + Commands::BatchClaim(args) => BatchClaim::handle(global, args).await, + Commands::ListValidatorActivities(args) => ListActivities::handle(global, args).await, + } + } +} + +#[derive(Debug, Subcommand)] +pub(crate) enum Commands { + BatchClaim(BatchClaimArgs), + ListValidatorActivities(ListActivitiesArgs), +} diff --git a/ipc/observability/src/config.rs b/ipc/observability/src/config.rs index 1a73fa905a..029c5a5f88 100644 --- a/ipc/observability/src/config.rs +++ b/ipc/observability/src/config.rs @@ -7,28 +7,6 @@ use std::path::PathBuf; use strum; use tracing_appender; use tracing_appender::rolling::Rotation; -use tracing_subscriber::filter::EnvFilter; - -#[serde_as] -#[derive(Debug, Deserialize, Clone, Default, strum::EnumString, strum::Display)] -#[strum(serialize_all = "snake_case")] -#[serde(rename_all = "lowercase")] -pub enum LogLevel { - Off, - Error, - Warn, - #[default] - Info, - Debug, - Trace, -} - -impl From for EnvFilter { - fn from(val: LogLevel) -> Self { - // By default EnvFilter uses INFO, just like our default log level. - EnvFilter::try_new(val.to_string()).unwrap_or_default() - } -} #[serde_as] #[derive(Debug, Deserialize, Clone, strum::EnumString, strum::Display)] @@ -73,14 +51,14 @@ pub struct TracingSettings { #[serde_as] #[derive(Debug, Deserialize, Clone, Default)] pub struct ConsoleLayerSettings { - pub level: Option, + pub level: Option, } #[serde_as] #[derive(Debug, Deserialize, Clone, Default)] pub struct FileLayerSettings { pub enabled: bool, - pub level: Option, + pub level: Option, pub directory: Option, pub max_log_files: Option, pub rotation: Option, diff --git a/ipc/observability/src/traces.rs b/ipc/observability/src/traces.rs index d2699542fd..13e19ca0a9 100644 --- a/ipc/observability/src/traces.rs +++ b/ipc/observability/src/traces.rs @@ -1,7 +1,7 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use crate::config::{FileLayerSettings, LogLevel, TracingSettings}; +use crate::config::{FileLayerSettings, TracingSettings}; use crate::tracing_layers::DomainEventFilterLayer; use std::num::NonZeroUsize; use tracing::Level; @@ -95,7 +95,7 @@ pub fn set_global_tracing_subscriber(config: &TracingSettings) -> Vec anyhow::Result
{ let conn = self.get_connection(&parent)?; @@ -275,6 +276,7 @@ impl IpcProvider { supply_source, collateral_source, validator_gater, + validator_rewarder, }; conn.manager() @@ -743,6 +745,46 @@ impl IpcProvider { .set_federated_power(from, subnet, validators, public_keys, federated_power) .await } + + pub async fn list_validator_activities( + &self, + subnet: &SubnetID, + validator: &Address, + from: ChainEpoch, + to: ChainEpoch, + ) -> anyhow::Result> { + let conn = self.get_connection(subnet)?; + conn.manager() + .get_validator_activities(validator, from, to) + .await + } + + pub async fn batch_claim( + &self, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + from: ChainEpoch, + to: ChainEpoch, + validator: &Address, + ) -> anyhow::Result<()> { + let conn = self.get_connection(reward_source_subnet)?; + + let proofs = conn + .manager() + .get_validator_claim_proofs(validator, from, to) + .await?; + if proofs.is_empty() { + return Err(anyhow!( + "address {} has no reward to claim", + validator.to_string() + )); + } + + let conn = self.get_connection(reward_claim_subnet)?; + conn.manager() + .batch_claim(validator, reward_claim_subnet, reward_source_subnet, proofs) + .await + } } /// Lotus JSON keytype format diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 21fc3416fe..43a62c4114 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -11,7 +11,7 @@ use ipc_actors_abis::{ checkpointing_facet, gateway_getter_facet, gateway_manager_facet, gateway_messenger_facet, lib_gateway, lib_quorum, lib_staking_change_log, register_subnet_facet, subnet_actor_checkpointing_facet, subnet_actor_getter_facet, subnet_actor_manager_facet, - subnet_actor_reward_facet, + subnet_actor_reward_facet, validator_reward_facet, }; use ipc_api::evm::{fil_to_eth_amount, payload_to_evm_address, subnet_id_to_evm_addresses}; use ipc_api::validator::from_contract_validators; @@ -27,7 +27,7 @@ use crate::config::Subnet; use crate::lotus::message::ipc::SubnetInfo; use crate::manager::subnet::{ BottomUpCheckpointRelayer, GetBlockHashResult, SubnetGenesisInfo, TopDownFinalityQuery, - TopDownQueryPayload, + TopDownQueryPayload, ValidatorRewarder, }; use crate::manager::{EthManager, SubnetManager}; use anyhow::{anyhow, Context, Result}; @@ -44,12 +44,16 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo, ValidatorStakingInfo}; use ipc_api::subnet::ConstructParams; use ipc_api::subnet_id::SubnetID; +use ipc_observability::lazy_static; use ipc_wallet::{EthKeyAddress, EvmKeyStore, PersistentKeyStore}; +use merkle_tree_rs::format::Raw; +use merkle_tree_rs::standard::{LeafType, StandardMerkleTree}; use num_traits::ToPrimitive; use std::result; @@ -278,6 +282,7 @@ impl SubnetManager for EthSubnetManager { supply_source: register_subnet_facet::Asset::try_from(params.supply_source)?, collateral_source: register_subnet_facet::Asset::try_from(params.collateral_source)?, validator_gater: payload_to_evm_address(params.validator_gater.payload())?, + validator_rewarder: payload_to_evm_address(params.validator_rewarder.payload())?, }; tracing::info!("creating subnet on evm with params: {params:?}"); @@ -771,7 +776,7 @@ impl SubnetManager for EthSubnetManager { address, Arc::new(self.ipc_contract_info.provider.clone()), ); - let raw = contract.collateral_source().call().await?; + let raw = contract.supply_source().call().await?; Ok(Asset::try_from(raw)?) } @@ -1278,6 +1283,149 @@ impl BottomUpCheckpointRelayer for EthSubnetManager { } } +lazy_static!( + /// ABI types of the Merkle tree which contains validator addresses and their voting power. + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["uint64".to_owned(), "address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; +); + +#[async_trait] +impl ValidatorRewarder for EthSubnetManager { + async fn get_validator_claim_proofs( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result> { + let contract = checkpointing_facet::CheckpointingFacet::new( + self.ipc_contract_info.gateway_addr, + Arc::new(self.ipc_contract_info.provider.clone()), + ); + + let ev = contract + .event::() + .from_block(from_checkpoint as u64) + .to_block(to_checkpoint as u64) + .address(ValueOrArray::Value(contract.address())); + + let validator_evm_addr = payload_to_evm_address(validator_addr.payload())?; + + let mut proofs = vec![]; + for (event, meta) in query_with_meta(ev, contract.client()).await? { + tracing::debug!("found event at height: {}", meta.block_number); + + let mut activities = vec![]; + let mut maybe_validator = None; + for validator in event.report.validators { + let payload = vec![ + event.checkpoint_height.to_string(), + format!("{:?}", validator.validator), + validator.blocks_committed.to_string(), + hex::encode(validator.metadata.as_ref()), + ]; + + if validator.validator == validator_evm_addr { + let summary = ValidatorSummary { + checkpoint_height: event.checkpoint_height, + validator: *validator_addr, + blocks_committed: validator.blocks_committed, + metadata: validator.metadata.to_vec(), + }; + maybe_validator = Some((payload.clone(), summary)); + } + + activities.push(payload); + } + + let tree = StandardMerkleTree::::of(&activities, &VALIDATOR_SUMMARY_FIELDS) + .context("failed to construct Merkle tree")?; + + let Some((payload, summary)) = maybe_validator else { + tracing::info!( + "target validator address has not activities in epoch {}", + meta.block_number + ); + continue; + }; + let proof = tree.get_proof(LeafType::LeafBytes(payload))?; + + proofs.push(ValidatorClaimProof { + summary, + proof: proof.into_iter().map(|v| v.into()).collect(), + }); + } + + Ok(proofs) + } + + /// Get the reward for specific validator in a subnet + async fn get_validator_activities( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result> { + let contract = checkpointing_facet::CheckpointingFacet::new( + self.ipc_contract_info.gateway_addr, + Arc::new(self.ipc_contract_info.provider.clone()), + ); + + let ev = contract + .event::() + .from_block(from_checkpoint as u64) + .to_block(to_checkpoint as u64) + .address(ValueOrArray::Value(contract.address())); + + let mut activities = vec![]; + let validator_eth_addr = payload_to_evm_address(validator_addr.payload())?; + + for (event, meta) in query_with_meta(ev, contract.client()).await? { + tracing::debug!("found event at height: {}", meta.block_number); + for validator in event.report.validators { + if validator.validator != validator_eth_addr { + continue; + } + + activities.push(ValidatorSummary { + validator: *validator_addr, + checkpoint_height: event.checkpoint_height, + blocks_committed: validator.blocks_committed, + metadata: validator.metadata.to_vec(), + }); + } + } + + Ok(activities) + } + + /// Claim the reward in batch + async fn batch_claim( + &self, + submitter: &Address, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + proofs: Vec, + ) -> Result<()> { + let signer = Arc::new(self.get_signer(submitter)?); + let contract = validator_reward_facet::ValidatorRewardFacet::new( + contract_address_from_subnet(reward_claim_subnet)?, + signer.clone(), + ); + + let call = contract.batch_claim(validator_reward_facet::BatchClaimProofs { + subnet_id: validator_reward_facet::SubnetID::try_from(reward_source_subnet)?, + proofs: proofs + .into_iter() + .map(validator_reward_facet::ValidatorClaimProof::try_from) + .collect::>>()?, + }); + let call = call_with_premium_estimation(signer, call).await?; + + call.send().await?; + + Ok(()) + } +} + /// Receives an input `FunctionCall` and returns a new instance /// after estimating an optimal `gas_premium` for the transaction pub(crate) async fn call_with_premium_estimation( diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index cc47ab093a..35c7626d02 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -9,6 +9,7 @@ use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; use ipc_api::checkpoint::{ BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, + ValidatorClaimProof, ValidatorSummary, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; @@ -20,7 +21,9 @@ use crate::lotus::message::ipc::SubnetInfo; /// Trait to interact with a subnet and handle its lifecycle. #[async_trait] -pub trait SubnetManager: Send + Sync + TopDownFinalityQuery + BottomUpCheckpointRelayer { +pub trait SubnetManager: + Send + Sync + TopDownFinalityQuery + BottomUpCheckpointRelayer + ValidatorRewarder +{ /// Deploys a new subnet actor on the `parent` subnet and with the /// configuration passed in `ConstructParams`. /// The result of the function is the ID address for the subnet actor from which the final @@ -279,3 +282,31 @@ pub trait BottomUpCheckpointRelayer: Send + Sync { /// Get the current epoch in the current subnet async fn current_epoch(&self) -> Result; } + +/// The validator reward related functions, such as check reward and claim reward for mining blocks +/// in the child subnet +#[async_trait] +pub trait ValidatorRewarder: Send + Sync { + /// Obtain the proofs needed for the validator to batch claim the rewards + async fn get_validator_claim_proofs( + &self, + validator_addr: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result>; + /// Get the reward for specific validator in the current subnet gateway + async fn get_validator_activities( + &self, + validator: &Address, + from_checkpoint: ChainEpoch, + to_checkpoint: ChainEpoch, + ) -> Result>; + /// Claim the reward in batches + async fn batch_claim( + &self, + submitter: &Address, + reward_claim_subnet: &SubnetID, + reward_source_subnet: &SubnetID, + proofs: Vec, + ) -> Result<()>; +} diff --git a/specs/drafts/observability.md b/specs/drafts/observability.md index 3a8bd2e4a6..7b0ab1f019 100644 --- a/specs/drafts/observability.md +++ b/specs/drafts/observability.md @@ -25,33 +25,33 @@ Once we capture the trace object (hopefully with negligible overhead), and we en - The journal. - The metrics. -| Done | Domain | Subsystem | Event | Trace | Metrics | -|------|-----------|----------------|------------------------------------|----------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| -| x | Topdown | Parent syncer | Parent RPC calls | ParentRpcCalled{source, json-rpc method, status, latency} | topdown_parent_rpc_call_total{source, method, status}++ (counter) | -| x | | | | | topdown_parent_rpc_call_latency_secs{source, method, status} (histogram) | -| x | | | Parent finality locally acquired | ParentFinalityAcquired{source, block height, block hash, commitment hash, num messages, num validator changes} | topdown_parent_finality_latest_acquired_height{source} (gauge) | -| x | | | Parent finality gossip received | ParentFinalityPeerVoteReceived{validator, block height, block hash, commitment hash} | topdown_parent_finality_voting_latest_received_height{validator} (gauge) | -| x | | | Parent finality peer gossip sent | ParentFinalityPeerVoteSent{block height, block hash, commitment hash} | topdown_parent_finality_voting_latest_sent_height (gauge) | -| x | | | Parent finality quorum reached | ParentFinalityPeerQuorumReached{block height, block hash, commitment hash, weight} | topdown_parent_finality_voting_quorum_height (gauge) | -| x | | | | | topdown_parent_finality_voting_quorum_weight (gauge) | -| x | | | Parent finality committed on chain | ParentFinalityCommitted{parent height, block hash, local height, proposer} | topdown_parent_finality_committed_height (gauge) | -| x | Bottomup | Checkpointing | Checkpoint submitted | CheckpointSubmitted{height, hash} | bottomup_checkpoint_finalized_height (gauge) | -| x | | | Checkpoint created | CheckpointCreated{height, hash, msg_count, config_number} | bottomup_checkpoint_created_total (counter) | -| x | | | | | bottomup_checkpoint_created_height (gauge) | -| x | | | | | bottomup_checkpoint_created_msgcount (gauge) | -| x | | | | | bottomup_checkpoint_created_confignum (gauge) | -| x | | | Checkpoint signed | CheckpointSigned{role, height, hash, validator} | bottomup_checkpoint_signed_height{validator} (gauge) | -| x | Consensus | Block proposal | Block proposal received | BlockProposalReceived{height, hash, size, tx_count, validator} | consensus_block_proposal_received_height (gauge) | -| x | | | Block proposal sent | BlockProposalSent{validator, height, size, tx_count} | consensus_block_proposal_sent_height (gauge) | -| x | | | Block proposal evaluated | BlockProposalEvaluated{height, hash, size, tx_count, validator, accept, reason} | consensus_block_proposal_accepted_height (gauge) | -| x | | | | | consensus_block_proposal_rejected_height (gauge) | -| x | | | Block proposal committed | BlockCommitted{height, app_hash} | consensus_block_committed_height (gauge) | -| x | Mpool | Message pool | Message received | MpoolReceived{message, accept, reason} | mpool_received{accept} (counter) | -| x | Execution | VM execution | Executing message | MsgExec{purpose, message, height, duration, exit_code} | exec_fvm_check_execution_time_secs (histogram) | -| x | | | | | exec_fvm_estimate_execution_time_secs (histogram) | -| x | | | | | exec_fvm_apply_execution_time_secs (histogram) | -| x | | | | | exec_fvm_call_execution_time_secs (histogram) | -| x | Tracing | Errors | Error while processing tracing | TracingError{affected_event, reason} | tracing_errors{event} (counter) | +| Done | Domain | Subsystem | Event | Trace | Metrics | +| ---- | --------- | -------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| x | Topdown | Parent syncer | Parent RPC calls | ParentRpcCalled{source, json-rpc method, status, latency} | ipc_topdown_parent_rpc_call_total{source, method, status}++ (counter) | +| x | | | | | ipc_topdown_parent_rpc_call_latency_secs{source, method, status} (histogram) | +| x | | | Parent finality locally acquired | ParentFinalityAcquired{source, block height, block hash, commitment hash, num messages, num validator changes} | ipc_topdown_parent_finality_latest_acquired_height{source} (gauge) | +| x | | | Parent finality gossip received | ParentFinalityPeerVoteReceived{validator, block height, block hash, commitment hash} | ipc_topdown_parent_finality_voting_latest_received_height{validator} (gauge) | +| x | | | Parent finality peer gossip sent | ParentFinalityPeerVoteSent{block height, block hash, commitment hash} | ipc_topdown_parent_finality_voting_latest_sent_height (gauge) | +| x | | | Parent finality quorum reached | ParentFinalityPeerQuorumReached{block height, block hash, commitment hash, weight} | ipc_topdown_parent_finality_voting_quorum_height (gauge) | +| x | | | | | ipc_topdown_parent_finality_voting_quorum_weight (gauge) | +| x | | | Parent finality committed on chain | ParentFinalityCommitted{parent height, block hash, local height, proposer} | ipc_topdown_parent_finality_committed_height (gauge) | +| x | Bottomup | Checkpointing | Checkpoint submitted | CheckpointSubmitted{height, hash} | ipc_bottomup_checkpoint_finalized_height (gauge) | +| x | | | Checkpoint created | CheckpointCreated{height, hash, msg_count, config_number} | ipc_bottomup_checkpoint_created_total (counter) | +| x | | | | | ipc_bottomup_checkpoint_created_height (gauge) | +| x | | | | | ipc_bottomup_checkpoint_created_msgcount (gauge) | +| x | | | | | ipc_bottomup_checkpoint_created_confignum (gauge) | +| x | | | Checkpoint signed | CheckpointSigned{role, height, hash, validator} | ipc_bottomup_checkpoint_signed_height{validator} (gauge) | +| x | Consensus | Block proposal | Block proposal received | BlockProposalReceived{height, hash, size, tx_count, validator} | ipc_consensus_block_proposal_received_height (gauge) | +| x | | | Block proposal sent | BlockProposalSent{validator, height, size, tx_count} | ipc_consensus_block_proposal_sent_height (gauge) | +| x | | | Block proposal evaluated | BlockProposalEvaluated{height, hash, size, tx_count, validator, accept, reason} | ipc_consensus_block_proposal_accepted_height (gauge) | +| x | | | | | ipc_consensus_block_proposal_rejected_height (gauge) | +| x | | | Block proposal committed | BlockCommitted{height, app_hash} | ipc_consensus_block_committed_height (gauge) | +| x | Mpool | Message pool | Message received | MpoolReceived{message, accept, reason} | ipc_mpool_received{accept} (counter) | +| x | Execution | VM execution | Executing message | MsgExec{purpose, message, height, duration, exit_code} | ipc_exec_fvm_check_execution_time_secs (histogram) | +| x | | | | | ipc_exec_fvm_estimate_execution_time_secs (histogram) | +| x | | | | | ipc_exec_fvm_apply_execution_time_secs (histogram) | +| x | | | | | ipc_exec_fvm_call_execution_time_secs (histogram) | +| x | Tracing | Errors | Error while processing tracing | TracingError{affected_event, reason} | ipc_tracing_errors{event} (counter) | | | ## Fine-grained VM metrics @@ -98,7 +98,7 @@ Example config: [tracing] [tracing.console] -level = "trace" # Options: off, error, warn, info, debug, trace (default: trace) +level = "trace" # Eg. "info,my_crate::module=trace" - https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives ``` ### File Tracing @@ -108,7 +108,7 @@ Example config: ```toml [tracing.file] enabled = true # Options: true, false -level = "trace" # Options: off, error, warn, info, debug, trace (default: trace) +level = "trace" # Eg. "info,my_crate::module=trace" - https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives directory = "/path/to/log/directory" max_log_files = 5 # Number of files to keep after rotation rotation = "daily" # Options: minutely, hourly, daily, never