Skip to content

Commit ff299b5

Browse files
committed
feat: improved chain meta reporting algo
1 parent d1174b9 commit ff299b5

File tree

2 files changed

+112
-1
lines changed

2 files changed

+112
-1
lines changed

test/integration/uexecutor/vote_chain_meta_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,3 +415,79 @@ func TestVoteChainMetaContractState(t *testing.T) {
415415
require.Equal(t, new(big.Int).SetUint64(observedAt), got)
416416
})
417417
}
418+
419+
// TestVoteChainMetaAbsoluteStaleness verifies that when all validators' observedAt timestamps
420+
// are older than ObservedAtStalenessThresholdSeconds relative to the current block time,
421+
// the EVM contract is NOT updated (it retains its previous value).
422+
//
423+
// These tests call VoteChainMeta directly on the keeper (bypassing authz) so that
424+
// block time can be freely manipulated without hitting authz grant expiry.
425+
func TestVoteChainMetaAbsoluteStaleness(t *testing.T) {
426+
chainId := "eip155:11155111"
427+
threshold := uexecutortypes.ObservedAtStalenessThresholdSeconds // 300
428+
429+
universalCoreAddr := utils.GetDefaultAddresses().HandlerAddr
430+
431+
readGasPrice := func(t *testing.T, testApp *app.ChainApp, ctx sdk.Context) *big.Int {
432+
t.Helper()
433+
ucABI, err := uexecutortypes.ParseUniversalCoreABI()
434+
require.NoError(t, err)
435+
caller, _ := testApp.UexecutorKeeper.GetUeModuleAddress(ctx)
436+
res, err := testApp.EVMKeeper.CallEVM(ctx, ucABI, caller, universalCoreAddr, false, "gasPriceByChainNamespace", chainId)
437+
require.NoError(t, err)
438+
return new(big.Int).SetBytes(res.Ret)
439+
}
440+
441+
t.Run("stale single vote does not update contract", func(t *testing.T) {
442+
testApp, ctx, _, vals := setupVoteChainMetaTest(t, 1)
443+
444+
staleObservedAt := uint64(1_700_000_000)
445+
// Block time is far past the staleness window
446+
staleCtx := ctx.WithBlockTime(time.Unix(int64(staleObservedAt+threshold+60), 0))
447+
448+
valAddr, err := sdk.ValAddressFromBech32(vals[0].OperatorAddress)
449+
require.NoError(t, err)
450+
451+
require.NoError(t, testApp.UexecutorKeeper.VoteChainMeta(staleCtx, valAddr, chainId,
452+
100_000_000_000, 12345, staleObservedAt))
453+
454+
// Vote was stored in state
455+
stored, found, err := testApp.UexecutorKeeper.GetChainMeta(staleCtx, chainId)
456+
require.NoError(t, err)
457+
require.True(t, found)
458+
require.Equal(t, uint64(100_000_000_000), stored.Prices[0])
459+
460+
// Contract must NOT have been updated — should still be 0
461+
require.Zero(t, readGasPrice(t, testApp, staleCtx).Sign(),
462+
"contract must not be updated when all validators are stale")
463+
})
464+
465+
t.Run("all validators stale does not update contract", func(t *testing.T) {
466+
testApp, ctx, _, vals := setupVoteChainMetaTest(t, 3)
467+
468+
freshObservedAt := uint64(1_700_000_000)
469+
470+
// First vote with fresh block time → contract gets updated
471+
freshCtx := ctx.WithBlockTime(time.Unix(int64(freshObservedAt), 0))
472+
for i := 0; i < 3; i++ {
473+
valAddr, err := sdk.ValAddressFromBech32(vals[i].OperatorAddress)
474+
require.NoError(t, err)
475+
require.NoError(t, testApp.UexecutorKeeper.VoteChainMeta(freshCtx, valAddr, chainId,
476+
200_000_000_000, uint64(12345+i), freshObservedAt+uint64(i)))
477+
}
478+
require.Equal(t, new(big.Int).SetUint64(200_000_000_000), readGasPrice(t, testApp, freshCtx))
479+
480+
// Re-vote with same old timestamps but block time past staleness window
481+
futureCtx := ctx.WithBlockTime(time.Unix(int64(freshObservedAt+threshold+60), 0))
482+
for i := 0; i < 3; i++ {
483+
valAddr, err := sdk.ValAddressFromBech32(vals[i].OperatorAddress)
484+
require.NoError(t, err)
485+
require.NoError(t, testApp.UexecutorKeeper.VoteChainMeta(futureCtx, valAddr, chainId,
486+
999_000_000_000, uint64(99999+i), freshObservedAt+uint64(i)))
487+
}
488+
489+
// Contract must retain the old fresh value — stale votes must not overwrite it
490+
require.Equal(t, new(big.Int).SetUint64(200_000_000_000), readGasPrice(t, testApp, futureCtx),
491+
"contract must retain last good value when all validators report stale data")
492+
})
493+
}

