diff --git a/Cargo.lock b/Cargo.lock index 731ca28a41..0c7a060ff5 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 fc82cff9bb..e3a27d90de 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 56700d7493..cdb6982dcd 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 eb47b7fedb..052b233dea 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 433f57185e..579e64d5f0 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 b18e8b5516..903ac87409 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 1dfa33df5a..8f0ffd7d8d 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 a88cf10266..fafbecc0ca 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 3983c76eea..c403815137 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 1dc78be10d..9d00cda8db 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 c130b2ad5d..ad540d61f5 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 2d7761d10c..eb7626a0e0 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 51d4ac0ae1..50c0491e4e 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 0f7d784e82..095ffa9351 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 53d17edbfe..066c2e72e3 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 0000000000..037db293f9 --- /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 ec166cd81e..f3b911eafe 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 121b8c3309..9dfe76e4c3 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 db2f720ab5..69e53166a7 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 9b1b4e50bc..3b239b5995 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 7313e35c68..1bf25a3f0d 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 c7a8c30a94..ecfd54c187 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 0000000000..6a2dc98cb7 Binary files /dev/null and b/docs/fendermint/diagrams/activity_rollup1.png differ diff --git a/docs/fendermint/subnet_activities.md b/docs/fendermint/subnet_activities.md new file mode 100644 index 0000000000..cf2c6d0383 --- /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 bd8ca5d7e8..e06f33cc00 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 57993af93c..4409ba7739 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 38143279f9..26d7d529b8 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 886262036e..0c06eaa258 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 791bc688aa..f8f84faa9b 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 7377b78b37..b7ae7b88bd 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 0000000000..3ad297d4f5 --- /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 c1b675bb79..997bb4496d 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 7e0af92136..01ba886f80 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 405b8c22fc..60f82f8a7f 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 be5248fb55..572df6af76 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 39c6f2e345..a82fae6e14 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 87ddad1cb4..0000000000 --- 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 1c509c2288..0000000000 --- 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 0000000000..9948d0c531 --- /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 53f2e7c3a4..7a7586bccb 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 0000000000..f44173aaf1 --- /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 4cb835c014..5aca2db831 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 00814b4fa2..3006218a5e 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 7f02b06118..f17e03f687 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 2a4ce64329..85962ab4b2 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 f26f634bf7..65b0a94386 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 7fb7143dfb..2eb04c752c 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 3816b9170b..77d264fb22 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 5edb1a7d89..e19691454d 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 7722a19d05..dd40829eaa 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 6cdb106bac..efc0522fa4 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 7209de34d1..f3b666e76c 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 758f1306d9..4a9c7bbe93 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 f6c7cb27cb..aad51e6b4a 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 503cdfbfa6..43d68971a5 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 62cbf983b1..a520959492 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 11cd5d9461..02c02f6922 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 }