mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-23 09:18:21 +00:00
## Summary Replace per-era rewards submission with EigenLayer-aligned window-based submission. Instead of submitting one rewards message per era, points are now accumulated into fixed-duration time-aligned windows as blocks are produced, and inflation is proportionally distributed across windows at era boundaries. Closed windows are submitted as `OperatorDirectedRewardsSubmissions` to EigenLayer via Snowbridge. This ensures rewards submissions match EigenLayer's expected window boundaries (`GENESIS_REWARDS_TIMESTAMP` + N × `CALCULATION_INTERVAL_SECONDS`), eliminating the mismatch between Substrate era timing and EigenLayer reward windows. ### How rewards windows work **Points accumulation**: Each time `reward_by_ids()` is called (at session end), the current timestamp is snapped to the nearest EigenLayer-aligned window boundary. Points are written to `WindowOperatorPoints[window_start]`, so all sessions ending within the same window contribute to the same bucket. **Inflation allocation**: At era end, the era's scaled inflation is split across all windows that overlap with the era's time span, proportionally to the overlap duration: ``` overlap = min(era_end, window_end) - max(era_start, window_start) portion = scaled_inflation * overlap / era_duration ``` Integer division remainder is assigned to the last overlapping window to ensure conservation. **Submission**: `process_closed_windows()` iterates all fully elapsed windows starting from `NextWindowToSubmit`. For each closed window, if it has both points and inflation, a cross-chain message is sent via Snowbridge. Empty or zero-point windows are skipped. The pointer advances regardless of success or failure. ### Key changes #### Pallet (`external-validators-rewards`) - **Window-based point accumulation**: `reward_by_ids()` now writes operator points into `WindowOperatorPoints[window_start]` in addition to the existing per-era storage, where the window is determined by snapping the current timestamp to the nearest EigenLayer-aligned boundary - **Proportional inflation allocation**: At era end, `allocate_era_inflation_to_windows()` distributes the era's scaled inflation across overlapping windows based on time overlap, with rounding remainder assigned to the last window - **Closed window submission**: `process_closed_windows()` iterates fully elapsed windows, combines their points and inflation into a `RewardsPeriodUtils`, and sends the cross-chain message. A `NextWindowToSubmit` pointer ensures idempotent progression - **Removed legacy path**: The per-era `generate_era_rewards_utils` and direct submission in `on_era_end` are removed. The `rewards_duration()` method is removed from `RewardsSubmissionConfig` trait — duration now comes from `RewardsPeriodUtils` - **Renamed `EraRewardsUtils` to `RewardsPeriodUtils`**: The struct and its fields are renamed to reflect window/period semantics (`era_index` → `period_index`, `era_start_timestamp` → `period_start`) - **New pallet config types**: `RewardsWindowGenesisTimestamp` (immutable EigenLayer genesis), `RewardsWindowDuration` (governance-tunable via `RewardsDuration` runtime param), and `UnixTime` - **Storage version bump**: 0 → 1 for the three new storage items - **New events**: `RewardsWindowSubmitted`, `RewardsWindowSubmissionFailed`, `RewardsWindowSkipped` (replaces `RewardsMessageSent`) #### Contracts (`DataHavenServiceManager.sol`) - **Duplicate operator merge**: Replace `_sortOperatorRewards` with `_sortAndMergeDuplicateOperators` — after solochain-to-ETH address translation, multiple solochain addresses can map to the same ETH operator (e.g., operator deregistered and re-registered with a new solochain address within the same reward window). The new function sorts then merges consecutive duplicates by summing their amounts, satisfying EigenLayer's strict ascending uniqueness requirement. ### New storage items | Storage | Type | Purpose | |---------|------|---------| | `WindowOperatorPoints` | `Map<u32, BTreeMap<H160, u32>>` | Per-window operator reward points | | `WindowInflationAmount` | `Map<u32, u128>` | Per-window allocated inflation | | `NextWindowToSubmit` | `Value<u32>` | Pointer to next window to process | ### Out of scope (follow-up PRs) - **Translation robustness**: Retain `validatorSolochainAddressToEthAddress` on deregistration to prevent `UnknownSolochainAddress` reverts for delayed window submissions - **Retry-safe failure handling**: Preserve failed windows for replay - **Retained window snapshots**: Audit/replay support - **Benchmarks/weights**: Re-run pallet benchmarks for updated `on_era_end` weight ## Test plan - [x] Pallet unit tests pass (`cargo test -p pallet-external-validators-rewards` — 81 tests) - [x] `window_mode_attributes_points_to_aligned_window` — points land in correct aligned window - [x] `window_mode_submits_closed_windows_and_advances_pointer` — closed window submission and pointer advancement - [x] `window_mode_era_inflation_split_across_multiple_windows` — pro-rata inflation splitting across 4 windows with remainder conservation - [x] `window_mode_submits_multiple_closed_windows_in_single_era_end` — batch submission of multiple closed windows in one `on_era_end` - [x] `window_mode_delivery_failure_emits_submission_failed_event` — delivery failure emits `RewardsWindowSubmissionFailed`, clears storage, advances pointer - [x] Existing pallet tests updated to use window-based events (`RewardsWindowSubmitted`, `RewardsWindowSkipped`) - [x] Contract tests pass (`forge test --match-contract RewardsSubmitter` — 12 tests) - [x] `test_submitRewards_mergesDuplicateTranslatedOperators` — two solochain addresses mapping to same ETH operator are merged into single entry - [x] Runtime configs compile with new config types (mainnet/stagenet/testnet) - [x] E2E test with local network to verify cross-chain message reaches EigenLayer contracts --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| build.rs | ||
| Cargo.toml | ||