From 22f8b515dbd6984689d815ddc847b44156f36f31 Mon Sep 17 00:00:00 2001 From: raulk Date: Wed, 2 Oct 2024 17:14:23 +0100 Subject: [PATCH] prototype the validator rewards feature. --- .../gateway/router/CheckpointingFacet.sol | 9 ++++++- .../interfaces/IValidatorRewarder.sol | 18 +++++++++++++ .../contracts/lib/LibSubnetActorStorage.sol | 19 ++++++++++++++ contracts/contracts/structs/CrossNet.sol | 25 +++++++++++++++++++ .../subnet/SubnetActorCheckpointingFacet.sol | 4 +++ .../subnet/SubnetActorRewardFacet.sol | 12 +++++++++ .../vm/interpreter/src/fvm/checkpoint.rs | 7 ++++++ ipc/api/src/checkpoint.rs | 1 + ipc/cli/src/commands/checkpoint/relayer.rs | 7 ++++++ 9 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 contracts/contracts/interfaces/IValidatorRewarder.sol diff --git a/contracts/contracts/gateway/router/CheckpointingFacet.sol b/contracts/contracts/gateway/router/CheckpointingFacet.sol index a49780cc8c..1b55a015a8 100644 --- a/contracts/contracts/gateway/router/CheckpointingFacet.sol +++ b/contracts/contracts/gateway/router/CheckpointingFacet.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol"; -import {BottomUpCheckpoint} from "../../structs/CrossNet.sol"; +import {BottomUpCheckpoint, ActivitySummary, ActivitySummaryCommitted} from "../../structs/CrossNet.sol"; import {LibGateway} from "../../lib/LibGateway.sol"; import {LibQuorum} from "../../lib/LibQuorum.sol"; import {Subnet} from "../../structs/Subnet.sol"; @@ -49,6 +49,7 @@ contract CheckpointingFacet is GatewayActorModifiers { /// @param membershipWeight - the total weight of the membership function createBottomUpCheckpoint( BottomUpCheckpoint calldata checkpoint, + // TODO(rewarder) ActivitySummary calldata summary, bytes32 membershipRootHash, uint256 membershipWeight ) external systemActorOnly { @@ -56,6 +57,9 @@ contract CheckpointingFacet is GatewayActorModifiers { revert CheckpointAlreadyExists(); } + // TODO(rewarder): compute the commitment to the summary and set it in the checkpoint. + // Collect summaries to relay and put them in the checkpoint. Reset the pending summaries map. + LibQuorum.createQuorumInfo({ self: s.checkpointQuorumMap, objHeight: checkpoint.blockHeight, @@ -64,6 +68,9 @@ contract CheckpointingFacet is GatewayActorModifiers { membershipWeight: membershipWeight, majorityPercentage: s.majorityPercentage }); + + // TODO(rewarder): emit an ActivitySummaryCommittedevent so relayers can pick it up. + LibGateway.storeBottomUpCheckpoint(checkpoint); } diff --git a/contracts/contracts/interfaces/IValidatorRewarder.sol b/contracts/contracts/interfaces/IValidatorRewarder.sol new file mode 100644 index 0000000000..6001dfb227 --- /dev/null +++ b/contracts/contracts/interfaces/IValidatorRewarder.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {SubnetID} from "../structs/Subnet.sol"; + +/// @title ValidatorRewarder interface. +/// +/// @dev Implement this interface and supply the address of the implementation contract at subnet creation to process +/// subnet summaries at this level, and disburse rewards to validators based on their block production activity. +/// +/// This interface will be called by the subnet actor when a relayer presents a +interface IValidatorRewarder { + /// @notice Called by the subnet manager contract to instruct the rewarder to process the subnet summary and + /// disburse any relevant rewards. + /// The + /// @dev This method should revert if the summary is invalid; this will cause the + function disburseRewards(SubnetID memory id, ActivitySummary memory summary) external; +} diff --git a/contracts/contracts/lib/LibSubnetActorStorage.sol b/contracts/contracts/lib/LibSubnetActorStorage.sol index eaf800ca5a..474f84c0f4 100644 --- a/contracts/contracts/lib/LibSubnetActorStorage.sol +++ b/contracts/contracts/lib/LibSubnetActorStorage.sol @@ -65,6 +65,25 @@ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet address[] genesisBalanceKeys; /// @notice The validator gater, if address(0), no validator gating is performed address validatorGater; + + /// BEGIN Validator Rewards. + + /// @notice The validator rewarder. + /// If address(0), this subnet does not process activity summaries, but instead forwards them to the parent + /// network via bottom-up checkpoints. + /// If address(0), and this is the root network, summaries are discarded (> /dev/null). + /// TODO(rewarder): set this address correctly from the constructor. + address validatorRewarder; + /// @notice Summaries pending to be processed. + /// If the validator rewarder is non-zero, these denote summaries presentable at this level. + /// If the validator rewarder is zero, these summaries must be relayed upwards in the next bottom-up checkpoint. + /// Partitioned by subnet ID, in the sequence they must be presented. + /// TODO(rewarder): optimize this pair of data structures. + mapping(SubnetID => bytes32[]) pendingSummaries; + /// @notice Index over presentable summaries back to the subnet ID, so we can locate them quickly when they're presented. + /// Only used if the validator rewarder is non-zero. + /// TODO(rewarder): optimize this pair of data structures. + mapping(bytes32 => SubnetID) presentableSummaries; } library LibSubnetActorStorage { diff --git a/contracts/contracts/structs/CrossNet.sol b/contracts/contracts/structs/CrossNet.sol index 368554b608..8c96fb1b9a 100644 --- a/contracts/contracts/structs/CrossNet.sol +++ b/contracts/contracts/structs/CrossNet.sol @@ -29,6 +29,31 @@ struct BottomUpCheckpoint { uint64 nextConfigurationNumber; /// @dev Batch of messages to execute. IpcEnvelope[] msgs; + /// @dev A commitment to the summary of our chain activity since the previous checkpoint and this one. + bytes32 summary; + /// @dev Summaries relayed upwards from descendants of this subnet. + /// NOTE: Not merkelized to keep it simple, but we will merkelize later to scale better. + RelayedSummary[] relayedSummaries; +} + +struct ActivitySummary { + /// @dev The block range the activity summary spans; these are the local heights of the start and the end, inclusive. + uint256[2] blockRange; + /// @dev The validators whose activity we're reporting about. + address[] validators; + /// @dev The number of blocks committed by each validator in the position they appear in the validators array. + /// If there is a configuration change applied at this checkpoint, this carries information about the _old_ validator set. + uint64[] blocksCommitted; +} + +event ActivitySummaryCommitted(bytes32 indexed commitment, ActivitySummary summary); + +struct RelayedSummary { + /// @dev The subnet IDs whose activity is being relayed. + SubnetID subnet; + /// @dev The commitment to the summary, so it can be presented later by the relayer. + /// A blake2b hash of the summary generated by abi.encode'ing the ActivitySummary and hashing it via the Eth precompile. + bytes32 commitment; } /// @notice A batch of bottom-up messages for execution. diff --git a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol index 667433d8bb..84aefa1464 100644 --- a/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol +++ b/contracts/contracts/subnet/SubnetActorCheckpointingFacet.sol @@ -42,6 +42,10 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, s.lastBottomUpCheckpointHeight = checkpoint.blockHeight; + // TODO(rewarder): if we have a non-zero validator rewarder at this level, queue the commitment for processing in storage (add to pending and presentable summaries). + // If we have a zero validator rewarder at this level, and we are the L1, discard the incoming commitments. + // If we have a zero validator rewarder at this level, and we are not the L1, relay the commitments upwards (add to pending summaries). + // Commit in gateway to distribute rewards IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint); diff --git a/contracts/contracts/subnet/SubnetActorRewardFacet.sol b/contracts/contracts/subnet/SubnetActorRewardFacet.sol index 1b8b84b7db..4862e352bb 100644 --- a/contracts/contracts/subnet/SubnetActorRewardFacet.sol +++ b/contracts/contracts/subnet/SubnetActorRewardFacet.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.23; +import {ActivitySummary} from "../structs/CrossNet.sol"; import {QuorumObjKind} from "../structs/Quorum.sol"; import {Pausable} from "../lib/LibPausable.sol"; import {ReentrancyGuard} from "../lib/LibReentrancyGuard.sol"; @@ -13,6 +14,17 @@ import {Asset} from "../structs/Subnet.sol"; contract SubnetActorRewardFacet is SubnetActorModifiers, ReentrancyGuard, Pausable { using AssetHelper for Asset; + // TODO(rewards): add this function so that relayers can submit summaries to process reward payouts in the root network. + function submitSummary(SubnetID subnetId, ActivitySummary memory summary) external nonReentrant whenNotPaused { + // TODO(rewards): + // 1. Check that the subnet is active. + // 2. Check that the subnet has a non-zero ValidatorRewarder. + // 3. Hash the activity summary to get the commitment. + // 4. Validate that the commitment is pending and presentable, and validate that it matches the expected subnet. + // 5. Send the summary to the ValidatorRewarder#disburseRewards. + // 6. If OK (not reverted), drop the summary from the pending and presentable commitments. + } + /// @notice Validator claims their released collateral. function claim() external nonReentrant whenNotPaused { uint256 amount = LibStaking.claimCollateral(msg.sender); diff --git a/fendermint/vm/interpreter/src/fvm/checkpoint.rs b/fendermint/vm/interpreter/src/fvm/checkpoint.rs index ab9b2c0d59..19890678d2 100644 --- a/fendermint/vm/interpreter/src/fvm/checkpoint.rs +++ b/fendermint/vm/interpreter/src/fvm/checkpoint.rs @@ -96,6 +96,13 @@ where let num_msgs = msgs.len(); + // TODO(rewards): query block producers for the blocks from the last checkpointed epoch to the current one. + // Ideally keep a live cache of block producers, append to it when new blocks are committed, and prune it when generating a checkpoint. + // But for now, we can try to keep it simple and query CometBFT, although that adds latency. + // If we do this, this method seems to be the quickest way: https://docs.cometbft.com/main/rpc/#/Info/block_search + + // TODO(rewards): populate the ActivitySummary struct with the information above, and pass it to the create_bottom_up_checkpoint call. + // Construct checkpoint. let checkpoint = BottomUpCheckpoint { subnet_id, diff --git a/ipc/api/src/checkpoint.rs b/ipc/api/src/checkpoint.rs index 88f41f8917..0d07d701ac 100644 --- a/ipc/api/src/checkpoint.rs +++ b/ipc/api/src/checkpoint.rs @@ -94,6 +94,7 @@ pub struct BottomUpCheckpoint { pub next_configuration_number: u64, /// The list of messages for execution pub msgs: Vec, + // TODO(rewards): add new fields and data types for summaries and commitments. } pub fn serialize_vec_bytes_to_vec_hex, S>( diff --git a/ipc/cli/src/commands/checkpoint/relayer.rs b/ipc/cli/src/commands/checkpoint/relayer.rs index 12a1218803..2db1122a84 100644 --- a/ipc/cli/src/commands/checkpoint/relayer.rs +++ b/ipc/cli/src/commands/checkpoint/relayer.rs @@ -32,6 +32,13 @@ impl CommandLineHandler for BottomUpRelayer { async fn handle(global: &GlobalArguments, arguments: &Self::Arguments) -> anyhow::Result<()> { log::debug!("start bottom up relayer with args: {:?}", arguments); + // TODO(rewards): enable the relayer to watch multiple subnets at once. + + // TODO(rewards): add a new flag --process-summaries to activate processing summaries on all subnets. + // Enabling this mode makes the relayer watch for ActivitySummaryCommitted events, and stores the summaries in a database. + // It then tracks which summaries have been committed to the root (right now we only support submitting to the L1), to chase + // after those and present them via SubnetActor#submitSummary in order to trigger reward payout. + // Prometheus metrics match &arguments.metrics_address { Some(addr) => {