x/uexecutor/keeper/chain_meta.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,26 @@ func (k Keeper) SetChainMeta(ctx context.Context, chainID string, chainMeta type
2626
return k.ChainMetas.Set(ctx, chainID, chainMeta)
2727
}
2828

29+
// isObservedAtStale returns true if observedAt (Unix seconds) is older than
30+
// ObservedAtStalenessThresholdSeconds relative to the current block time.
31+
// When true the data should not be pushed to the UniversalCore contract.
32+
// Returns false when block time is zero or negative (not yet configured).
33+
func isObservedAtStale(sdkCtx sdk.Context, observedAt uint64) bool {
34+
blockTimeUnix := sdkCtx.BlockTime().Unix()
35+
if blockTimeUnix <= 0 {
36+
// Block time not configured (e.g. genesis or test setup) — skip staleness check.
37+
return false
38+
}
39+
blockTimeSec := uint64(blockTimeUnix)
40+
return blockTimeSec > observedAt &&
41+
blockTimeSec-observedAt > types.ObservedAtStalenessThresholdSeconds
42+
}
43+
2944
// VoteChainMeta processes a universal validator's vote on chain metadata (gas price + chain height + observed timestamp).
3045
// It accumulates votes, computes the median price, and calls setChainMeta on the UniversalCore contract.
46+
// If the median observedAt is older than ObservedAtStalenessThresholdSeconds relative to the current
47+
// block time, the EVM contract is NOT updated — all validators are considered stale and retaining
48+
// the last good contract state is preferred over pushing stale data.
3149
func (k Keeper) VoteChainMeta(ctx context.Context, universalValidator sdk.ValAddress, observedChainId string, price, blockNumber, observedAt uint64) error {
3250
sdkCtx := sdk.UnwrapSDKContext(ctx)
3351

@@ -51,6 +69,12 @@ func (k Keeper) VoteChainMeta(ctx context.Context, universalValidator sdk.ValAdd
5169
return sdkerrors.Wrap(err, "failed to set initial chain meta entry")
5270
}
5371

72+
if isObservedAtStale(sdkCtx, observedAt) {
73+
sdkCtx.Logger().Info("VoteChainMeta: skipping EVM update — single vote is stale",
74+
"chain", observedChainId, "observedAt", observedAt)
75+
return nil
76+
}
77+
5478
priceBig := math.NewUint(price).BigInt()
5579
chainHeightBig := math.NewUint(blockNumber).BigInt()
5680
observedAtBig := math.NewUint(observedAt).BigInt()
@@ -90,12 +114,23 @@ func (k Keeper) VoteChainMeta(ctx context.Context, universalValidator sdk.ValAdd
90114
return sdkerrors.Wrap(err, "failed to set updated chain meta entry")
91115
}
92116

117+
// If the median observedAt is stale relative to the current block time, all
118+
// validators are considered offline/lagging. Skip the EVM update so the
119+
// contract retains its last known good value rather than being overwritten
120+
// with stale data.
121+
medianObservedAt := entry.ObservedAts[medianIdx]
122+
if isObservedAtStale(sdkCtx, medianObservedAt) {
123+
sdkCtx.Logger().Info("VoteChainMeta: skipping EVM update — all validators stale",
124+
"chain", observedChainId, "medianObservedAt", medianObservedAt)
125+
return nil
126+
}
127+
93128
// Use the full observation tuple from the median-price validator.
94129
// chainHeight and observedAt are NOT independent medians — they are the
95130
// co-indexed values from whichever validator submitted the median price.
96131
medianPrice := math.NewUint(entry.Prices[medianIdx]).BigInt()
97132
coChainHeight := math.NewUint(entry.ChainHeights[medianIdx]).BigInt()
98-
coObservedAt := math.NewUint(entry.ObservedAts[medianIdx]).BigInt()
133+
coObservedAt := math.NewUint(medianObservedAt).BigInt()
99134
if _, evmErr := k.CallUniversalCoreSetChainMeta(sdkCtx, observedChainId, medianPrice, coChainHeight, coObservedAt); evmErr != nil {
100135
return sdkerrors.Wrap(evmErr, "failed to call EVM setChainMeta")
101136
}

0 commit comments

Comments
 (0)