diff --git a/CHANGELOG.md b/CHANGELOG.md index 963ab39e6..70b4b2b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ and https://github.com/cosmos/cosmos-s ### State Machine Breaking +* (x/oracle) Persist the live cycle-list query data so `GetCurrentQueryInCycleList` no longer indexes `GetCyclelist()[Peek()]`. Oracle consensus version 6 backfills prefix 47. Fixes halt after `MsgUpdateCyclelist` shrinks the list (v6.1.8). * (x/oracle) Remove `MsgBatchSubmitValue`, `SubmitValueItem`, and `MsgUpdateMaxBatchSize`. Oracle consensus version 5 deletes leftover `MaxBatchSize` store bytes. Reporters submit values with `MsgSubmitValue` only (v6.1.7). ### Client Breaking diff --git a/app/upgrades.go b/app/upgrades.go index 922e57dce..cbe233100 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/tellor-io/layer/app/upgrades" - v_6_1_7 "github.com/tellor-io/layer/app/upgrades/v6.1.7" v_6_1_8 "github.com/tellor-io/layer/app/upgrades/v6.1.8" upgradetypes "cosmossdk.io/x/upgrade/types" @@ -14,7 +13,6 @@ var ( // `Upgrades` defines the upgrade handlers and store loaders for the application. // New upgrades should be added to this slice after they are implemented. Upgrades = []*upgrades.Upgrade{ - &v_6_1_7.Upgrade, &v_6_1_8.Upgrade, } Forks = []upgrades.Fork{} @@ -23,17 +21,6 @@ var ( // setupUpgradeHandlers registers the upgrade handlers to perform custom upgrade // logic and state migrations for software upgrades. func (app *App) setupUpgradeHandlers() { - if app.UpgradeKeeper.HasHandler(v_6_1_7.UpgradeName) { - panic(fmt.Sprintf("Cannot register duplicate upgrade handler '%s'", v_6_1_7.UpgradeName)) - } - app.UpgradeKeeper.SetUpgradeHandler( - v_6_1_7.UpgradeName, - v_6_1_7.CreateUpgradeHandler( - app.ModuleManager(), - app.configurator, - ), - ) - if app.UpgradeKeeper.HasHandler(v_6_1_8.UpgradeName) { panic(fmt.Sprintf("Cannot register duplicate upgrade handler '%s'", v_6_1_8.UpgradeName)) } diff --git a/app/upgrades/v6.1.8/upgrade.go b/app/upgrades/v6.1.8/upgrade.go index 9689aaf78..9156ba104 100644 --- a/app/upgrades/v6.1.8/upgrade.go +++ b/app/upgrades/v6.1.8/upgrade.go @@ -20,7 +20,10 @@ Upgrade to v6.1.8 includes (since v6.1.7): - The last-consensus map is backfilled by bridge 5→6 from the latest consensus aggregate per queryId (current PowerThreshold, flagged included); live writes are in CreateNewReportSnapshots; prefix 24; StoreUpgrades empty. - + - Oracle cycle-list: persist the live query data in CurrentCycleListQuery (prefix 47) + so GetCurrentQueryInCycleList no longer indexes GetCyclelist()[Peek()]. Oracle + consensus version 5→6 backfills the item from the sequencer with a wrap on OOB. + StoreUpgrades stays empty (new prefix in the existing oracle store). No other custom state migration is required beyond RunMigrations. */ diff --git a/e2e/attestation_test.go b/e2e/attestation_test.go index 51703c120..dcd6e93b9 100644 --- a/e2e/attestation_test.go +++ b/e2e/attestation_test.go @@ -153,51 +153,73 @@ func TestConsensusAttestation(t *testing.T) { require.Equal(attestationDataRes.LastConsensusTimestamp, timestamp) // lastConsTs should equal report1 timestamp } - // sleep until cycle list query data is the same as the previous report + // Wait until the same cycle-list query is live again, then report from only + // one validator (non-consensus). SpotPrice windows are 2 blocks and the + // cycle list has only 3 queries, so SubmitCycleList's re-query can miss the + // window and land on a different query — leaving get-current-aggregate on + // the first 100% report. Submit the matched QueryData directly and retry + // if the window already expired. + prevTimestamp := timestamp var success bool + deadline := time.Now().Add(2 * time.Minute) for !success { + require.True(time.Now().Before(deadline), "timed out waiting for non-consensus second report") + cycleListRes, _, err := e2e.QueryWithTimeout(ctx, validators[0].Node, "oracle", "current-cyclelist-query") require.NoError(err) var cycleList e2e.QueryCurrentCyclelistQueryResponse err = json.Unmarshal(cycleListRes, &cycleList) require.NoError(err) - if cycleList.QueryData == currentCycleList.QueryData { - success = true - // Report for the cycle list from 1 val so not a consensus report - txHash, err := e2e.SubmitCycleList(ctx, validators[0].Node, validators[0].AccAddr, value, "5loya") - require.NoError(err) - fmt.Println("validator [ 0 ] reported at tx:", txHash) + if cycleList.QueryData != currentCycleList.QueryData { + time.Sleep(500 * time.Millisecond) + continue + } - // Wait 1 block for report to be included, then 1 for aggregation - err = testutil.WaitForBlocks(ctx, 2, validators[0].Node) - require.NoError(err) - } else { + txHashBytes, _, err := validators[0].Node.Exec(ctx, + validators[0].Node.TxCommand(validators[0].AccAddr, "oracle", "submit-value", currentCycleList.QueryData, value, "--fees", "5loya", "--keyring-dir", validators[0].Node.HomeDir()), + validators[0].Node.Chain.Config().Env) + if err != nil { + fmt.Println("second-round submit failed (likely window expired), retrying:", err) time.Sleep(500 * time.Millisecond) + continue } - } + txHash, err := e2e.GetTxHashFromExec(txHashBytes) + if err != nil { + fmt.Println("second-round submit tx failed, retrying:", err) + time.Sleep(500 * time.Millisecond) + continue + } + fmt.Println("validator [ 0 ] reported at tx:", txHash) - // wait 1 more block - err = testutil.WaitForBlocks(ctx, 1, validators[0].Node) - require.NoError(err) + // Wait for inclusion + aggregation, then confirm current aggregate moved + // to a new timestamp with 50% power before continuing. + require.NoError(testutil.WaitForBlocks(ctx, 3, validators[0].Node)) + for i := 0; i < 10; i++ { + res, _, err = e2e.QueryWithTimeout(ctx, validators[0].Node, "oracle", "get-current-aggregate-report", queryId1) + require.NoError(err) + err = json.Unmarshal(res, ¤tAggRes) + require.NoError(err) + if currentAggRes.Timestamp != prevTimestamp && currentAggRes.Aggregate != nil && currentAggRes.Aggregate.AggregatePower == "5000000" { + success = true + break + } + require.NoError(testutil.WaitForBlocks(ctx, 1, validators[0].Node)) + } + if !success { + fmt.Println("current aggregate not yet non-consensus for queryId1; waiting for next cycle") + } + } - // get reports by reporter - var queryId3 string - reports, _, err := e2e.QueryWithTimeout(ctx, validators[0].Node, "oracle", "get-reportsby-reporter", validators[0].AccAddr, "--page-limit", "2") + // Newest report must be for the same queryId as the first consensus report. + reports, _, err := e2e.QueryWithTimeout(ctx, validators[0].Node, "oracle", "get-reportsby-reporter", validators[0].AccAddr, "--page-limit", "1", "--page-reverse") require.NoError(err) var reportsRes e2e.QueryMicroReportsResponse err = json.Unmarshal(reports, &reportsRes) require.NoError(err) - fmt.Println("reports from: ", validators[0].AccAddr, ": ", reportsRes) - require.Equal(len(reportsRes.MicroReports), 2) // val0 should have two reports now - queryId3 = reportsRes.MicroReports[0].QueryID - require.Equal(queryId1, queryId3) // make sure query is same as first report, then we can reuse the decoded queryId from earlier + fmt.Println("newest report from: ", validators[0].AccAddr, ": ", reportsRes) + require.Equal(len(reportsRes.MicroReports), 1) + require.Equal(reportsRes.MicroReports[0].QueryID, queryId1) - // query GetCurrentAggregateReport to get aggregate timestamp - res, _, err = e2e.QueryWithTimeout(ctx, validators[0].Node, "oracle", "get-current-aggregate-report", queryId1) - require.NoError(err) - err = json.Unmarshal(res, ¤tAggRes) - require.NoError(err) - prevTimestamp := timestamp timestamp = currentAggRes.Timestamp fmt.Println("timestamp: ", timestamp) fmt.Println("currentAggRes: ", currentAggRes) diff --git a/tests/integration/tbr_distribution_test.go b/tests/integration/tbr_distribution_test.go index 8ab4f0427..0be215ba8 100644 --- a/tests/integration/tbr_distribution_test.go +++ b/tests/integration/tbr_distribution_test.go @@ -664,9 +664,14 @@ func (s *IntegrationTestSuite) TestPerAggregatePowerShareNaturalFlow() { // Advance block height so any old queries are clearly expired ctx = ctx.WithBlockHeight(5) - // Reset cycle to start fresh: set sequencer to 2, run EndBlocker which wraps to 0 + cyclelist, err := s.Setup.Oraclekeeper.GetCyclelist(ctx) + s.NoError(err) + require.Equal(len(cyclelist), 3, "Need at least 3 queries in cyclelist") + + // Reset cycle to start fresh: put the live item on the last query with a + // matching sequencer, then EndBlocker wraps to q[0] and initializes QueryMeta. + s.NoError(s.Setup.Oraclekeeper.CurrentCycleListQuery.Set(ctx, cyclelist[2])) s.NoError(s.Setup.Oraclekeeper.CyclelistSequencer.Set(ctx, 2)) - // Run EndBlocker to properly rotate and set up Q1 with fresh expiration _, err = s.Setup.App.EndBlocker(ctx) s.NoError(err) @@ -695,11 +700,6 @@ func (s *IntegrationTestSuite) TestPerAggregatePowerShareNaturalFlow() { oracleMsgServer := keeper.NewMsgServerImpl(s.Setup.Oraclekeeper) - // Get cyclelist for comparison - cyclelist, err := s.Setup.Oraclekeeper.GetCyclelist(ctx) - s.NoError(err) - require.Equal(len(cyclelist), 3, "Need at least 3 queries in cyclelist") - // Track queries queryCount := 0 queriesSubmitted := make(map[string]bool) diff --git a/x/oracle/keeper/cycle_list.go b/x/oracle/keeper/cycle_list.go index 4a39000a3..773839fb7 100644 --- a/x/oracle/keeper/cycle_list.go +++ b/x/oracle/keeper/cycle_list.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "strconv" "github.com/tellor-io/layer/utils" @@ -53,6 +54,9 @@ func (k Keeper) RotateQueries(ctx context.Context) error { return err } max := len(q) + if max == 0 { + return fmt.Errorf("cycle list is empty") + } switch { case n >= uint64(max-1): // n could be gt if the cycle list is updated, otherwise n == max-1 @@ -71,6 +75,9 @@ func (k Keeper) RotateQueries(ctx context.Context) error { } // next query queryId = utils.QueryIDFromData(q[n]) + if err := k.CurrentCycleListQuery.Set(ctx, q[n]); err != nil { + return err + } // increment query opportunities for liveness tracking if err := k.IncrementQueryOpportunities(ctx, queryId); err != nil { @@ -159,17 +166,7 @@ func emitRotateQueriesEvent(sdkCtx sdk.Context, queryId, nextId string) { } func (k Keeper) GetCurrentQueryInCycleList(ctx context.Context) ([]byte, error) { - idx, err := k.CyclelistSequencer.Peek(ctx) - if err != nil { - return nil, err - } - - q, err := k.GetCyclelist(ctx) - if err != nil { - return nil, err - } - - return q[idx], nil + return k.CurrentCycleListQuery.Get(ctx) } func (k Keeper) GetNextCurrentQueryInCycleList(ctx context.Context) ([]byte, error) { @@ -209,5 +206,13 @@ func (k Keeper) GenesisCycleList(ctx context.Context, cyclelist [][]byte) error return err } } - return nil + q, err := k.GetCyclelist(ctx) + if err != nil { + return err + } + if len(q) == 0 { + return fmt.Errorf("cycle list is empty") + } + // Map iteration order (keyed by queryId), not the input slice order. + return k.CurrentCycleListQuery.Set(ctx, q[0]) } diff --git a/x/oracle/keeper/cycle_list_test.go b/x/oracle/keeper/cycle_list_test.go index bdc808f3e..0af0eaf16 100644 --- a/x/oracle/keeper/cycle_list_test.go +++ b/x/oracle/keeper/cycle_list_test.go @@ -5,8 +5,10 @@ import ( "github.com/tellor-io/layer/testutil/sample" "github.com/tellor-io/layer/utils" minttypes "github.com/tellor-io/layer/x/mint/types" + "github.com/tellor-io/layer/x/oracle/types" regtypes "github.com/tellor-io/layer/x/registry/types" + "cosmossdk.io/collections" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,8 +16,10 @@ import ( ) const ( - queryData = "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003747262000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000" - queryType = "SpotPrice" + queryData = "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000003747262000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000" + queryType = "SpotPrice" + maticQueryDataHex = "0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000056D6174696300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000" + ampleforthQueryDataHex = "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000019416d706c65666f727468437573746f6d53706f74507269636500000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" ) func (s *KeeperTestSuite) TestGetCycleList() { @@ -53,6 +57,7 @@ func (s *KeeperTestSuite) TestRotateQueries() { firstQuery, err := k.GetCurrentQueryInCycleList(s.ctx) require.NoError(err) + require.Equal(list[0], firstQuery) require.NoError(k.RotateQueries(s.ctx)) require.Contains(list, firstQuery) @@ -60,6 +65,9 @@ func (s *KeeperTestSuite) TestRotateQueries() { require.NoError(err) require.Contains(list, secondQuery) require.NotEqual(firstQuery, secondQuery) + idx, err := k.CyclelistSequencer.Peek(s.ctx) + require.NoError(err) + require.Equal(list[idx], secondQuery) require.NoError(k.RotateQueries(s.ctx)) thirdQuery, err := k.GetCurrentQueryInCycleList(s.ctx) @@ -67,11 +75,17 @@ func (s *KeeperTestSuite) TestRotateQueries() { require.Contains(list, thirdQuery) require.NotEqual(firstQuery, thirdQuery) require.NotEqual(secondQuery, thirdQuery) + idx, err = k.CyclelistSequencer.Peek(s.ctx) + require.NoError(err) + require.Equal(list[idx], thirdQuery) // Rotate through a couple times for i := 0; i < 10; i++ { query, err := s.oracleKeeper.GetCurrentQueryInCycleList(s.ctx) require.NoError(err) + idx, err := s.oracleKeeper.CyclelistSequencer.Peek(s.ctx) + require.NoError(err) + require.Equal(list[idx], query) err = s.oracleKeeper.RotateQueries(s.ctx) require.NoError(err) require.Contains(list, query) @@ -81,8 +95,18 @@ func (s *KeeperTestSuite) TestRotateQueries() { func (s *KeeperTestSuite) TestGetCurrentQueryInCycleList() { require := s.Require() k := s.oracleKeeper - _, err := k.GetCurrentQueryInCycleList(s.ctx) + + idx, err := k.CyclelistSequencer.Peek(s.ctx) + require.NoError(err) + require.Equal(uint64(0), idx) + + q, err := k.GetCyclelist(s.ctx) + require.NoError(err) + require.Greater(len(q), 0) + + current, err := k.GetCurrentQueryInCycleList(s.ctx) require.NoError(err) + require.Equal(q[0], current) } func (s *KeeperTestSuite) TestInitCycleListQuery() { @@ -148,3 +172,126 @@ func (s *KeeperTestSuite) TestGetNextCurrentQueryInCycleList() { require.NotNil(query) require.NotEqual(currentQuery, query) } + +func (s *KeeperTestSuite) mockLivenessAccounts() { + s.T().Helper() + add := sample.AccAddressBytes() + baseAccount := authtypes.NewBaseAccountWithAddress(add) + permissions := []string{authtypes.Minter, authtypes.Burner, authtypes.Staking} + testModuleAccount := authtypes.NewModuleAccount(baseAccount, "time_based_rewards", permissions...) + s.accountKeeper.On("GetModuleAccount", s.ctx, minttypes.TimeBasedRewards).Return(sdk.ModuleAccountI(testModuleAccount)) + s.bankKeeper.On("GetBalance", s.ctx, testModuleAccount.GetAddress(), "loya").Return(sdk.Coin{Amount: math.ZeroInt(), Denom: "loya"}) +} + +func (s *KeeperTestSuite) TestGetCurrentQueryInCycleList_ShrinkDoesNotPanic() { + require := s.Require() + k := s.oracleKeeper + ctx := s.ctx + + q, err := k.GetCyclelist(ctx) + require.NoError(err) + require.Len(q, 3) + live := q[1] + require.NoError(k.CurrentCycleListQuery.Set(ctx, live)) + require.NoError(k.CyclelistSequencer.Set(ctx, 1)) + + matic := hexutil.MustDecode(maticQueryDataHex) + require.NoError(k.Cyclelist.Clear(ctx, nil)) + require.NoError(k.InitCycleListQuery(ctx, [][]byte{matic})) + + current, err := k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + require.Equal(live, current) + + idx, err := k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(1), idx) + + s.mockLivenessAccounts() + s.registryKeeper.On("GetSpec", ctx, "SpotPrice").Return(regtypes.DataSpec{ReportBlockWindow: 2}, nil) + + require.NoError(k.RotateQueries(ctx)) + + idx, err = k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(0), idx) + + current, err = k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + newList, err := k.GetCyclelist(ctx) + require.NoError(err) + require.Len(newList, 1) + require.Equal(newList[0], current) +} + +func (s *KeeperTestSuite) TestRotateQueries_InRangeUpdateAdvancesFromIndex() { + require := s.Require() + k := s.oracleKeeper + ctx := s.ctx + + matic := hexutil.MustDecode(maticQueryDataHex) + ampleforth := hexutil.MustDecode(ampleforthQueryDataHex) + require.NoError(k.InitCycleListQuery(ctx, [][]byte{matic, ampleforth})) + + q, err := k.GetCyclelist(ctx) + require.NoError(err) + require.GreaterOrEqual(len(q), 5) + + live := q[2] + require.NoError(k.CurrentCycleListQuery.Set(ctx, live)) + require.NoError(k.CyclelistSequencer.Set(ctx, 2)) + + shrunk := q[:4] + require.NoError(k.Cyclelist.Clear(ctx, nil)) + require.NoError(k.InitCycleListQuery(ctx, shrunk)) + + idx, err := k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(2), idx) + + current, err := k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + require.Equal(live, current) + + newList, err := k.GetCyclelist(ctx) + require.NoError(err) + require.Len(newList, 4) + + queryId := utils.QueryIDFromData(live) + require.NoError(k.Query.Set(ctx, collections.Join(queryId, uint64(1)), types.QueryMeta{ + Id: 1, + Amount: math.ZeroInt(), + Expiration: uint64(ctx.BlockHeight()) + 10, + QueryData: live, + CycleList: true, + RegistrySpecBlockWindow: 2, + })) + require.NoError(k.RotateQueries(ctx)) + current, err = k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + require.Equal(live, current) + idx, err = k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(2), idx) + + require.NoError(k.Query.Set(ctx, collections.Join(queryId, uint64(1)), types.QueryMeta{ + Id: 1, + Amount: math.ZeroInt(), + Expiration: 0, + QueryData: live, + CycleList: true, + RegistrySpecBlockWindow: 2, + })) + s.registryKeeper.On("GetSpec", ctx, "SpotPrice").Return(regtypes.DataSpec{ReportBlockWindow: 2}, nil) + s.registryKeeper.On("GetSpec", ctx, "AmpleforthCustomSpotPrice").Return(regtypes.DataSpec{ReportBlockWindow: 2}, nil) + + require.NoError(k.RotateQueries(ctx)) + + idx, err = k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(3), idx) + + current, err = k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + require.Equal(newList[3], current) +} diff --git a/x/oracle/keeper/keeper.go b/x/oracle/keeper/keeper.go index 5440766ba..619bdf6fc 100644 --- a/x/oracle/keeper/keeper.go +++ b/x/oracle/keeper/keeper.go @@ -39,25 +39,26 @@ var ethUsdQueryId, _ = hex.DecodeString("83a7f3d48786ac2667503a61e8c415438ed2922 type ( Keeper struct { - cdc codec.BinaryCodec - storeService store.KVStoreService - Params collections.Item[types.Params] - accountKeeper types.AccountKeeper - bankKeeper types.BankKeeper - bridgeKeeper types.BridgeKeeper - registryKeeper types.RegistryKeeper - reporterKeeper types.ReporterKeeper - Schema collections.Schema - CyclelistSequencer collections.Sequence // key: queryId, tipper - TipperTotal collections.Map[collections.Pair[[]byte, uint64], math.Int] // key: tipperAcc, blockNumber - TotalTips collections.Map[uint64, math.Int] // key: blockNumber - Nonces collections.Map[[]byte, uint64] // key: queryId - Reports *collections.IndexedMap[collections.Triple[[]byte, []byte, uint64], types.MicroReport, types.ReportsIndex] // key: queryId, reporter, queryMeta.id - QuerySequencer collections.Sequence - Query *collections.IndexedMap[collections.Pair[[]byte, uint64], types.QueryMeta, types.QueryMetaIndex] // key: queryId, id - Aggregates *collections.IndexedMap[collections.Pair[[]byte, uint64], types.Aggregate, types.AggregatesIndex] // key: queryId, timestamp - Cyclelist collections.Map[[]byte, []byte] // key: queryId - QueryDataLimit collections.Item[types.QueryDataLimit] // query data bytes limit + cdc codec.BinaryCodec + storeService store.KVStoreService + Params collections.Item[types.Params] + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + bridgeKeeper types.BridgeKeeper + registryKeeper types.RegistryKeeper + reporterKeeper types.ReporterKeeper + Schema collections.Schema + CyclelistSequencer collections.Sequence // key: queryId, tipper + TipperTotal collections.Map[collections.Pair[[]byte, uint64], math.Int] // key: tipperAcc, blockNumber + TotalTips collections.Map[uint64, math.Int] // key: blockNumber + Nonces collections.Map[[]byte, uint64] // key: queryId + Reports *collections.IndexedMap[collections.Triple[[]byte, []byte, uint64], types.MicroReport, types.ReportsIndex] // key: queryId, reporter, queryMeta.id + QuerySequencer collections.Sequence + Query *collections.IndexedMap[collections.Pair[[]byte, uint64], types.QueryMeta, types.QueryMetaIndex] // key: queryId, id + Aggregates *collections.IndexedMap[collections.Pair[[]byte, uint64], types.Aggregate, types.AggregatesIndex] // key: queryId, timestamp + Cyclelist collections.Map[[]byte, []byte] // key: queryId + CurrentCycleListQuery collections.Item[[]byte] // query data of the live cycle-list query + QueryDataLimit collections.Item[types.QueryDataLimit] // query data bytes limit // the address capable of executing a MsgUpdateParams message. Typically, this // should be the x/gov module account. authority string @@ -162,6 +163,9 @@ func NewKeeper( ), // Cyclelist maps the queryId (hash of the query data) to the queryData for queries that are in the cycle list Cyclelist: collections.NewMap(sb, types.CyclelistPrefix, "cyclelist", collections.BytesKey, collections.BytesValue), + // CurrentCycleListQuery is the query data of the live cycle-list query. GetCurrentQueryInCycleList + // reads this item rather than indexing GetCyclelist() by CyclelistSequencer.Peek(). + CurrentCycleListQuery: collections.NewItem(sb, types.CurrentCycleListQueryPrefix, "current_cycle_list_query", collections.BytesValue), // CyclelistSequencer is an id generator for cycle list queries that increments when called until the max of len(cycleListQueries) is reached // then it resets. CyclelistSequencer: collections.NewSequence(sb, types.CycleSeqPrefix, "cycle_sequencer"), diff --git a/x/oracle/keeper/migrations.go b/x/oracle/keeper/migrations.go index 47e5d229f..c3e306bec 100644 --- a/x/oracle/keeper/migrations.go +++ b/x/oracle/keeper/migrations.go @@ -7,6 +7,7 @@ import ( "github.com/tellor-io/layer/x/oracle/migrations/fork" v4 "github.com/tellor-io/layer/x/oracle/migrations/v4" v5 "github.com/tellor-io/layer/x/oracle/migrations/v5" + v6 "github.com/tellor-io/layer/x/oracle/migrations/v6" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -49,3 +50,10 @@ func (m Migrator) Migrate3to4(ctx sdk.Context) error { func (m Migrator) Migrate4to5(ctx sdk.Context) error { return v5.MigrateStore(ctx, m.keeper.storeService) } + +// Migrate5to6 migrates from version 5 to 6. +// This backfills CurrentCycleListQuery from CyclelistSequencer.Peek() with a wrap +// when the sequencer is past the cycle-list length. +func (m Migrator) Migrate5to6(ctx sdk.Context) error { + return v6.MigrateStore(ctx, m.keeper.storeService) +} diff --git a/x/oracle/keeper/msg_update_cyclelist_test.go b/x/oracle/keeper/msg_update_cyclelist_test.go index b860353e3..5e13f9528 100644 --- a/x/oracle/keeper/msg_update_cyclelist_test.go +++ b/x/oracle/keeper/msg_update_cyclelist_test.go @@ -42,3 +42,34 @@ func (s *KeeperTestSuite) TestMsgUpdateCycleList() { _, err = s.msgServer.UpdateCyclelist(ctx, &req) require.ErrorContains(err, "cyclelist is empty") } + +func (s *KeeperTestSuite) TestMsgUpdateCycleListLeavesCurrentAndSequencer() { + require := s.Require() + ctx := s.ctx + k := s.oracleKeeper + regK := s.registryKeeper + + q, err := k.GetCyclelist(ctx) + require.NoError(err) + require.GreaterOrEqual(len(q), 2) + + live := q[1] + require.NoError(k.CurrentCycleListQuery.Set(ctx, live)) + require.NoError(k.CyclelistSequencer.Set(ctx, 1)) + + matic, _ := hex.DecodeString("00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000953706F745072696365000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000C00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000056D6174696300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000037573640000000000000000000000000000000000000000000000000000000000") + regK.On("GetSpec", ctx, "SpotPrice").Return(regtypes.DataSpec{}, nil) + _, err = s.msgServer.UpdateCyclelist(ctx, &types.MsgUpdateCyclelist{ + Authority: k.GetAuthority(), + Cyclelist: [][]byte{matic}, + }) + require.NoError(err) + + current, err := k.GetCurrentQueryInCycleList(ctx) + require.NoError(err) + require.Equal(live, current) + + idx, err := k.CyclelistSequencer.Peek(ctx) + require.NoError(err) + require.Equal(uint64(1), idx) +} diff --git a/x/oracle/migrations/v6/store.go b/x/oracle/migrations/v6/store.go new file mode 100644 index 000000000..147a960a4 --- /dev/null +++ b/x/oracle/migrations/v6/store.go @@ -0,0 +1,48 @@ +package v6 + +import ( + "context" + "fmt" + + "github.com/tellor-io/layer/x/oracle/types" + + "cosmossdk.io/collections" + "cosmossdk.io/core/store" +) + +// MigrateStore backfills CurrentCycleListQuery from the pre-v6 getter: +// GetCyclelist()[CyclelistSequencer.Peek()], wrapping to q[0] when Peek is OOB. +// The sequencer is left unchanged. Does not walk the Query store. +func MigrateStore(ctx context.Context, storeService store.KVStoreService) error { + sb := collections.NewSchemaBuilder(storeService) + cyclelist := collections.NewMap(sb, types.CyclelistPrefix, "cyclelist", collections.BytesKey, collections.BytesValue) + cyclelistSequencer := collections.NewSequence(sb, types.CycleSeqPrefix, "cycle_sequencer") + current := collections.NewItem(sb, types.CurrentCycleListQueryPrefix, "current_cycle_list_query", collections.BytesValue) + + idx, err := cyclelistSequencer.Peek(ctx) + if err != nil { + return err + } + + iter, err := cyclelist.Iterate(ctx, nil) + if err != nil { + return err + } + q, err := iter.Values() + if err != nil { + iter.Close() + return err + } + if err := iter.Close(); err != nil { + return err + } + if len(q) == 0 { + return fmt.Errorf("cycle list is empty") + } + + data := q[0] + if idx < uint64(len(q)) { + data = q[idx] + } + return current.Set(ctx, data) +} diff --git a/x/oracle/migrations/v6/store_test.go b/x/oracle/migrations/v6/store_test.go new file mode 100644 index 000000000..3bf5d0ef0 --- /dev/null +++ b/x/oracle/migrations/v6/store_test.go @@ -0,0 +1,78 @@ +package v6_test + +import ( + "testing" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + cosmosdb "github.com/cosmos/cosmos-db" + "github.com/stretchr/testify/require" + v6 "github.com/tellor-io/layer/x/oracle/migrations/v6" + "github.com/tellor-io/layer/x/oracle/types" + + "cosmossdk.io/collections" + "cosmossdk.io/core/store" + "cosmossdk.io/log" + sdkStore "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/runtime" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func setupMigrateStore(t *testing.T) (sdk.Context, store.KVStoreService, collections.Map[[]byte, []byte], collections.Sequence, collections.Item[[]byte]) { + t.Helper() + storeKey := storetypes.NewKVStoreKey(types.StoreKey) + db := cosmosdb.NewMemDB() + stateStore := sdkStore.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) + require.NoError(t, stateStore.LoadLatestVersion()) + + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + storeService := runtime.NewKVStoreService(storeKey) + + sb := collections.NewSchemaBuilder(storeService) + cyclelist := collections.NewMap(sb, types.CyclelistPrefix, "cyclelist", collections.BytesKey, collections.BytesValue) + sequencer := collections.NewSequence(sb, types.CycleSeqPrefix, "cycle_sequencer") + current := collections.NewItem(sb, types.CurrentCycleListQueryPrefix, "current_cycle_list_query", collections.BytesValue) + _, err := sb.Build() + require.NoError(t, err) + return ctx, storeService, cyclelist, sequencer, current +} + +func TestMigrateStore_InBoundsIdxCopiesQAtIdx(t *testing.T) { + ctx, storeService, cyclelist, sequencer, current := setupMigrateStore(t) + + require.NoError(t, cyclelist.Set(ctx, []byte{0}, []byte("q0"))) + require.NoError(t, cyclelist.Set(ctx, []byte{1}, []byte("q1"))) + require.NoError(t, cyclelist.Set(ctx, []byte{2}, []byte("q2"))) + require.NoError(t, sequencer.Set(ctx, 1)) + + require.NoError(t, v6.MigrateStore(ctx, storeService)) + + got, err := current.Get(ctx) + require.NoError(t, err) + require.Equal(t, []byte("q1"), got) + + idx, err := sequencer.Peek(ctx) + require.NoError(t, err) + require.Equal(t, uint64(1), idx) +} + +func TestMigrateStore_OOBIdxCopiesQ0(t *testing.T) { + ctx, storeService, cyclelist, sequencer, current := setupMigrateStore(t) + + require.NoError(t, cyclelist.Set(ctx, []byte{0}, []byte("q0"))) + require.NoError(t, cyclelist.Set(ctx, []byte{1}, []byte("q1"))) + require.NoError(t, sequencer.Set(ctx, 99)) + + require.NoError(t, v6.MigrateStore(ctx, storeService)) + + got, err := current.Get(ctx) + require.NoError(t, err) + require.Equal(t, []byte("q0"), got) + + idx, err := sequencer.Peek(ctx) + require.NoError(t, err) + require.Equal(t, uint64(99), idx) +} diff --git a/x/oracle/module.go b/x/oracle/module.go index e880219fe..1d32d5d5e 100644 --- a/x/oracle/module.go +++ b/x/oracle/module.go @@ -125,6 +125,9 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { if err := cfg.RegisterMigration(types.ModuleName, 4, m.Migrate4to5); err != nil { panic(fmt.Sprintf("failed to migrate x/%s from version 4 to 5: %v", types.ModuleName, err)) } + if err := cfg.RegisterMigration(types.ModuleName, 5, m.Migrate5to6); err != nil { + panic(fmt.Sprintf("failed to migrate x/%s from version 5 to 6: %v", types.ModuleName, err)) + } } // RegisterInvariants registers the invariants of the module. If an invariant deviates from its predicted value, the InvariantRegistry triggers appropriate logic (most often the chain will be halted) @@ -148,7 +151,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // ConsensusVersion is a sequence number for state-breaking change of the module. It should be incremented on each consensus-breaking change introduced by the module. To avoid wrong/empty versions, the initial version should be set to 1 -func (AppModule) ConsensusVersion() uint64 { return 5 } +func (AppModule) ConsensusVersion() uint64 { return 6 } // BeginBlock contains the logic that is automatically triggered at the beginning of each block func (am AppModule) BeginBlock(ctx context.Context) error { diff --git a/x/oracle/types/keys.go b/x/oracle/types/keys.go index d9b6ef925..8e4be7721 100644 --- a/x/oracle/types/keys.go +++ b/x/oracle/types/keys.go @@ -76,6 +76,8 @@ var ( // MaxOpenCommitmentByReporter maps reporter address -> monotonic max query // Expiration height from reports submitted by that reporter (updated on SubmitValue). MaxOpenCommitmentByReporterPrefix = collections.NewPrefix(46) + // CurrentCycleListQueryPrefix stores the query data of the live cycle-list query. + CurrentCycleListQueryPrefix = collections.NewPrefix(47) ) func KeyPrefix(p string) []byte {