2025-05-27 16:14:15 +00:00
|
|
|
import { parseArgs } from "node:util";
|
2025-05-27 08:49:53 +00:00
|
|
|
import { datahaven } from "@polkadot-api/descriptors";
|
|
|
|
|
import { createClient } from "polkadot-api";
|
|
|
|
|
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
|
test: refactor e2e tests (#365)
This PR significantly refactors and improves the end-to-end testing
framework and infrastructure. The primary focus was on simplifying the
test suites, improving reliability through better resource management,
and hardening the relayer infrastructure.
All E2E tests are now passing on the CI and demonstrate consistent
reliability when run locally.
### Key Changes
#### 1. E2E Test Suite Refactor & Cleanup
* **Simplified Test Logic**: Heavily refactored the core test suites
(`native-token-transfer.test.ts`, `rewards-message.test.ts`, and
`validator-set-update.test.ts`). The new implementation is much cleaner,
utilizing shared helpers to reduce boilerplate.
* **Utility Consolidation**: Removed redundant utility files
(`storage.ts`, `rewards-helpers.ts`) and simplified `events.ts`. Event
waiting now uses `rxjs` for Substrate and native `viem` watchers for
Ethereum, which is more robust and easier to maintain.
* **Better Connector Management**: Unified the creation and cleanup of
test clients in `ConnectorFactory`. It now handles the lifecycle of
WebSocket connections more gracefully, including clearing the
`socketClientCache` to prevent reconnection noise during teardown.
#### 2. Infrastructure & Stability
* **Relayer Relaunch Policy**: Added a restart policy for Snowbridge
relayer containers. They are now configured with `--restart
on-failure:5`, ensuring that relayers automatically relaunch if they
crash during the sensitive initialization phase.
* **WebSocket Integration**:
* Updated the `ConnectorFactory` to prefer **WebSockets** for the
Ethereum public client, which is essential for efficient, event-heavy
E2E testing.
* Enhanced `launchKurtosisNetwork` to correctly identify and register
the Execution Layer's WebSocket endpoint from Kurtosis.
* **Disabled Contract Injection**: This PR temporarily disables the
automatic injection of contracts into the genesis state by default.
* *Reason*: I encountered issues generating a valid `state-diff.json`
for the latest contract versions. Even after applying several
workarounds, the injected state remained unstable. As a result, I've
reverted to manual contract deployment during the launch sequence for
better reliability for now.
#### 3. Documentation & Maintenance
* Removed obsolete documentation (`event-utilities-guide.md`) that no
longer reflects the simplified event-handling API.
* Cleaned up `test/launcher/validators.ts` and moved logic into more
appropriate helpers.
---------
Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
2025-12-24 12:31:40 +00:00
|
|
|
import { getWsProvider } from "polkadot-api/ws-provider/node";
|
2025-06-12 08:24:03 +00:00
|
|
|
import { getEvmEcdsaSigner, logger, SUBSTRATE_FUNDED_ACCOUNTS } from "utils";
|
2025-12-22 14:57:32 +00:00
|
|
|
import { parseJsonToParameters } from "utils/types";
|
2025-05-27 08:49:53 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets DataHaven runtime parameters on the specified RPC URL from a JSON file.
|
|
|
|
|
*/
|
|
|
|
|
export const setDataHavenParameters = async (
|
2025-12-22 14:57:32 +00:00
|
|
|
rpcUrl: string,
|
|
|
|
|
parametersFilePath: string
|
2025-05-27 08:49:53 +00:00
|
|
|
): Promise<boolean> => {
|
2025-12-22 14:57:32 +00:00
|
|
|
const parametersJson = await Bun.file(parametersFilePath).json();
|
|
|
|
|
const parameters = parseJsonToParameters(parametersJson).filter((p) => p.value !== undefined);
|
2025-05-27 08:49:53 +00:00
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
if (parameters.length === 0) {
|
|
|
|
|
logger.warn("⚠️ No parameters to set.");
|
|
|
|
|
return false;
|
2025-05-27 08:49:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client = createClient(withPolkadotSdkCompat(getWsProvider(rpcUrl)));
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-22 14:57:32 +00:00
|
|
|
const dhApi = client.getTypedApi(datahaven);
|
|
|
|
|
const signer = getEvmEcdsaSigner(SUBSTRATE_FUNDED_ACCOUNTS.ALITH.privateKey);
|
2025-05-27 08:49:53 +00:00
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
// Log parameters being set
|
|
|
|
|
for (const p of parameters) {
|
|
|
|
|
logger.debug(`🔧 Setting ${p.name} = ${p.value!.asHex()}`);
|
|
|
|
|
}
|
2025-05-27 08:49:53 +00:00
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
// Build parameter calls
|
|
|
|
|
const calls = parameters.map(
|
|
|
|
|
(p) =>
|
|
|
|
|
dhApi.tx.Parameters.set_parameter({
|
|
|
|
|
key_value: {
|
|
|
|
|
type: "RuntimeConfig",
|
|
|
|
|
value: { type: p.name, value: [p.value] }
|
|
|
|
|
}
|
|
|
|
|
}).decodedCall
|
|
|
|
|
);
|
2025-05-27 08:49:53 +00:00
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
// Batch all calls and wrap in sudo
|
|
|
|
|
const tx = dhApi.tx.Sudo.sudo({
|
|
|
|
|
call: dhApi.tx.Utility.batch_all({ calls }).decodedCall
|
|
|
|
|
});
|
2025-05-27 08:49:53 +00:00
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
const result = await tx.signAndSubmit(signer);
|
2025-05-27 08:49:53 +00:00
|
|
|
|
feat: automated validator set submission with era targeting (#433)
## 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>
2026-02-20 09:31:44 +00:00
|
|
|
// sudo always returns Ok at the extrinsic level — check the Sudid event
|
|
|
|
|
// for the inner call result
|
|
|
|
|
const sudidEvent = result.events.find(
|
|
|
|
|
(e: any) => e.type === "Sudo" && e.value?.type === "Sudid"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!sudidEvent) {
|
|
|
|
|
logger.error("❌ Sudo.Sudid event not found in transaction events");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sudoResult = (sudidEvent.value as any).value.sudo_result;
|
|
|
|
|
if (sudoResult.type === "Err") {
|
|
|
|
|
logger.error(`❌ Sudo inner call failed: ${JSON.stringify(sudoResult)}`);
|
2025-12-22 14:57:32 +00:00
|
|
|
return false;
|
2025-05-27 08:49:53 +00:00
|
|
|
}
|
2025-12-22 14:57:32 +00:00
|
|
|
|
|
|
|
|
logger.success("Runtime parameters set successfully");
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
|
|
|
return false;
|
2025-05-27 08:49:53 +00:00
|
|
|
} finally {
|
|
|
|
|
client.destroy();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
// CLI entry point
|
2025-05-27 08:49:53 +00:00
|
|
|
if (import.meta.main) {
|
|
|
|
|
const { values } = parseArgs({
|
|
|
|
|
args: process.argv,
|
|
|
|
|
options: {
|
2025-12-22 14:57:32 +00:00
|
|
|
rpcUrl: { type: "string", short: "r" },
|
|
|
|
|
parametersFile: { type: "string", short: "f" }
|
2025-05-27 08:49:53 +00:00
|
|
|
},
|
|
|
|
|
strict: true
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
if (!values.rpcUrl || !values.parametersFile) {
|
|
|
|
|
console.error("Usage: --rpc-url <url> --parameters-file <path>");
|
2025-05-27 08:49:53 +00:00
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 14:57:32 +00:00
|
|
|
setDataHavenParameters(values.rpcUrl, values.parametersFile)
|
|
|
|
|
.then((ok) => process.exit(ok ? 0 : 1))
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
console.error(e);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
2025-05-27 08:49:53 +00:00
|
|
|
}
|