fix: rewards message test flakiness (#377)

## Summary

This PR removes sources of flakiness in the rewards message E2E test by:
1. Selecting a validator whose Ethereum key is not shared with the
relayers
2. Using specific block numbers for balance reads to avoid stale RPC
data

## Changes

- Select a non-relayer validator from rewardPoints.individual when
claiming rewards
- Remove the conditional updateSolochainAddressForValidator block
(mapping is set during registration)
- Read balances at specific block numbers to ensure deterministic
comparisons

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
This commit is contained in:
Ahmad Kaouk 2026-01-05 16:13:41 +01:00 committed by GitHub
parent ae03649a4f
commit 72c092acfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,5 +1,5 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { CROSS_CHAIN_TIMEOUTS, logger } from "utils";
import { ANVIL_FUNDED_ACCOUNTS, CROSS_CHAIN_TIMEOUTS, logger } from "utils";
import { type Address, decodeEventLog, type Hex, isAddressEqual, padHex } from "viem";
import validatorSet from "../configs/validator-set.json";
import { BaseTestSuite } from "../framework";
@ -135,7 +135,22 @@ describe("Rewards Message Flow", () => {
expect(rewardPoints).toBeDefined();
expect(rewardPoints.total).toBeGreaterThan(0);
const [validatorAccount, points] = rewardPoints.individual[0];
const relayerEthAccount = ANVIL_FUNDED_ACCOUNTS[1].publicKey.toLowerCase();
const pick = rewardPoints.individual.find(([account]: [any, any]) => {
const v = validatorSet.validators.find(
(cfg) => cfg.solochainAddress.toLowerCase() === String(account).toLowerCase()
);
return v && v.publicKey.toLowerCase() !== relayerEthAccount;
});
const [validatorAccount, points] = (pick ?? rewardPoints.individual[0]) as [any, any];
const match = validatorSet.validators.find(
(v) => v.solochainAddress.toLowerCase() === String(validatorAccount).toLowerCase()
);
if (!match) {
throw new Error(
`Validator config not found for solochain address ${String(validatorAccount)}`
);
}
// Generate merkle proof via runtime API
const merkleProof = await dhApi.apis.ExternalValidatorsRewardsApi.generate_rewards_merkle_proof(
@ -146,33 +161,9 @@ describe("Rewards Message Flow", () => {
// Get validator credentials and create operator wallet
const factory = suite.getConnectorFactory();
const match = validatorSet.validators.find(
(v) => v.solochainAddress.toLowerCase() === String(validatorAccount).toLowerCase()
);
const operatorWallet = factory.createWalletClient(match!.privateKey as `0x${string}`);
const resolvedOperator: Address = operatorWallet.account.address;
// Ensure the ServiceManager maps the operator ETH address to the solochain address
const expectedSolochain = String(validatorAccount) as Address;
const mappedSolochain = (await publicClient.readContract({
address: serviceManager.address as Address,
abi: serviceManager.abi,
functionName: "validatorEthAddressToSolochainAddress",
args: [resolvedOperator]
})) as Address;
if (mappedSolochain.toLowerCase() !== expectedSolochain.toLowerCase()) {
const updateTx = await operatorWallet.writeContract({
address: serviceManager.address as Address,
abi: serviceManager.abi,
functionName: "updateSolochainAddressForValidator",
args: [expectedSolochain],
chain: null
});
const updateReceipt = await publicClient.waitForTransactionReceipt({ hash: updateTx });
expect(updateReceipt.status).toBe("success");
}
// Ensure claim not already recorded
const claimedBefore = (await publicClient.readContract({
address: rewardsRegistry.address,
@ -182,10 +173,17 @@ describe("Rewards Message Flow", () => {
})) as boolean;
expect(claimedBefore).toBe(false);
// Record balances for validation
const operatorBalanceBefore = await publicClient.getBalance({ address: resolvedOperator });
// Record balances at a specific block to avoid stale RPC reads
const balanceBlockNumber = Number(await publicClient.getBlockNumber());
const operatorBalanceBefore = await publicClient.getBalance({
address: resolvedOperator,
blockNumber: balanceBlockNumber
});
const registryBalanceBefore = BigInt(
await publicClient.getBalance({ address: rewardsRegistry.address as Address })
await publicClient.getBalance({
address: rewardsRegistry.address as Address,
blockNumber: balanceBlockNumber
})
);
// Submit claim transaction
@ -238,14 +236,19 @@ describe("Rewards Message Flow", () => {
expect(claimedAfter).toBe(true);
// Validate RewardsRegistry balance decrease matches claimed rewards
const registryBalanceAfter = BigInt(
await publicClient.getBalance({ address: rewardsRegistry.address as Address })
);
const claimBlockNumber = Number(claimReceipt.blockNumber);
const registryBalanceAfter = await publicClient.getBalance({
address: rewardsRegistry.address as Address,
blockNumber: claimBlockNumber
});
expect(registryBalanceBefore - registryBalanceAfter).toEqual(claimArgs.rewardsAmount);
expect(claimArgs.rewardsAmount).toEqual(BigInt(points));
// Validate operator received rewards (accounting for gas)
const operatorBalanceAfter = await publicClient.getBalance({ address: resolvedOperator });
const operatorBalanceAfter = await publicClient.getBalance({
address: resolvedOperator,
blockNumber: claimBlockNumber
});
const gasCost = BigInt(claimReceipt.gasUsed) * BigInt(claimReceipt.effectiveGasPrice);
const netBalanceChange = BigInt(operatorBalanceAfter) - BigInt(operatorBalanceBefore);
// Operator balance should have changed by: rewards - gasCost