Skip to content

persist per-topic NonceV2 epoch sequence (ENGN-8910) - #970

Open
zale144 wants to merge 2 commits into
feat/scheduler-epoch-fsmfrom
alek/engn-8910-epoch-nonce-format-per-topic-sequence-persistence
Open

persist per-topic NonceV2 epoch sequence (ENGN-8910)#970
zale144 wants to merge 2 commits into
feat/scheduler-epoch-fsmfrom
alek/engn-8910-epoch-nonce-format-per-topic-sequence-persistence

Conversation

@zale144

@zale144 zale144 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a per-topic topicLastEpochNonce store and AllocateNextEpochNonce so each StartEpoch gets a unique NonceV2 (Bech32 deferred).
  • Round-trip the last-nonce map through genesis (topic_last_epoch_nonces).
  • Cover monotonic allocation and export/import with unit tests.

Test plan

  • go test ./x/emissions/keeper/ -run 'TestKeeperTestSuite/TestAllocateNextEpochNonceMonotonicPerTopic|TestKeeperTestSuite/TestTopicLastEpochNonceGenesisRoundTrip'
  • CI green on this PR

Linear: ENGN-8910

Made with Cursor


Summary by cubic

Persists per-topic types.NonceV2 epoch sequences and makes StartEpoch allocate the next per-topic nonce; sequences survive restarts and export/import. Keeps types.NonceV2, defers Bech32. Linear: ENGN-8910.

  • Adds topicLastEpochNonce map (TopicLastEpochNonceKey=112) with GetTopicLastEpochNonce and AllocateNextEpochNonce; first allocation is ZeroNonce().NextNonce() (V1, payload 1).
  • Wires StartEpoch to AllocateNextEpochNonce so every epoch is unique per topic.
  • Genesis: new topic_last_epoch_nonces (types.TopicIdAndEpochNonce) with Init/Export; NewGenesisState initializes it empty.
  • Tests: per-topic monotonic allocation and genesis round-trip in x/emissions/keeper/epoch_nonce_test.go.
  • Lint/format: reformat v10 protos; update x/emissions/proto/buf.yaml rules for EpochState; fix golangci issues (explicit FSM Advance asserts, NonceV2.Version bitmask with //nolint:gosec, add context in tests, minor static checks).

Written for commit 35530f6. Summary will update on new commits.

Review in cubic

Wire AllocateNextEpochNonce into StartEpoch and round-trip the last-nonce
map through genesis so epochs stay unique across restarts.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf Linter / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed⏩ skippedAug 20, 2026, 6:26 PM

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 10 files

Not reviewed (too large): x/emissions/api/emissions/v10/genesis.pulsar.go (~2,188 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="x/emissions/keeper/epoch.go">

<violation number="1" location="x/emissions/keeper/epoch.go:103">
P2: Starting an epoch panics once the persisted sequence reaches the uint56 limit, instead of returning a recoverable allocation error. Guard the maximum payload before calling `NextNonce` so an exhausted sequence cannot halt this execution path.</violation>
</file>

Linked issue analysis

Linked issue: ENGN-8910: Epoch nonce format + per-topic sequence persistence

Status Acceptance criteria Notes
Add per-topic sequence store for last allocated epoch NonceV2 and persist it through genesis/export-import The keeper now includes topicLastEpochNonce map, genesis init/export iterate and set logic, and the genesis proto/types were extended to carry TopicLastEpochNonces.
Wire per-topic sequence into StartEpoch so every new epoch gets a unique NonceV2 per topic StartEpoch now calls AllocateNextEpochNonce and no longer uses a zero nonce placeholder; AllocateNextEpochNonce computes NextNonce() and persists it as the topic's last nonce.
Unit tests cover monotonic per-topic allocation and genesis round-trip (sequence survives restart/export-import) Added tests exercise monotonic allocation across topics and verify that exported genesis carries last-nonce rows which are initialized by InitGenesis and produce the same next allocations after import.
Architecture diagram
sequenceDiagram
    participant Trigger as Epoch Trigger (BeginBlocker/Msg)
    participant Keeper as Keeper
    participant Store as topicLastEpochNonce Store
    participant GenesisExporter as Genesis Export
    participant GenesisImporter as Genesis Import

    Note over Trigger,Store: NEW: Per-topic epoch nonce allocation

    Trigger->>Keeper: StartEpoch(topicID)
    Keeper->>Keeper: AllocateNextEpochNonce(topicID)

    Keeper->>Store: Get(topicID)
    alt First allocation for topic
        Store-->>Keeper: not found (ErrNotFound)
        Keeper->>Keeper: lastNonce = ZeroNonce()
    else Subsequent allocation
        Store-->>Keeper: lastNonce
    end

    Keeper->>Keeper: nextNonce = lastNonce.NextNonce()
    Keeper->>Store: Set(topicID, nextNonce)
    Store-->>Keeper: ok
    Keeper->>Keeper: Create epoch with nextNonce
    Keeper-->>Trigger: epoch started

    Note over Trigger,Store: Monotonic per topic: NonceV2 version V1, payload increments

    Note over GenesisExporter,GenesisImporter: NEW: Genesis round-trip for nonces

    GenesisExporter->>Keeper: ExportGenesis(ctx)
    Keeper->>Store: Iterate()
    Store-->>Keeper: topicID → NonceV2 pairs
    Keeper->>Keeper: Build TopicLastEpochNonces slice
    Keeper-->>GenesisExporter: genesis data with nonces

    GenesisImporter->>Keeper: InitGenesis(ctx, data)
    Keeper->>Keeper: Iterate TopicLastEpochNonces
    loop each entry
        Keeper->>Store: Set(topicID, nonce)
        Store-->>Keeper: ok
    end
    Keeper-->>GenesisImporter: genesis initialized
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

lastNonce = types.ZeroNonce()
}

nextNonce := lastNonce.NextNonce()

@cubic-dev-ai cubic-dev-ai Bot Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Starting an epoch panics once the persisted sequence reaches the uint56 limit, instead of returning a recoverable allocation error. Guard the maximum payload before calling NextNonce so an exhausted sequence cannot halt this execution path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At x/emissions/keeper/epoch.go, line 103:

<comment>Starting an epoch panics once the persisted sequence reaches the uint56 limit, instead of returning a recoverable allocation error. Guard the maximum payload before calling `NextNonce` so an exhausted sequence cannot halt this execution path.</comment>

<file context>
@@ -75,6 +76,37 @@ func (k *Keeper) setupEpochFSMEngine() {
+		lastNonce = types.ZeroNonce()
+	}
+
+	nextNonce := lastNonce.NextNonce()
+	if err := k.topicLastEpochNonce.Set(ctx, topicID, nextNonce); err != nil {
+		return types.NonceV2(0), err
</file context>
Suggested change
nextNonce := lastNonce.NextNonce()
if lastNonce.Payload() == (uint64(1)<<56)-1 {
return types.NonceV2(0), errors.New("epoch nonce sequence exhausted")
}
nextNonce := lastNonce.NextNonce()
Fix with cubic

Format v10 protos, ignore EpochState prefix rules (INIT is a real zero
state), and clear gosec/exhaustruct/forcetypeassert/ineffassign/staticcheck
findings so CI lint can pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant