mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
### PR Description Add a comprehensive end-to-end test that validates rewards distribution across the full system (chain → bridge → execution environment). ### Use cases covered - Verify the rewards infrastructure is correctly deployed and reachable. - Detect the end-of-era rewards emission and capture its essential data. - Confirm the cross-chain delivery and execution of the rewards message. - Ensure the rewards registry updates with the new root and can be queried. - Generate per-validator proofs for claiming rewards. - Successfully claim rewards for a validator and validate the payout is reflected. - Prevent a second (double) claim for the same index with a proper rejection. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
228 lines
7 KiB
TypeScript
228 lines
7 KiB
TypeScript
import validatorSet from "../configs/validator-set.json";
|
|
import { waitForDataHavenEvent } from "./events";
|
|
import { logger } from "./logger";
|
|
import type { DataHavenApi } from "./papi";
|
|
|
|
// Small hex helper
|
|
const toHex = (x: unknown): `0x${string}` => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const anyX: any = x as any;
|
|
if (anyX?.asHex) return anyX.asHex();
|
|
const s = anyX?.toString?.() ?? "";
|
|
return `0x${s}` as `0x${string}`;
|
|
};
|
|
|
|
// External Validators Rewards Events (normalized)
|
|
export interface RewardsMessageSent {
|
|
messageId: `0x${string}`;
|
|
merkleRoot: `0x${string}`;
|
|
eraIndex: number;
|
|
totalPoints: bigint;
|
|
inflation: bigint;
|
|
}
|
|
|
|
// Era tracking utilities
|
|
export async function getCurrentEra(dhApi: DataHavenApi): Promise<number> {
|
|
// Get the active era from ExternalValidators pallet
|
|
const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue();
|
|
|
|
// ActiveEra can be null at chain genesis
|
|
if (!activeEra) {
|
|
return 0;
|
|
}
|
|
|
|
return activeEra.index;
|
|
}
|
|
|
|
export function getEraLengthInBlocks(dhApi: DataHavenApi): number {
|
|
// Read constants directly from runtime metadata
|
|
const consts: any = (dhApi as unknown as { consts?: unknown }).consts ?? {};
|
|
const epochDuration = Number(consts?.Babe?.EpochDuration ?? 10); // blocks per session
|
|
const sessionsPerEra = Number(consts?.ExternalValidators?.SessionsPerEra ?? 1);
|
|
return epochDuration * sessionsPerEra;
|
|
}
|
|
|
|
export async function getBlocksUntilEraEnd(dhApi: DataHavenApi): Promise<number> {
|
|
const currentBlock = await dhApi.query.System.Number.getValue();
|
|
const eraLength = getEraLengthInBlocks(dhApi) || 10;
|
|
const mod = currentBlock % eraLength;
|
|
return mod === 0 ? eraLength : eraLength - mod;
|
|
}
|
|
|
|
// Validator monitoring and rewards data
|
|
export interface EraRewardPoints {
|
|
total: number;
|
|
individual: Map<string, number>;
|
|
}
|
|
|
|
export async function getEraRewardPoints(
|
|
dhApi: DataHavenApi,
|
|
eraIndex: number
|
|
): Promise<EraRewardPoints | null> {
|
|
try {
|
|
const rewardPoints =
|
|
await dhApi.query.ExternalValidatorsRewards.RewardPointsForEra.getValue(eraIndex);
|
|
|
|
if (!rewardPoints) {
|
|
return null;
|
|
}
|
|
|
|
// Convert the storage format to our interface
|
|
const individual = new Map<string, number>();
|
|
for (const [account, points] of rewardPoints.individual) {
|
|
individual.set(account.toString(), points);
|
|
}
|
|
|
|
return {
|
|
total: rewardPoints.total,
|
|
individual
|
|
};
|
|
} catch (error) {
|
|
logger.error(`Failed to get era reward points for era ${eraIndex}: ${error}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Merkle proof generation using DataHaven runtime API
|
|
export interface ValidatorProofData {
|
|
validatorAccount: string;
|
|
operatorAddress: string;
|
|
points: number;
|
|
proof: string[];
|
|
leaf: string;
|
|
numberOfLeaves: number;
|
|
leafIndex: number;
|
|
}
|
|
|
|
export async function generateMerkleProofForValidator(
|
|
dhApi: DataHavenApi,
|
|
validatorAccount: string,
|
|
eraIndex: number
|
|
): Promise<{ proof: string[]; leaf: string; numberOfLeaves: number; leafIndex: number } | null> {
|
|
try {
|
|
// Call the runtime API to generate merkle proof
|
|
const merkleProof = await dhApi.apis.ExternalValidatorsRewardsApi.generate_rewards_merkle_proof(
|
|
validatorAccount,
|
|
eraIndex
|
|
);
|
|
|
|
if (!merkleProof) {
|
|
logger.debug(
|
|
`No merkle proof available for validator ${validatorAccount} in era ${eraIndex}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Convert the proof to hex strings
|
|
const proof = merkleProof.proof.map((node: unknown) => toHex(node));
|
|
|
|
const leaf = toHex(merkleProof.leaf);
|
|
|
|
const numberOfLeaves = Number(merkleProof.number_of_leaves as bigint);
|
|
const leafIndex = Number(merkleProof.leaf_index as bigint);
|
|
|
|
return { proof, leaf, numberOfLeaves, leafIndex };
|
|
} catch (error) {
|
|
logger.error(`Failed to generate merkle proof for validator ${validatorAccount}: ${error}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validator credentials containing operator address and private key
|
|
*/
|
|
export interface ValidatorCredentials {
|
|
operatorAddress: `0x${string}`;
|
|
privateKey: `0x${string}` | null;
|
|
}
|
|
|
|
/**
|
|
* Gets validator credentials (operator address and private key) by solochain address
|
|
* @param validatorAccount The validator's solochain address
|
|
* @returns The validator's credentials including operator address and private key
|
|
*/
|
|
export function getValidatorCredentials(validatorAccount: string): ValidatorCredentials {
|
|
const normalizedAccount = validatorAccount.toLowerCase();
|
|
|
|
// Find matching validator by solochain address
|
|
const match = validatorSet.validators.find(
|
|
(v) => v.solochainAddress.toLowerCase() === normalizedAccount
|
|
);
|
|
|
|
if (match) {
|
|
return {
|
|
operatorAddress: match.publicKey as `0x${string}`,
|
|
privateKey: match.privateKey as `0x${string}`
|
|
};
|
|
}
|
|
|
|
// Fallback: assume the input is already an Ethereum address, but no private key available
|
|
logger.debug(`No mapping found for ${validatorAccount}, using as-is without private key`);
|
|
return {
|
|
operatorAddress: validatorAccount as `0x${string}`,
|
|
privateKey: null
|
|
};
|
|
}
|
|
|
|
// Generate merkle proofs for all validators in an era
|
|
export async function generateMerkleProofsForEra(
|
|
dhApi: DataHavenApi,
|
|
eraIndex: number
|
|
): Promise<Map<string, ValidatorProofData>> {
|
|
// Get era reward points
|
|
const eraPoints = await getEraRewardPoints(dhApi, eraIndex);
|
|
if (!eraPoints) {
|
|
logger.warn(`No reward points found for era ${eraIndex}`);
|
|
return new Map();
|
|
}
|
|
|
|
const entries = await Promise.all(
|
|
[...eraPoints.individual].map(async ([validatorAccount, points]) => {
|
|
const merkleData = await generateMerkleProofForValidator(dhApi, validatorAccount, eraIndex);
|
|
if (!merkleData) return null;
|
|
const credentials = getValidatorCredentials(validatorAccount);
|
|
const value: ValidatorProofData = {
|
|
validatorAccount,
|
|
operatorAddress: credentials.operatorAddress,
|
|
points,
|
|
proof: merkleData.proof,
|
|
leaf: merkleData.leaf,
|
|
numberOfLeaves: merkleData.numberOfLeaves,
|
|
leafIndex: merkleData.leafIndex
|
|
};
|
|
return [credentials.operatorAddress, value] as const;
|
|
})
|
|
);
|
|
|
|
const filtered = entries.filter(Boolean) as [string, ValidatorProofData][];
|
|
const proofs = new Map(filtered);
|
|
logger.info(`Generated ${proofs.size} merkle proofs for era ${eraIndex}`);
|
|
return proofs;
|
|
}
|
|
|
|
// Rewards message event -> normalized return
|
|
|
|
export async function waitForRewardsMessageSent(
|
|
dhApi: DataHavenApi,
|
|
expectedEra?: number,
|
|
timeout = 120000
|
|
): Promise<RewardsMessageSent | null> {
|
|
const result = await waitForDataHavenEvent({
|
|
api: dhApi,
|
|
pallet: "ExternalValidatorsRewards",
|
|
event: "RewardsMessageSent",
|
|
filter: expectedEra !== undefined ? (event: any) => event.era_index === expectedEra : undefined,
|
|
timeout
|
|
});
|
|
|
|
if (!result?.data) return null;
|
|
|
|
const data: any = result.data;
|
|
return {
|
|
messageId: data.message_id.asHex(),
|
|
merkleRoot: data.rewards_merkle_root.asHex(),
|
|
eraIndex: data.era_index,
|
|
totalPoints: data.total_points,
|
|
inflation: data.inflation_amount
|
|
};
|
|
}
|