## 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>
## Summary
This PR aligns the `external-validator-slashes` retry flow with the same
general approach used by the rewards retry ring
(https://github.com/datahaven-xyz/datahaven/pull/462).
The main goal is to make slash retries preserve their original era
identity, avoid head-of-line blocking by rotating failed work to the
back of the queue, and add a governance escape hatch for manually
retrying queued slash batches.
## Changes
- Reworked `external-validator-slashes` to use a bounded ring-buffer
queue for unsent slash batches instead of retrying from an unbounded raw
slash queue.
- Stored the original slash era together with each queued batch so
retries keep the same outbound message identity across later eras.
- Added a governance-only retry extrinsic for queued slash eras,
mirroring the rewards retrial ring approach.
- Added new retry-related errors and events for queued slash retries and
queue-full handling.
- Updated retry-path logging so expected send failures are treated as
warnings instead of noisy errors during passing tests.
- Extended pallet tests to cover:
- preserved original era on retry
- back-of-queue rotation on failed sends
- governance retry success and failure cases
- bounded queue capacity behavior
- multi-era queue progression
- Updated benchmarking and pallet weights for the new retry path.
- Wired the new governance origin and weight changes into mainnet,
testnet, and stagenet runtime configs.
## Polkadot upgrade 2503
In this PR, we upgrade form Polkadot SDK 2412 to Polkadot SDK 2503. In
order to upgrade the SDK we need to upgrade some dependencies :
StorageHub and frontier simultaneously.
## What changes
### Trivial changes
* https://github.com/paritytech/polkadot-sdk/pull/7634 -> The new trait
is required in all the pallets using scale encoding.
* https://github.com/paritytech/polkadot-sdk/pull/7043 -> Remove
deprecated `sp-std` and replace with `alloc` or `core`.
* https://github.com/paritytech/polkadot-sdk/pull/6140 -> Accurate
weight reclaim with frame_system::WeightReclaim
### Breaking changes
* https://github.com/paritytech/polkadot-sdk/pull/2072 -> There is a
change in `pallet-referenda`. Now, the tracks are retrieved as a list of
`Track`s. Also, the names of the tracks might have some trailing null
values (`\0`). This means display representation of the tracks' names
must be sanitized.
* https://github.com/paritytech/polkadot-sdk/pull/5723 -> adds the
ability for these pallets to specify their source of the block number.
This is useful when these pallets are migrated from the relay chain to a
parachain and vice versa. (Not entirely sure it is breaking as it is
being marked as backward compatible).
* https://github.com/paritytech/polkadot-sdk/pull/6338 -> Update
Referenda to Support Block Number Provider
### Notable changes
* https://github.com/paritytech/polkadot-sdk/pull/5703 -> Not changes
required in the codebase but impact fastSync mode. Should improve
testing.
* https://github.com/paritytech/polkadot-sdk/pull/5842 -> Removes
`libp2p` types in authority-discovery, and replace them with network
backend agnostic types from `sc-network-types`. The `sc-network`
interface is therefore updated accordingly.
## What changes in Frontier
* https://github.com/polkadot-evm/frontier/pull/1693 -> Add support for
EIP 7702 which has been enable with Pectra. This EIP add a new field
`AuthorizationList` in Ethereum transaction.
Changes example :
```rust
#[test]
fn validate_transaction_fails_on_filtered_call() {
...
pallet_evm::Call::<Runtime>::call {
source: H160::default(),
target: H160::default(),
input: Vec::new(),
value: sp_core::U256::zero(),
gas_limit: 21000,
max_fee_per_gas: sp_core::U256::zero(),
max_priority_fee_per_gas: Some(sp_core::U256::zero()),
nonce: None,
access_list: Vec::new(),
authorization_list: Vec::new(),
}
.into(),
```
## ⚠️ Breaking Changes ⚠️
* Upgrade to Stirage hub v0.5.1
* Support for Ethereum latest upgrade (txs now have the
`authoriation_list` for support EIP 7702)
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
When `send_rewards_message` fails at era end (bridge paused, queue full,
etc.), tokens have already been minted to the Ethereum sovereign account
but the `submitRewards` message to EigenLayer is silently lost. This
creates an inconsistency: wHAVE tokens exist on DataHaven but validators
never receive their rewards for that era.
This PR adds:
- **Automatic retry via `on_initialize`** — processes one failed era per
block
- **Head-of-line blocking avoidance** — failed entries are moved to the
back of the queue so a single stuck era doesn't block retries for
subsequent ones
- **Governance escape hatch** — `retry_unsent_reward_era` extrinsic
gated by configurable `GovernanceOrigin`
- **Automatic cleanup** — expired entries (reward points pruned past
`HistoryDepth`) are discarded
### Context: HistoryDepth window
Reward points are kept in storage for `HistoryDepth` eras (64 on
mainnet, ~16 days with 6-hour eras). Retries are only possible while the
data exists. After that window, the entry is automatically expired and
dropped from the queue. This means governance has up to ~16 days to
intervene if automatic retries keep failing.
### Storage design
A ring buffer (`StorageMap<u32, (EraIndex, u32, u128)>` + head/tail
pointers) with capacity 64, matching `HistoryDepth`. Each entry stores:
- `era_index` — which era's rewards message failed
- `era_start_timestamp` — preserved from the original era (seconds since
epoch)
- `scaled_inflation` — the exact minted amount (stored because
recomputing later could yield a different value if
`EraInflationProvider` has changed)
### Retry behavior
| Scenario | Action |
|----------|--------|
| Queue empty | Return minimal weight (2 reads) |
| Entry reward points pruned | Remove entry, emit `UnsentEraExpired` |
| Retry succeeds | Remove entry, emit `RewardsMessageRetried` |
| Retry fails | Move entry to back of queue, try next entry next block |
### Changes
- **`lib.rs`** — Ring buffer storage, events
(`RewardsMessageSendFailed`, `RewardsMessageRetried`,
`UnsentEraExpired`, `UnsentQueueFull`), errors, `on_initialize` hook,
`retry_unsent_reward_era` extrinsic with configurable
`GovernanceOrigin`, modified `on_era_end` (queue on failure) and
`on_era_start` (prune expired entries where `idx <=
era_index_to_delete`)
- **`mock.rs`** — Configurable `send_message_fails` flag on
`MockOkOutboundQueue`
- **`tests.rs`** — 14 new test cases covering all
retry/expiry/governance paths including head-of-line blocking avoidance
- **`benchmarking.rs`** — 5 new benchmarks (empty queue, expired entry,
success, failure, governance extrinsic)
- **`weights.rs`** — New weight functions in trait and both impls
- **Runtime configs** — `GovernanceOrigin = EnsureRoot<AccountId>` +
placeholder weight functions for mainnet/stagenet/testnet
## Test plan
- [x] `cargo test -p pallet-external-validators-rewards` — 90 tests pass
- [x] `cargo clippy -p pallet-external-validators-rewards` — no new
warnings
- [x] `cargo check -p datahaven-mainnet-runtime` — compiles
- [ ] Run benchmarks to generate production weight values
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gonza Montiel <gonzamontiel@users.noreply.github.com>
Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
Co-authored-by: Tobi Demeco <50408393+TDemeco@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
This PR rename the `rewardsAgentOrigin`, `rewardsMessageOrigin`, etc...
into a less specific less now that the Snowbrige Agent is also being
used to relay slashing messages.
This PR also have a fix to register the Agent address instead of the
operator node address to check the sender of the message. Without this
fix we could never relay rewards or execute slashing because we would
get an error regarding the message.
## What changed
* Removing the prefix `rewards` everytime we were refering the
snowbridge agent (to clarify that the agent is not only being used by
the reward feature)
* Fix the deployment script to register the `agentAddress` as the
required sender for relaying substrate message
## What is missing
[ ] ~~Rename `onlyRewardsInitiator` and `rewardsInitiator` in the
`DatahavenServiceManager.sol ` for something that would include
slashing~~ This should be done in another PR.
[x] Check the Testnet Deploy script to make sure it is using the agent
address
---------
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
Introduces typed offence classification, a linear Perbill-to-WAD
conversion for EigenLayer slashing, historical offence filtering, and a
new E2E test proving end-to-end liveness detection through
`pallet_im_online`.
---
### OffenceKind enum
New `OffenceKind` enum classifies consensus offences:
- `LivenessOffence` — missed heartbeats (ImOnline)
- `BabeEquivocation` — double block production
- `GrandpaEquivocation` — double finality votes
- `BeefyEquivocation` — double BEEFY votes / fork voting / future block
voting
- `Custom(BoundedVec<u8, 256>)` — manual / governance slashes
Each variant carries a human-readable description string through the
Snowbridge message to EigenLayer's
`DatahavenServiceManager.slashValidatorsOperator()`.
### EquivocationReportWrapper
Generic wrapper around `ReportOffence` wired for BABE, GRANDPA, BEEFY,
and ImOnline in all three runtimes:
1. **Filters historical offences** — discards reports whose session
predates the bonding period, using `BondedEras` storage (analogous to
`FilterHistoricalOffences` in `pallet_staking`, but adapted to this
pallet's own era tracking).
2. **Tags offence kind** — stores the `OffenceKind` in
`PendingOffenceKind` double-map `(SessionIndex, ValidatorId)` before
delegating to `pallet_offences`. The `on_offence` handler reads it via
`take()` in the same block.
3. **Cleans up on failure** — removes stale `PendingOffenceKind` entries
if the inner reporter returns an error (e.g. duplicate report),
preventing them from leaking into unrelated future offences.
### Perbill to WAD conversion and MaxSlashWad
#### How Substrate computes slash fractions
Each offence type in Substrate defines its own
`slash_fraction(offenders_count)` returning a `Perbill`:
| Offence | Formula | Typical range |
|---|---|---|
| **BABE equivocation** | `min((3k/n)^2, 1)` | 1 offender / 100
validators: ~0.09%; 1/2: capped to 100% |
| **GRANDPA equivocation** | `min((3k/n)^2, 1)` | Same as BABE |
| **BEEFY double-vote** | `min((3k/n)^2, 1)` | Same as BABE/GRANDPA |
| **BEEFY fork/future voting** | Fixed `50%` | Always 50% |
| **ImOnline liveness** | `min(3*(k - floor(n/10) - 1)/n, 1) * 7%` | 10%
or fewer offline: **0%**; ~33% offline: ~5%; ~43% offline: 7% (max) |
Where `k` = number of concurrent offenders, `n` = validator set size.
**Key behavior for small validator sets (E2E):** With n=2, the ImOnline
threshold is `floor(2/10) + 1 = 1`. A single offender (`k=1`) fails
`checked_sub(1)` giving `Perbill(0)`. This means no `Slashes` storage
entry is created (since `compute_slash` returns `None` when the new
fraction doesn't exceed the prior slash), but the `SlashReported` event
is still emitted, proving the full detection pipeline works.
#### Linear conversion to EigenLayer WAD
The Substrate `Perbill` is linearly mapped to a WAD value capped by
`MaxSlashWad`:
```
WAD = perbill.deconstruct() * MaxSlashWad / 1_000_000_000
```
- `MaxSlashWad` default: **5e16** (= 5% in WAD format, where 1e18 =
100%)
- Governance-changeable dynamic runtime parameter (codec index 46)
- `Perbill(100%)` maps to exactly `MaxSlashWad` (the cap)
- `Perbill(0%)` maps to 0 (no slash sent to EigenLayer)
#### Concrete examples (with default MaxSlashWad = 5%)
| Scenario | Substrate Perbill | WAD sent to EigenLayer | EigenLayer % |
|---|---|---|---|
| BABE equivocation (1 of 100 validators) | ~0.09% | ~4.5e13 | ~0.0045%
|
| BABE equivocation (1 of 2 validators) | 100% (capped) | 5e16 | 5%
(max) |
| BEEFY fork voting | 50% | 2.5e16 | 2.5% |
| ImOnline liveness (1 of 2 offline) | 0% | 0 (no slash) | 0% |
| ImOnline liveness (~33% of large set offline) | ~5% | ~2.5e15 | ~0.25%
|
| Manual `force_inject_slash` at 20% | 20% | 1e16 | 1% |
| Manual `force_inject_slash` at 100% | 100% | 5e16 | 5% (max) |
The same WAD value is applied uniformly to all configured strategies via
the `SlashingRequest` struct sent through Snowbridge to
`DatahavenServiceManager.slashValidatorsOperator()`.
### E2E liveness slashing test
New test scenario (`should detect and slash an unresponsive validator`)
validates the full liveness detection pipeline:
1. Pauses bob's Docker container (preserving GRANDPA state via `docker
pause`)
2. Waits 200s (>= 2 full sessions) for `pallet_im_online` to detect
missed heartbeats
3. Unpauses bob to restore GRANDPA finality (2/2 validators needed)
4. Polls for `SlashReported` event (not `Slashes` storage — see slash
fraction note above)
5. Verifies the event confirms the full pipeline: `pallet_im_online ->
EquivocationReportWrapper -> pallet_offences -> on_offence`
The test uses `try/finally` to always unpause bob, `{ at: "best" }`
queries for non-finalized chain state during the pause, and drains prior
`SlashReported` events before starting.
### Tests
- **10 new unit tests**: `PendingOffenceKind` double-map semantics,
session isolation, wrapper historical filtering, error cleanup, WAD
conversion (100%, 50%, 0%), offence kind description propagation
- **New mock infrastructure**: `MockInnerReporter`, `MockOffence`,
`MockOkOutboundQueue` with slash data capture
- **E2E**: Updated `force_inject_slash` test to use `offence_kind` enum,
new liveness detection test
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gonza Montiel <gonzamontiel@users.noreply.github.com>
Co-authored-by: undercover-cactus <lola@moonsonglabs.com>
## Changes
### Add `pallet-grandpa-benchmarking` crate
Upstream `pallet-grandpa` does not benchmark `report_equivocation`, only
the raw `check_equivocation_proof` crypto proxy.
This PR adds a new `pallet-grandpa-benchmarking` wrapper crate (modelled
after `pallet-session-benchmarking`) that benchmarks both
`report_equivocation` and `note_stalled` against the DataHaven runtimes,
using upstream `check_equivocation_proof()`.
### Add node's benchmark pallet subcommand to benchmarks script
Running the bench for `report_equivocation` requires a real ed25519
verifier. We typically use `frame-omni-bencher`, but this helper
executes the runtime as a WASM blob. In that environment the ed25519
host function does not work as a real verifier, and since the
equivocation proof contains real signatures created outside the WASM
sandbox, the verification always fails and the extrinsic returns
`InvalidEquivocationProof`. So, benchmarks for `pallet_grandpa` must run
via the node's `benchmark pallet` subcommand.
The `run-benchmarks.sh` script was updated to support this case running
directly via node. Any palet included in the new `NODE_PALLETS` list,
routes selected pallets through the node binary, while all other pallets
continue to use `frame-omni-bencher`.
### Calculate proper weights for production
Weight files for all three runtimes (stagenet, testnet, mainnet) are
updated with the
new `report_equivocation(v, n)` linear weight function derived from real
measurements.
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
The rewards message sent to EigenLayer AVS via Snowbridge was reporting
the **full inflation amount** (100%) as the distributable rewards, while
only **80%** was actually minted to the rewards account (the other 20%
goes to treasury). This mismatch could cause underfunding, failed reward
claims, or accounting discrepancies on the Ethereum side.
### Root Cause
In `on_era_end`, the same `scaled_inflation` value was passed to both
`mint_inflation` (which internally splits 80/20) and
`generate_era_rewards_utils` (which builds the outbound AVS message).
The message therefore claimed more tokens were available for
distribution than were actually in the rewards account.
### Fix
- Changed `HandleInflation::mint_inflation` to return a self-documenting
`InflationMintResult` struct instead of `DispatchResult`:
```rust
pub struct InflationMintResult {
pub rewards_amount: u128, // minted to rewards account (for AVS
distribution)
pub treasury_amount: u128, // minted to treasury account
}
```
- Restructured `on_era_end` to use `mint_result.rewards_amount`
(post-treasury split) when building the AVS message and emitting the
event
- Added an explicit reward-points check before minting to preserve the
existing behavior of skipping inflation when no validators earned
rewards
### Files Changed
- **`types.rs`** — Added `InflationMintResult` struct; updated
`HandleInflation` trait signature
- **`inflation.rs`** — Updated handler to return `InflationMintResult`;
strengthened unit tests to assert both fields
- **`lib.rs`** — Restructured `on_era_end`: check points → mint → build
message with `mint_result.rewards_amount`
- **`mock.rs`** — Updated mock to return `InflationMintResult`
- **Runtime configs** (mainnet, stagenet, testnet) — Updated wrapper
impls
- **`tests.rs`** — Updated event assertion to expect post-treasury
amount
## Test plan
- [x] All 76 pallet unit tests pass (`cargo test -p
pallet-external-validators-rewards --lib`)
- [ ] CI passes
## Era-targeted validator set submission with dedicated submitter role
> **Note:** This PR includes a detailed specification at
[`specs/validator-set-submission/validator-set-submission.md`](https://github.com/datahaven-xyz/datahaven/blob/feat/validator-set-submitter/specs/validator-set-submission/validator-set-submission.md)
that covers the design rationale, submission lifecycle, era-targeting
rules, and failure modes. Reading the spec first will make the contract,
pallet, and daemon changes easier to follow.
### Summary
- Introduce a dedicated `validatorSetSubmitter` role on
`DataHavenServiceManager`, separating validator set submission authority
from the contract owner
- Replace the unscoped `sendNewValidatorSet` with
`sendNewValidatorSetForEra`, which encodes a `targetEra` into the
Snowbridge message payload
- Add server-side era validation in the `external-validators` pallet to
reject stale, duplicate, or out-of-range submissions
- Add a long-running TypeScript daemon that watches session changes and
automatically submits each era's validator set at the right time
### Contract changes (`contracts/`)
- **New `validatorSetSubmitter` storage slot** — set during `initialize`
and rotatable via `setValidatorSetSubmitter` (owner-only). The storage
gap is decremented accordingly.
- **`sendNewValidatorSet` → `sendNewValidatorSetForEra`** — accepts a
`uint64 targetEra` parameter and is restricted to
`onlyValidatorSetSubmitter` instead of `onlyOwner`.
- **`buildNewValidatorSetMessageForEra`** — the
`NewValidatorSetPayload.externalIndex` is now caller-supplied instead of
hardcoded to `0`.
- **New events** — `ValidatorSetSubmitterUpdated`,
`ValidatorSetMessageSubmitted`.
- **New error** — `OnlyValidatorSetSubmitter`.
- **New test suite** — `ValidatorSetSubmitter.t.sol` covering submitter
set/rotate, access control, era encoding, and legacy function removal.
### Pallet changes (`operator/`)
- **`validate_target_era`** in `external-validators` — enforces
`activeEra < targetEra <= activeEra + 1` and `targetEra > ExternalIndex`
(dedup guard).
- **New errors** — `TargetEraTooOld`, `TargetEraTooNew`,
`DuplicateOrStaleTargetEra`.
- **Tests** — five new test cases for era boundary conditions (next-era
acceptance, old-era rejection, too-new rejection, duplicate rejection,
genesis behavior). Existing `era_hooks_with_external_index` test updated
to use valid target eras.
- **Runtime test fixes** — `external_index: 0` → `1` in
mainnet/stagenet/testnet EigenLayer message processor tests to satisfy
the new validation.
### Validator set submitter daemon
(`test/tools/validator-set-submitter/`)
- Event-driven service that subscribes to finalized
`Session.CurrentIndex` via Polkadot-API `watchValue`.
- Submits once per era during the last session, targeting `ActiveEra +
1`.
- Tracks submitted eras to avoid duplicates; skips if `ExternalIndex`
already covers the target.
- Startup self-checks: Ethereum connectivity, DataHaven connectivity,
on-chain submitter authorization.
- Supports `--dry-run` mode and YAML configuration.
- Graceful shutdown on `SIGINT`/`SIGTERM`.
### Test & tooling updates
- **E2E test** (`validator-set-update.test.ts`) — calls
`sendNewValidatorSetForEra` with a computed `targetEra` and filters the
substrate event by `external_index`.
- **`update-validator-set.ts` script** — accepts `--target-era` flag;
defaults to era 1 for fresh networks.
- **CLI launch** — wires validator set update as an interactive step
after relayer launch.
- **`package.json`** — new `submitter` and `submitter:dry-run` scripts.
- Regenerated contract bindings, PAPI metadata, state-diff, and storage
layout snapshots.
### Test plan
- [x] `forge test` — passes, including new `ValidatorSetSubmitter.t.sol`
- [x] `cargo test` — passes, including new era-validation tests in
`external-validators`
- [x] `bun test:e2e` — validator-set-update suite passes with
era-targeted flow
- [x] Manual: run submitter daemon against local network (`bun
submitter`), verify it submits once per era at the correct session
## ⚠️ Breaking Changes ⚠️
- **`sendNewValidatorSet` removed** — replaced by
`sendNewValidatorSetForEra(uint64 targetEra, ...)`. Callers must now
supply a `targetEra` parameter.
- **Access control changed** — validator set submission is now
restricted to the `validatorSetSubmitter` role instead of the contract
`owner`. The submitter address is set during `initialize` and rotatable
via `setValidatorSetSubmitter` (owner-only).
- **`external-validators` pallet now validates `targetEra`** — messages
with a stale, duplicate, or out-of-range `external_index` are rejected
on-chain. Existing integrations sending `external_index: 0` will fail
validation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Add missing weights
The aim of this PR is to complete our weights by enabling more runtime
benchmarks from the pallets used in DataHaven. I will start this effort
with Storage Hub pallets.
## What's included
- [x] `pallet_nfts`
- [x] Added signing helper for pallet nfts
- [x] Add pallet to benchmarks
- [x] Run benches and generate weights
- [x] Wire up weights to runtimes
- [x] `pallet_session`
- [x] Added a `pallet_session_benchmarking` crate
- [x] Added signing helpers
- [x] Add pallet to benchmarks
- [x] Run benches and generate weights
- [x] Wire up weights to runtimes
- [x] `pallet_payment_streams`
- [x] Add `TreasuryAccount` helper and configure properly
- [x] Add pallet to benchmarks
- [x] Run benches and generate weights
- [x] Wire up weights to runtimes
- [x] `pallet_storage_providers`
- [x] Add `TreasuryAccount` helper and configure properly
- [x] Add pallet to benchmarks
- [x] Run benches and generate weights
- [x] Wire up weights to runtimes
- [x] `pallet_file_system`
- [x] Add pallet to benchmarks and configure properly
- [ ] Run benches and generate weights
- [x] Wire up weights to runtimes
- [x] `pallet_proofs_dealer`
- [x] Add pallet to benchmarks and configure properly
- [x] Run benches and generate weights
- [x] Wire up weights to runtimes
## What's not included
- `pallet_identity` - We'll enable it once we update to `2506`, that
will allow us to have a BenchmarkHelper in the config on the pallet (see
[here](ac28323e7d/operator/runtime/mainnet/src/configs/mod.rs (L632-L643)))
- `pallet_grandpa` - the upstream pallet defines
[benchmarks](bbc435c766/substrate/frame/grandpa/src/benchmarking.rs (L25))
for `check_equivocation_proof` and `note_stalled`, but the required
weights to be wired are actually `report_equivocation`,
`report_equivocation_unsigned` and `note_stalled`. That means including
`pallet_grandpa` in the benchmarks results in an inconsistent
`WeightInfo` implementation, so further understanding in the pallet's
approach to benchmarking is needed.
- `pallet_file_system` -> Run benches and generate weights. Weights will
fail because of a hardcoded `AccountId32` on the
[benchmarks](57d2a195d5/pallets/file-system/src/benchmark_proofs.rs (L69-L71)).
I'll create a PR for SH soon.
These two are left for a follow up PR.
## Summary
- Add multi-environment deployment support (stagenet, testnet, mainnet)
to CLI and contracts
- Configure stagenet and testnet runtimes with correct genesis hashes
and Snowbridge Agent IDs
- Add CLI commands for BEEFY checkpoint updates and rewards origin
computation
- Add ETH validator strategies (native beacon chain ETH + LSTs) to all
config files
## Changes
### Runtime Configuration
**Stagenet Runtime:**
- Set `StagenetGenesisHash` to DataHaven stagenet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
**Testnet Runtime:**
- Set `TestnetGenesisHash` to DataHaven testnet genesis hash
- Configure `RewardsAgentOrigin` with computed Snowbridge Agent ID
- Add tests verifying rewards account derivation and agent ID
computation
The Rewards Agent ID is computed following Snowbridge's location
description pattern:
```
blake2_256(SCALE_ENCODE("GlobalConsensus", ByGenesis(genesis), "AccountKey20", rewards_account))
```
### CLI Enhancements
- All contracts subcommands (`status`, `deploy`, `verify`,
`update-metadata`) now accept `--environment` option
- Config and deployment files use environment-prefixed naming (e.g.,
`stagenet-hoodi.json`, `testnet-hoodi.json`)
- New `update-beefy-checkpoint` command that:
- Connects to a live DataHaven chain via WebSocket RPC
- Fetches all BEEFY data at the same finalized block for consistency
- Uses parallel queries with `Promise.all` for better performance
- Computes authority hashes (keccak256 of Ethereum addresses derived
from BEEFY public keys)
- Uses Snowbridge's quorum formula `n - floor((n-1)/3)` for strictly >
2/3 majority
- New `update-rewards-origin` command that computes the Snowbridge Agent
ID for the rewards pallet
- Centralized validation via `contractsPreActionHook` for all contract
commands
- Environment validation against allowlist (`stagenet`, `testnet`,
`mainnet`)
### Contract Changes
- Network validation uses explicit allowlist instead of suffix matching
- Added `initialValidatorSetId` and `nextValidatorSetId` fields to
`SnowbridgeConfig` struct
- `DeployBase.s.sol` now uses config values for validator set IDs
instead of hardcoded 0/1
- `DeployParams.s.sol` loads validator set IDs from config with
backwards compatibility
### Validator Strategies
Added ETH-equivalent strategies to allow validators to stake using
native ETH or LSTs:
**All Networks:**
- `0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0` - Native beacon chain ETH
(virtual strategy)
**Hoodi Testnet:**
- `0xf8a1a66130d614c7360e868576d5e59203475fe0` - stETH
- `0x24579aD4fe83aC53546E5c2D3dF5F85D6383420d` - WETH
**Ethereum Mainnet:**
- `0x93c4b944D05dfe6df7645A86cd2206016c51564D` - stETH
- `0x1BeE69b7dFFfA4E2d53C2a2Df135C388AD25dCD2` - rETH
- `0x54945180dB7943c0ed0FEE7EdaB2Bd24620256bc` - cbETH
### Config Files
- `stagenet-hoodi.json` - Hoodi testnet with stagenet EigenLayer
addresses
- `testnet-hoodi.json` - Hoodi testnet with testnet EigenLayer addresses
- `mainnet-ethereum.json` - Ethereum mainnet with mainnet EigenLayer
addresses
- Removed `hoodi.json` (replaced by environment-prefixed files)
## Usage
```bash
# Deploy to stagenet on Hoodi
bun cli contracts deploy --chain hoodi --environment stagenet
# Update BEEFY checkpoint from live chain
bun cli contracts update-beefy-checkpoint \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Compute rewards origin for a chain
bun cli contracts update-rewards-origin \
--chain hoodi \
--environment stagenet \
--rpc-url wss://services.datahaven-dev.network/stagenet
# Check deployment status
bun cli contracts status --chain hoodi --environment stagenet
```
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
## Summary
- Import `pallet-proxy-genesis-companion` from Moonbeam to enable proxy
account configuration at genesis time
- Configure the pallet in all runtimes (mainnet, stagenet, testnet) with
pallet index 106
- Add `Serialize`/`Deserialize` derives to `ProxyType` enum to satisfy
`MaybeSerializeDeserialize` bounds
- Include mock runtime and unit tests adapted for polkadot-stable2412-6
This pallet extends `pallet-proxy` with genesis configuration support,
allowing proxy relationships to be established at chain genesis rather
than requiring extrinsic calls after launch.
### Key adaptations from Moonbeam
The pallet was modified to work with the DataHaven SDK version
(polkadot-stable2412-6):
- Removed `BlockNumberProvider` associated type constraint (not present
in this version of pallet-proxy)
- Uses `frame_system::pallet_prelude::BlockNumberFor<T>` directly for
delay parameter
- Uses `MaybeSerializeDeserialize` trait bound for `ProxyType`
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
### Summary
Set username deletion to use a 30‑day grace period (in blocks) and added
a non‑zero username deposit, both based on the Moonbeam's runtimes. This
makes username unbinding wait before deletion and makes authority‑issued
usernames non‑free, mitigating the sybil vector while aligning with
existing identity config patterns.
### Changes
- Configures for `pallet_identity`
- `UsernameGracePeriod = 30 * DAYS`
- `UsernameGracePeriod = deposit(0, MaxUsernameLength::get())`
Upgrades to StorageHub version v0.3.3. This upgrade requires both a
runtime and client upgrade.
---------
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
## Summary
- Increase `MaxBatchConfirmStorageRequests` runtime constant from 10 to
100
- Applied across all runtime environments: mainnet, stagenet, and
testnet
## Test plan
- [x] Verify builds pass for all runtime configurations
## Summary
This PR integrate the slashing feature with EigenLayer. With this PR,
slashing can now be relayed to our Datahaven AVS and then executed
within EigenLayer. In addition some refactoring of the original slashing
pallet has been done.
## Motivation
To avoid misbehaving actor in the network, Datahaven has implemented a
slashing pallet in which offenses can be reported and then if adequate
can lead to a sanction on the misbehaving node. It incentive nodes to
only follow good behavior in addition to the reward incentive. The
rewards flow is managed directly into EigenLayer (see
https://github.com/datahaven-xyz/datahaven/pull/351).
## Slashing flow
<img width="2355" height="946" alt="Slashing Flow"
src="https://github.com/user-attachments/assets/c1ddc3dc-2a7e-429d-94e0-1e02a3f65246"
/>
## What changes
* Implemented `slashValidatorsOperator` in `DataHavenServiceManager`. It
received all the slashing requests batched (every new era the queued
slashing are being relayed from substrate to Ethereum). It handle the
slashing of the operators reported into the Validator set.
* Added a `slashes_adapter.rs` utility file to remove the duplication
for each runtime. In addition, we made use of the `sol!` macro from
alloy to encode the calldata for the Ethereum call. This avoid rewriting
encoding logic and allow to remove the hardcoded selector value used to
call the slashing function.
* Added some tests in solidity to test the registering and slashing of
an operator in Ethereum via Eigen Layer.
* Added e2e tests that test the injection of a slash request, it being
relayed via the snowbridge relayer and executed by our Datahaven AVS.
## What could be better
* We are only deploying one strategy for now so it is hardcoded in the
slashing flow. We should be able to update the pallet in case we are
adding a new strategy. So communication from Ethereum should be relayed.
* We don't have error being return in case the slashing fail. Which
could happen if we don't have the right number of strategy or the
validator is not registered... etc.
* More tests for the unhappy path
This PR upgrades the StorageHub version to
[v0.3.1](https://github.com/Moonsong-Labs/storage-hub/releases/tag/v0.3.1).
The changes applied are the ones suggested in the corresponding release
notes, which in short are:
- Adding the `get_number_of_active_users_of_provider` runtime API to the
`PaymentStreams` pallet runtime APIs.
- Supporting `--max-open-forests` CLI param (has defaults).
- Supporting Prometheus telemetry.
IMPORTANT: This upgrade requires a Runtime upgrade as well as a Client
upgrade.
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Upgrade to StorageHub version 0.3.0. This is a minor release, including
breaking changes.
## ⚠️ Breaking Changes ⚠️
The changes applied in this PR are according to the suggested changes in
StorageHub's [v0.3.0
release](https://github.com/Moonsong-Labs/storage-hub/releases/tag/v0.3.0)
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
## Summary
Use block authorship as direct proof of liveness for the 30% liveness
component of validator rewards. Validators who author at least one block
in a session are considered online and receive the full liveness bonus.
## Problem
The rewards pallet was checking validator liveness via ImOnline
**after** the session had rotated - at which point ImOnline had already
cleared its `AuthoredBlocks` storage. This caused all validators to
appear offline, resulting in only ~70% of expected rewards being
allocated (missing the 30% liveness bonus).
## Solution
Use **block authorship as the proxy for liveness**:
- A validator who authored at least one block is definitively online
- Liveness is determined directly in `award_session_performance_points`
via `blocks_authored > 0`
- No dependency on external liveness checks (ImOnline)
### Rewards Formula
- **60%** Block authorship (proportional to blocks produced)
- **30%** Liveness (full bonus if authored ≥1 block, zero otherwise)
- **10%** Base reward (for being in the validator set)
### Files Changed
- `pallets/external-validators-rewards/src/lib.rs` - Core logic changes
- `pallets/external-validators-rewards/src/mock.rs` - Test mock updates
- `pallets/external-validators-rewards/src/tests.rs` - Test updates
- `runtime/{mainnet,testnet,stagenet}/src/configs/mod.rs` - Config
updates
## Testing
- All 76 pallet tests pass
- Local testing should show correct points per session (e.g., 3200
points for 2 validators with 10 blocks)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
This PR replaces the percentage-based compounding inflation model with a
**linear (non-compounding) inflation model** where a fixed amount of
tokens is minted annually, regardless of current total supply.
### Key Changes
- **`ExternalRewardsEraInflationProvider`** now calculates per-era
inflation from a fixed annual amount instead of a percentage of current
total issuance
- New **`InflationAnnualAmount`** runtime parameter using the formula:
`5_000_000 * HAVE * SUPPLY_FACTOR`
- Consistent configuration across all runtimes using `SUPPLY_FACTOR`
### Inflation Configuration
| Runtime | SUPPLY_FACTOR | Genesis Supply | Annual Inflation | Per-Era
Inflation |
|---------|---------------|----------------|------------------|-------------------|
| **Mainnet** | 100 | 10B HAVE | 500M HAVE (5%) | ~342,231 HAVE |
| **Stagenet** | 1 | 100M HAVE | 5M HAVE (5%) | ~3,422 HAVE |
| **Testnet** | 1 | 100M HAVE | 5M HAVE (5%) | ~3,422 HAVE |
### Benefits
- **Predictable rewards**: Validators and stakers receive consistent
emissions
- **Publicly auditable**: All emissions recorded on-chain
- **Non-compounding**: Same absolute amount minted each year (not
percentage of growing supply)
- **Governance-upgradeable**: `InflationAnnualAmount` can be changed via
runtime parameters
### Comparison: Before vs After
| Aspect | Before (Compounding) | After (Linear) |
|--------|---------------------|----------------|
| Formula | 5% × current_supply | Fixed 500M HAVE |
| Year 1 (10B supply) | 500M HAVE | 500M HAVE |
| Year 2 (10.5B supply) | 525M HAVE | 500M HAVE |
| Year 10 | ~814M HAVE | 500M HAVE |
## ⚠️ Breaking Changes ⚠️
- **Runtime parameter renamed**: `InflationTargetedAnnualRate` (Perbill)
→ `InflationAnnualAmount` (Balance)
- Old: percentage-based rate applied to current total issuance
- New: fixed annual amount in base units (wei)
- **`ExternalRewardsEraInflationProvider` type parameters changed**:
- Removed: `Balances` (fungible::Inspect) and `AnnualRate`
(Get<Perbill>)
- Added: `AnnualAmount` (Get<u128>)
- **Inflation behavior change**: Inflation is now linear (fixed amount)
instead of compounding (percentage of supply)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Gonza Montiel <gonzamontiel@users.noreply.github.com>
## Summary
Building on #304, this PR implements two complementary mechanisms to
improve validator incentives and network performance:
1. **Performance-Based Validator Rewards** (session-level)
2. **Inflation Scaling** (era-level)
## Reward Model Comparison
### Old Model (main branch) vs New Model
| Metric | Old Model (20 pts/block) | New Model (320 pts/block pool) |
|--------|--------------------------|--------------------------------|
| **Per Block** | Author: 20 pts, Others: 0 | Author: ~196 pts, Others:
~4 pts each |
| **Formula** | Direct author reward | 60% authoring + 30% liveness +
10% base |
| **Per Session** (600 blocks, 32 validators) | 12,000 total pts |
192,000 total pts |
| **Per Validator/Session** (uniform) | ~375 pts | ~6,000 pts |
| **Per Validator/Era** (6 sessions) | ~2,250 pts | ~36,000 pts |
| **Offline Validator** | 0 pts | ~600 pts/session (base only) |
| **Over-performer (150% blocks)** | 150% of fair share | Up to 130%
reward (soft cap) |
### Key Differences
- **Pool-based**: New model adds 320 points to a shared pool per block,
distributed via formula
- **Liveness rewarded**: 30% of rewards go to validators who are online
(heartbeat OR block authorship)
- **Base guarantee**: 10% ensures all active validators receive minimum
rewards
- **Soft cap**: Prevents extreme over-performance rewards (max 150% of
fair share credited)
## Performance-Based Validator Rewards
Introduces a **60/30/10 reward formula** that rewards validators based
on their contribution during each session:
- **60%** based on block production (with soft cap allowing up to 150%
of fair share)
- **30%** based on liveness (ImOnline heartbeat OR block authorship)
- **10%** guaranteed base reward for all active validators
### Key Features
- Tracks individual validator block authorship per session
- Calculates fair share dynamically: `fair_share = total_blocks /
total_validator_count`
- Fair share uses **total** validator count (including whitelisted)
since all validators occupy block slots
- **Soft cap**: Over-performers can earn credit up to 150% of their fair
share (configurable via `OperatorRewardsFairShareCap` at 50%)
- With 60% BlockAuthoringWeight, this gives over-performers up to **30%
bonus reward**
- **BasePointsPerBlock**: Defines points added to pool per block
produced (default: 320)
- Integrates with SessionManager for automatic point awards at session
end
- Excludes whitelisted validators from rewards (but includes them in
fair share calculation)
- Slashing check disabled but hook retained for future use
- Points accumulate across sessions within an era
### Dynamic Parameters (Governance-Adjustable)
- `OperatorRewardsBlockAuthoringWeight`: Weight for block authoring
(default: 60%)
- `OperatorRewardsLivenessWeight`: Weight for liveness (default: 30%)
- `OperatorRewardsFairShareCap`: Soft cap percentage above fair share
(default: 50%)
## Inflation Scaling
Implements **dynamic inflation scaling** that adjusts total inflation
based on network block production:
- **Minimum**: 20% of base inflation (network halt protection)
- **Maximum**: 100% of base inflation (caps at expected blocks)
- **Linear scaling** between minimum and maximum based on performance
### Scaling Examples
- 0% blocks produced → 20% inflation (safety floor)
- 50% blocks produced → 60% inflation
- 100% blocks produced → 100% inflation
- >100% blocks produced → capped at 100%
### Configuration
- **ExpectedBlocksPerEra**: Computed as `SessionsPerEra ×
EpochDurationInBlocks`
- **MinInflationPercent**: 20%
- **MaxInflationPercent**: 100%
## Combined Effect
These mechanisms work together to create a comprehensive incentive
structure:
1. **Session rewards** encourage individual validator performance and
uptime
2. **Era inflation scaling** incentivizes collective network health
3. **Minimum inflation floor** protects against network halt
4. **Soft cap** allows over-performers to earn up to 30% bonus while
preventing extreme centralization
## Implementation Details
### Pallet Changes
- Add `BlocksAuthoredInSession` storage for per-validator tracking
- Add `BlocksProducedInEra` storage for total network tracking (cleaned
up with HistoryDepth)
- Add `note_block_author()` function called on block production
- Add `award_session_performance_points()` function with configurable
60/30/10 formula
- Add `calculate_scaled_inflation()` function for era-level scaling
- Update `on_era_end()` to use scaled inflation
- Integrate with SessionManager via wrapper types
- Defensive weight validation: proportionally scales if sum > 100%
### Configuration Parameters
- `ValidatorSet`: Provides active validator list
- `LivenessCheck`: Uses `ImOnline::is_online()` (heartbeat OR block
authorship)
- `SlashingCheck`: Integration with slashing pallet (currently disabled)
- `BasePointsPerBlock`: Points added to pool per block (default: 320)
- `BlockAuthoringWeight`: Dynamic parameter (60%)
- `LivenessWeight`: Dynamic parameter (30%)
- `FairShareCap`: Dynamic parameter (50%)
- `ExpectedBlocksPerEra`: Computed from session/epoch config
- `MinInflationPercent`: 20%
- `MaxInflationPercent`: 100%
### Runtime Updates
- Full configuration added to mainnet, testnet, and stagenet runtimes
- Dynamic parameters added to `runtime_params.rs` for governance control
- Uses `prod_or_fast!()` macro for environment-specific parameters
- `ValidatorIsOnline` uses `ImOnline::is_online()` for accurate liveness
detection
## Testing
- **76 tests passing** ✅
- Comprehensive coverage of both mechanisms
### Test Coverage
- Inflation scaling at 0%, 25%, 50%, 75%, 100%, >100% blocks
- Session performance with 60/30/10 formula
- Fair share calculations with soft cap (150%)
- Whitelisted validator exclusion from rewards (with correct fair share
using total count)
- Total points verification (sum of individual = total)
- Whitelisted over-producer scenarios
- Overflow protection (large block counts, near-u32::MAX)
- End-to-end session to era flow
- MockLivenessCheck mirrors ImOnline behavior (block authorship =
online)
- Multiple eras with different performance levels
- Edge cases (zero participation, single validator, large numbers)
- BlocksProducedInEra cleanup on era start
## ⚠️ Breaking Changes ⚠️
### Reward Distribution
Previously, rewards were distributed equally among all validators
regardless of their contribution. Now:
- **Performance-based**: Validators earn rewards proportional to their
block production (60%), liveness (30%), and a guaranteed base (10%)
- **Pool-based**: `BasePointsPerBlock` defines points added to pool per
block (320), distributed via formula
- **Fair share uses total validators**: Ensures non-whitelisted aren't
penalized for whitelisted validators' block slots
- **Soft cap**: Block production rewards allow up to 150% of fair share
(50% cap = 30% bonus with 60% weight)
- **Slashing check disabled**: Hook retained for future use, but
currently not applied
### Inflation Mechanism
Previously, the full calculated inflation was minted each era. Now:
- **Scaled by performance**: Total inflation scales between 20%-100%
based on actual blocks produced vs expected
- **Safety floor**: Even with zero blocks, 20% of inflation is still
minted to prevent complete halt
- **Network incentive**: Collective block production directly impacts
total rewards available
### Pallet Configuration
The `pallet-external-validators-rewards` Config now requires additional
types:
- `BlockAuthoringWeight`, `LivenessWeight`, `FairShareCap` for reward
formula
- `ValidatorSet`, `LivenessCheck`, `SlashingCheck` for validator
tracking
- `ExpectedBlocksPerEra`, `MinInflationPercent`, `MaxInflationPercent`
for inflation scaling
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
### Context
The function `v2_sendMessage()` on Snowbridge Gateway contract is
**permissionless** (I'm shocked this is the design choice). Any
EOA/contract on Ethereum can build a message and send it through our DH
bridge. While we don't change our Snowbridge fork, then this will
continue to be the case.
### Problem
We use `v2_sendMessage()` to send **permissioned** operations to our
chain. For instance: update our validator set message (coming next,
_slashing-related_ messages). So we do need to restrict the processing
of the incoming messages on the Substrate side.
### Fix
- I've added a check to `EigenLayerMessageProcessor` that enforces
`message.origin` to be only a configured `AuthorisedOrigin`.
- I've added an `AuthorisedOrigin` to
`pallet_external_validators::Config`
- I've configured the `AuthorisedOrigin` to be
`DatahavenServiceManagerAddress` in all three runtimes
### Stages
- [x] Implementation
- [x] Runtime integration tests
- [x] Collect `DatahavenServiceManagerAddress` parameter for e2e tests
to work
Fixes https://github.com/datahaven-xyz/sr-datahaven/issues/12
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
We have two instances of `pallet_collective`
- `pallet_collective_treasury_council`
- `pallet_collective_technical_committee`
Our weights template automatically generates an implementation for
`pallet_collective_treasury_council::WeightInfo` or
`pallet_collective_technical_committee::WeightInfo`, which don't exist,
making the compilation fail right after running benches.
I created aliases for both pallet names, and added a small tweak to the
template so it does not break anymore.
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
## DataHaven Native Transfer Precompile
Implements EVM precompile at address
`0x00000000000000000000000000000007F5` (2073) to expose
`pallet-datahaven-native-transfer` functionality to the EVM layer.
### Features
- **Transfer to Ethereum**: Locks native tokens and sends them via
Snowbridge to Ethereum addresses
- **Pause/Unpause**: Admin controls to halt/resume transfers
- **View Functions**: Query paused state, total locked balance, and
sovereign account address
### Implementation
- Precompile using `#[precompile_utils::precompile]` macro with proper
gas accounting
- 15+ test cases covering success/failure scenarios
- Solidity interface with NatSpec documentation for contract integration
Enables seamless cross-chain transfers of DataHaven native tokens to
Ethereum L1.
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## ⚠️ Breaking Changes ⚠️
Upgrades to SH version 0.2.0. Breaking changes for this version are
outlined in the corresponding
[release](https://github.com/Moonsong-Labs/storage-hub/releases/tag/v0.2.0).
Particularly, in this PR, the following breaking changes are implemented
for DH node operators:
### Breaking CLI changes vs `main`
- **Fisherman vs provider role**
- `--fisherman` now has `conflicts_with = "provider"`
(`FishermanConfigurations::fisherman`).
- Any existing scripts that started a node with both `--provider` and
`--fisherman` will now fail clap validation.
- **Removed / replaced fisherman tuning flags**
- The following flags no longer exist and will cause errors if still
used:
- `--fisherman-incomplete-sync-max` (field
`fisherman_incomplete_sync_max`)
- `--fisherman-incomplete-sync-page-size` (field
`fisherman_incomplete_sync_page_size`)
- `--fisherman-sync-mode-min-blocks-behind` (field
`fisherman_sync_mode_min_blocks_behind`)
- They are replaced by:
- `--fisherman-batch-interval-seconds`
(`fisherman_batch_interval_seconds`, default `60`)
- `--fisherman-batch-deletion-limit` (`fisherman_batch_deletion_limit`,
default `1000`)
- **MSP DB wiring no longer piggybacks on the indexer DB**
- Previously, enabling the indexer (`IndexerConfigurations`) also wired
its DB pool into the MSP move‑bucket path via
`with_indexer_db_pool(maybe_indexer_db_pool)`.
- Now, MSP DB access is **only** configured if you pass the new
`--msp-database-url` provider flag; the indexer’s `--indexer` /
`--indexer-database-url` no longer implicitly provide DB access to MSP
logic. This will change behaviour for MSP nodes that relied on just the
indexer flags.
### New / additive CLI flags (non‑breaking but behaviourally relevant)
- **Provider flags**
- `--pending-db-url` (`pending_db_url`, env `SH_PENDING_DB_URL`) for
persisting pending extrinsics.
- `--internal-buffer-size` (`internal_buffer_size`, default `1024`) for
DB chunk batching during file transfer.
- **Reordered but unchanged**
- `--msp-distribute-files` still exists (bool flag), just moved within
`ProviderConfigurations`; name and type are unchanged, but now also
explicitly toggles `enable_msp_distribute_files` only when
`provider_type == msp`.
Set `GasLimitPovSizeRatio` to 0 across all runtime environments
(mainnet, stagenet, testnet) since DataHaven operates as a solo chain
and doesn't need to account for Proof-of-Validity size constraints that
parachains require.
## ⚠️ Breaking Changes ⚠️
- `GasLimitPovSizeRatio` is now set to 0 across all runtimes
- Gas calculations will no longer account for POV size constraints
## Summary
Removes old runtime migrations that have already been executed on
Stagenet and Testnet environments, reducing code complexity and
maintenance burden.
## Changes
### Migration Cleanup
- **Removed `evm_alias::EvmAliasMigration`** (~532 lines)
- Multi-block migration that renamed the Frontier EVM pallet alias from
`Evm` to `EVM`
- Migrated AccountCodes, AccountCodesMetadata, and AccountStorages
- **Removed `evm_chain_id::EvmChainIdMigration`**
- Single-step migration that updated stored EVM chain IDs to match new
configuration
- Applied to testnet (55931) and stagenet (55932)
### Runtime Updates
- **Simplified `MultiBlockMigrationList`** to empty tuple `()` in
`runtime/common/src/migrations.rs`
- **Updated all runtime configs** to use simplified migration list:
- `runtime/mainnet/src/configs/mod.rs`
- `runtime/stagenet/src/configs/mod.rs`
- `runtime/testnet/src/configs/mod.rs`
- Removed `Runtime` type parameter from migration configurations
### What Remains
- `pallet_migrations` infrastructure stays in place for future
migrations
- Migration test file (`mainnet/tests/migrations.rs`) preserved for
testing pallet administrative functions
- Configuration types and constants (cursor/identifier lengths,
handlers)
## Impact
- **Code reduction**: 532 lines removed
- **No functional change**: These migrations have already executed
successfully
- **Future-ready**: Migration infrastructure remains for new migrations
when needed
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
Replaced the `DefaultFailedMigrationHandler` (which completely froze the
chain on migration failures) with `EnterSafeModeOnFailedMigration`
across all three runtimes (mainnet, stagenet, testnet). When a migration
fails, the chain now automatically enters SafeMode instead of freezing,
allowing governance to intervene and fix issues while preventing regular
user transactions.
## Problem
Previously, when a runtime migration failed, the chain would use
`FreezeChainOnFailedMigration`, which completely halted all operations
including governance functions. This made it impossible to recover from
migration failures without manual intervention at the node level.
## Solution
Implemented `EnterSafeModeOnFailedMigration` which:
- **Enters SafeMode** when a migration fails: the chain remains
_indefinitely_ under safe mode until it is disabled, either with Sudo or
Governance.
- **Allows governance operations** to continue (Sudo, SafeMode, TxPause,
Preimage, Scheduler, etc.)
- **Blocks regular user transactions** to prevent interaction with
potentially inconsistent storage
- **Falls back to freezing** if SafeMode cannot be entered
## Changes
### Core Implementation
- **`runtime/common/src/migrations.rs`**: Added
`FailedMigrationHandler<SafeMode>` type alias that wraps
`EnterSafeModeOnFailedMigration` with comprehensive documentation
- **All three runtimes** (`mainnet`, `stagenet`, `testnet`):
- Updated `pallet_migrations::Config::FailedMigrationHandler` to use
`FailedMigrationHandler<SafeMode>`
- Removed obsolete TODO comments
### Tests
Added comprehensive migration failure tests to all three runtimes:
- **`failed_migration_enters_safe_mode`**: Verifies SafeMode is
activated, expiry is set, and event is emitted
- **`safe_mode_allows_governance_during_migration_failure`**: Confirms
governance can exit SafeMode after migration failure
- **`migrations_force_calls_are_root_only`**: Existing test for
migration management permissions
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
## Summary
- Enables BEEFY equivocation reporting which was previously disabled
(set to `()`)
- Configures `pallet_beefy::EquivocationReportSystem` in all three
runtimes (mainnet, stagenet, testnet)
- Without this fix, validators could sign conflicting BEEFY commitments
without any slashing consequences
## Problem
The `EquivocationReportSystem` type in `pallet_beefy::Config` was set to
`()`, which completely disabled BEEFY equivocation reporting. This is a
security issue because:
1. BEEFY validators could sign two different commitments at the same
block height (equivocation)
2. There was no mechanism to report and slash such misbehavior
3. This undermines the security guarantees of the BEEFY consensus
protocol
## Solution
Configure the proper equivocation report system using the same pattern
as BABE and GRANDPA:
```rust
type EquivocationReportSystem =
pallet_beefy::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
```
This uses:
- `Offences` pallet to record equivocations
- `Historical` pallet for validator proof verification
- `ReportLongevity` parameter (based on bonding duration) for the
reporting window
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
This PR introduces a configurable inflation system for validator rewards
with an annual target rate and optional treasury allocation.
## Changes
### Inflation Mechanism
- **Annual inflation rate runtime parameter**: Set to 5% default
- **EraInflationProvider**: Calculates per-era inflation based on total
issuance and annual rate
- Formula: `per_era_inflation = (total_issuance × annual_rate) /
eras_per_year`
### Treasury Allocation
- **InflationTreasuryProportion parameter**: Set to 20% default
- **ExternalRewardsInflationHandler**: Mints inflation and distributes
between:
- 80% to rewards account (for validator rewards)
- 20% to treasury account
- Treasury receives allocation via `mul_floor()`, with remainder going
to rewards to ensure no tokens lost to rounding
### Runtime Integration
- Configured across all three runtimes: mainnet, testnet, and stagenet
- Consistent parameters across all environments
### Testing
- Updated all tests to account for 80/20 split between rewards and
treasury
- Added precision tolerance (±1 unit) for Perbill rounding edge cases
---------
Co-authored-by: Claude <noreply@anthropic.com>