-
Notifications
You must be signed in to change notification settings - Fork 96
[P0] Make handling of warm keys deterministic #986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: upgrade-v0.2.12
Are you sure you want to change the base?
Changes from 7 commits
8410fbb
ab44f95
36bc095
a334443
7b8f359
5f3e1ae
1e98450
1b54432
7b60578
fbf4922
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| 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 | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add log at error level here if
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
heitor-lassarote marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
}
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
| } | ||
| } | ||
|
heitor-lassarote marked this conversation as resolved.
Outdated
|
||
| 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)) | ||
| return (total*2 + 2) / 3 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package host | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "subnet/storage" | ||
| "subnet/types" | ||
| ) | ||
|
|
||
| func makeSlotGroup(n int) []types.SlotAssignment { | ||
| g := make([]types.SlotAssignment, n) | ||
| for i := 0; i < n; i++ { | ||
| g[i] = types.SlotAssignment{SlotID: uint32(i), ValidatorAddress: "v"} | ||
| } | ||
| return g | ||
| } | ||
|
|
||
| // newMemorySessionWithNonces creates an escrow session and appends empty diffs for nonces 1..latestNonce. | ||
| func newMemorySessionWithNonces(t *testing.T, escrowID string, group []types.SlotAssignment, latestNonce uint64) *storage.Memory { | ||
| t.Helper() | ||
| store := storage.NewMemory() | ||
| err := store.CreateSession(storage.CreateSessionParams{ | ||
| EscrowID: escrowID, | ||
| Group: group, | ||
| }) | ||
| require.NoError(t, err) | ||
| for n := uint64(1); n <= latestNonce; n++ { | ||
| err := store.AppendDiff(escrowID, types.DiffRecord{Diff: types.Diff{Nonce: n}}) | ||
| require.NoError(t, err) | ||
| } | ||
| return store | ||
| } | ||
|
|
||
| func addSignatures(t *testing.T, store *storage.Memory, escrowID string, byNonce map[uint64][]uint32) { | ||
| t.Helper() | ||
| for nonce, slots := range byNonce { | ||
| for _, slotID := range slots { | ||
| err := store.AddSignature(escrowID, nonce, slotID, []byte{1}) | ||
| require.NoError(t, err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // F=0 when no nonce reaches the 2/3 slot threshold (4 slots → need 3; only 1 signs nonce 1). | ||
| func TestComputeFinalizedNonce_F0_insufficientSigners(t *testing.T) { | ||
| group := makeSlotGroup(4) | ||
| store := newMemorySessionWithNonces(t, "e1", group, 1) | ||
| addSignatures(t, store, "e1", map[uint64][]uint32{1: {0}}) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 1, group) | ||
| require.Equal(t, uint64(0), f) | ||
| } | ||
|
|
||
| // F=N when ≥2/3 slots have signed at some nonce ≥ N; a later partial nonce does not raise F past the | ||
| // last nonce that still had a supermajority at that height. | ||
| func TestComputeFinalizedNonce_F3_partialNonce4DoesNotRaise(t *testing.T) { | ||
| group := makeSlotGroup(4) | ||
| store := newMemorySessionWithNonces(t, "e1", group, 4) | ||
| addSignatures(t, store, "e1", map[uint64][]uint32{ | ||
| 3: {0, 1, 2}, | ||
| 4: {0, 1}, | ||
| }) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 4, group) | ||
| require.Equal(t, uint64(3), f) | ||
| } | ||
|
|
||
| // Signing only at a high nonce implies confirmation of all lower nonces (transitivity). | ||
| func TestComputeFinalizedNonce_transitivity_onlyHighNonceSupermajority(t *testing.T) { | ||
| group := makeSlotGroup(4) | ||
| store := newMemorySessionWithNonces(t, "e1", group, 5) | ||
| // No signatures on nonces 1–4; slots 0,1,2 sign only at nonce 5 → confirms 1..5. | ||
| addSignatures(t, store, "e1", map[uint64][]uint32{ | ||
| 5: {0, 1, 2}, | ||
| }) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 5, group) | ||
| require.Equal(t, uint64(5), f) | ||
| } | ||
|
|
||
| func TestComputeFinalizedNonce_threshold_groupOf3(t *testing.T) { | ||
| group := makeSlotGroup(3) // ceil(2/3 * 3) = 2 | ||
| store := newMemorySessionWithNonces(t, "e1", group, 1) | ||
| addSignatures(t, store, "e1", map[uint64][]uint32{1: {0, 1}}) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 1, group) | ||
| require.Equal(t, uint64(1), f) | ||
| } | ||
|
|
||
| func TestComputeFinalizedNonce_threshold_groupOf6(t *testing.T) { | ||
| group := makeSlotGroup(6) // ceil(2/3 * 6) = 4 | ||
| store := newMemorySessionWithNonces(t, "e1", group, 1) | ||
| addSignatures(t, store, "e1", map[uint64][]uint32{1: {0, 1, 2, 3}}) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 1, group) | ||
| require.Equal(t, uint64(1), f) | ||
| } | ||
|
|
||
| func TestComputeFinalizedNonce_latestNonceZero(t *testing.T) { | ||
| group := makeSlotGroup(4) | ||
| store := newMemorySessionWithNonces(t, "e1", group, 1) | ||
|
|
||
| f := computeFinalizedNonce(store, "e1", 0, group) | ||
| require.Equal(t, uint64(0), f) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -821,16 +821,36 @@ func (h *Host) AccumulateGossipSig(nonce uint64, stateHash, sig []byte, senderSl | |
| } | ||
|
|
||
| // ApplyRecoveredDiffs applies diffs fetched during gossip recovery. | ||
| // For each nonce, if F is the BFT-finalized nonce from local signatures | ||
| // (see computeFinalizedNonce), nonces at or below F may trust WarmKeyDelta from | ||
| // the wire without mainnet resolution; nonces above F fall back to ResolveWarmKey. | ||
| // Returns GossipSig for each successfully applied nonce. | ||
| func (h *Host) ApplyRecoveredDiffs(ctx context.Context, diffs []types.Diff) ([]gossip.GossipSig, error) { | ||
| func (h *Host) ApplyRecoveredDiffs(ctx context.Context, diffs []types.DiffRecord) ([]gossip.GossipSig, error) { | ||
| _ = ctx // interface hook; recovery is synchronous under h.mu today | ||
|
heitor-lassarote marked this conversation as resolved.
|
||
|
|
||
| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
|
|
||
| var latestNonce uint64 | ||
| for _, rec := range diffs { | ||
| if rec.Nonce > latestNonce { | ||
| latestNonce = rec.Nonce | ||
| } | ||
| } | ||
|
|
||
| var finalized uint64 | ||
| if h.store != nil { | ||
| finalized = computeFinalizedNonce(h.store, h.escrowID, latestNonce, h.group) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are calling here sigs, err := store.GetSignatures(escrowID, n)but it seams store is empty as peer signatures written after catch-up gossip exchange Am I right or missed something? where do we fill the store?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean that at the next lines we inject warm keys only for finalized nonce: if rec.Nonce <= finalized && len(rec.WarmKeyDelta) > 0 {
h.sm.InjectWarmKeys(rec.WarmKeyDelta)
}So we have following situation:
I think we need somehow get signatures at |
||
| } | ||
|
|
||
|
Comment on lines
+841
to
+845
|
||
| var sigs []gossip.GossipSig | ||
|
|
||
| for _, diff := range diffs { | ||
| if err := h.applyAndPersist(diff); err != nil { | ||
| return sigs, fmt.Errorf("apply recovered diff nonce %d: %w", diff.Nonce, err) | ||
| for _, rec := range diffs { | ||
| if rec.Nonce <= finalized && len(rec.WarmKeyDelta) > 0 { | ||
| h.sm.InjectWarmKeys(rec.WarmKeyDelta) | ||
| } | ||
| if err := h.applyAndPersist(rec.Diff); err != nil { | ||
| return sigs, fmt.Errorf("apply recovered diff nonce %d: %w", rec.Nonce, err) | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is the possible attack from peer: Then If attacker provides change in warm key where there was no change, we will not be able to catch up and recover at all even on retry as warm key is already broken. We should make a transaction here and rollback if
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Sign state with acceptance check (same path as HandleRequest). | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.