From 69e04a958a9f2d564b89feaab587f421030ce200 Mon Sep 17 00:00:00 2001 From: raulk Date: Mon, 25 Nov 2024 18:14:01 +0700 Subject: [PATCH] feat(node): integration rewards, review comments. (#1205) Co-authored-by: cryptoAtwill Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> --- Cargo.lock | 2 +- contracts/binding/Cargo.toml | 2 +- contracts/contracts/activities/Activity.sol | 92 +++--- .../activities/IValidatorRewarder.sol | 4 +- .../activities/LibActivityMerkleVerifier.sol | 21 +- .../activities/ValidatorRewardFacet.sol | 89 +++--- .../examples/ValidatorRewarderMap.sol | 22 +- .../gateway/router/CheckpointingFacet.sol | 8 +- .../lib/LibSubnetRegistryStorage.sol | 2 +- contracts/contracts/structs/CrossNet.sol | 4 +- .../subnet/SubnetActorCheckpointingFacet.sol | 4 +- contracts/foundry.toml | 1 + contracts/tasks/deploy-registry.ts | 6 +- contracts/tasks/validator-rewarder.ts | 2 +- contracts/test/IntegrationTestBase.sol | 15 +- contracts/test/helpers/ActivityHelper.sol | 48 +++ contracts/test/helpers/MerkleTreeHelper.sol | 11 +- contracts/test/helpers/SelectorLibrary.sol | 6 +- .../test/integration/GatewayDiamond.t.sol | 33 +- .../integration/GatewayDiamondToken.t.sol | 7 +- contracts/test/integration/MultiSubnet.t.sol | 7 +- .../test/integration/SubnetActorDiamond.t.sol | 181 ++++------- docs/fendermint/diagrams/activity_rollup1.png | Bin 0 -> 119471 bytes docs/fendermint/subnet_activities.md | 78 +++++ extras/axelar-token/foundry.toml | 3 +- extras/linked-token/foundry.toml | 2 +- .../linked-token/test/MultiSubnetTest.t.sol | 12 +- fendermint/actors/activity-tracker/Cargo.toml | 4 +- fendermint/actors/activity-tracker/src/lib.rs | 102 +++--- .../actors/activity-tracker/src/state.rs | 108 ++----- .../actors/activity-tracker/src/types.rs | 28 ++ fendermint/eth/api/Cargo.toml | 2 +- fendermint/eth/api/src/apis/eth.rs | 1 + fendermint/testing/contract-test/Cargo.toml | 2 +- fendermint/vm/actor_interface/src/ipc.rs | 11 +- fendermint/vm/interpreter/Cargo.toml | 1 + .../interpreter/src/fvm/activities/actor.rs | 105 ------ .../vm/interpreter/src/fvm/activities/mod.rs | 49 --- .../vm/interpreter/src/fvm/activity/actor.rs | 75 +++++ .../fvm/{activities => activity}/merkle.rs | 26 +- .../vm/interpreter/src/fvm/activity/mod.rs | 156 +++++++++ .../vm/interpreter/src/fvm/checkpoint.rs | 88 +++--- fendermint/vm/interpreter/src/fvm/exec.rs | 6 +- fendermint/vm/interpreter/src/fvm/externs.rs | 12 - fendermint/vm/interpreter/src/fvm/mod.rs | 2 +- .../vm/interpreter/src/fvm/state/exec.rs | 63 +--- .../vm/interpreter/src/fvm/state/ipc.rs | 23 +- fendermint/vm/message/Cargo.toml | 2 +- ipc/api/src/checkpoint.rs | 72 +++-- ipc/api/src/evm.rs | 93 +++--- ipc/cli/src/commands/validator/batch_claim.rs | 17 +- ipc/cli/src/commands/validator/list.rs | 5 +- ipc/provider/Cargo.toml | 2 +- ipc/provider/src/lib.rs | 39 +-- ipc/provider/src/manager/evm/manager.rs | 299 +++++++++++++----- ipc/provider/src/manager/subnet.rs | 26 +- ipc/wallet/Cargo.toml | 2 +- 57 files changed, 1147 insertions(+), 936 deletions(-) create mode 100644 contracts/test/helpers/ActivityHelper.sol create mode 100644 docs/fendermint/diagrams/activity_rollup1.png create mode 100644 docs/fendermint/subnet_activities.md create mode 100644 fendermint/actors/activity-tracker/src/types.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/activities/actor.rs delete mode 100644 fendermint/vm/interpreter/src/fvm/activities/mod.rs create mode 100644 fendermint/vm/interpreter/src/fvm/activity/actor.rs rename fendermint/vm/interpreter/src/fvm/{activities => activity}/merkle.rs (55%) create mode 100644 fendermint/vm/interpreter/src/fvm/activity/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 731ca28a4..0c7a060ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2769,7 +2769,6 @@ version = "0.1.0" dependencies = [ "anyhow", "cid", - "fil_actor_eam", "fil_actors_evm_shared", "fil_actors_runtime", "frc42_dispatch", @@ -2782,6 +2781,7 @@ dependencies = [ "num-derive 0.3.3", "num-traits", "serde", + "serde_tuple", ] [[package]] diff --git a/contracts/binding/Cargo.toml b/contracts/binding/Cargo.toml index fc82cff9b..e3a27d90d 100644 --- a/contracts/binding/Cargo.toml +++ b/contracts/binding/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] ethers = { workspace = true, features = ["abigen", "ws"] } -fvm_shared = { workspace = true, features = ["crypto"] } +fvm_shared = { workspace = true } anyhow = { workspace = true } [build-dependencies] diff --git a/contracts/contracts/activities/Activity.sol b/contracts/contracts/activities/Activity.sol index 56700d749..cdb6982dc 100644 --- a/contracts/contracts/activities/Activity.sol +++ b/contracts/contracts/activities/Activity.sol @@ -3,55 +3,63 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -event ActivityReportCreated(uint64 checkpointHeight, ActivityReport report); +// Event to be emitted within the subnet when a new activity summary has been recorded. +event ActivityRollupRecorded(uint64 checkpointHeight, FullActivityRollup rollup); -/// The full validator activities report -struct ActivityReport { - ValidatorActivityReport[] validators; +// Carries a set of reports summarising various aspects of the activity that took place in the subnet between the +// previous checkpoint and the checkpoint this summary is committed into. If this is the first checkpoint, the summary +// contains information about the subnet's activity since genesis. +// In the future we'll be having more kinds of activity reports here. +struct FullActivityRollup { + /// A report of consensus-level activity that took place in the subnet between the previous checkpoint + /// and the checkpoint this summary is committed into. + /// @dev If there is a configuration change applied at this checkpoint, this carries information + /// about the _old_ validator set. + Consensus.FullSummary consensus; } -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; +// Compressed representation of the activity summary that can be embedded in checkpoints to propagate up the hierarchy. +struct CompressedActivityRollup { + Consensus.CompressedSummary consensus; } -/// The summary for the child subnet activities that should be submitted to the parent subnet -/// together with a bottom up checkpoint -struct ActivitySummary { - /// The total number of distintive validators that have mined - uint64 totalActiveValidators; - /// The activity commitment for validators - bytes32 commitment; +/// Namespace for consensus-level activity summaries. +library Consensus { + type MerkleHash is bytes32; - // TODO: add relayed rewarder commitment -} + // Aggregated stats for consensus-level activity. + struct AggregatedStats { + /// The total number of unique validators that have mined within this period. + uint64 totalActiveValidators; + /// The total number of blocks committed by all validators during this period. + uint64 totalNumBlocksCommitted; + } -/// 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. - /// 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 full activity summary for consensus-level activity. + struct FullSummary { + AggregatedStats stats; + /// The breakdown of activity per validator. + ValidatorData[] data; + } -/// The proof required for validators to claim rewards -struct ValidatorClaimProof { - ValidatorSummary summary; - bytes32[] proof; -} + // The compresed representation of the activity summary for consensus-level activity suitable for embedding in a checkpoint. + struct CompressedSummary { + AggregatedStats stats; + /// The commitment for the validator details, so that we don't have to transmit them in full. + MerkleHash dataRootCommitment; + } + + struct ValidatorData { + /// @dev The validator whose activity we're reporting about, identified by the Ethereum address corresponding + /// to its secp256k1 pubkey. + address validator; + /// @dev The number of blocks committed by this validator during the summarised period. + uint64 blocksCommitted; + } -/// The proofs to batch claim validator rewards in a specific subnet -struct BatchClaimProofs { - SubnetID subnetId; - ValidatorClaimProof[] proofs; + /// The payload for validators to claim rewards + struct ValidatorClaim { + ValidatorData data; + MerkleHash[] proof; + } } diff --git a/contracts/contracts/activities/IValidatorRewarder.sol b/contracts/contracts/activities/IValidatorRewarder.sol index eb47b7fed..052b233de 100644 --- a/contracts/contracts/activities/IValidatorRewarder.sol +++ b/contracts/contracts/activities/IValidatorRewarder.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; -import {ValidatorSummary} from "./Activity.sol"; +import {Consensus} from "./Activity.sol"; /// @title ValidatorRewarder interface. /// @@ -14,7 +14,7 @@ interface IValidatorRewarder { /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and /// disburse any relevant rewards. /// @dev This method should revert if the summary is invalid; this will cause the - function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external; + function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) external; } /// @title Validator reward setup interface diff --git a/contracts/contracts/activities/LibActivityMerkleVerifier.sol b/contracts/contracts/activities/LibActivityMerkleVerifier.sol index 433f57185..579e64d5f 100644 --- a/contracts/contracts/activities/LibActivityMerkleVerifier.sol +++ b/contracts/contracts/activities/LibActivityMerkleVerifier.sol @@ -3,25 +3,24 @@ pragma solidity ^0.8.23; import {SubnetID} from "../structs/Subnet.sol"; import {InvalidProof} from "../errors/IPCErrors.sol"; -import {ValidatorSummary} from "./Activity.sol"; +import {Consensus} from "./Activity.sol"; import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; /// Verifies the proof to the commitment in subnet activity summary library LibActivityMerkleVerifier { function ensureValidProof( bytes32 commitment, - ValidatorSummary calldata summary, - bytes32[] calldata proof + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] 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) - ) - ) - ); - bool valid = MerkleProof.verify({proof: proof, root: commitment, leaf: leaf}); + bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(detail.validator, detail.blocksCommitted)))); + // converting proof to bytes32[] + bytes32[] memory proofBytes = new bytes32[](proof.length); + for (uint256 i = 0; i < proof.length; i++) { + proofBytes[i] = Consensus.MerkleHash.unwrap(proof[i]); + } + bool valid = MerkleProof.verify({proof: proofBytes, root: commitment, leaf: leaf}); if (!valid) { revert InvalidProof(); } diff --git a/contracts/contracts/activities/ValidatorRewardFacet.sol b/contracts/contracts/activities/ValidatorRewardFacet.sol index b18e8b551..903ac8740 100644 --- a/contracts/contracts/activities/ValidatorRewardFacet.sol +++ b/contracts/contracts/activities/ValidatorRewardFacet.sol @@ -1,27 +1,32 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {Consensus} from "./Activity.sol"; +import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {IValidatorRewarder, IValidatorRewardSetup} from "./IValidatorRewarder.sol"; +import {LibActivityMerkleVerifier} from "./LibActivityMerkleVerifier.sol"; +import {LibDiamond} from "../lib/LibDiamond.sol"; +import {NotValidator, SubnetNoTargetCommitment, CommitmentAlreadyInitialized, ValidatorAlreadyClaimed, NotGateway, NotOwner} from "../errors/IPCErrors.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.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 ValidatorRewardFacet is ReentrancyGuard, Pausable { - function batchClaim(BatchClaimProofs[] calldata payload) external nonReentrant whenNotPaused { - uint256 len = payload.length; + function batchSubnetClaim( + SubnetID calldata subnet, + uint64[] calldata checkpointHeights, + Consensus.ValidatorClaim[] calldata claims + ) external nonReentrant whenNotPaused { + require(checkpointHeights.length == claims.length, "length mismatch"); + uint256 len = claims.length; for (uint256 i = 0; i < len; ) { - _batchClaimInSubnet(payload[i]); + _claim(subnet, checkpointHeights[i], claims[i].data, claims[i].proof); unchecked { i++; } @@ -30,43 +35,33 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { /// Validators claim their reward for doing work in the child subnet function claim( - SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + SubnetID calldata subnet, + uint64 checkpointHeight, + Consensus.ValidatorData calldata data, + Consensus.MerkleHash[] calldata proof ) external nonReentrant whenNotPaused { - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - _claim(s, subnetId, summary, proof); + _claim(subnet, checkpointHeight, data, proof); } // ======== Internal functions =========== function handleRelay() internal pure { - // no opt for now + // no-op for now return; } - function _batchClaimInSubnet(BatchClaimProofs calldata payload) internal { - uint256 len = payload.proofs.length; - ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); - - for (uint256 i = 0; i < len; ) { - _claim(s, payload.subnetId, payload.proofs[i].summary, payload.proofs[i].proof); - unchecked { - i++; - } - } - } - function _claim( - ValidatorRewardStorage storage s, SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + uint64 checkpointHeight, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof ) internal { + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + // note: No need to check if the subnet is active. If the subnet is not active, the checkpointHeight // note: will never exist. - if (msg.sender != summary.validator) { + if (msg.sender != detail.validator) { revert NotValidator(msg.sender); } @@ -74,7 +69,7 @@ contract ValidatorRewardFacet is ReentrancyGuard, Pausable { return handleRelay(); } - LibValidatorReward.handleDistribution(s, subnetId, summary, proof); + LibValidatorReward.handleDistribution(subnetId, checkpointHeight, detail, proof); } } @@ -102,7 +97,7 @@ struct ValidatorRewardStorage { } /// The payload for list commitments query -struct ListCommimentDetail { +struct ListCommitmentDetail { /// The child subnet checkpoint height uint64 checkpointHeight; /// The actual commiment of the activities @@ -121,7 +116,7 @@ library LibValidatorReward { function initNewDistribution( SubnetID calldata subnetId, uint64 checkpointHeight, - bytes32 commitment, + Consensus.MerkleHash commitment, uint64 totalActiveValidators ) internal { ValidatorRewardStorage storage ds = facetStorage(); @@ -132,24 +127,24 @@ library LibValidatorReward { revert CommitmentAlreadyInitialized(); } - ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), commitment); + ds.commitments[subnetKey].set(bytes32(uint256(checkpointHeight)), Consensus.MerkleHash.unwrap(commitment)); ds.distributions[subnetKey][checkpointHeight].totalValidators = totalActiveValidators; } function listCommitments( SubnetID calldata subnetId - ) internal view returns (ListCommimentDetail[] memory listDetails) { + ) internal view returns (ListCommitmentDetail[] memory listDetails) { ValidatorRewardStorage storage ds = facetStorage(); bytes32 subnetKey = subnetId.toHash(); uint256 size = ds.commitments[subnetKey].length(); - listDetails = new ListCommimentDetail[](size); + listDetails = new ListCommitmentDetail[](size); for (uint256 i = 0; i < size; ) { (bytes32 heightBytes32, bytes32 commitment) = ds.commitments[subnetKey].at(i); - listDetails[i] = ListCommimentDetail({ + listDetails[i] = ListCommitmentDetail({ checkpointHeight: uint64(uint256(heightBytes32)), commitment: commitment }); @@ -178,18 +173,20 @@ library LibValidatorReward { } function handleDistribution( - ValidatorRewardStorage storage s, SubnetID calldata subnetId, - ValidatorSummary calldata summary, - bytes32[] calldata proof + uint64 checkpointHeight, + Consensus.ValidatorData calldata detail, + Consensus.MerkleHash[] calldata proof ) internal { + ValidatorRewardStorage storage s = LibValidatorReward.facetStorage(); + bytes32 subnetKey = subnetId.toHash(); - bytes32 commitment = ensureValidCommitment(s, subnetKey, summary.checkpointHeight); - LibActivityMerkleVerifier.ensureValidProof(commitment, summary, proof); + bytes32 commitment = ensureValidCommitment(s, subnetKey, checkpointHeight); + LibActivityMerkleVerifier.ensureValidProof(commitment, detail, proof); - validatorTryClaim(s, subnetKey, summary.checkpointHeight, summary.validator); - IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, summary); + validatorTryClaim(s, subnetKey, checkpointHeight, detail.validator); + IValidatorRewarder(s.validatorRewarder).disburseRewards(subnetId, detail); } function ensureValidCommitment( diff --git a/contracts/contracts/examples/ValidatorRewarderMap.sol b/contracts/contracts/examples/ValidatorRewarderMap.sol index 1dfa33df5..8f0ffd7d8 100644 --- a/contracts/contracts/examples/ValidatorRewarderMap.sol +++ b/contracts/contracts/examples/ValidatorRewarderMap.sol @@ -2,34 +2,30 @@ pragma solidity ^0.8.23; import {IValidatorRewarder} from "../activities/IValidatorRewarder.sol"; -import {ValidatorSummary} from "../activities/Activity.sol"; +import {Consensus} from "../activities/Activity.sol"; import {SubnetID} from "../structs/Subnet.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/// An example validator rewarder implementation that tracks the accumulated -/// reward for each valdiator only. -contract ValidatorRewarderMap is IValidatorRewarder { +/// An example validator rewarder implementation that only tracks the cumulative number of +/// blocks committed by each validator. +contract ValidatorRewarderMap is IValidatorRewarder, Ownable { SubnetID public subnetId; - address public owner; mapping(address => uint64) public blocksCommitted; - constructor() { - owner = msg.sender; - } + constructor() Ownable(msg.sender) {} - function setSubnet(SubnetID calldata id) external { - require(msg.sender == owner, "not owner"); + function setSubnet(SubnetID calldata id) external onlyOwner { require(id.route.length > 0, "root not allowed"); - subnetId = id; } - function disburseRewards(SubnetID calldata id, ValidatorSummary calldata summary) external { + function disburseRewards(SubnetID calldata id, Consensus.ValidatorData calldata detail) 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; + blocksCommitted[detail.validator] += detail.blocksCommitted; } } diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index a88cf1026..fafbecc0c 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -17,7 +17,7 @@ import {CrossMsgHelper} from "../../lib/CrossMsgHelper.sol"; import {IpcEnvelope, SubnetID} from "../../structs/CrossNet.sol"; import {SubnetIDHelper} from "../../lib/SubnetIDHelper.sol"; -import {ActivityReportCreated, ActivityReport} from "../../activities/Activity.sol"; +import {ActivityRollupRecorded, FullActivityRollup} from "../../activities/Activity.sol"; contract CheckpointingFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; @@ -49,12 +49,12 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @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 + /// @param fullSummary - the full validators' activities summary function createBUChptWithActivities( BottomUpCheckpoint calldata checkpoint, bytes32 membershipRootHash, uint256 membershipWeight, - ActivityReport calldata activityReport + FullActivityRollup calldata fullSummary ) external systemActorOnly { if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) { revert CheckpointAlreadyExists(); @@ -71,7 +71,7 @@ contract CheckpointingFacet is GatewayActorModifiers { LibGateway.storeBottomUpCheckpoint(checkpoint); - emit ActivityReportCreated(uint64(checkpoint.blockHeight), activityReport); + emit ActivityRollupRecorded(uint64(checkpoint.blockHeight), fullSummary); } /// @notice creates a new bottom-up checkpoint diff --git a/contracts/contracts/lib/LibSubnetRegistryStorage.sol b/contracts/contracts/lib/LibSubnetRegistryStorage.sol index 3983c76ee..c40381513 100644 --- a/contracts/contracts/lib/LibSubnetRegistryStorage.sol +++ b/contracts/contracts/lib/LibSubnetRegistryStorage.sol @@ -11,8 +11,8 @@ struct SubnetRegistryActorStorage { address SUBNET_ACTOR_GETTER_FACET; // 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 + // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_REWARD_FACET; // solhint-disable-next-line var-name-mixedcase address SUBNET_ACTOR_CHECKPOINTING_FACET; diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 1dc78be10..9d00cda8d 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 {ActivitySummary} from "../activities/Activity.sol"; +import {CompressedActivityRollup} from "../activities/Activity.sol"; uint64 constant MAX_MSGS_PER_BATCH = 10; uint256 constant BATCH_PERIOD = 100; @@ -31,7 +31,7 @@ struct BottomUpCheckpoint { /// @dev Batch of messages to execute. IpcEnvelope[] msgs; /// @dev The activity summary from child subnet to parent subnet - ActivitySummary activities; + CompressedActivityRollup activities; } struct RelayedSummary { diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index c130b2ad5..ad540d61f 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -53,8 +53,8 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, LibValidatorReward.initNewDistribution( checkpoint.subnetID, uint64(checkpoint.blockHeight), - checkpoint.activities.commitment, - checkpoint.activities.totalActiveValidators + checkpoint.activities.consensus.dataRootCommitment, + checkpoint.activities.consensus.stats.totalActiveValidators ); // confirming the changes in membership in the child diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 2d7761d10..eb7626a0e 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -16,6 +16,7 @@ remappings = [ "murky/=lib/murky/src/", ] allow_paths = ["../node_modules"] +solc = "0.8.23" [fuzz] runs = 512 diff --git a/contracts/tasks/deploy-registry.ts b/contracts/tasks/deploy-registry.ts index 51d4ac0ae..50c0491e4 100644 --- a/contracts/tasks/deploy-registry.ts +++ b/contracts/tasks/deploy-registry.ts @@ -37,17 +37,17 @@ task('deploy-registry') }, { name: 'SubnetActorPauseFacet' }, { name: 'SubnetActorRewardFacet' }, - { + { name: 'SubnetActorCheckpointingFacet', libraries: ['SubnetIDHelper'], }, { name: 'DiamondCutFacet' }, { name: 'DiamondLoupeFacet' }, { name: 'OwnershipFacet' }, - { + { name: 'ValidatorRewardFacet', libraries: ['SubnetIDHelper'], - }, + }, ) const registryFacets = await Deployments.deploy( diff --git a/contracts/tasks/validator-rewarder.ts b/contracts/tasks/validator-rewarder.ts index 0f7d784e8..095ffa935 100644 --- a/contracts/tasks/validator-rewarder.ts +++ b/contracts/tasks/validator-rewarder.ts @@ -44,4 +44,4 @@ task('validator-rewarder-set-subnet') 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 53d17edbf..066c2e72e 100644 --- a/contracts/test/IntegrationTestBase.sol +++ b/contracts/test/IntegrationTestBase.sol @@ -46,7 +46,7 @@ import {GatewayFacetsHelper} from "./helpers/GatewayFacetsHelper.sol"; import {SubnetActorFacetsHelper} from "./helpers/SubnetActorFacetsHelper.sol"; import {DiamondFacetsHelper} from "./helpers/DiamondFacetsHelper.sol"; -import {ActivitySummary} from "../contracts/activities/Activity.sol"; +import {FullActivityRollup, CompressedActivityRollup, Consensus} from "../contracts/activities/Activity.sol"; import {ValidatorRewarderMap} from "../contracts/examples/ValidatorRewarderMap.sol"; import {ValidatorRewardFacet} from "../contracts/activities/ValidatorRewardFacet.sol"; @@ -934,9 +934,14 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, blockHash: keccak256(abi.encode(h)), nextConfigurationNumber: nextConfigNum - 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({ - totalActiveValidators: uint64(validators.length), - commitment: bytes32(uint256(nextConfigNum)) + activities: CompressedActivityRollup({ + consensus: Consensus.CompressedSummary({ + stats: Consensus.AggregatedStats({ + totalActiveValidators: uint64(validators.length), + totalNumBlocksCommitted: 3 + }), + dataRootCommitment: Consensus.MerkleHash.wrap(bytes32(uint256(nextConfigNum))) + }) }) }); @@ -956,7 +961,7 @@ contract IntegrationTestBase is Test, TestParams, TestRegistry, TestSubnetActor, function confirmChange( address[] memory validators, uint256[] memory privKeys, - ActivitySummary memory activities + CompressedActivityRollup memory activities ) internal { uint256 n = validators.length; diff --git a/contracts/test/helpers/ActivityHelper.sol b/contracts/test/helpers/ActivityHelper.sol new file mode 100644 index 000000000..037db293f --- /dev/null +++ b/contracts/test/helpers/ActivityHelper.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {Consensus, CompressedActivityRollup} from "../../contracts/activities/Activity.sol"; + +library ActivityHelper { + function newCompressedActivityRollup( + uint64 totalActiveValidators, + uint64 totalNumBlocksCommitted, + bytes32 detailsRootCommitment + ) internal pure returns (CompressedActivityRollup memory compressed) { + Consensus.CompressedSummary memory summary = newCompressedSummary( + totalActiveValidators, + totalNumBlocksCommitted, + detailsRootCommitment + ); + compressed.consensus = summary; + return compressed; + } + + function newCompressedSummary( + uint64 totalActiveValidators, + uint64 totalNumBlocksCommitted, + bytes32 detailsRootCommitment + ) internal pure returns (Consensus.CompressedSummary memory summary) { + summary.stats.totalActiveValidators = totalActiveValidators; + summary.stats.totalNumBlocksCommitted = totalNumBlocksCommitted; + summary.dataRootCommitment = Consensus.MerkleHash.wrap(detailsRootCommitment); + } + + function wrapBytes32Array(bytes32[] memory data) internal pure returns (Consensus.MerkleHash[] memory wrapped) { + uint256 length = data.length; + + if (length == 0) { + return wrapped; + } + + wrapped = new Consensus.MerkleHash[](data.length); + for (uint256 i = 0; i < length; ) { + wrapped[i] = Consensus.MerkleHash.wrap(data[i]); + unchecked { + i++; + } + } + + return wrapped; + } +} diff --git a/contracts/test/helpers/MerkleTreeHelper.sol b/contracts/test/helpers/MerkleTreeHelper.sol index ec166cd81..f3b911eaf 100644 --- a/contracts/test/helpers/MerkleTreeHelper.sol +++ b/contracts/test/helpers/MerkleTreeHelper.sol @@ -34,26 +34,21 @@ library MerkleTreeHelper { function createMerkleProofsForActivities( address[] memory addrs, - uint64[] memory blocksMined, - bytes[] memory metadatas + uint64[] memory blocksMined ) 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"); - } + 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(addrs[i], blocksMined[i], metadatas[i]))) - ); + data[i] = keccak256(bytes.concat(keccak256(abi.encode(addrs[i], blocksMined[i])))); } root = merkleTree.getRoot(data); diff --git a/contracts/test/helpers/SelectorLibrary.sol b/contracts/test/helpers/SelectorLibrary.sol index 121b8c330..9dfe76e4c 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"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a21d5ff200000000000000000000000000000000000000000000000000000000ed915e7d000000000000000000000000000000000000000000000000000000009628ea6400000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000553b4e7bf00000000000000000000000000000000000000000000000000000000a0316672000000000000000000000000000000000000000000000000000000002ea952910000000000000000000000000000000000000000000000000000000036bfdf6700000000000000000000000000000000000000000000000000000000ac81837900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -97,7 +97,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("SubnetActorCheckpointingFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021b6bda5d00000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", + hex"00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002dcab3d7800000000000000000000000000000000000000000000000000000000cc2dc2b900000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } @@ -125,7 +125,7 @@ library SelectorLibrary { if (keccak256(abi.encodePacked(facetName)) == keccak256(abi.encodePacked("ValidatorRewardFacet"))) { return abi.decode( - hex"000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000023cca8af0000000000000000000000000000000000000000000000000000000006be7503e00000000000000000000000000000000000000000000000000000000", + hex"0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000202eca6eb00000000000000000000000000000000000000000000000000000000f9d3434c00000000000000000000000000000000000000000000000000000000", (bytes4[]) ); } diff --git a/contracts/test/integration/GatewayDiamond.t.sol b/contracts/test/integration/GatewayDiamond.t.sol index db2f720ab..69e53166a 100644 --- a/contracts/test/integration/GatewayDiamond.t.sol +++ b/contracts/test/integration/GatewayDiamond.t.sol @@ -39,7 +39,8 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeTokenMock { using SubnetIDHelper for SubnetID; @@ -1070,7 +1071,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({ @@ -1079,7 +1080,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // failed to create a checkpoint with zero membership weight @@ -1121,7 +1122,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 2, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1145,7 +1146,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointSource.selector); @@ -1167,7 +1168,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1214,7 +1215,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); @@ -1235,7 +1236,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpoint2 = BottomUpCheckpoint({ @@ -1244,7 +1245,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block2"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1309,7 +1310,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1371,7 +1372,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1455,7 +1456,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1490,7 +1491,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1535,7 +1536,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); // create a checkpoint @@ -1584,7 +1585,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block"), nextConfigurationNumber: 1, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); gatewayDiamond.checkpointer().createBottomUpCheckpoint(checkpoint, membershipRoot, 10); @@ -1648,7 +1649,7 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase, SubnetWithNativeT blockHash: keccak256("block1"), nextConfigurationNumber: 1, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(caller); diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 9b1b4e50b..3b239b599 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -33,7 +33,8 @@ import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.so import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract GatewayDiamondTokenTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -166,7 +167,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.prank(address(saDiamond)); @@ -225,7 +226,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { blockHeight: gatewayDiamond.getter().bottomUpCheckPeriod(), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(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 7313e35c6..1bf25a3f0 100644 --- a/contracts/test/integration/MultiSubnet.t.sol +++ b/contracts/test/integration/MultiSubnet.t.sol @@ -45,7 +45,8 @@ import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; import "forge-std/console.sol"; -import {ActivitySummary} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract MultiSubnetTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -1351,7 +1352,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: batch.msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); @@ -1381,7 +1382,7 @@ contract MultiSubnetTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/contracts/test/integration/SubnetActorDiamond.t.sol b/contracts/test/integration/SubnetActorDiamond.t.sol index c7a8c30a9..ecfd54c18 100644 --- a/contracts/test/integration/SubnetActorDiamond.t.sol +++ b/contracts/test/integration/SubnetActorDiamond.t.sol @@ -43,9 +43,10 @@ import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; import {SubnetValidatorGater} from "../../contracts/examples/SubnetValidatorGater.sol"; -import {ActivitySummary, ValidatorSummary, BatchClaimProofs, ValidatorClaimProof} from "../../contracts/activities/Activity.sol"; +import {FullActivityRollup, Consensus} from "../../contracts/activities/Activity.sol"; import {ValidatorRewarderMap} from "../../contracts/examples/ValidatorRewarderMap.sol"; import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; contract SubnetActorDiamondTest is Test, IntegrationTestBase { using SubnetIDHelper for SubnetID; @@ -694,7 +695,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -703,7 +704,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -804,7 +805,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); BottomUpCheckpoint memory checkpointWithIncorrectHeight = BottomUpCheckpoint({ @@ -813,7 +814,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -842,7 +843,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { // submit another again checkpoint.blockHeight = 2; - checkpoint.activities = ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}); + checkpoint.activities = ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))); hash = keccak256(abi.encode(checkpoint)); for (uint256 i = 0; i < 3; i++) { @@ -899,7 +900,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect"); @@ -912,7 +913,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(2))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect"); @@ -924,7 +925,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(3))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -936,7 +937,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(4))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(CannotSubmitFutureCheckpoint.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -947,7 +948,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(5))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -961,7 +962,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(6))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -975,7 +976,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(7))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -989,7 +990,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(8))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.expectRevert(InvalidCheckpointEpoch.selector); submitCheckpointInternal(checkpoint, validators, signatures, keys); @@ -1000,7 +1001,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(9))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1014,7 +1015,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: new IpcEnvelope[](0), - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(10))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); submitCheckpointInternal(checkpoint, validators, signatures, keys); require( @@ -1056,7 +1057,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block1"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(0)}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); vm.deal(address(saDiamond), 100 ether); @@ -1100,7 +1101,7 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { blockHash: keccak256("block2"), nextConfigurationNumber: 0, msgs: msgs, - activities: ActivitySummary({totalActiveValidators: 1, commitment: bytes32(uint256(1))}) + activities: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) }); hash = keccak256(abi.encode(checkpoint)); @@ -2364,7 +2365,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { powers[3] = 10000; saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2372,62 +2372,45 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot, bytes32[][] memory proofs) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, 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] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + ActivityHelper.wrapBytes32Array(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] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[1], blocksCommitted: blocksMined[1]}), + ActivityHelper.wrapBytes32Array(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] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[2], blocksCommitted: blocksMined[2]}), + ActivityHelper.wrapBytes32Array(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] + uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), + Consensus.ValidatorData({validator: addrs[3], blocksCommitted: blocksMined[3]}), + ActivityHelper.wrapBytes32Array(proofs[3]) ); // check @@ -2472,7 +2455,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); } - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2480,55 +2462,41 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot1)); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot2)); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); - BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); - ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); - claimProofs[0] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); - claimProofs[1] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2, - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs2[0] - }); + Consensus.ValidatorClaim[] memory claimProofs = new Consensus.ValidatorClaim[](2); + uint64[] memory checkpointHeights = new uint64[](2); + + checkpointHeights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + checkpointHeights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()) * 2; - batchProofs[0] = BatchClaimProofs({ - subnetId: subnetId, - proofs: claimProofs + claimProofs[0] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs1[0]) + }); + claimProofs[1] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs2[0]) }); - saDiamond.validatorReward().batchClaim(batchProofs); + saDiamond.validatorReward().batchSubnetClaim(subnetId, checkpointHeights, claimProofs); // check assert(m.blocksCommitted(addrs[0]) == 2); } - function testGatewayDiamond_ValidatorBatchClaimMiningReward_NoDoubleClaim() public { ValidatorRewarderMap m = new ValidatorRewarderMap(); { @@ -2564,7 +2532,6 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { saDiamond.manager().setFederatedPower(addrs, pubkeys, powers); } - bytes[] memory metadata = new bytes[](addrs.length); uint64[] memory blocksMined = new uint64[](addrs.length); blocksMined[0] = 1; @@ -2572,50 +2539,36 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase { (bytes32 activityRoot1, bytes32[][] memory proofs1) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - (bytes32 activityRoot2, bytes32[][] memory proofs2) = MerkleTreeHelper.createMerkleProofsForActivities( addrs, - blocksMined, - metadata + blocksMined ); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot1})); - confirmChange(addrs, privKeys, ActivitySummary({totalActiveValidators: 2, commitment: activityRoot2})); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot1)); + confirmChange(addrs, privKeys, ActivityHelper.newCompressedActivityRollup(2, 3, activityRoot2)); vm.startPrank(addrs[0]); vm.deal(addrs[0], 1 ether); - BatchClaimProofs[] memory batchProofs = new BatchClaimProofs[](1); - ValidatorClaimProof[] memory claimProofs = new ValidatorClaimProof[](2); - claimProofs[0] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); - claimProofs[1] = ValidatorClaimProof({ - summary: ValidatorSummary({ - checkpointHeight: uint64(gatewayDiamond.getter().bottomUpCheckPeriod()), - validator: addrs[0], - blocksCommitted: blocksMined[0], - metadata: metadata[0] - }), - proof: proofs1[0] - }); + Consensus.ValidatorClaim[] memory claimProofs = new Consensus.ValidatorClaim[](2); + uint64[] memory heights = new uint64[](2); - batchProofs[0] = BatchClaimProofs({ - subnetId: subnetId, - proofs: claimProofs + heights[0] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + heights[1] = uint64(gatewayDiamond.getter().bottomUpCheckPeriod()); + + claimProofs[0] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs1[0]) + }); + claimProofs[1] = Consensus.ValidatorClaim({ + data: Consensus.ValidatorData({validator: addrs[0], blocksCommitted: blocksMined[0]}), + proof: ActivityHelper.wrapBytes32Array(proofs2[0]) }); vm.expectRevert(ValidatorAlreadyClaimed.selector); - saDiamond.validatorReward().batchClaim(batchProofs); + saDiamond.validatorReward().batchSubnetClaim(subnetId, heights, claimProofs); } // ----------------------------------------------------------------------------------------------------------------- diff --git a/docs/fendermint/diagrams/activity_rollup1.png b/docs/fendermint/diagrams/activity_rollup1.png new file mode 100644 index 0000000000000000000000000000000000000000..6a2dc98cb771c9d6f72de8816e959a08b9c2d21a GIT binary patch literal 119471 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

CY8Mp1Eup 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 AfdBvi literal 0 HcmV?d00001 diff --git a/docs/fendermint/subnet_activities.md b/docs/fendermint/subnet_activities.md new file mode 100644 index 000000000..cf2c6d038 --- /dev/null +++ b/docs/fendermint/subnet_activities.md @@ -0,0 +1,78 @@ +# Subnet activities roll up + +## Overview +This doc introduces the notion of child subnet rolls up its activities through bottom-up checkpoints to its parent. +This is a mechanism to synthesise and propagate selective information about the activity that occurred inside a subnet to the parent. +The definition of `Activity` can be application specific, but generally, it represents critical information about the child subnet that +is valuable and can be acted upon in the parent(if there is one). Some sample activities could be: +- The blocks mined by each validator +- The total number of blocks mined in between bottom up checkpoints. +- ... + +The parent can choose to either relay this information up the hierarchy, or consume the incoming report for some effect, such as: +- rewarding validators by minting a token +- rebalancing the validator set based on performance metrics +- tracking subnet state +- ... + +For each application, it is possible to define its own `Activity` struct to handle different scenarios, but IPC tracks +the blocks mined by each validator and total number of blocks mined in between bottom up checkpoints out of the box. + +## Overall flow +The key idea is that child subnet tracks these activities and submits `commitment` to the parent subnet through bottom up +checkpoints. In the parent to trigger downstream effects, one needs to provide proofs to interact with the activities in the child subnet. +As an example, consider validator rewarding. The commitment is a merkle root over the blocks mined by each validator. This root is carried +to the parent as part of the bottom up checkpoint. The validator can claim its reward in the parent subnet by submitting a +merkle proof. The merkle proof can be deduced from child subnet. + +The key components are shown below: +![Activity Rollup Key Components](diagrams/activity_rollup1.png) +- Activity tracker actor: A fvm actor that tracks and aggregates the blockchain's activities. It allows querying and purging of subnet activities. +- Bottom up checkpoint: The bottom up checkpoint contains the commitment to subnet activities. +- Diamond facets: The different diamond facets to handle the follow-ups in the parent. IPC ships with `ValidatorRewardFacet` by default, but one can choose not to deploy it if not needed. + +## Key data models +The overall activity struct submitted in the child subnet is FullActivityRollUp. It carries a set of aggregated reports on various aspects of the +activity that took place in the subnet between the previous checkpoint and the checkpoint this summary is committed into. If this is the +first checkpoint, the summary contains information about the subnet's activity since genesis. + +The first report we introduce is the consensus level report. This report contains: +- Aggregated block mining stats: + - Total active validators: the total number of unique validators that have mined within this period. + - Total number of blocks committed: the total number of blocks committed by all validators during this period. +- Validator data: Contains an array of addresses and blocks mined for each validator. One can add more fields to track more activities, such as uptime metrics . + +We emit the full activities roll up as a local event, `ActivityRollupRecorded`, in the subnet, for stakeholders (e.g. validators) to monitor and present +any relevant payloads up in the hierarchy to trigger downstream effects. + +Over time, `FullActivityRollUp` can bloat, e.g. if the validator count in a subnet grows. When combined with xnet messages, the checkpoint size can become large, +which has detrimental effects on gas costs at the parent, and could even potentially exceed message size limits. Therefore, instead of embedding +the payloads in the checkpoint, we use a compressed summary in the bottom up checkpoint: CompressedActivityRollup. This struct contains the distilled information +that is critical to the parent subnet. At the moment, it contains the consensus level summary for validator rewarding. + +## Activity tacking in child subnet +TODO + +## Roll up in action +Once the activities are tracked in the child subnet, fendermint will generate the compressed summary and attach it as part of the bottom up checkpoint. +The generation of the compressed summary for each report could be different and generally application specific. +Once a quorum is reached in the child subnet, the relayer will submit the bottom up checkpoint to the parent subnet actor. Since the compressed +summary is part of the bottom up checkpoint, the compressed summary is forwarded to the parent subnet. + +## Validator rewards +For IPC, the validator rewarding mechanism is shipped out of the box. When the bottom up checkpoint is constructed, fendermint will query all +validator mining details from the activity tracker. It aggregates the number of blocks mined for each validator and merklize over the array. +Note that the validators are sorted by their address and blocks mined for unique ordering. +Since the `FullActivityRollUp` contains all validators' mining activities, validator can filter the `ActivityRollupRecorded` events against +fendermint's eth rpc endpoints to obtain the full list of activities to derive the merkle proof. There is an `ipc-cli` command to help validators +claim the reward. + +The bottom up checkpoint submitted in the parent subnet will initialize a new reward distribution for that checkpoint height. When a validator claims +the reward, the subnet actor will verify the merkle proof for the pending checkpoint height, and call the ValidatorRewarder implementation if it +succeeds, marking the fragment as claimed in its state, so it cannot be double claimed. + +IPC `ValidatorRewarderFacet` tracks the claim history of each validator to avoid double claiming. However, the detailed reward distribution +logic is actually handled by a custom contract, namely `IValidatorRewarder`. When the subnet is created, the creator can specify the address +of the validator rewarder implementation and when eligible validator claims the reward, the subnet actor will pass the validator address and +blocks mined to the rewarder implementation. As an example, the rewarder implementation could mint erc20 based on the parameters sent from subnet +actor. \ No newline at end of file diff --git a/extras/axelar-token/foundry.toml b/extras/axelar-token/foundry.toml index bd8ca5d7e..e06f33cc0 100644 --- a/extras/axelar-token/foundry.toml +++ b/extras/axelar-token/foundry.toml @@ -5,4 +5,5 @@ libs = ["node_modules", "lib"] fs_permissions = [{ access = "read-write", path = "./out"}] remappings = [ "@consensus-shipyard/=node_modules/@consensus-shipyard/" -] \ No newline at end of file +] +allow_paths = ["../../contracts"] \ No newline at end of file diff --git a/extras/linked-token/foundry.toml b/extras/linked-token/foundry.toml index 57993af93..4409ba773 100644 --- a/extras/linked-token/foundry.toml +++ b/extras/linked-token/foundry.toml @@ -7,4 +7,4 @@ remappings = [ "@ipc/=node_modules/@consensus-shipyard/ipc-contracts/", ## this murky remapping is only needed transitively for testing; we should try to get rid of this. "murky/=node_modules/@consensus-shipyard/ipc-contracts/lib/murky/src/", -] +] \ No newline at end of file diff --git a/extras/linked-token/test/MultiSubnetTest.t.sol b/extras/linked-token/test/MultiSubnetTest.t.sol index 38143279f..26d7d529b 100644 --- a/extras/linked-token/test/MultiSubnetTest.t.sol +++ b/extras/linked-token/test/MultiSubnetTest.t.sol @@ -28,6 +28,7 @@ import {GatewayGetterFacet} from "@ipc/contracts/gateway/GatewayGetterFacet.sol" import {SubnetActorCheckpointingFacet} from "@ipc/contracts/subnet/SubnetActorCheckpointingFacet.sol"; import {CheckpointingFacet} from "@ipc/contracts/gateway/router/CheckpointingFacet.sol"; import {FvmAddressHelper} from "@ipc/contracts/lib/FvmAddressHelper.sol"; +import {Consensus, CompressedActivityRollup} from "@ipc/contracts/activities/Activity.sol"; import {IpcEnvelope, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality, IpcMsgKind, ResultMsg, CallMsg} from "@ipc/contracts/structs/CrossNet.sol"; import {SubnetIDHelper} from "@ipc/contracts/lib/SubnetIDHelper.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -383,7 +384,16 @@ contract MultiSubnetTest is IntegrationTestBase { blockHeight: batch.blockHeight, blockHash: keccak256("block1"), nextConfigurationNumber: 0, - msgs: batch.msgs + msgs: batch.msgs, + activities: CompressedActivityRollup({ + consensus: Consensus.CompressedSummary({ + stats: Consensus.AggregatedStats({ + totalActiveValidators: 1, + totalNumBlocksCommitted: 1 + }), + dataRootCommitment: Consensus.MerkleHash.wrap(bytes32(0)) + }) + }) }); vm.startPrank(FilAddress.SYSTEM_ACTOR); diff --git a/fendermint/actors/activity-tracker/Cargo.toml b/fendermint/actors/activity-tracker/Cargo.toml index 886262036..0c06eaa25 100644 --- a/fendermint/actors/activity-tracker/Cargo.toml +++ b/fendermint/actors/activity-tracker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fendermint_actor_activity_tracker" -description = "Tracks fendermint block mining activities" +description = "Tracks subnet activity and generates rollups to submit to the parent in checkpoints" license.workspace = true edition.workspace = true authors.workspace = true @@ -14,7 +14,6 @@ crate-type = ["cdylib", "lib"] [dependencies] anyhow = { workspace = true } cid = { workspace = true } -fil_actor_eam = { workspace = true } fil_actors_runtime = { workspace = true } fvm_ipld_blockstore = { workspace = true } fvm_ipld_encoding = { workspace = true } @@ -24,6 +23,7 @@ multihash = { workspace = true } num-derive = { workspace = true } num-traits = { workspace = true } serde = { workspace = true } +serde_tuple = { workspace = true } hex-literal = { workspace = true } frc42_dispatch = { workspace = true } diff --git a/fendermint/actors/activity-tracker/src/lib.rs b/fendermint/actors/activity-tracker/src/lib.rs index 791bc688a..f8f84faa9 100644 --- a/fendermint/actors/activity-tracker/src/lib.rs +++ b/fendermint/actors/activity-tracker/src/lib.rs @@ -1,21 +1,19 @@ -// Copyright 2021-2023 Protocol Labs +// Copyright 2021-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use fil_actors_runtime::actor_error; +use crate::state::ConsensusData; +pub use crate::state::State; +use crate::types::FullActivityRollup; use fil_actors_runtime::builtin::singletons::SYSTEM_ACTOR_ADDR; use fil_actors_runtime::runtime::{ActorCode, Runtime}; -use fil_actors_runtime::{actor_dispatch, ActorError}; -use fvm_ipld_encoding::tuple::*; -use fvm_shared::address::Address; -use fvm_shared::clock::ChainEpoch; +use fil_actors_runtime::{actor_dispatch, ActorError, EAM_ACTOR_ID}; +use fil_actors_runtime::{actor_error, DEFAULT_HAMT_CONFIG}; +use fvm_shared::address::{Address, Payload}; use fvm_shared::METHOD_CONSTRUCTOR; use num_derive::FromPrimitive; -use serde::{Deserialize, Serialize}; - -pub use crate::state::State; -pub use crate::state::ValidatorSummary; mod state; +pub mod types; #[cfg(feature = "fil-actor")] fil_actors_runtime::wasm_trampoline!(ActivityTrackerActor); @@ -24,64 +22,82 @@ pub const IPC_ACTIVITY_TRACKER_ACTOR_NAME: &str = "activity_tracker"; pub struct ActivityTrackerActor; -#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone)] -pub struct BlockedMinedParams { - pub validator: Address, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct GetActivitiesResult { - pub activities: Vec, - pub start_height: ChainEpoch, -} - #[derive(FromPrimitive)] #[repr(u64)] pub enum Method { Constructor = METHOD_CONSTRUCTOR, - BlockMined = frc42_dispatch::method_hash!("BlockMined"), - GetActivities = frc42_dispatch::method_hash!("GetActivities"), - PurgeActivities = frc42_dispatch::method_hash!("PurgeActivities"), + RecordBlockCommitted = frc42_dispatch::method_hash!("RecordBlockCommitted"), + CommitActivity = frc42_dispatch::method_hash!("CommitActivity"), + PendingActivity = frc42_dispatch::method_hash!("PendingActivity"), +} + +trait ActivityTracker { + /// Hook for the consensus layer to report that the validator committed a new block. + fn record_block_committed(rt: &impl Runtime, validator: Address) -> Result<(), ActorError>; + + /// Commits the pending activity into an activity rollup. + /// Currently, this constructs an activity rollup from the internal state, and then resets the internal state. + /// In the future, this might actually write the activity rollup to the gateway directly, instead of relying on the client to move it around. + /// Returns the activity rollup as a Solidity ABI-encoded type, in raw byte form. + fn commit_activity(rt: &impl Runtime) -> Result; + + /// Queries the activity that has been accumulated since the last commit, and is pending a flush. + fn pending_activity(rt: &impl Runtime) -> Result; } impl ActivityTrackerActor { pub fn constructor(rt: &impl Runtime) -> Result<(), ActorError> { let st = State::new(rt.store())?; rt.create(&st)?; - Ok(()) } +} - pub fn block_mined(rt: &impl Runtime, block: BlockedMinedParams) -> Result<(), ActorError> { +impl ActivityTracker for ActivityTrackerActor { + fn record_block_committed(rt: &impl Runtime, validator: Address) -> Result<(), ActorError> { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + // Reject non-f410 addresses. + if !matches!(validator.payload(), Payload::Delegated(d) if d.namespace() == EAM_ACTOR_ID && d.subaddress().len() == 20) + { + return Err( + actor_error!(illegal_argument; "validator address must be a valid f410 address"), + ); + } + rt.transaction(|st: &mut State, rt| { - st.incr_validator_block_committed(rt, &block.validator) - })?; + let mut consensus = + ConsensusData::load(rt.store(), &st.consensus, DEFAULT_HAMT_CONFIG, "consensus")?; - Ok(()) + let mut v = consensus.get(&validator)?.cloned().unwrap_or_default(); + v.blocks_committed += 1; + consensus.set(&validator, v)?; + + st.consensus = consensus.flush()?; + + Ok(()) + }) } - pub fn purge_activities(rt: &impl Runtime) -> Result<(), ActorError> { + fn commit_activity(rt: &impl Runtime) -> Result { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; + // Obtain the pending rollup from state. + let rollup = rt.state::()?.pending_activity_rollup(rt)?; + rt.transaction(|st: &mut State, rt| { - st.purge_validator_block_committed(rt)?; - st.reset_start_height(rt) + st.consensus = ConsensusData::flush_empty(rt.store(), DEFAULT_HAMT_CONFIG)?; + st.tracking_since = rt.curr_epoch(); + Ok(()) })?; - Ok(()) + Ok(rollup) } - pub fn get_activities(rt: &impl Runtime) -> Result { + fn pending_activity(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, - }) + rt.state::()?.pending_activity_rollup(rt) } } @@ -94,8 +110,8 @@ impl ActorCode for ActivityTrackerActor { actor_dispatch! { Constructor => constructor, - BlockMined => block_mined, - GetActivities => get_activities, - PurgeActivities => purge_activities, + RecordBlockCommitted => record_block_committed, + CommitActivity => commit_activity, + PendingActivity => pending_activity, } } diff --git a/fendermint/actors/activity-tracker/src/state.rs b/fendermint/actors/activity-tracker/src/state.rs index 7377b78b3..b7ae7b88b 100644 --- a/fendermint/actors/activity-tracker/src/state.rs +++ b/fendermint/actors/activity-tracker/src/state.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT +use crate::types::{FullActivityRollup, ValidatorStats}; use cid::Cid; use fil_actors_runtime::runtime::Runtime; use fil_actors_runtime::{ActorError, Map2, DEFAULT_HAMT_CONFIG}; @@ -9,100 +10,45 @@ use fvm_shared::address::Address; use fvm_shared::clock::ChainEpoch; use serde::{Deserialize, Serialize}; -pub type BlockCommittedMap = Map2; -pub type BlockCommitted = u64; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct ValidatorSummary { - pub validator: Address, - pub block_committed: BlockCommitted, - pub metadata: Vec, -} - #[derive(Deserialize, Serialize, Debug, Clone)] pub struct State { - pub start_height: ChainEpoch, - pub blocks_committed: Cid, // BlockCommittedMap + pub tracking_since: ChainEpoch, + pub consensus: Cid, // ConsensusData } +pub type ConsensusData = Map2; + impl State { 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()?, - }) - } - - pub fn reset_start_height(&mut self, rt: &impl Runtime) -> Result<(), ActorError> { - self.start_height = rt.curr_epoch(); - Ok(()) - } - - 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", - )?; - - for v in all_validators { - validators.delete(&v.validator)?; - } - - self.blocks_committed = validators.flush()?; - - Ok(()) - } - - 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 - } else { - 1 + let state = State { + tracking_since: 0, + consensus: ConsensusData::flush_empty(store, DEFAULT_HAMT_CONFIG)?, }; - - validators.set(validator, v)?; - - self.blocks_committed = validators.flush()?; - - Ok(()) + Ok(state) } - pub fn validator_activities( + /// Returns the pending activity rollup. + pub fn pending_activity_rollup( &self, rt: &impl Runtime, - ) -> Result, ActorError> { - let mut result = vec![]; - - 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 { + let consensus = { + let cid = &rt.state::()?.consensus; + ConsensusData::load(rt.store(), cid, DEFAULT_HAMT_CONFIG, "consensus") + }?; + + // Populate the rollup struct. + let mut rollup = FullActivityRollup::default(); + consensus.for_each(|validator_addr, validator_stats| { + rollup.consensus.stats.total_active_validators += 1; + rollup.consensus.stats.total_num_blocks_committed += validator_stats.blocks_committed; + rollup + .consensus + .data + .insert(validator_addr, validator_stats.clone()); Ok(()) })?; - Ok(result) + Ok(rollup) } } diff --git a/fendermint/actors/activity-tracker/src/types.rs b/fendermint/actors/activity-tracker/src/types.rs new file mode 100644 index 000000000..3ad297d4f --- /dev/null +++ b/fendermint/actors/activity-tracker/src/types.rs @@ -0,0 +1,28 @@ +// Copyright 2021-2023 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; +use fvm_shared::address::Address; +use std::collections::HashMap; + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct AggregatedStats { + pub total_active_validators: u64, + pub total_num_blocks_committed: u64, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct FullConsensusSummary { + pub stats: AggregatedStats, + pub data: HashMap, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct FullActivityRollup { + pub consensus: FullConsensusSummary, +} + +#[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq, Default)] +pub struct ValidatorStats { + pub blocks_committed: u64, +} diff --git a/fendermint/eth/api/Cargo.toml b/fendermint/eth/api/Cargo.toml index c1b675bb7..997bb4496 100644 --- a/fendermint/eth/api/Cargo.toml +++ b/fendermint/eth/api/Cargo.toml @@ -32,7 +32,7 @@ tokio = { workspace = true } tower-http = { workspace = true } fil_actors_evm_shared = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_encoding = { workspace = true } fendermint_crypto = { path = "../../crypto" } diff --git a/fendermint/eth/api/src/apis/eth.rs b/fendermint/eth/api/src/apis/eth.rs index 7e0af9213..01ba886f8 100644 --- a/fendermint/eth/api/src/apis/eth.rs +++ b/fendermint/eth/api/src/apis/eth.rs @@ -1016,6 +1016,7 @@ where // Filter by address. if !addrs.is_empty() && addrs.intersection(&emitters).next().is_none() { + height = height.increment(); continue; } diff --git a/fendermint/testing/contract-test/Cargo.toml b/fendermint/testing/contract-test/Cargo.toml index 405b8c22f..60f82f8a7 100644 --- a/fendermint/testing/contract-test/Cargo.toml +++ b/fendermint/testing/contract-test/Cargo.toml @@ -13,7 +13,7 @@ anyhow = { workspace = true } cid = { workspace = true } ethers = { workspace = true } fvm = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_blockstore = { workspace = true } hex = { workspace = true } rand = { workspace = true } diff --git a/fendermint/vm/actor_interface/src/ipc.rs b/fendermint/vm/actor_interface/src/ipc.rs index be5248fb5..572df6af7 100644 --- a/fendermint/vm/actor_interface/src/ipc.rs +++ b/fendermint/vm/actor_interface/src/ipc.rs @@ -118,6 +118,11 @@ lazy_static! { name: "OwnershipFacet", abi: ia::ownership_facet::OWNERSHIPFACET_ABI.to_owned(), }, + EthFacet { + name: "ValidatorRewardFacet", + abi: ia::validator_reward_facet::VALIDATORREWARDFACET_ABI.to_owned(), + }, + // ========== IF YOU WANT TO ADD FACET FOR SUBNET, APPEND HERE ========== // The registry has its own facets: // https://github.com/consensus-shipyard/ipc-solidity-actors/blob/b01a2dffe367745f55111a65536a3f6fea9165f5/scripts/deploy-registry.template.ts#L58-L67 EthFacet { @@ -129,10 +134,6 @@ 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(), - }, ], }, ), @@ -546,7 +547,7 @@ pub mod subnet { let param_type = BottomUpCheckpoint::param_type(); // Captured value of `abi.encode` in Solidity. - let expected_abi: Bytes = "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000156b736f342ab34d9afe4234a92bdb190c35b2e8d822d9601b00b9d7089b190f0100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000abc8e314f58b4de5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007b11cf9ca8ccee13bb3d003c97af5c18434067a90000000000000000000000003d9019b8bf3bfd5e979ddc3b2761be54af867c470000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(); + let expected_abi: Bytes = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000156b736f342ab34d9afe4234a92bdb190c35b2e8d822d9601b00b9d7089b190f01000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000abc8e314f58b4de5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000007b11cf9ca8ccee13bb3d003c97af5c18434067a90000000000000000000000003d9019b8bf3bfd5e979ddc3b2761be54af867c470000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(); // XXX: It doesn't work with `decode_whole`. let expected_tokens = diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index 39c6f2e34..a82fae6e1 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -83,6 +83,7 @@ fendermint_testing = { path = "../../testing", features = ["golden"] } fvm = { workspace = true, features = ["arb", "testing"] } fendermint_vm_genesis = { path = "../genesis", features = ["arb"] } multihash = { workspace = true } +hex = { workspace = true } [features] default = [] diff --git a/fendermint/vm/interpreter/src/fvm/activities/actor.rs b/fendermint/vm/interpreter/src/fvm/activities/actor.rs deleted file mode 100644 index 87ddad1cb..000000000 --- a/fendermint/vm/interpreter/src/fvm/activities/actor.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -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, DB: Blockstore + Clone + 'static> { - pub(crate) executor: &'a mut FvmExecState, - pub(crate) epoch: ChainEpoch, -} - -impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker - for ActorActivityTracker<'a, DB> -{ - type ValidatorSummaryDetail = ValidatorSummary; - - fn track_block_mined(&mut self, block: BlockMined) -> anyhow::Result<()> { - let params = fendermint_actor_activity_tracker::BlockedMinedParams { - validator: fvm_shared::address::Address::from(EthAddress::from(block.validator)), - }; - - 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::BlockMined as u64, - params: fvm_ipld_encoding::RawBytes::serialize(params)?, - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - self.apply_implicit_message(msg)?; - 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.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<()> { - 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::PurgeActivities as u64, - params: fvm_ipld_encoding::RawBytes::default(), - value: Default::default(), - version: Default::default(), - gas_fee_cap: Default::default(), - gas_premium: Default::default(), - }; - - self.apply_implicit_message(msg)?; - Ok(()) - } -} - -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 { - Ok(apply_ret) - } - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activities/mod.rs b/fendermint/vm/interpreter/src/fvm/activities/mod.rs deleted file mode 100644 index 1c509c228..000000000 --- a/fendermint/vm/interpreter/src/fvm/activities/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -//! Tracks the current blockchain block mining activities and propagates to the parent subnet if -//! needed. - -pub mod actor; -mod merkle; - -use crate::fvm::activities::merkle::MerkleProofGen; -use fendermint_actor_activity_tracker::ValidatorSummary; -use fendermint_crypto::PublicKey; -use ipc_api::checkpoint::ActivitySummary; -use std::fmt::Debug; - -pub struct BlockMined { - pub(crate) validator: PublicKey, -} - -#[derive(Debug, Clone)] -pub struct ActivityDetails { - pub details: Vec, -} - -/// Tracks the validator activities in the current blockchain -pub trait ValidatorActivityTracker { - type ValidatorSummaryDetail: Clone + Debug; - - /// Mark the validator has mined the target block. - 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>; - - /// Purge the current validator activities summary - fn purge_activities(&mut self) -> anyhow::Result<()>; -} - -impl ActivityDetails { - pub fn commitment(&self) -> anyhow::Result { - let gen = MerkleProofGen::new(self.details.as_slice())?; - Ok(ActivitySummary { - total_active_validators: self.details.len() as u64, - commitment: gen.root().to_fixed_bytes().to_vec(), - }) - } -} diff --git a/fendermint/vm/interpreter/src/fvm/activity/actor.rs b/fendermint/vm/interpreter/src/fvm/activity/actor.rs new file mode 100644 index 000000000..9948d0c53 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/activity/actor.rs @@ -0,0 +1,75 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::fvm::activity::{FullActivity, ValidatorActivityTracker}; +use crate::fvm::state::FvmExecState; +use crate::fvm::FvmMessage; +use anyhow::Context; +use fendermint_actor_activity_tracker::types::FullActivityRollup; +use fendermint_crypto::PublicKey; +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::address::Address; + +pub struct ActorActivityTracker<'a, DB: Blockstore + Clone + 'static> { + pub(crate) executor: &'a mut FvmExecState, +} + +impl<'a, DB: Blockstore + Clone + 'static> ValidatorActivityTracker + for ActorActivityTracker<'a, DB> +{ + fn record_block_committed(&mut self, validator: PublicKey) -> anyhow::Result<()> { + let address: Address = EthAddress::from(validator).into(); + + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: 0, // irrelevant + gas_limit: i64::MAX as u64, // exclude this from gas restriction + method_num: fendermint_actor_activity_tracker::Method::RecordBlockCommitted as u64, + params: fvm_ipld_encoding::RawBytes::serialize(address)?, + value: Default::default(), + version: Default::default(), + gas_fee_cap: Default::default(), + gas_premium: Default::default(), + }; + + self.apply_implicit_message(msg)?; + Ok(()) + } + + fn commit_activity(&mut self) -> anyhow::Result { + let msg = FvmMessage { + from: system::SYSTEM_ACTOR_ADDR, + to: ACTIVITY_TRACKER_ACTOR_ADDR, + sequence: 0, // irrelevant + gas_limit: i64::MAX as u64, // exclude this from gas restriction + method_num: fendermint_actor_activity_tracker::Method::CommitActivity 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.execute_implicit(msg)?; + let r = + fvm_ipld_encoding::from_slice::(&apply_ret.msg_receipt.return_data) + .context("failed to parse validator activities")?; + r.try_into() + } +} + +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 { + Ok(apply_ret) + } + } +} diff --git a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs b/fendermint/vm/interpreter/src/fvm/activity/merkle.rs similarity index 55% rename from fendermint/vm/interpreter/src/fvm/activities/merkle.rs rename to fendermint/vm/interpreter/src/fvm/activity/merkle.rs index 53f2e7c3a..7a7586bcc 100644 --- a/fendermint/vm/interpreter/src/fvm/activities/merkle.rs +++ b/fendermint/vm/interpreter/src/fvm/activity/merkle.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::Context; -use fendermint_actor_activity_tracker::ValidatorSummary; -use ipc_api::evm::payload_to_evm_address; +use ipc_actors_abis::checkpointing_facet::ValidatorData; use ipc_observability::lazy_static; use merkle_tree_rs::format::Raw; use merkle_tree_rs::standard::StandardMerkleTree; @@ -12,7 +11,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!["address".to_owned(), "uint64".to_owned()]; ); /// The merkle tree based proof verification to interact with solidity contracts @@ -21,25 +20,16 @@ pub(crate) struct MerkleProofGen { } impl MerkleProofGen { + pub fn pack_validator(v: &ValidatorData) -> Vec { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + } + pub fn root(&self) -> Hash { self.tree.root() } -} -impl MerkleProofGen { - pub fn new(values: &[ValidatorSummary]) -> 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), - ] - }) - }) - .collect::>>()?; + pub fn new(values: &[ValidatorData]) -> anyhow::Result { + let values = values.iter().map(Self::pack_validator).collect::>(); let tree = StandardMerkleTree::of(&values, &VALIDATOR_SUMMARY_FIELDS) .context("failed to construct Merkle tree")?; diff --git a/fendermint/vm/interpreter/src/fvm/activity/mod.rs b/fendermint/vm/interpreter/src/fvm/activity/mod.rs new file mode 100644 index 000000000..f44173aaf --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/activity/mod.rs @@ -0,0 +1,156 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +//! Tracks the current blockchain block mining activities and propagates to the parent subnet if +//! needed. + +pub mod actor; +mod merkle; + +use crate::fvm::activity::merkle::MerkleProofGen; +use fendermint_crypto::PublicKey; +use ipc_actors_abis::checkpointing_facet::{ + AggregatedStats, CompressedActivityRollup, CompressedSummary, FullActivityRollup, FullSummary, + ValidatorData, +}; +use ipc_api::evm::payload_to_evm_address; + +/// Wrapper for FullActivityRollup with some utility functions +pub struct FullActivity(FullActivityRollup); + +/// Tracks the validator activities in the current blockchain +pub trait ValidatorActivityTracker { + /// Mark the validator has mined the target block. + fn record_block_committed(&mut self, validator: PublicKey) -> anyhow::Result<()>; + + /// Get the validators activities summary since the checkpoint height + fn commit_activity(&mut self) -> anyhow::Result; +} + +impl TryFrom for FullActivity { + type Error = anyhow::Error; + + fn try_from( + value: fendermint_actor_activity_tracker::types::FullActivityRollup, + ) -> Result { + let f = FullActivityRollup { + consensus: FullSummary { + stats: AggregatedStats { + total_active_validators: value.consensus.stats.total_active_validators, + total_num_blocks_committed: value.consensus.stats.total_num_blocks_committed, + }, + data: value + .consensus + .data + .into_iter() + .map(|(addr, data)| { + Ok(ValidatorData { + validator: payload_to_evm_address(addr.payload())?, + blocks_committed: data.blocks_committed, + }) + }) + .collect::>>()?, + }, + }; + Ok(Self::new(f)) + } +} + +impl FullActivity { + pub fn new(mut full: FullActivityRollup) -> Self { + full.consensus.data.sort_by(|a, b| { + let cmp = a.validator.cmp(&b.validator); + if cmp.is_eq() { + // Address will be unique, do this just in case equal + a.blocks_committed.cmp(&b.blocks_committed) + } else { + cmp + } + }); + Self(full) + } + + pub fn compressed(&self) -> anyhow::Result { + let gen = MerkleProofGen::new(self.0.consensus.data.as_slice())?; + Ok(CompressedActivityRollup { + consensus: CompressedSummary { + stats: self.0.consensus.stats.clone(), + data_root_commitment: gen.root().to_fixed_bytes(), + }, + }) + } + + pub fn into_inner(self) -> FullActivityRollup { + self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::fvm::activity::FullActivity; + use ipc_actors_abis::checkpointing_facet::{ + AggregatedStats, FullActivityRollup, FullSummary, ValidatorData, + }; + use rand::prelude::SliceRandom; + use rand::thread_rng; + use std::str::FromStr; + + #[test] + fn test_commitment() { + let mut v = vec![ + ValidatorData { + validator: ethers::types::Address::from_str( + "0xB29C00299756135ec5d6A140CA54Ec77790a99d6", + ) + .unwrap(), + blocks_committed: 1, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x28345a43c2fBae4412f0AbadFa06Bd8BA3f58867", + ) + .unwrap(), + blocks_committed: 2, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x1A79385eAd0e873FE0C441C034636D3Edf7014cC", + ) + .unwrap(), + blocks_committed: 10, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x76B9d5a35C46B1fFEb37aadf929f1CA63a26A829", + ) + .unwrap(), + blocks_committed: 4, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x3c5cc76b07cb02a372e647887bD6780513659527", + ) + .unwrap(), + blocks_committed: 3, + }, + ]; + + for _ in 0..10 { + v.shuffle(&mut thread_rng()); + let full = FullActivityRollup { + consensus: FullSummary { + stats: AggregatedStats { + total_active_validators: 1, + total_num_blocks_committed: 2, + }, + data: v.clone(), + }, + }; + let details = FullActivity::new(full); + assert_eq!( + hex::encode(details.compressed().unwrap().consensus.data_root_commitment), + "5519955f33109df3338490473cb14458640efdccd4df05998c4c439738280ab0" + ); + } + } +} diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index 4cb835c01..5aca2db83 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -1,42 +1,35 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::collections::HashMap; -use std::time::Duration; - +use super::observe::{ + CheckpointCreated, CheckpointFinalized, CheckpointSigned, CheckpointSignedRole, +}; +use super::state::ipc::tokens_to_burn; +use super::{ + broadcast::Broadcaster, + state::{ipc::GatewayCaller, FvmExecState}, + ValidatorContext, +}; +use crate::fvm::activity::ValidatorActivityTracker; +use crate::fvm::exec::BlockEndEvents; use anyhow::{anyhow, Context}; use ethers::abi::Tokenizable; -use tendermint::block::Height; -use tendermint_rpc::endpoint::commit; -use tendermint_rpc::{endpoint::validators, Client, Paging}; - -use fvm_ipld_blockstore::Blockstore; -use fvm_shared::{address::Address, chainid::ChainID}; - use fendermint_crypto::PublicKey; use fendermint_crypto::SecretKey; 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 fvm_ipld_blockstore::Blockstore; +use fvm_shared::{address::Address, chainid::ChainID}; use ipc_actors_abis::checkpointing_facet as checkpoint; use ipc_actors_abis::gateway_getter_facet as getter; use ipc_api::staking::ConfigurationNumber; use ipc_observability::{emit, serde::HexEncodableBlockHash}; - -use super::observe::{ - CheckpointCreated, CheckpointFinalized, CheckpointSigned, CheckpointSignedRole, -}; -use super::state::ipc::tokens_to_burn; -use super::{ - broadcast::Broadcaster, - state::{ipc::GatewayCaller, FvmExecState}, - ValidatorContext, -}; -use crate::fvm::activities::ValidatorActivityTracker; -use crate::fvm::exec::BlockEndEvents; +use std::collections::HashMap; +use std::time::Duration; +use tendermint::block::Height; +use tendermint_rpc::endpoint::commit; +use tendermint_rpc::{endpoint::validators, Client, Paging}; /// Validator voting power snapshot. #[derive(Debug, Clone, PartialEq, Eq)] @@ -101,7 +94,7 @@ where let num_msgs = msgs.len(); - let activities = state.activities_tracker().get_activities_summary()?; + let full_activity_rollup = state.activities_tracker().commit_activity()?; // Construct checkpoint. let checkpoint = BottomUpCheckpoint { @@ -110,31 +103,21 @@ where block_hash, next_configuration_number, msgs, - activities: activities.commitment()?.try_into()?, + activities: full_activity_rollup.compressed()?, }; // Save the checkpoint in the ledger. // Pass in the current power table, because these are the validators who can sign this 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::>>()?, - }; let ret = gateway - .create_bu_ckpt_with_activities(state, checkpoint.clone(), &curr_power_table.0, report) + .create_bottom_up_checkpoint( + state, + checkpoint.clone(), + &curr_power_table.0, + full_activity_rollup.into_inner(), + ) .context("failed to store checkpoint")?; event_tracker.push((ret.apply_ret.events, ret.emitters)); - state.activities_tracker().purge_activities()?; - // Figure out the power updates if there was some change in the configuration. let power_updates = if next_configuration_number == 0 { PowerUpdates(Vec::new()) @@ -266,9 +249,22 @@ where block_hash: cp.block_hash, next_configuration_number: cp.next_configuration_number, msgs: convert_tokenizables(cp.msgs)?, - activities: checkpoint::ActivitySummary { - total_active_validators: cp.activities.total_active_validators, - commitment: cp.activities.commitment, + activities: checkpoint::CompressedActivityRollup { + consensus: checkpoint::CompressedSummary { + stats: checkpoint::AggregatedStats { + total_active_validators: cp + .activities + .consensus + .stats + .total_active_validators, + total_num_blocks_committed: cp + .activities + .consensus + .stats + .total_num_blocks_committed, + }, + data_root_commitment: cp.activities.consensus.data_root_commitment, + }, }, }; diff --git a/fendermint/vm/interpreter/src/fvm/exec.rs b/fendermint/vm/interpreter/src/fvm/exec.rs index 00814b4fa..3006218a5 100644 --- a/fendermint/vm/interpreter/src/fvm/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/exec.rs @@ -7,7 +7,7 @@ use super::{ state::FvmExecState, BlockGasLimit, FvmMessage, FvmMessageInterpreter, }; -use crate::fvm::activities::{BlockMined, ValidatorActivityTracker}; +use crate::fvm::activity::ValidatorActivityTracker; use crate::ExecInterpreter; use anyhow::Context; use async_trait::async_trait; @@ -204,9 +204,7 @@ where let mut block_end_events = BlockEndEvents::default(); if let Some(pubkey) = state.block_producer() { - state - .activities_tracker() - .track_block_mined(BlockMined { validator: pubkey })?; + state.activities_tracker().record_block_committed(pubkey)?; } let next_gas_market = state.finalize_gas_market()?; diff --git a/fendermint/vm/interpreter/src/fvm/externs.rs b/fendermint/vm/interpreter/src/fvm/externs.rs index 7f02b0611..f17e03f68 100644 --- a/fendermint/vm/interpreter/src/fvm/externs.rs +++ b/fendermint/vm/interpreter/src/fvm/externs.rs @@ -36,18 +36,6 @@ 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/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 2a4ce6432..85962ab4b 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -15,7 +15,7 @@ pub mod upgrades; #[cfg(any(test, feature = "bundle"))] pub mod bundle; -pub mod activities; +pub mod activity; pub(crate) mod gas; pub(crate) mod topdown; diff --git a/fendermint/vm/interpreter/src/fvm/state/exec.rs b/fendermint/vm/interpreter/src/fvm/state/exec.rs index f26f634bf..65b0a9438 100644 --- a/fendermint/vm/interpreter/src/fvm/state/exec.rs +++ b/fendermint/vm/interpreter/src/fvm/state/exec.rs @@ -1,13 +1,11 @@ // 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::activity::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; @@ -16,7 +14,6 @@ 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, @@ -118,8 +115,6 @@ where params: FvmUpdatableParams, /// Indicate whether the parameters have been updated. params_dirty: bool, - - executor_info: ExecutorInfo, } impl FvmExecState @@ -169,11 +164,6 @@ where power_scale: params.power_scale, }, params_dirty: false, - - executor_info: ExecutorInfo { - engine_pool: engine, - store: blockstore.clone(), - }, }) } @@ -303,10 +293,7 @@ where } pub fn activities_tracker(&mut self) -> ActorActivityTracker { - ActorActivityTracker { - epoch: self.block_height(), - executor: self, - } + ActorActivityTracker { executor: self } } /// Collect all the event emitters' delegated addresses, for those who have any. @@ -369,22 +356,6 @@ where f(&mut self.params); self.params_dirty = true; } - - 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 @@ -425,33 +396,3 @@ fn check_error(e: anyhow::Error) -> (ApplyRet, ActorAddressMap) { }; (ret, Default::default()) } - -/// 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 7fb7143df..2eb04c752 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -123,28 +123,7 @@ impl GatewayCaller { state: &mut FvmExecState, checkpoint: checkpointing_facet::BottomUpCheckpoint, power_table: &[Validator], - ) -> 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_bottom_up_checkpoint(checkpoint, tree.root_hash().0, total_power) - }) - } - - /// 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, + activities: checkpointing_facet::FullActivityRollup, ) -> 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. diff --git a/fendermint/vm/message/Cargo.toml b/fendermint/vm/message/Cargo.toml index 3816b9170..77d264fb2 100644 --- a/fendermint/vm/message/Cargo.toml +++ b/fendermint/vm/message/Cargo.toml @@ -22,7 +22,7 @@ quickcheck = { workspace = true, optional = true } rand = { workspace = true, optional = true } cid = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } fvm_ipld_encoding = { workspace = true } ipc-api = { workspace = true } diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 5edb1a7d8..e19691454 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -76,40 +76,52 @@ pub struct BottomUpMsgBatch { pub msgs: Vec, } -/// The commitments for the child subnet activities that should be submitted to the parent subnet -/// together with a bottom up checkpoint +/// Compressed representation of the activity summary that can be embedded in checkpoints to propagate up the hierarchy. #[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, +pub struct CompressedActivityRollup { + pub consensus: consensus::CompressedSummary, } -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct BatchClaimProofs { - pub subnet_id: SubnetID, - pub proofs: Vec, -} +/// Namespace for consensus-level activity summaries. +/// XYZ(raulk) move to activity module +pub mod consensus { + use fvm_shared::address::Address; + use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] -pub struct ValidatorClaimProof { - pub summary: ValidatorSummary, - pub proof: Vec<[u8; 32]>, -} + /// Aggregated stats for consensus-level activity. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct AggregatedStats { + /// The total number of unique validators that have mined within this period. + pub total_active_validators: u64, + /// The total number of blocks committed by all validators during this period. + pub total_num_blocks_committed: u64, + } -#[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 commitment: Vec, - // TODO: add relayed activity commitment + /// The commitments for the child subnet activities that should be submitted to the parent subnet + /// together with a bottom up checkpoint + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct ValidatorData { + /// The validator address + pub validator: Address, + /// The number of blocks mined + pub blocks_committed: u64, + } + + // The full activity summary for consensus-level activity. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct FullSummary { + pub stats: AggregatedStats, + /// The breakdown of activity per validator. + pub data: Vec, + } + + /// The compresed representation of the activity summary for consensus-level activity suitable for embedding in a checkpoint. + #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] + pub struct CompressedSummary { + pub stats: AggregatedStats, + /// The commitment for the validator details, so that we don't have to transmit them in full. + pub data_root_commitment: Vec, + } } #[serde_as] @@ -131,7 +143,7 @@ pub struct BottomUpCheckpoint { /// The list of messages for execution pub msgs: Vec, /// The activity commitment from child subnet to parent subnet - pub activities: ActivitySummary, + pub activity_rollup: CompressedActivityRollup, } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/api/src/evm.rs b/ipc/api/src/evm.rs index 7722a19d0..dd40829ea 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::{ActivitySummary, BatchClaimProofs, BottomUpCheckpoint}; -use crate::checkpoint::{BottomUpMsgBatch, ValidatorClaimProof}; +use crate::checkpoint::BottomUpMsgBatch; +use crate::checkpoint::{consensus, BottomUpCheckpoint, CompressedActivityRollup}; use crate::cross::{IpcEnvelope, IpcMsgKind}; use crate::staking::StakingChange; use crate::staking::StakingChangeRequest; @@ -122,14 +122,55 @@ 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::ActivitySummary { + impl TryFrom for $module::AggregatedStats { type Error = anyhow::Error; - fn try_from(c: ActivitySummary) -> Result { - Ok($module::ActivitySummary { + fn try_from(c: consensus::AggregatedStats) -> Result { + Ok($module::AggregatedStats { total_active_validators: c.total_active_validators, - commitment: c - .commitment + total_num_blocks_committed: c.total_num_blocks_committed, + }) + } + } + + impl TryFrom for $module::CompressedActivityRollup { + type Error = anyhow::Error; + + fn try_from(c: CompressedActivityRollup) -> Result { + Ok($module::CompressedActivityRollup { + consensus: c.consensus.try_into()?, + }) + } + } + + impl From<$module::CompressedActivityRollup> for CompressedActivityRollup { + fn from(value: $module::CompressedActivityRollup) -> Self { + CompressedActivityRollup { + consensus: consensus::CompressedSummary { + stats: consensus::AggregatedStats { + total_active_validators: value.consensus.stats.total_active_validators, + total_num_blocks_committed: value + .consensus + .stats + .total_num_blocks_committed, + }, + data_root_commitment: value.consensus.data_root_commitment.to_vec(), + }, + } + } + } + + impl TryFrom for $module::CompressedSummary { + type Error = anyhow::Error; + + fn try_from(c: consensus::CompressedSummary) -> Result { + Ok($module::CompressedSummary { + stats: c + .stats + .try_into() + .map_err(|_| anyhow!("cannot convert aggregated stats"))?, + data_root_commitment: c + .data_root_commitment .try_into() .map_err(|_| anyhow!("cannot convert bytes32"))?, }) @@ -150,7 +191,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map($module::IpcEnvelope::try_from) .collect::, _>>()?, - activities: checkpoint.activities.try_into()?, + activities: checkpoint.activity_rollup.try_into()?, }) } } @@ -169,10 +210,7 @@ macro_rules! bottom_up_checkpoint_conversion { .into_iter() .map(IpcEnvelope::try_from) .collect::, _>>()?, - activities: ActivitySummary { - total_active_validators: value.activities.total_active_validators, - commitment: value.activities.commitment.to_vec(), - }, + activity_rollup: value.activities.into(), }) } } @@ -277,22 +315,6 @@ 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( @@ -322,21 +344,6 @@ pub fn fil_to_eth_amount(amount: &TokenAmount) -> anyhow::Result { Ok(U256::from_dec_str(&str)?) } -impl TryFrom for validator_reward_facet::BatchClaimProofs { - type Error = anyhow::Error; - - fn try_from(v: BatchClaimProofs) -> Result { - Ok(Self { - subnet_id: validator_reward_facet::SubnetID::try_from(&v.subnet_id)?, - proofs: v - .proofs - .into_iter() - .map(validator_reward_facet::ValidatorClaimProof::try_from) - .collect::, _>>()?, - }) - } -} - impl TryFrom for top_down_finality_facet::StakingChange { type Error = anyhow::Error; diff --git a/ipc/cli/src/commands/validator/batch_claim.rs b/ipc/cli/src/commands/validator/batch_claim.rs index 6cdb106ba..efc0522fa 100644 --- a/ipc/cli/src/commands/validator/batch_claim.rs +++ b/ipc/cli/src/commands/validator/batch_claim.rs @@ -18,11 +18,8 @@ pub(crate) struct BatchClaimArgs { pub from: ChainEpoch, #[arg(long, help = "The checkpoint height to claim to")] pub to: ChainEpoch, - #[arg( - long, - help = "The source subnets that generated the reward, use ',' to separate subnets" - )] - pub reward_source_subnets: String, + #[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, } @@ -38,18 +35,14 @@ impl CommandLineHandler for BatchClaim { let provider = get_ipc_provider(global)?; - let reward_source_subnets = arguments - .reward_source_subnets - .split(',') - .map(SubnetID::from_str) - .collect::, _>>()?; + 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( + .batch_subnet_claim( &reward_claim_subnet, - &reward_source_subnets, + &reward_source_subnet, arguments.from, arguments.to, &validator, diff --git a/ipc/cli/src/commands/validator/list.rs b/ipc/cli/src/commands/validator/list.rs index 7209de34d..f3b666e76 100644 --- a/ipc/cli/src/commands/validator/list.rs +++ b/ipc/cli/src/commands/validator/list.rs @@ -40,10 +40,9 @@ impl CommandLineHandler for ListActivities { .await?; println!("found total {} entries", r.len()); - for v in r { - println!("checkpoint height: {}", v.checkpoint_height); + for (checkpoint_height, v) in r { + println!(" checkpoint height: {}", checkpoint_height); println!(" addr: {}", v.validator); - println!(" metadata: {}", hex::encode(v.metadata)); println!(" locks_committed: {}", v.blocks_committed); } diff --git a/ipc/provider/Cargo.toml b/ipc/provider/Cargo.toml index 758f1306d..4a9c7bbe9 100644 --- a/ipc/provider/Cargo.toml +++ b/ipc/provider/Cargo.toml @@ -42,7 +42,7 @@ zeroize = { workspace = true } fil_actors_runtime = { workspace = true } fvm_ipld_encoding = { workspace = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } merkle-tree-rs = { path = "../../ext/merkle-tree-rs" } diff --git a/ipc/provider/src/lib.rs b/ipc/provider/src/lib.rs index f6c7cb27c..aad51e6b4 100644 --- a/ipc/provider/src/lib.rs +++ b/ipc/provider/src/lib.rs @@ -9,9 +9,8 @@ use config::Config; use fvm_shared::{ address::Address, clock::ChainEpoch, crypto::signature::SignatureType, econ::TokenAmount, }; -use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpointBundle, QuorumReachedEvent, ValidatorSummary, -}; +use ipc_api::checkpoint::consensus::ValidatorData; +use ipc_api::checkpoint::{BottomUpCheckpointBundle, QuorumReachedEvent}; use ipc_api::evm::payload_to_evm_address; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; use ipc_api::subnet::{Asset, PermissionMode}; @@ -754,48 +753,34 @@ impl IpcProvider { validator: &Address, from: ChainEpoch, to: ChainEpoch, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let conn = self.get_connection(subnet)?; conn.manager() - .get_validator_activities(validator, from, to) + .query_validator_rewards(validator, from, to) .await } - pub async fn batch_claim( + pub async fn batch_subnet_claim( &self, reward_claim_subnet: &SubnetID, - reward_source_subnets: &[SubnetID], + reward_source_subnet: &SubnetID, // TODO(review): eventually support multiple source subnets from: ChainEpoch, to: ChainEpoch, validator: &Address, ) -> anyhow::Result<()> { - let mut batch_proofs = vec![]; - for source_subnet in reward_source_subnets { - let conn = self.get_connection(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_source_subnet)?; - batch_proofs.push(BatchClaimProofs { - subnet_id: source_subnet.clone(), - proofs, - }); - } + let claims = conn + .manager() + .query_reward_claims(validator, from, to) + .await?; let parent = reward_claim_subnet .parent() .ok_or_else(|| anyhow!("no parent found"))?; let conn = self.get_connection(&parent)?; conn.manager() - .batch_claim(validator, reward_claim_subnet, batch_proofs) + .batch_subnet_claim(validator, reward_claim_subnet, reward_source_subnet, claims) .await } } diff --git a/ipc/provider/src/manager/evm/manager.rs b/ipc/provider/src/manager/evm/manager.rs index 503cdfbfa..43d68971a 100644 --- a/ipc/provider/src/manager/evm/manager.rs +++ b/ipc/provider/src/manager/evm/manager.rs @@ -36,15 +36,17 @@ use ethers::abi::Tokenizable; use ethers::contract::abigen; use ethers::prelude::k256::ecdsa::SigningKey; use ethers::prelude::{Signer, SignerMiddleware}; -use ethers::providers::{Authorization, Http, Middleware, Provider}; +use ethers::providers::{Authorization, Http, Provider}; use ethers::signers::{LocalWallet, Wallet}; -use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, I256, U256}; +use ethers::types::{BlockId, Eip1559TransactionRequest, ValueOrArray, H256, I256, U256}; +use ethers::middleware::Middleware; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; +use ipc_actors_abis::validator_reward_facet::ValidatorClaim; use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, - ValidatorClaimProof, ValidatorSummary, + consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, + Signature, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo, ValidatorStakingInfo}; @@ -1284,124 +1286,128 @@ 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!["address".to_owned(), "uint64".to_owned(), "bytes".to_owned()]; + /// ABI types of the Merkle tree which contains validator addresses and their committed block count. + pub static ref VALIDATOR_SUMMARY_FIELDS: Vec = vec!["address".to_owned(), "uint64".to_owned()]; ); #[async_trait] impl ValidatorRewarder for EthSubnetManager { - async fn get_validator_claim_proofs( + /// Query validator claims, indexed by checkpoint height, to batch claim rewards. + async fn query_reward_claims( &self, validator_addr: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result> { + ) -> 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::() + .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 validator_eth_addr = payload_to_evm_address(validator_addr.payload())?; - let mut proofs = vec![]; + let mut claims = 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![ - 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::debug!( + "found activity bundle published at height: {}", + meta.block_number + ); + + // Check if we have claims for this validator in this block. + let our_data = event + .rollup + .consensus + .data + .iter() + .find(|v| v.validator == validator_eth_addr); + + // If we don't, skip this block. + let Some(data) = our_data else { tracing::info!( - "target validator address has not activities in epoch {}", + "target validator address has no reward claims in epoch {}", meta.block_number ); continue; }; - let proof = tree.get_proof(LeafType::LeafBytes(payload))?; - proofs.push(ValidatorClaimProof { - summary, + let proof = gen_merkle_proof(&event.rollup.consensus.data, data)?; + + // Construct the claim and add it to the list. + let claim = ValidatorClaim { + // Even though it's the same struct but still need to do a mapping due to + // different crate from ethers-rs + data: validator_reward_facet::ValidatorData { + validator: data.validator, + blocks_committed: data.blocks_committed, + }, proof: proof.into_iter().map(|v| v.into()).collect(), - }); + }; + claims.push((event.checkpoint_height, claim)); } - Ok(proofs) + Ok(claims) } - /// Get the reward for specific validator in a subnet - async fn get_validator_activities( + /// Query validator rewards in the current subnet, without obtaining proofs. + async fn query_validator_rewards( &self, validator_addr: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result> { + ) -> 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::() + .event::() .from_block(from_checkpoint as u64) .to_block(to_checkpoint as u64) .address(ValueOrArray::Value(contract.address())); - let mut activities = vec![]; + let mut rewards = 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 { + tracing::debug!( + "found activity bundle published at height: {}", + meta.block_number + ); + + // Check if we have rewards for this validator in this block. + if let Some(data) = event + .rollup + .consensus + .data + .iter() + .find(|v| v.validator == validator_eth_addr) + { + // TODO type conversion. + let data = ValidatorData { validator: *validator_addr, - checkpoint_height: event.checkpoint_height, - blocks_committed: validator.blocks_committed, - metadata: validator.metadata.to_vec(), - }); + blocks_committed: data.blocks_committed, + }; + rewards.push((meta.block_number.as_u64(), data)); } } - Ok(activities) + Ok(rewards) } - /// Claim the reward in batch - async fn batch_claim( + /// Claim validator rewards in a batch for the specified subnet. + async fn batch_subnet_claim( &self, submitter: &Address, reward_claim_subnet: &SubnetID, - payloads: Vec, + reward_origin_subnet: &SubnetID, + claims: Vec<(u64, ValidatorClaim)>, ) -> Result<()> { let signer = Arc::new(self.get_signer(submitter)?); let contract = validator_reward_facet::ValidatorRewardFacet::new( @@ -1409,12 +1415,14 @@ impl ValidatorRewarder for EthSubnetManager { signer.clone(), ); - let p = payloads - .into_iter() - .map(validator_reward_facet::BatchClaimProofs::try_from) - .collect::>>()?; - let call = contract.batch_claim(p); - let call = call_with_premium_and_pending_block(signer, call).await?; + // separate the Vec of tuples claims into two Vecs of Height and Claim + let (heights, claims): (Vec, Vec) = claims.into_iter().unzip(); + + let call = { + let call = + contract.batch_subnet_claim(reward_origin_subnet.try_into()?, heights, claims); + call_with_premium_and_pending_block(signer, call).await? + }; call.send().await?; @@ -1422,6 +1430,63 @@ impl ValidatorRewarder for EthSubnetManager { } } +fn gen_merkle_proof( + validator_data: &[checkpointing_facet::ValidatorData], + validator: &checkpointing_facet::ValidatorData, +) -> anyhow::Result> { + // Utilty function to pack validator data into a vector of strings for proof generation. + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let tree = gen_merkle_tree(validator_data, pack_validator_data)?; + + let leaf = pack_validator_data(validator); + tree.get_proof(LeafType::LeafBytes(leaf)) +} + +fn gen_merkle_tree Vec>( + validator_data: &[checkpointing_facet::ValidatorData], + pack_validator_data: F, +) -> anyhow::Result> { + let leaves = order_validator_data(validator_data)? + .iter() + .map(pack_validator_data) + .collect::>(); + StandardMerkleTree::::of(&leaves, &VALIDATOR_SUMMARY_FIELDS) + .context("failed to construct Merkle tree") +} + +fn order_validator_data( + validator_data: &[checkpointing_facet::ValidatorData], +) -> anyhow::Result> { + let mut mapped = validator_data + .iter() + .map(|a| ethers_address_to_fil_address(&a.validator).map(|v| (v, a.blocks_committed))) + .collect::, _>>()?; + + mapped.sort_by(|a, b| { + let cmp = a.0.cmp(&b.0); + if cmp.is_eq() { + // Address will be unique, do this just in case equal + a.1.cmp(&b.1) + } else { + cmp + } + }); + + let back_to_eth = |(fvm_addr, blocks): (Address, u64)| { + payload_to_evm_address(fvm_addr.payload()).map(|v| checkpointing_facet::ValidatorData { + validator: v, + blocks_committed: blocks, + }) + }; + mapped + .into_iter() + .map(back_to_eth) + .collect::, _>>() +} + /// Takes a `FunctionCall` input and returns a new instance with an estimated optimal `gas_premium`. /// The function also uses the pending block number to help retrieve the latest nonce /// via `get_transaction_count` with the `pending` parameter. @@ -1648,8 +1713,11 @@ impl TryFrom for SubnetInfo { #[cfg(test)] mod tests { - use crate::manager::evm::manager::contract_address_from_subnet; + use crate::manager::evm::manager::{contract_address_from_subnet, gen_merkle_tree}; + use ethers::core::rand::prelude::SliceRandom; + use ethers::core::rand::{random, thread_rng}; use fvm_shared::address::Address; + use ipc_actors_abis::checkpointing_facet::{checkpointing_facet, ValidatorData}; use ipc_api::subnet_id::SubnetID; use std::str::FromStr; @@ -1664,4 +1732,91 @@ mod tests { "0x2e714a3c385ea88a09998ed74db265dae9853667" ); } + + /// test case that makes sure the commitment created for various addresses and blocks committed + /// are consistent + #[test] + fn test_validator_rewarder_claim_commitment() { + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let mut random_validator_data = vec![ + ValidatorData { + validator: ethers::types::Address::from_str( + "0xB29C00299756135ec5d6A140CA54Ec77790a99d6", + ) + .unwrap(), + blocks_committed: 1, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x1A79385eAd0e873FE0C441C034636D3Edf7014cC", + ) + .unwrap(), + blocks_committed: 10, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x28345a43c2fBae4412f0AbadFa06Bd8BA3f58867", + ) + .unwrap(), + blocks_committed: 2, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x3c5cc76b07cb02a372e647887bD6780513659527", + ) + .unwrap(), + blocks_committed: 3, + }, + ValidatorData { + validator: ethers::types::Address::from_str( + "0x76B9d5a35C46B1fFEb37aadf929f1CA63a26A829", + ) + .unwrap(), + blocks_committed: 4, + }, + ]; + random_validator_data.shuffle(&mut thread_rng()); + + let root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!( + hex::encode(root.0), + "5519955f33109df3338490473cb14458640efdccd4df05998c4c439738280ab0" + ); + } + + #[test] + fn test_validator_rewarder_claim_commitment_ii() { + let pack_validator_data = |v: &checkpointing_facet::ValidatorData| { + vec![format!("{:?}", v.validator), v.blocks_committed.to_string()] + }; + + let mut random_validator_data = (0..100) + .map(|_| ValidatorData { + validator: ethers::types::Address::random(), + blocks_committed: random::(), + }) + .collect::>(); + + random_validator_data.shuffle(&mut thread_rng()); + let root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + + random_validator_data.shuffle(&mut thread_rng()); + let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!(new_root, root); + + random_validator_data.shuffle(&mut thread_rng()); + let new_root = gen_merkle_tree(&random_validator_data, pack_validator_data) + .unwrap() + .root(); + assert_eq!(new_root, root); + } } diff --git a/ipc/provider/src/manager/subnet.rs b/ipc/provider/src/manager/subnet.rs index 62cbf983b..a52095949 100644 --- a/ipc/provider/src/manager/subnet.rs +++ b/ipc/provider/src/manager/subnet.rs @@ -7,9 +7,10 @@ use anyhow::Result; use async_trait::async_trait; use fvm_shared::clock::ChainEpoch; use fvm_shared::{address::Address, econ::TokenAmount}; +use ipc_actors_abis::validator_reward_facet::ValidatorClaim; use ipc_api::checkpoint::{ - BatchClaimProofs, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, Signature, - ValidatorClaimProof, ValidatorSummary, + consensus::ValidatorData, BottomUpCheckpoint, BottomUpCheckpointBundle, QuorumReachedEvent, + Signature, }; use ipc_api::cross::IpcEnvelope; use ipc_api::staking::{StakingChangeRequest, ValidatorInfo}; @@ -287,25 +288,28 @@ pub trait BottomUpCheckpointRelayer: Send + Sync { /// 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( + /// Query validator claims, indexed by checkpoint height, to batch claim rewards. + async fn query_reward_claims( &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( + ) -> Result>; + + /// Query validator rewards in the current subnet, without obtaining proofs. + async fn query_validator_rewards( &self, validator: &Address, from_checkpoint: ChainEpoch, to_checkpoint: ChainEpoch, - ) -> Result>; - /// Claim the reward in batches - async fn batch_claim( + ) -> Result>; + + /// Claim validator rewards in a batch for the specified subnet. + async fn batch_subnet_claim( &self, submitter: &Address, reward_claim_subnet: &SubnetID, - payloads: Vec, + reward_origin_subnet: &SubnetID, + claims: Vec<(u64, ValidatorClaim)>, ) -> Result<()>; } diff --git a/ipc/wallet/Cargo.toml b/ipc/wallet/Cargo.toml index 11cd5d946..02c02f692 100644 --- a/ipc/wallet/Cargo.toml +++ b/ipc/wallet/Cargo.toml @@ -15,7 +15,7 @@ base64 = { workspace = true } blake2b_simd = { workspace = true } bls-signatures = { version = "0.13.0", default-features = false, features = ["blst"] } ethers = { workspace = true, optional = true } -fvm_shared = { workspace = true } +fvm_shared = { workspace = true, features = ["crypto"] } hex = { workspace = true } libc = "0.2" libsecp256k1 = { workspace = true }