Skip to content
Open
Changes from 1 commit
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
61 changes: 61 additions & 0 deletions subnet/host/finality.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package host

import (
"subnet/storage"
"subnet/types"
)

// computeFinalizedNonce returns the highest nonce F such that >=2/3 of the
// session group (by slot count) has signed at nonce >= F.
//
// A slot that signed at nonce n is considered to have implicitly confirmed all
// nonces <= n by building on top of them.
func computeFinalizedNonce(store storage.Storage, escrowID string, latestNonce uint64, group []types.SlotAssignment) uint64 {
// confirmedBy[n] = bitmap of slots that have signed at nonce >= n.
// Bitmap128 is a value type (16 bytes); max group size is 128.
confirmedBy := make(map[uint64]types.Bitmap128)

for n := uint64(1); n <= latestNonce; n++ {
sigs, err := store.GetSignatures(escrowID, n)
if err != nil {
continue
}
Copy link
Copy Markdown
Collaborator

@akup akup Apr 8, 2026

Choose a reason for hiding this comment

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

We should add log at error level here if err != nil

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

for slotID := range sigs {
// This slot signed at n, confirming all nonces 1..n.
for prev := uint64(1); prev <= n; prev++ {
bm := confirmedBy[prev] // zero value if absent
bm.Set(slotID)
confirmedBy[prev] = bm
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here is O(N^2) cycle when O(N) is intended to be.
We just don't need inner cycle

for prev := uint64(1); prev <= n; prev++

It could be:

confirmedBy := make(map[uint64]types.Bitmap128)
// running = slots that have signed at any nonce >= current n.
var running types.Bitmap128
for n := latestNonce; n > 0; n-- {
	sigs, err := store.GetSignatures(escrowID, n)
	if err != nil {
		// keep previous running set; just skip adding new signers for this nonce
		confirmedBy[n] = running
		continue
	}
	// A slot that signed at n confirms all nonces <= n.
	// In reverse traversal, that means it belongs to running for n and all lower nonces.
	for slotID := range sigs {
		running.Set(slotID)
	}
	confirmedBy[n] = running
}

O(N) cycle

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Moreover we don't need next cycle, we can here add

...
confirmedBy[n] = running
if bitmapSlotWeight(confirmedBy[f], group) >= threshold {
  return n
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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


threshold := twoThirdsWeight(group)
for f := latestNonce; f > 0; f-- {
if bitmapSlotWeight(confirmedBy[f], group) >= threshold {
return f
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

computeFinalizedNonce uses twoThirdsWeight(group) as the threshold, but twoThirdsWeight currently computes ceil(2/3 * n). This is weaker than the finalization threshold used elsewhere (e.g. Host.checkFinalization uses 2*len(group)/3 + 1, i.e. >2/3). As a result, finalized may be overestimated (e.g. group size 3 => threshold 2 here vs 3 in checkFinalization), causing ApplyRecoveredDiffs to trust WarmKeyDelta from the wire before the nonce is actually finalized. Align the threshold calculation with the existing finalization rule (>2/3).

Copilot uses AI. Check for mistakes.
}
}
return 0 // warm-up period: not yet finalized
}

// bitmapSlotWeight counts the number of group slots whose bit is set in bm.
func bitmapSlotWeight(bm types.Bitmap128, group []types.SlotAssignment) uint32 {
var total uint32
for _, sa := range group {
if bm.IsSet(sa.SlotID) {
total++
}
}
return total
}

// twoThirdsWeight returns ceil(2/3 * totalSlots).
func twoThirdsWeight(group []types.SlotAssignment) uint32 {
total := uint32(len(group))
if total == 0 {
return 0
}
return (total*2 + 2) / 3
}