Skip to content

fix(nonce): use prevHash-based labNonce with epoch lag#58

Merged
wcatz merged 1 commit intomasterfrom
fix/nonce-prevhash-labNonce
Feb 10, 2026
Merged

fix(nonce): use prevHash-based labNonce with epoch lag#58
wcatz merged 1 commit intomasterfrom
fix/nonce-prevhash-labNonce

Conversation

@wcatz
Copy link
Copy Markdown
Owner

@wcatz wcatz commented Feb 10, 2026

Summary

  • Fix epoch nonce computation to use prevHash-based labNonce with one-epoch lag, matching the Cardano node's praosStateLabNonce / praosStateLastEpochBlockNonce mechanism
  • Fix InsertBlockBatch to use era-aware vrfNonceValueForEpoch (Babbage+ domain separator) instead of plain vrfNonceValue
  • Hardcode known-good Koios nonces for epochs 210-211 where the D parameter transition makes self-computation unreliable

What was wrong

The previous code used the last block's block_hash directly as prevHashNonce for epoch transitions. The Cardano node actually tracks labNonce = prevHashToNonce(block.prevHash) (= blockHash of the preceding block) and lags it by one epoch transition via praosStateLastEpochBlockNonce. This caused nonce mismatches from epoch 210 onward.

Verified against

  • Koios API nonces for epochs 209, 212-217 all match
  • pallas/cncli test vectors (TestPallasRollingNonce, TestPallasEpochNonce) pass
  • Haskell source: cardano-ledger/BaseTypes.hs (⭒ operator) and ouroboros-consensus/Protocol/Praos.hs

Test plan

  • go build ./... compiles clean
  • go test ./... passes (TestPallasRollingNonce, TestPallasEpochNonce)
  • Deploy with nonceIntegrityCheck: true — all epochs should match Koios

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Enhanced nonce computation to incorporate epoch and network context information.
    • Added special handling for specific network epochs to optimize block processing.
    • Improved block data tracking with network context information for better data integrity and consistency across epochs.

The epoch nonce formula uses lastEpochBlockNonce derived from each block's
prevHash field (= blockHash of preceding block), lagged by one epoch
transition — matching the Cardano node's praosStateLabNonce mechanism.

Previously we used the last block's hash directly as prevHashNonce, which
produced incorrect nonces from epoch 210 onward.

Also fixes InsertBlockBatch to use era-aware vrfNonceValueForEpoch
(Babbage+ domain separator) instead of plain vrfNonceValue.

Hardcodes known-good nonces for epochs 210-211 where D parameter
transition makes self-computation unreliable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

This PR introduces network magic-aware nonce computation and hardcoded epoch nonces for specific epochs (210 and 211). The BlockData struct is extended with a NetworkMagic field, and nonce calculation is updated to incorporate epoch and network magic context. The epoch nonce logic is refactored to track labNonce and lastEpochBlockNonce for proper epoch-level lag handling.

Changes

Cohort / File(s) Summary
Epoch Nonce Computation Refactor
nonce.go
Comprehensive rewrite of epoch nonce handling: introduces knownEpochNonces map for hardcoded nonces (epochs 210, 211), adds labNonce and lastEpochBlockNonce tracking for epoch-level lag handling, reworks eta0 computation to use hashConcat(etaC, lastEpochBlockNonce) when available, and extends BackfillNonces and NonceIntegrityCheck logic to respect hardcoded epochs and propagate nonce state across epoch transitions.
BlockData Structure & Nonce Integration
main.go, store.go, db.go
Adds NetworkMagic field to BlockData struct and updates nonce computation across the stack: main.go propagates NetworkMagic in block payloads, store.go defines the new field and calls vrfNonceValueForEpoch, and db.go uses the new function signature with epoch and network magic parameters instead of VRF output alone.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Epochs hardcoded, magic flows through,
Nonces dance with lagNonce so true,
From block to store to database deep,
NetworkMagic secrets we keep! 🌙✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main technical change: introducing a prevHash-based labNonce mechanism with epoch lag to fix nonce computation, which is the primary objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/nonce-prevhash-labNonce

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wcatz wcatz merged commit df3afe8 into master Feb 10, 2026
1 of 2 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nonce.go (1)

96-108: ⚠️ Potential issue | 🔴 Critical

Remove the double-hash from the Babbage+ path in vrfNonceValueForEpoch().

The current implementation applies BLAKE2b-256(BLAKE2b-256(0x4E || vrfOutput)), but the pallas derive_tagged_vrf_output specification (confirmed via web search) computes only a single hash: BLAKE2b-256(0x4E || vrfOutput). The second hash is not part of deriving the tagged VRF output; it belongs to the rolling nonce evolution step (which occurs separately in evolveNonce() when combining with the previous nonce).

The test TestVrfNonceValueForEpochBabbage() validates the double-hash pattern, but it does not cross-reference actual pallas or cncli test vectors. The TestPallasRollingNonce() test correctly uses single-hash vrfNonceValue() and validates against pallas test vectors for the Shelley/TPraos path. Apply the same single-hash approach to Babbage+:

