Skip to content

Commit bcaf737

Browse files
mergify[bot]Alex | Interchain Labs
andauthored
chore: audit epoch (backport #24610) (#24613)
Co-authored-by: Alex | Interchain Labs <[email protected]>
1 parent 81b5b75 commit bcaf737

File tree

6 files changed

+129
-16
lines changed

6 files changed

+129
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
109109
* (x/auth) [#23741](https://github.com/cosmos/cosmos-sdk/pull/23741) Support legacy global AccountNumber for legacy compatibility.
110110
* (baseapp) [#24526](https://github.com/cosmos/cosmos-sdk/pull/24526) Fix incorrect retention height when `commitHeight` equals `minRetainBlocks`.
111111
* (x/protocolpool) [#24594](https://github.com/cosmos/cosmos-sdk/pull/24594) Fix NPE when initializing module via depinject.
112+
* (x/epochs) [#24610](https://github.com/cosmos/cosmos-sdk/pull/24610) Fix semantics of `CurrentEpochStartHeight` being set before epoch has started.
112113

113114
## [v0.50.13](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.50.13) - 2025-03-12
114115

x/epochs/keeper/abci.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,27 @@ func (k *Keeper) BeginBlocker(ctx sdk.Context) error {
1313
start := telemetry.Now()
1414
defer telemetry.ModuleMeasureSince(types.ModuleName, start, telemetry.MetricKeyBeginBlocker)
1515

16-
header := ctx.BlockHeader()
16+
blockTime := ctx.BlockTime()
17+
blockHeight := ctx.BlockHeight()
18+
1719
err := k.EpochInfo.Walk(
1820
ctx,
1921
nil,
2022
func(key string, epochInfo types.EpochInfo) (stop bool, err error) {
2123
// If blocktime < initial epoch start time, return
22-
if header.Time.Before(epochInfo.StartTime) {
24+
if blockTime.Before(epochInfo.StartTime) {
2325
return false, nil
2426
}
2527
// if epoch counting hasn't started, signal we need to start.
2628
shouldInitialEpochStart := !epochInfo.EpochCountingStarted
2729

2830
epochEndTime := epochInfo.CurrentEpochStartTime.Add(epochInfo.Duration)
29-
shouldEpochStart := (header.Time.After(epochEndTime)) || shouldInitialEpochStart
31+
shouldEpochStart := (blockTime.After(epochEndTime)) || shouldInitialEpochStart
3032

3133
if !shouldEpochStart {
3234
return false, nil
3335
}
34-
epochInfo.CurrentEpochStartHeight = header.Height
36+
epochInfo.CurrentEpochStartHeight = blockHeight
3537

3638
if shouldInitialEpochStart {
3739
epochInfo.EpochCountingStarted = true

x/epochs/keeper/abci_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import (
1515
// of their initial conditions, and subsequent block height / times.
1616
func (suite *KeeperTestSuite) TestEpochInfoBeginBlockChanges() {
1717
block1Time := time.Unix(1656907200, 0).UTC()
18-
const defaultIdentifier = "hourly"
19-
const defaultDuration = time.Hour
20-
// eps is short for epsilon - in this case a negligible amount of time.
21-
const eps = time.Nanosecond
18+
const (
19+
defaultIdentifier = "hourly"
20+
defaultDuration = time.Hour
21+
// eps is short for epsilon - in this case a negligible amount of time.
22+
eps = time.Nanosecond
23+
)
2224

2325
tests := map[string]struct {
2426
// if identifier, duration is not set, we make it defaultIdentifier and defaultDuration.
@@ -69,8 +71,8 @@ func (suite *KeeperTestSuite) TestEpochInfoBeginBlockChanges() {
6971
},
7072
"StartTime in future won't get ticked on first block": {
7173
initialEpochInfo: types.EpochInfo{StartTime: block1Time.Add(time.Second), CurrentEpoch: 0, CurrentEpochStartTime: time.Time{}},
72-
// currentEpochStartHeight is 1 because that's when the timer was created on-chain
73-
expEpochInfo: types.EpochInfo{StartTime: block1Time.Add(time.Second), CurrentEpoch: 0, CurrentEpochStartTime: time.Time{}, CurrentEpochStartHeight: 1},
74+
// currentEpochStartHeight is 0 since it hasn't started or been triggered
75+
expEpochInfo: types.EpochInfo{StartTime: block1Time.Add(time.Second), CurrentEpoch: 0, CurrentEpochStartTime: time.Time{}, CurrentEpochStartHeight: 0},
7476
},
7577
"StartTime in past will get ticked on first block": {
7678
initialEpochInfo: types.EpochInfo{StartTime: block1Time.Add(-time.Second), CurrentEpoch: 0, CurrentEpochStartTime: time.Time{}},

x/epochs/keeper/epoch.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ func (k *Keeper) AddEpochInfo(ctx sdk.Context, epoch types.EpochInfo) error {
3333
if epoch.StartTime.IsZero() {
3434
epoch.StartTime = ctx.BlockTime()
3535
}
36-
if epoch.CurrentEpochStartHeight == 0 {
36+
if epoch.CurrentEpochStartHeight == 0 && !epoch.StartTime.After(ctx.BlockTime()) {
3737
epoch.CurrentEpochStartHeight = ctx.BlockHeight()
3838
}
3939
return k.EpochInfo.Set(ctx, epoch.Identifier, epoch)
4040
}
4141

4242
// AllEpochInfos iterate through epochs to return all epochs info.
4343
func (k *Keeper) AllEpochInfos(ctx sdk.Context) ([]types.EpochInfo, error) {
44-
epochs := []types.EpochInfo{}
44+
var epochs []types.EpochInfo
4545
err := k.EpochInfo.Walk(
4646
ctx,
4747
nil,
@@ -62,5 +62,9 @@ func (k *Keeper) NumBlocksSinceEpochStart(ctx sdk.Context, identifier string) (i
6262
if err != nil {
6363
return 0, fmt.Errorf("epoch with identifier %s not found", identifier)
6464
}
65+
if ctx.BlockTime().Before(epoch.StartTime) {
66+
return 0, fmt.Errorf("epoch with identifier %s has not started yet: start time: %s", identifier, epoch.StartTime)
67+
}
68+
6569
return ctx.BlockHeight() - epoch.CurrentEpochStartHeight, nil
6670
}

x/epochs/keeper/epoch_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,27 @@ func (s *KeeperTestSuite) TestAddEpochInfo() {
4949
},
5050
expErr: true,
5151
},
52+
"start in future": {
53+
addedEpochInfo: types.EpochInfo{
54+
Identifier: defaultIdentifier,
55+
StartTime: startBlockTime.Add(time.Hour),
56+
Duration: defaultDuration,
57+
CurrentEpoch: 0,
58+
CurrentEpochStartHeight: 0,
59+
CurrentEpochStartTime: time.Time{},
60+
EpochCountingStarted: false,
61+
},
62+
expEpochInfo: types.EpochInfo{
63+
Identifier: defaultIdentifier,
64+
StartTime: startBlockTime.Add(time.Hour),
65+
Duration: defaultDuration,
66+
CurrentEpoch: 0,
67+
CurrentEpochStartHeight: 0,
68+
CurrentEpochStartTime: time.Time{},
69+
EpochCountingStarted: false,
70+
},
71+
expErr: false,
72+
},
5273
}
5374
for name, test := range tests {
5475
s.Run(name, func() {
@@ -99,3 +120,89 @@ func (s *KeeperTestSuite) TestEpochLifeCycle() {
99120
s.Require().Equal(allEpochs[3].Identifier, "monthly")
100121
s.Require().Equal(allEpochs[4].Identifier, "week")
101122
}
123+
124+
func (s *KeeperTestSuite) TestNumBlocksSinceEpochStart() {
125+
s.SetupTest()
126+
127+
startBlockHeight := int64(100)
128+
startBlockTime := time.Unix(1656907200, 0).UTC()
129+
duration := time.Hour
130+
131+
s.Ctx = s.Ctx.WithBlockHeight(startBlockHeight).WithBlockTime(startBlockTime)
132+
133+
tests := map[string]struct {
134+
setupEpoch types.EpochInfo
135+
advanceBlockDelta int64
136+
advanceTimeDelta time.Duration
137+
expErr bool
138+
expBlocksSince int64
139+
}{
140+
"same block as start": {
141+
setupEpoch: types.EpochInfo{
142+
Identifier: "epoch_same_block",
143+
StartTime: startBlockTime,
144+
Duration: duration,
145+
CurrentEpoch: 0,
146+
CurrentEpochStartHeight: startBlockHeight,
147+
CurrentEpochStartTime: startBlockTime,
148+
EpochCountingStarted: true,
149+
},
150+
advanceBlockDelta: 0,
151+
advanceTimeDelta: 0,
152+
expErr: false,
153+
expBlocksSince: 0,
154+
},
155+
"after 5 blocks": {
156+
setupEpoch: types.EpochInfo{
157+
Identifier: "epoch_after_five",
158+
StartTime: startBlockTime,
159+
Duration: duration,
160+
CurrentEpoch: 0,
161+
CurrentEpochStartHeight: startBlockHeight,
162+
CurrentEpochStartTime: startBlockTime,
163+
EpochCountingStarted: true,
164+
},
165+
advanceBlockDelta: 5,
166+
advanceTimeDelta: time.Minute * 5, // just to simulate realistic advancement
167+
expErr: false,
168+
expBlocksSince: 5,
169+
},
170+
"epoch not started yet": {
171+
setupEpoch: types.EpochInfo{
172+
Identifier: "epoch_future",
173+
StartTime: startBlockTime.Add(time.Hour),
174+
Duration: duration,
175+
CurrentEpoch: 0,
176+
CurrentEpochStartHeight: 0,
177+
CurrentEpochStartTime: time.Time{},
178+
EpochCountingStarted: false,
179+
},
180+
advanceBlockDelta: 0,
181+
advanceTimeDelta: 0,
182+
expErr: true,
183+
expBlocksSince: 0,
184+
},
185+
}
186+
187+
for name, tc := range tests {
188+
s.Run(name, func() {
189+
s.SetupTest()
190+
s.Ctx = s.Ctx.WithBlockHeight(startBlockHeight).WithBlockTime(startBlockTime)
191+
192+
err := s.EpochsKeeper.AddEpochInfo(s.Ctx, tc.setupEpoch)
193+
s.Require().NoError(err)
194+
195+
// Advance block height and time if needed
196+
s.Ctx = s.Ctx.WithBlockHeight(startBlockHeight + tc.advanceBlockDelta).
197+
WithBlockTime(startBlockTime.Add(tc.advanceTimeDelta))
198+
199+
blocksSince, err := s.EpochsKeeper.NumBlocksSinceEpochStart(s.Ctx, tc.setupEpoch.Identifier)
200+
if tc.expErr {
201+
s.Require().Error(err)
202+
} else {
203+
s.Require().NoError(err)
204+
s.Require().Equal(tc.expBlocksSince, blocksSince)
205+
}
206+
})
207+
}
208+
}

x/epochs/types/genesis.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@ import (
55
"time"
66
)
77

8-
// DefaultIndex is the default capability global index.
9-
const DefaultIndex uint64 = 1
10-
118
func NewGenesisState(epochs []EpochInfo) *GenesisState {
129
return &GenesisState{Epochs: epochs}
1310
}
1411

15-
// DefaultGenesis returns the default Capability genesis state.
12+
// DefaultGenesis returns the default Epochs genesis state.
1613
func DefaultGenesis() *GenesisState {
1714
epochs := []EpochInfo{
1815
NewGenesisEpochInfo("day", time.Hour*24), // alphabetical order

0 commit comments

Comments
 (0)