fix(nonce): use prevHash-based labNonce with epoch lag#58
Conversation
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>
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🔴 CriticalRemove the double-hash from the Babbage+ path in
vrfNonceValueForEpoch().The current implementation applies
BLAKE2b-256(BLAKE2b-256(0x4E || vrfOutput)), but the pallasderive_tagged_vrf_outputspecification (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 inevolveNonce()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. TheTestPallasRollingNonce()test correctly uses single-hashvrfNonceValue()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 inevolveNonce()as intended.
🧹 Nitpick comments (2)
nonce.go (2)
298-304: Silently discardinghex.DecodeStringerror on hardcoded nonce.Line 301: if the hardcoded hex string were ever malformed,
noncewould beniland 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 editsknownEpochNoncesin 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 explicitPreviewNetworkMagiccase tovrfNonceValueForEpochfor consistency.The function currently handles
PreprodNetworkMagicwithPreprodBabbageStartEpoch, but falls through to mainnet'sBabbageStartEpochforPreviewNetworkMagic. 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.,ShelleyStartEpochchecks 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 }
| // 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) | ||
| } |
There was a problem hiding this comment.
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.
fix(nonce): use prevHash-based labNonce with epoch lag
Summary
prevHash-basedlabNoncewith one-epoch lag, matching the Cardano node'spraosStateLabNonce/praosStateLastEpochBlockNoncemechanismInsertBlockBatchto use era-awarevrfNonceValueForEpoch(Babbage+ domain separator) instead of plainvrfNonceValueWhat was wrong
The previous code used the last block's
block_hashdirectly asprevHashNoncefor epoch transitions. The Cardano node actually trackslabNonce = prevHashToNonce(block.prevHash)(= blockHash of the preceding block) and lags it by one epoch transition viapraosStateLastEpochBlockNonce. This caused nonce mismatches from epoch 210 onward.Verified against
cardano-ledger/BaseTypes.hs(⭒ operator) andouroboros-consensus/Protocol/Praos.hsTest plan
go build ./...compiles cleango test ./...passes (TestPallasRollingNonce, TestPallasEpochNonce)nonceIntegrityCheck: true— all epochs should match Koios🤖 Generated with Claude Code
Summary by CodeRabbit