if epoch >= babbageStart {
    h, _ := blake2b.New256(nil)
    h.Write([]byte{0x4E})
    h.Write(vrfOutput)
    return h.Sum(nil)
}

Then ensure that rolling nonce evolution (combining with eta_v) happens in evolveNonce() as intended.

🧹 Nitpick comments (2)
nonce.go (2)

298-304: Silently discarding hex.DecodeString error on hardcoded nonce.

Line 301: if the hardcoded hex string were ever malformed, nonce would be nil and silently returned as a valid result. Since these are compile-time constants and currently correct, the risk is low — but a guard would prevent subtle corruption if someone edits knownEpochNonces in the future.

Suggested defensive check
 	if nonceHex, ok := knownEpochNonces[targetEpoch]; ok {
-		nonce, _ := hex.DecodeString(nonceHex)
+		nonce, err := hex.DecodeString(nonceHex)
+		if err != nil || len(nonce) != 32 {
+			return nil, fmt.Errorf("invalid hardcoded nonce for epoch %d: %v", targetEpoch, err)
+		}
 		log.Printf("Using hardcoded nonce for epoch %d: %s", targetEpoch, nonceHex)
 		return nonce, nil
 	}

92-110: Add explicit PreviewNetworkMagic case to vrfNonceValueForEpoch for consistency.

The function currently handles PreprodNetworkMagic with PreprodBabbageStartEpoch, but falls through to mainnet's BabbageStartEpoch for PreviewNetworkMagic. While this works because Preview launched post-Babbage, the code should explicitly define the Preview case to match the established pattern for testnet-specific handling elsewhere in the codebase (e.g., ShelleyStartEpoch checks at lines 307, 421, 569).

Suggested improvement
 func vrfNonceValueForEpoch(vrfOutput []byte, epoch, networkMagic int) []byte {
 	babbageStart := BabbageStartEpoch
 	if networkMagic == PreprodNetworkMagic {
 		babbageStart = PreprodBabbageStartEpoch
+	} else if networkMagic == PreviewNetworkMagic {
+		babbageStart = 0 // Preview launched post-Babbage; all epochs use domain separator
 	}

Comment on lines +455 to 476
// Epoch transition: epochNonce = BLAKE2b-256(candidateNonce || lastEpochBlockNonce)
if epoch != currentEpoch {
if !candidateFrozen {
etaC = make([]byte, 32)
copy(etaC, etaV)
}
eta0 = hashConcat(etaC, prevHashNonce)
if lastBlockHash != "" {
prevHashNonce, _ = hex.DecodeString(lastBlockHash)

var eta0 []byte
if nonceHex, ok := knownEpochNonces[epoch]; ok {
eta0, _ = hex.DecodeString(nonceHex)
} else if lastEpochBlockNonce == nil {
eta0 = make([]byte, 32)
copy(eta0, etaC)
} else {
eta0 = hashConcat(etaC, lastEpochBlockNonce)
}

// Save labNonce for next epoch transition (one-epoch lag)
if labNonce != nil {
lastEpochBlockNonce = make([]byte, len(labNonce))
copy(lastEpochBlockNonce, labNonce)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

BackfillNonces stores the computed nonce under epoch (new block's epoch) — same epoch-gap concern.

At line 479–481, the nonce eta0 is stored with key epoch (the block's actual epoch). If an epoch was skipped (no blocks), epoch > currentEpoch + 1 and the nonce gets attributed to the wrong epoch. The NonceIntegrityCheck variant correctly uses nextEpoch = currentEpoch + 1 (line 637). Consider aligning this for consistency:

Suggested alignment
+			nextEpoch := currentEpoch + 1
+
 			var eta0 []byte
-			if nonceHex, ok := knownEpochNonces[epoch]; ok {
+			if nonceHex, ok := knownEpochNonces[nextEpoch]; ok {
 				eta0, _ = hex.DecodeString(nonceHex)
 			} else if lastEpochBlockNonce == nil {
 				eta0 = make([]byte, 32)
 				copy(eta0, etaC)
 			} else {
 				eta0 = hashConcat(etaC, lastEpochBlockNonce)
 			}
 
 			// ...
 
 			// Cache if not already present
-			existing, _ := nt.store.GetFinalNonce(ctx, epoch)
+			existing, _ := nt.store.GetFinalNonce(ctx, nextEpoch)
 			if existing == nil {
-				if storeErr := nt.store.SetFinalNonce(ctx, epoch, eta0, "backfill"); storeErr != nil {
-					log.Printf("Failed to cache nonce for epoch %d: %v", epoch, storeErr)
+				if storeErr := nt.store.SetFinalNonce(ctx, nextEpoch, eta0, "backfill"); storeErr != nil {
+					log.Printf("Failed to cache nonce for epoch %d: %v", nextEpoch, storeErr)

The same issue applies in ComputeEpochNonce at line 362 (if epoch == targetEpoch). On mainnet this is a no-op since no epoch has been empty, but the inconsistency with NonceIntegrityCheck's approach could cause confusion.

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