Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion db.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (s *PgStore) GetForgedSlots(ctx context.Context, epoch int) ([]uint64, erro
func (s *PgStore) InsertBlockBatch(ctx context.Context, blocks []BlockData) error {
rows := make([][]interface{}, len(blocks))
for i, b := range blocks {
nonceValue := vrfNonceValue(b.VrfOutput)
nonceValue := vrfNonceValueForEpoch(b.VrfOutput, b.Epoch, b.NetworkMagic)
rows[i] = []interface{}{int64(b.Slot), b.Epoch, b.BlockHash, b.VrfOutput, nonceValue}
}
_, err := s.pool.CopyFrom(ctx,
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ func (i *Indexer) runChainTail() error {
i.networkMagic,
i.nodeAddresses[0],
func(slot uint64, epoch int, blockHash string, vrfOutput []byte) {
blockCh <- BlockData{Slot: slot, Epoch: epoch, BlockHash: blockHash, VrfOutput: vrfOutput}
blockCh <- BlockData{Slot: slot, Epoch: epoch, BlockHash: blockHash, VrfOutput: vrfOutput, NetworkMagic: i.networkMagic}
},
onCaughtUp,
)
Expand Down
175 changes: 132 additions & 43 deletions nonce.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import (
// Used as the initial eta_v seed for full chain sync nonce evolution.
const ShelleyGenesisHash = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"

// knownEpochNonces contains hardcoded epoch nonces for early Shelley epochs
// where the D parameter (decentralisation) transition makes self-computation
// unreliable. Epochs 208-209 compute correctly; 210-211 are affected by the
// D=1→0.96 transition. From epoch 212 onward, self-computation matches Koios.
var knownEpochNonces = map[int]string{
210: "61f6c54f7f47f2e6b2fae5c4fa82c4125298f0f56ec4b400c8aa2b61e67daa20",
211: "4b16efae7144bfc96cac5e8b8ab5de4ff0de030e10d044e72aac38013b64eea5",
}

// NonceTracker accumulates VRF nonce contributions from chain sync blocks
// and evolves the epoch nonce for leader schedule calculation.
type NonceTracker struct {
Expand Down Expand Up @@ -281,8 +290,19 @@ func (nt *NonceTracker) GetNonceForEpoch(epoch int) ([]byte, error) {
// Streams all blocks from Shelley genesis, evolving the nonce and freezing at the
// stability window of each epoch, then computing:
//
// η(new) = BLAKE2b-256(η_c || η_ph) (per pallas/cncli)
// epochNonce = BLAKE2b-256(candidateNonce || lastEpochBlockNonce)
//
// The lastEpochBlockNonce is derived from the prevHash field of each block header
// (= blockHash of the preceding block), lagged by one epoch transition. This matches
// the Cardano node's praosStateLabNonce / praosStateLastEpochBlockNonce mechanism.
func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int) ([]byte, error) {
// Check hardcoded early epoch nonces first
if nonceHex, ok := knownEpochNonces[targetEpoch]; ok {
nonce, _ := hex.DecodeString(nonceHex)
log.Printf("Using hardcoded nonce for epoch %d: %s", targetEpoch, nonceHex)
return nonce, nil
}

shelleyStart := ShelleyStartEpoch
if nt.networkMagic == PreprodNetworkMagic {
shelleyStart = PreprodShelleyStartEpoch
Expand All @@ -294,11 +314,15 @@ func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int)
genesisHash, _ := hex.DecodeString(ShelleyGenesisHash)
etaV := make([]byte, 32)
copy(etaV, genesisHash)
eta0 := make([]byte, 32) // eta_0(shelleyStart) = shelley genesis hash
eta0 := make([]byte, 32)
copy(eta0, genesisHash)
etaC := make([]byte, 32)
prevHashNonce := make([]byte, 32) // η_ph — NeutralNonce at Shelley start
var lastBlockHash string

// labNonce tracks prevHashToNonce(block.prevHash) = blockHash of previous block.
// lastEpochBlockNonce is labNonce saved at the previous epoch transition (one epoch lag).
var prevBlockHash string
var labNonce []byte
var lastEpochBlockNonce []byte // nil = NeutralNonce

currentEpoch := shelleyStart
candidateFrozen := false
Expand All @@ -315,18 +339,26 @@ func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int)
return nil, fmt.Errorf("scanning block: %w", err)
}

// Epoch transition: η(new) = BLAKE2b-256(η_c || η_ph)
if epoch != currentEpoch {
if !candidateFrozen {
etaC = make([]byte, 32)
copy(etaC, etaV)
}
eta0 = hashConcat(etaC, prevHashNonce)
if lastBlockHash != "" {
prevHashNonce, _ = hex.DecodeString(lastBlockHash)

// epochNonce = candidateNonce ⭒ lastEpochBlockNonce
if lastEpochBlockNonce == nil {
eta0 = make([]byte, 32)
copy(eta0, etaC)
} else {
eta0 = hashConcat(etaC, lastEpochBlockNonce)
}

// Save labNonce for next epoch transition
if labNonce != nil {
lastEpochBlockNonce = make([]byte, len(labNonce))
copy(lastEpochBlockNonce, labNonce)
}

// If we just transitioned INTO the target epoch, we have eta_0(target)
if epoch == targetEpoch {
rows.Close()
log.Printf("Computed nonce for epoch %d: %s", targetEpoch, hex.EncodeToString(eta0))
Expand All @@ -337,10 +369,6 @@ func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int)
candidateFrozen = false
}

// Evolve eta_v
etaV = evolveNonce(etaV, nonceValue)
lastBlockHash = blockHash

// Freeze candidate at era-correct stability window
if !candidateFrozen {
epochStart := GetEpochStartSlot(epoch, nt.networkMagic)
Expand All @@ -351,6 +379,14 @@ func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int)
candidateFrozen = true
}
}

// Update labNonce: blockHash of previous block (= prevHash of current block)
if prevBlockHash != "" {
labNonce, _ = hex.DecodeString(prevBlockHash)
}

etaV = evolveNonce(etaV, nonceValue)
prevBlockHash = blockHash
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("row iteration: %w", err)
Expand All @@ -361,10 +397,17 @@ func (nt *NonceTracker) ComputeEpochNonce(ctx context.Context, targetEpoch int)
etaC = make([]byte, 32)
copy(etaC, etaV)
}
if lastBlockHash != "" {
prevHashNonce, _ = hex.DecodeString(lastBlockHash)
if labNonce != nil {
lastEpochBlockNonce = make([]byte, len(labNonce))
copy(lastEpochBlockNonce, labNonce)
}
var result []byte
if lastEpochBlockNonce == nil {
result = make([]byte, 32)
copy(result, etaC)
} else {
result = hashConcat(etaC, lastEpochBlockNonce)
}
result := hashConcat(etaC, prevHashNonce)
log.Printf("Computed nonce for epoch %d: %s", targetEpoch, hex.EncodeToString(result))
return result, nil
}
Expand All @@ -382,11 +425,11 @@ func (nt *NonceTracker) BackfillNonces(ctx context.Context) error {
genesisHash, _ := hex.DecodeString(ShelleyGenesisHash)
etaV := make([]byte, 32)
copy(etaV, genesisHash)
eta0 := make([]byte, 32)
copy(eta0, genesisHash)
etaC := make([]byte, 32)
prevHashNonce := make([]byte, 32) // η_ph — NeutralNonce at Shelley start
var lastBlockHash string

var prevBlockHash string
var labNonce []byte
var lastEpochBlockNonce []byte // nil = NeutralNonce

currentEpoch := shelleyStart
candidateFrozen := false
Expand All @@ -409,15 +452,27 @@ func (nt *NonceTracker) BackfillNonces(ctx context.Context) error {
return fmt.Errorf("scanning block: %w", scanErr)
}

// Epoch transition: η(new) = BLAKE2b-256(η_c || η_ph)
// 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)
}
Comment on lines +455 to 476
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.


// Cache if not already present
Expand All @@ -437,10 +492,6 @@ func (nt *NonceTracker) BackfillNonces(ctx context.Context) error {
candidateFrozen = false
}

// Evolve eta_v
etaV = evolveNonce(etaV, nonceValue)
lastBlockHash = blockHash

// Freeze candidate at stability window
if !candidateFrozen {
epochStart := GetEpochStartSlot(epoch, nt.networkMagic)
Expand All @@ -451,6 +502,14 @@ func (nt *NonceTracker) BackfillNonces(ctx context.Context) error {
candidateFrozen = true
}
}

// Update labNonce: blockHash of previous block
if prevBlockHash != "" {
labNonce, _ = hex.DecodeString(prevBlockHash)
}

etaV = evolveNonce(etaV, nonceValue)
prevBlockHash = blockHash
}
if err := rows.Err(); err != nil {
return fmt.Errorf("row iteration: %w", err)
Expand Down Expand Up @@ -515,8 +574,10 @@ func (nt *NonceTracker) NonceIntegrityCheck(ctx context.Context) (*IntegrityRepo
etaV := make([]byte, 32)
copy(etaV, genesisHash)
etaC := make([]byte, 32)
prevHashNonce := make([]byte, 32) // NeutralNonce at Shelley start
var lastBlockHash string

var prevBlockHash string
var labNonce []byte
var lastEpochBlockNonce []byte // nil = NeutralNonce

currentEpoch := shelleyStart
candidateFrozen := false
Expand Down Expand Up @@ -555,12 +616,25 @@ func (nt *NonceTracker) NonceIntegrityCheck(ctx context.Context) (*IntegrityRepo
etaC = make([]byte, 32)
copy(etaC, etaV)
}
eta0 := hashConcat(etaC, prevHashNonce)
if lastBlockHash != "" {
prevHashNonce, _ = hex.DecodeString(lastBlockHash)

nextEpoch := currentEpoch + 1
var eta0 []byte
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)
}

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

computed = append(computed, epochNonce{epoch: currentEpoch + 1, nonce: eta0})
computed = append(computed, epochNonce{epoch: nextEpoch, nonce: eta0})
currentEpoch = epoch
candidateFrozen = false
}
Expand All @@ -571,11 +645,6 @@ func (nt *NonceTracker) NonceIntegrityCheck(ctx context.Context) (*IntegrityRepo
vrfErrors++
}

// Evolve using recomputed nonce (not stored), ensuring end-to-end correctness
etaV = evolveNonce(etaV, recomputedNonce)
lastBlockHash = blockHash
blockCount++

// Freeze candidate at stability window
if !candidateFrozen {
epochStart := GetEpochStartSlot(epoch, nt.networkMagic)
Expand All @@ -586,6 +655,16 @@ func (nt *NonceTracker) NonceIntegrityCheck(ctx context.Context) (*IntegrityRepo
candidateFrozen = true
}
}

// Update labNonce: blockHash of previous block
if prevBlockHash != "" {
labNonce, _ = hex.DecodeString(prevBlockHash)
}

// Evolve using recomputed nonce (not stored), ensuring end-to-end correctness
etaV = evolveNonce(etaV, recomputedNonce)
prevBlockHash = blockHash
blockCount++
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("row iteration: %w", err)
Expand All @@ -597,11 +676,21 @@ func (nt *NonceTracker) NonceIntegrityCheck(ctx context.Context) (*IntegrityRepo
etaC = make([]byte, 32)
copy(etaC, etaV)
}
if lastBlockHash != "" {
prevHashNonce, _ = hex.DecodeString(lastBlockHash)
if labNonce != nil {
lastEpochBlockNonce = make([]byte, len(labNonce))
copy(lastEpochBlockNonce, labNonce)
}
nextEpoch := currentEpoch + 1
var eta0 []byte
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)
}
eta0 := hashConcat(etaC, prevHashNonce)
computed = append(computed, epochNonce{epoch: currentEpoch + 1, nonce: eta0})
computed = append(computed, epochNonce{epoch: nextEpoch, nonce: eta0})
}

scanTime := time.Since(start)
Expand Down
11 changes: 6 additions & 5 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import (

// BlockData holds block information for batch processing during chain sync.
type BlockData struct {
Slot uint64
Epoch int
BlockHash string
VrfOutput []byte
Slot uint64
Epoch int
BlockHash string
VrfOutput []byte
NetworkMagic int
}

// BlockNonceRows is an iterator over blocks for nonce computation.
Expand Down Expand Up @@ -330,7 +331,7 @@ func (s *SqliteStore) InsertBlockBatch(ctx context.Context, blocks []BlockData)
defer stmt.Close()

for _, b := range blocks {
nonceValue := vrfNonceValue(b.VrfOutput)
nonceValue := vrfNonceValueForEpoch(b.VrfOutput, b.Epoch, b.NetworkMagic)
if _, err := stmt.ExecContext(ctx, int64(b.Slot), b.Epoch, b.BlockHash, b.VrfOutput, nonceValue); err != nil {
return fmt.Errorf("insert slot %d: %w", b.Slot, err)
}
Expand Down