From 3396fa82b1442d5742ef4c023481b45fa23035f8 Mon Sep 17 00:00:00 2001 From: JimboJ <40345116+jimjbrettj@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:35:47 -0700 Subject: [PATCH] client(consensus/grandpa): implement grandpa justification logic (#3454) Co-authored-by: Timothy Wu --- client/consensus/grandpa/authorities.go | 14 +- client/consensus/grandpa/authorities_test.go | 304 +++++----- client/consensus/grandpa/aux_schema.go | 45 +- client/consensus/grandpa/aux_schema_test.go | 116 ++-- client/consensus/grandpa/change_tree_test.go | 24 +- client/consensus/grandpa/environment_test.go | 216 +++---- client/consensus/grandpa/interfaces.go | 15 + client/consensus/grandpa/justification.go | 313 ++++++++++ .../consensus/grandpa/justification_test.go | 540 ++++++++++++++++++ client/consensus/grandpa/lib.go | 53 +- client/consensus/grandpa/lib_test.go | 51 ++ pkg/finality-grandpa/lib.go | 11 +- pkg/finality-grandpa/round_test.go | 8 +- pkg/finality-grandpa/voter_test.go | 6 +- pkg/finality-grandpa/voting_round.go | 2 +- 15 files changed, 1398 insertions(+), 320 deletions(-) create mode 100644 client/consensus/grandpa/justification.go create mode 100644 client/consensus/grandpa/justification_test.go create mode 100644 client/consensus/grandpa/lib_test.go diff --git a/client/consensus/grandpa/authorities.go b/client/consensus/grandpa/authorities.go index f7dc3c2755..2b5462685f 100644 --- a/client/consensus/grandpa/authorities.go +++ b/client/consensus/grandpa/authorities.go @@ -46,8 +46,8 @@ type hashNumber[H, N any] struct { number N } -// medianAuthoritySet represents the median and new set when a forced hashNumber has occurred -type medianAuthoritySet[H comparable, N constraints.Unsigned, ID AuthorityID] struct { +// appliedChanges represents the median and new set when a forced change has occurred +type appliedChanges[H comparable, N constraints.Unsigned, ID AuthorityID] struct { median N set AuthoritySet[H, N, ID] } @@ -117,7 +117,7 @@ func (sas *SharedAuthoritySet[H, N, ID]) currentLimit(min N) (limit *N) { //noli func (sas *SharedAuthoritySet[H, N, ID]) applyForcedChanges(bestHash H, //nolint //skipcq: SCC-U1000 bestNumber N, isDescendentOf IsDescendentOf[H], - telemetry *Telemetry) (newSet *medianAuthoritySet[H, N, ID], err error) { + telemetry *Telemetry) (newSet *appliedChanges[H, N, ID], err error) { sas.mtx.Lock() defer sas.mtx.Unlock() return sas.inner.applyForcedChanges(bestHash, bestNumber, isDescendentOf, telemetry) @@ -481,7 +481,7 @@ func (authSet *AuthoritySet[H, N, ID]) currentLimit(min N) (limit *N) { func (authSet *AuthoritySet[H, N, ID]) applyForcedChanges(bestHash H, //skipcq: RVV-B0001 bestNumber N, isDescendentOf IsDescendentOf[H], - _ Telemetry) (newSet *medianAuthoritySet[H, N, ID], err error) { + _ Telemetry) (newSet *appliedChanges[H, N, ID], err error) { for _, change := range authSet.PendingForcedChanges { effectiveNumber := change.EffectiveNumber() @@ -516,14 +516,14 @@ func (authSet *AuthoritySet[H, N, ID]) applyForcedChanges(bestHash H, //skipcq: } } - // apply this hashNumber: make the set canonical - logger.Infof("👴 Applying authority set hashNumber forced at block #%d", change.CanonHeight) + // apply this change: make the set canonical + logger.Infof("👴 Applying authority set change forced at block #%d", change.CanonHeight) // TODO telemetry authSetChanges := authSet.AuthoritySetChanges authSetChanges.append(authSet.SetID, medianLastFinalized) - newSet = &medianAuthoritySet[H, N, ID]{ + newSet = &appliedChanges[H, N, ID]{ medianLastFinalized, AuthoritySet[H, N, ID]{ CurrentAuthorities: change.NextAuthorities, diff --git a/client/consensus/grandpa/authorities_test.go b/client/consensus/grandpa/authorities_test.go index 90d0b69e72..6b786d4e87 100644 --- a/client/consensus/grandpa/authorities_test.go +++ b/client/consensus/grandpa/authorities_test.go @@ -15,8 +15,8 @@ const ( hashB = "hash_b" hashC = "hash_c" hashD = "hash_d" - key = uint(1) - key2 = uint(2) + key = dummyAuthID(1) + key2 = dummyAuthID(2) ) func staticIsDescendentOf[H comparable](value bool) IsDescendentOf[H] { @@ -42,8 +42,8 @@ func TestDelayKind(t *testing.T) { } func TestCurrentLimitFiltersMin(t *testing.T) { - var currentAuthorities []Authority[uint] - currentAuthorities = append(currentAuthorities, Authority[uint]{ + var currentAuthorities []Authority[dummyAuthID] + currentAuthorities = append(currentAuthorities, Authority[dummyAuthID]{ Key: key, Weight: 1, }) @@ -51,7 +51,7 @@ func TestCurrentLimitFiltersMin(t *testing.T) { finalisedKind := Finalized{} delayKind := newDelayKind[uint](finalisedKind) - pendingChange1 := PendingChange[string, uint, uint]{ + pendingChange1 := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 1, @@ -59,7 +59,7 @@ func TestCurrentLimitFiltersMin(t *testing.T) { DelayKind: delayKind, } - pendingChange2 := PendingChange[string, uint, uint]{ + pendingChange2 := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 2, @@ -67,11 +67,11 @@ func TestCurrentLimitFiltersMin(t *testing.T) { DelayKind: delayKind, } - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: currentAuthorities, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -88,8 +88,8 @@ func TestCurrentLimitFiltersMin(t *testing.T) { } func TestChangesIteratedInPreOrder(t *testing.T) { - var currentAuthorities []Authority[uint] - currentAuthorities = append(currentAuthorities, Authority[uint]{ + var currentAuthorities []Authority[dummyAuthID] + currentAuthorities = append(currentAuthorities, Authority[dummyAuthID]{ Key: key, Weight: 1, }) @@ -100,15 +100,15 @@ func TestChangesIteratedInPreOrder(t *testing.T) { bestKind := Best[uint]{} delayKindBest := newDelayKind[uint](bestKind) - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: currentAuthorities, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 10, CanonHeight: 5, @@ -116,7 +116,7 @@ func TestChangesIteratedInPreOrder(t *testing.T) { DelayKind: delayKindFinalized, } - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 5, @@ -124,7 +124,7 @@ func TestChangesIteratedInPreOrder(t *testing.T) { DelayKind: delayKindFinalized, } - changeC := PendingChange[string, uint, uint]{ + changeC := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 5, CanonHeight: 10, @@ -149,7 +149,7 @@ func TestChangesIteratedInPreOrder(t *testing.T) { })) require.NoError(t, err) - changeD := PendingChange[string, uint, uint]{ + changeD := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 2, CanonHeight: 1, @@ -157,7 +157,7 @@ func TestChangesIteratedInPreOrder(t *testing.T) { DelayKind: delayKindBest, } - changeE := PendingChange[string, uint, uint]{ + changeE := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 2, CanonHeight: 0, @@ -171,7 +171,7 @@ func TestChangesIteratedInPreOrder(t *testing.T) { err = authorities.addPendingChange(changeE, staticIsDescendentOf[string](false)) require.NoError(t, err) - expectedChanges := []PendingChange[string, uint, uint]{ + expectedChanges := []PendingChange[string, uint, dummyAuthID]{ changeA, changeC, changeB, changeE, changeD, } pendingChanges := authorities.pendingChanges() @@ -179,22 +179,22 @@ func TestChangesIteratedInPreOrder(t *testing.T) { } func TestApplyChange(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) - var setB []Authority[uint] - setB = append(setB, Authority[uint]{ + var setB []Authority[dummyAuthID] + setB = append(setB, Authority[dummyAuthID]{ Key: key2, Weight: 5, }) @@ -202,7 +202,7 @@ func TestApplyChange(t *testing.T) { finalisedKind := Finalized{} delayKindFinalized := newDelayKind[uint](finalisedKind) - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 10, CanonHeight: 5, @@ -210,7 +210,7 @@ func TestApplyChange(t *testing.T) { DelayKind: delayKindFinalized, } - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setB, Delay: 10, CanonHeight: 5, @@ -224,7 +224,7 @@ func TestApplyChange(t *testing.T) { err = authorities.addPendingChange(changeB, staticIsDescendentOf[string](true)) require.NoError(t, err) - expectedChanges := []PendingChange[string, uint, uint]{ + expectedChanges := []PendingChange[string, uint, dummyAuthID]{ changeA, changeB, } pendingChanges := authorities.pendingChanges() @@ -251,7 +251,7 @@ func TestApplyChange(t *testing.T) { require.True(t, status.Changed) require.Nil(t, status.NewSetBlock) - expectedChanges = []PendingChange[string, uint, uint]{ + expectedChanges = []PendingChange[string, uint, dummyAuthID]{ changeA, } pendingChanges = authorities.pendingChanges() @@ -292,22 +292,22 @@ func TestApplyChange(t *testing.T) { } func TestDisallowMultipleChangesBeingFinalizedAtOnce(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) - var setC []Authority[uint] - setC = append(setC, Authority[uint]{ + var setC []Authority[dummyAuthID] + setC = append(setC, Authority[dummyAuthID]{ Key: key2, Weight: 5, }) @@ -315,7 +315,7 @@ func TestDisallowMultipleChangesBeingFinalizedAtOnce(t *testing.T) { finalisedKind := Finalized{} delayKindFinalized := newDelayKind[uint](finalisedKind) - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 10, CanonHeight: 5, @@ -323,7 +323,7 @@ func TestDisallowMultipleChangesBeingFinalizedAtOnce(t *testing.T) { DelayKind: delayKindFinalized, } - changeC := PendingChange[string, uint, uint]{ + changeC := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setC, Delay: 10, CanonHeight: 30, @@ -415,16 +415,16 @@ func TestDisallowMultipleChangesBeingFinalizedAtOnce(t *testing.T) { } func TestEnactsStandardChangeWorks(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) @@ -432,7 +432,7 @@ func TestEnactsStandardChangeWorks(t *testing.T) { finalisedKind := Finalized{} delayKindFinalized := newDelayKind[uint](finalisedKind) - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 10, CanonHeight: 5, @@ -440,7 +440,7 @@ func TestEnactsStandardChangeWorks(t *testing.T) { DelayKind: delayKindFinalized, } - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 10, CanonHeight: 20, @@ -491,22 +491,22 @@ func TestEnactsStandardChangeWorks(t *testing.T) { } func TestForceChanges(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) - var setB []Authority[uint] - setB = append(setB, Authority[uint]{ + var setB []Authority[dummyAuthID] + setB = append(setB, Authority[dummyAuthID]{ Key: key2, Weight: 5, }) @@ -517,7 +517,7 @@ func TestForceChanges(t *testing.T) { finalisedKindB := Best[uint]{0} delayKindFinalizedB := newDelayKind[uint](finalisedKindB) - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 10, CanonHeight: 5, @@ -525,7 +525,7 @@ func TestForceChanges(t *testing.T) { DelayKind: delayKindFinalizedA, } - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setB, Delay: 10, CanonHeight: 5, @@ -547,7 +547,7 @@ func TestForceChanges(t *testing.T) { require.NoError(t, err) require.Nil(t, res) - changeC := PendingChange[string, uint, uint]{ + changeC := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 3, CanonHeight: 8, @@ -564,7 +564,12 @@ func TestForceChanges(t *testing.T) { // let's try and apply the forced changes. // too early and there's no forced changes to apply - resForced, err := authorities.applyForcedChanges("hash_a10", 10, staticIsDescendentOf[string](true), nil) + resForced, err := authorities.applyForcedChanges( + "hash_a10", + 10, + staticIsDescendentOf[string](true), + nil, + ) require.NoError(t, err) require.Nil(t, resForced) @@ -574,13 +579,13 @@ func TestForceChanges(t *testing.T) { require.Nil(t, resForced) // on time -- chooses the right hashNumber for this fork - exp := medianAuthoritySet[string, uint, uint]{ + exp := appliedChanges[string, uint, dummyAuthID]{ median: 42, - set: AuthoritySet[string, uint, uint]{ + set: AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: setA, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{ setIDNumber[uint]{ SetID: 0, @@ -597,16 +602,16 @@ func TestForceChanges(t *testing.T) { func TestForceChangesWithNoDelay(t *testing.T) { // NOTE: this is a regression test - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) @@ -615,7 +620,7 @@ func TestForceChangesWithNoDelay(t *testing.T) { delayKindFinalized := newDelayKind[uint](finalisedKind) // we create a forced hashNumber with no Delay - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 0, CanonHeight: 5, @@ -628,22 +633,27 @@ func TestForceChangesWithNoDelay(t *testing.T) { require.NoError(t, err) // it should be enacted at the same block that signalled it - resForced, err := authorities.applyForcedChanges(hashA, 5, staticIsDescendentOf[string](false), nil) + resForced, err := authorities.applyForcedChanges( + hashA, + 5, + staticIsDescendentOf[string](false), + nil, + ) require.NoError(t, err) require.NotNil(t, resForced) } func TestForceChangesBlockedByStandardChanges(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } - var setA []Authority[uint] - setA = append(setA, Authority[uint]{ + var setA []Authority[dummyAuthID] + setA = append(setA, Authority[dummyAuthID]{ Key: key, Weight: 5, }) @@ -652,7 +662,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { delayKindFinalized := newDelayKind[uint](finalisedKind) // effective at #15 - changeA := PendingChange[string, uint, uint]{ + changeA := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 5, CanonHeight: 10, @@ -661,7 +671,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { } // effective #20 - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 0, CanonHeight: 20, @@ -670,7 +680,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { } // effective at #35 - changeC := PendingChange[string, uint, uint]{ + changeC := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 5, CanonHeight: 30, @@ -692,7 +702,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { delayKindFinalized2 := newDelayKind[uint](finalisedKind2) // effective at #45 - changeD := PendingChange[string, uint, uint]{ + changeD := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: setA, Delay: 5, CanonHeight: 40, @@ -705,7 +715,12 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { // the forced hashNumber cannot be applied since the pending changes it depends on // have not been applied yet. - _, err = authorities.applyForcedChanges("hash_d45", 45, staticIsDescendentOf[string](true), nil) + _, err = authorities.applyForcedChanges( + "hash_d45", + 45, + staticIsDescendentOf[string](true), + nil, + ) require.ErrorIs(t, err, errForcedAuthoritySetChangeDependencyUnsatisfied) require.Equal(t, 0, len(authorities.AuthoritySetChanges)) @@ -716,12 +731,22 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { BlockNumber: 15, }, } - _, err = authorities.applyStandardChanges("hash_a15", 15, staticIsDescendentOf[string](true), nil) + _, err = authorities.applyStandardChanges( + "hash_a15", + 15, + staticIsDescendentOf[string](true), + nil, + ) require.NoError(t, err) require.Equal(t, expChanges, authorities.AuthoritySetChanges) // but the forced hashNumber still depends on the next standard hashNumber - _, err = authorities.applyForcedChanges("hash_d45", 45, staticIsDescendentOf[string](true), nil) + _, err = authorities.applyForcedChanges( + "hash_d45", + 45, + staticIsDescendentOf[string](true), + nil, + ) require.ErrorIs(t, err, errForcedAuthoritySetChangeDependencyUnsatisfied) require.Equal(t, expChanges, authorities.AuthoritySetChanges) @@ -730,7 +755,12 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { SetID: 1, BlockNumber: 20, }) - _, err = authorities.applyStandardChanges(hashB, 20, staticIsDescendentOf[string](true), nil) + _, err = authorities.applyStandardChanges( + hashB, + 20, + staticIsDescendentOf[string](true), + nil, + ) require.NoError(t, err) require.Equal(t, expChanges, authorities.AuthoritySetChanges) @@ -741,34 +771,38 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) { SetID: 2, BlockNumber: 31, }) - exp := medianAuthoritySet[string, uint, uint]{ + exp := appliedChanges[string, uint, dummyAuthID]{ median: 31, - set: AuthoritySet[string, uint, uint]{ + set: AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: setA, SetID: 3, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: expChanges, }, } - resForced, err := authorities.applyForcedChanges(hashD, 45, staticIsDescendentOf[string](true), nil) + resForced, err := authorities.applyForcedChanges( + hashD, + 45, + staticIsDescendentOf[string](true), + nil) require.NoError(t, err) require.NotNil(t, resForced) require.Equal(t, exp, *resForced) } func TestNextChangeWorks(t *testing.T) { - var currentAuthorities []Authority[uint] - currentAuthorities = append(currentAuthorities, Authority[uint]{ + var currentAuthorities []Authority[dummyAuthID] + currentAuthorities = append(currentAuthorities, Authority[dummyAuthID]{ Key: key, Weight: 1, }) - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: currentAuthorities, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -777,7 +811,7 @@ func TestNextChangeWorks(t *testing.T) { // We have three pending changes with 2 possible roots that are enacted // immediately on finality (i.e. standard changes). - changeA0 := PendingChange[string, uint, uint]{ + changeA0 := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 5, @@ -785,7 +819,7 @@ func TestNextChangeWorks(t *testing.T) { DelayKind: delayKindFinalized, } - changeA1 := PendingChange[string, uint, uint]{ + changeA1 := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 10, @@ -793,7 +827,7 @@ func TestNextChangeWorks(t *testing.T) { DelayKind: delayKindFinalized, } - changeB := PendingChange[string, uint, uint]{ + changeB := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 4, @@ -864,7 +898,7 @@ func TestNextChangeWorks(t *testing.T) { // we a forced hashNumber at A10 (#8) finalisedKind2 := Best[uint]{0} delayKindFinalized2 := newDelayKind[uint](finalisedKind2) - changeA10 := PendingChange[string, uint, uint]{ + changeA10 := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: 8, @@ -887,12 +921,18 @@ func TestNextChangeWorks(t *testing.T) { func TestMaintainsAuthorityListInvariants(t *testing.T) { // empty authority lists are invalid - _, err := NewGenesisAuthoritySet[string, uint, uint]([]Authority[uint]{}) + _, err := NewGenesisAuthoritySet[string, uint, dummyAuthID]([]Authority[dummyAuthID]{}) require.NotNil(t, err) - _, err = NewAuthoritySet[string, uint, uint]([]Authority[uint]{}, 0, NewChangeTree[string, uint, uint](), nil, nil) + _, err = NewAuthoritySet[string, uint, dummyAuthID]( + []Authority[dummyAuthID]{}, + 0, + NewChangeTree[string, uint, dummyAuthID](), + nil, + nil, + ) require.NotNil(t, err) - invalidAuthoritiesWeight := []Authority[uint]{ + invalidAuthoritiesWeight := []Authority[dummyAuthID]{ { Key: key, Weight: 5, @@ -906,10 +946,16 @@ func TestMaintainsAuthorityListInvariants(t *testing.T) { // authority weight of zero is invalid _, err = NewGenesisAuthoritySet[string, uint](invalidAuthoritiesWeight) require.NotNil(t, err) - _, err = NewAuthoritySet[string, uint](invalidAuthoritiesWeight, 0, NewChangeTree[string, uint, uint](), nil, nil) + _, err = NewAuthoritySet[string, uint]( + invalidAuthoritiesWeight, + 0, + NewChangeTree[string, uint, dummyAuthID](), + nil, + nil, + ) require.NotNil(t, err) - authoritySet, err := NewGenesisAuthoritySet[string, uint, uint]([]Authority[uint]{{ + authoritySet, err := NewGenesisAuthoritySet[string, uint, dummyAuthID]([]Authority[dummyAuthID]{{ Key: key, Weight: 5, }}) @@ -917,7 +963,7 @@ func TestMaintainsAuthorityListInvariants(t *testing.T) { finalisedKind := Finalized{} delayKindFinalized := newDelayKind[uint](finalisedKind) - invalidChangeEmptyAuthorities := PendingChange[string, uint, uint]{ + invalidChangeEmptyAuthorities := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: nil, Delay: 10, CanonHeight: 5, @@ -932,7 +978,7 @@ func TestMaintainsAuthorityListInvariants(t *testing.T) { delayKind := Best[uint]{0} delayKindBest := newDelayKind[uint](delayKind) - invalidChangeAuthoritiesWeight := PendingChange[string, uint, uint]{ + invalidChangeAuthoritiesWeight := PendingChange[string, uint, dummyAuthID]{ NextAuthorities: invalidAuthoritiesWeight, Delay: 10, CanonHeight: 5, @@ -947,17 +993,17 @@ func TestMaintainsAuthorityListInvariants(t *testing.T) { } func TestCleanUpStaleForcedChangesWhenApplyingStandardChange(t *testing.T) { - var currentAuthorities []Authority[uint] - currentAuthorities = append(currentAuthorities, Authority[uint]{ + var currentAuthorities []Authority[dummyAuthID] + currentAuthorities = append(currentAuthorities, Authority[dummyAuthID]{ Key: key, Weight: 1, }) - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: currentAuthorities, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -999,11 +1045,11 @@ func TestCleanUpStaleForcedChangesWhenApplyingStandardChange(t *testing.T) { }) addPendingChangeFunction := func(canonHeight uint, canonHash string, forced bool) { - var change PendingChange[string, uint, uint] + var change PendingChange[string, uint, dummyAuthID] if forced { delayKind := Best[uint]{0} delayKindBest := newDelayKind[uint](delayKind) - change = PendingChange[string, uint, uint]{ + change = PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: canonHeight, @@ -1013,7 +1059,7 @@ func TestCleanUpStaleForcedChangesWhenApplyingStandardChange(t *testing.T) { } else { delayKind := Finalized{} delayKindFinalized := newDelayKind[uint](delayKind) - change = PendingChange[string, uint, uint]{ + change = PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: canonHeight, @@ -1052,17 +1098,17 @@ func TestCleanUpStaleForcedChangesWhenApplyingStandardChange(t *testing.T) { } func TestCleanUpStaleForcedChangesWhenApplyingStandardChangeAlternateCase(t *testing.T) { - var currentAuthorities []Authority[uint] - currentAuthorities = append(currentAuthorities, Authority[uint]{ + var currentAuthorities []Authority[dummyAuthID] + currentAuthorities = append(currentAuthorities, Authority[dummyAuthID]{ Key: key, Weight: 1, }) - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ CurrentAuthorities: currentAuthorities, SetID: 0, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -1104,11 +1150,11 @@ func TestCleanUpStaleForcedChangesWhenApplyingStandardChangeAlternateCase(t *tes }) addPendingChangeFunction := func(canonHeight uint, canonHash string, forced bool) { - var change PendingChange[string, uint, uint] + var change PendingChange[string, uint, dummyAuthID] if forced { delayKind := Best[uint]{0} delayKindBest := newDelayKind[uint](delayKind) - change = PendingChange[string, uint, uint]{ + change = PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: canonHeight, @@ -1118,7 +1164,7 @@ func TestCleanUpStaleForcedChangesWhenApplyingStandardChangeAlternateCase(t *tes } else { delayKind := Finalized{} delayKindFinalized := newDelayKind[uint](delayKind) - change = PendingChange[string, uint, uint]{ + change = PendingChange[string, uint, dummyAuthID]{ NextAuthorities: currentAuthorities, Delay: 0, CanonHeight: canonHeight, @@ -1342,7 +1388,7 @@ func TestIterFromWorks(t *testing.T) { func TestAuthoritySet_InvalidAuthorityList(t *testing.T) { type args struct { - authorities []Authority[uint] + authorities []Authority[dummyAuthID] } tests := []struct { name string @@ -1359,14 +1405,14 @@ func TestAuthoritySet_InvalidAuthorityList(t *testing.T) { { name: "emptyAuthorities", args: args{ - authorities: []Authority[uint]{}, + authorities: []Authority[dummyAuthID]{}, }, exp: true, }, { name: "invalidAuthoritiesWeight", args: args{ - authorities: []Authority[uint]{ + authorities: []Authority[dummyAuthID]{ { Weight: 0, }, @@ -1377,7 +1423,7 @@ func TestAuthoritySet_InvalidAuthorityList(t *testing.T) { { name: "validAuthorityList", args: args{ - authorities: []Authority[uint]{ + authorities: []Authority[dummyAuthID]{ { Weight: 1, }, diff --git a/client/consensus/grandpa/aux_schema.go b/client/consensus/grandpa/aux_schema.go index 79f7b0f37b..eedde8a3d0 100644 --- a/client/consensus/grandpa/aux_schema.go +++ b/client/consensus/grandpa/aux_schema.go @@ -5,6 +5,7 @@ package grandpa import ( "errors" + "fmt" "github.com/ChainSafe/gossamer/client/api" grandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa" @@ -16,7 +17,7 @@ var ( setStateKey = []byte("grandpa_completed_round") concludedRounds = []byte("grandpa_concluded_rounds") authoritySetKey = []byte("grandpa_voters") - bestJustification = []byte("grandpa_best_justification") //nolint + bestJustification = []byte("grandpa_best_justification") errValueNotFound = errors.New("value not found") ) @@ -195,15 +196,44 @@ func UpdateAuthoritySet[H comparable, N constraints.Unsigned, ID AuthorityID, Si // We always keep around the justification for the best finalized block and overwrite it // as we finalize new blocks, this makes sure that we don't store useless justifications // but can always prove finality of the latest block. -func UpdateBestJustification() { - // TODO impl when we have justification logic - panic("impl") +func updateBestJustification[ + Hash constraints.Ordered, + N constraints.Unsigned, + S comparable, + ID AuthorityID, + H Header[Hash, N]]( + justification Justification[Hash, N, S, ID, H], + write writeAux) error { + encodedJustificaiton, err := scale.Marshal(justification) + if err != nil { + return fmt.Errorf("marshalling: %w", err) + } + + insert := []api.KeyValue{ + {bestJustification, encodedJustificaiton}, //nolint + } + err = write(insert) + if err != nil { + return fmt.Errorf("inserting justification: %w", err) + } + return nil } // BestJustification Fetch the justification for the latest block finalized by GRANDPA, if any. -func BestJustification() { - // TODO impl when we have justification logic - panic("impl") +func BestJustification[ + Hash constraints.Ordered, + N constraints.Unsigned, + S comparable, + ID AuthorityID, + H Header[Hash, N]]( + store api.AuxStore) (*Justification[Hash, N, S, ID, H], error) { + justification := Justification[Hash, N, S, ID, H]{} + err := loadDecoded(store, bestJustification, &justification) + if err != nil { + return nil, err + } + + return &justification, nil } // WriteVoterSetState Write voter set state. @@ -249,5 +279,4 @@ func WriteConcludedRound[H comparable, N constraints.Unsigned, ID AuthorityID, S return err } return nil - } diff --git a/client/consensus/grandpa/aux_schema_test.go b/client/consensus/grandpa/aux_schema_test.go index 7ddd22d39c..70a3434139 100644 --- a/client/consensus/grandpa/aux_schema_test.go +++ b/client/consensus/grandpa/aux_schema_test.go @@ -73,7 +73,6 @@ func TestDummyStore(t *testing.T) { require.True(t, len(*store) == 2) del := []api.Key{setStateKey} - err = store.Insert(nil, del) require.NoError(t, err) require.True(t, len(*store) == 1) @@ -93,13 +92,13 @@ func TestLoadPersistentGenesis(t *testing.T) { store := newDummyStore(t) genesisHash := "a" genesisNumber := uint(21) - genesisAuths := []Authority[uint]{{ + genesisAuths := []Authority[dummyAuthID]{{ Key: key, Weight: 1, }} // Genesis Case - persistentData, err := loadPersistent[string, uint, uint, uint]( + persistentData, err := loadPersistent[string, uint, dummyAuthID, uint]( store, genesisHash, genesisNumber, @@ -107,14 +106,14 @@ func TestLoadPersistentGenesis(t *testing.T) { require.NoError(t, err) require.NotNil(t, persistentData) - genesisSet, err := NewGenesisAuthoritySet[string, uint, uint](genesisAuths) + genesisSet, err := NewGenesisAuthoritySet[string, uint, dummyAuthID](genesisAuths) require.NoError(t, err) state := finalityGrandpa.NewRoundState(finalityGrandpa.HashNumber[string, uint]{ Hash: genesisHash, Number: genesisNumber}) base := state.PrevoteGHOST - genesisState, err := NewLiveVoterSetState[string, uint, uint, uint](0, *genesisSet, *base) + genesisState, err := NewLiveVoterSetState[string, uint, dummyAuthID, uint](0, *genesisSet, *base) require.NoError(t, err) require.Equal(t, persistentData.authoritySet.inner, *genesisSet) @@ -137,20 +136,20 @@ func TestLoadPersistentNotGenesis(t *testing.T) { store := newDummyStore(t) genesisHash := "a" genesisNumber := uint(21) - genesisAuths := []Authority[uint]{{ + genesisAuths := []Authority[dummyAuthID]{{ Key: key, Weight: 1, }} // Auth set and Set state both written - genesisSet, err := NewGenesisAuthoritySet[string, uint, uint](genesisAuths) + genesisSet, err := NewGenesisAuthoritySet[string, uint, dummyAuthID](genesisAuths) require.NoError(t, err) state := finalityGrandpa.NewRoundState(finalityGrandpa.HashNumber[string, uint]{ Hash: genesisHash, Number: genesisNumber}) base := state.PrevoteGHOST - genesisState, err := NewLiveVoterSetState[string, uint, uint, uint](0, *genesisSet, *base) + genesisState, err := NewLiveVoterSetState[string, uint, dummyAuthID, uint](0, *genesisSet, *base) require.NoError(t, err) insert := []api.KeyValue{ @@ -160,7 +159,7 @@ func TestLoadPersistentNotGenesis(t *testing.T) { err = store.Insert(insert, nil) require.NoError(t, err) - persistentData, err := loadPersistent[string, uint, uint, uint]( + persistentData, err := loadPersistent[string, uint, dummyAuthID, uint]( store, genesisHash, genesisNumber, @@ -183,14 +182,14 @@ func TestLoadPersistentNotGenesis(t *testing.T) { err = store.Insert(insert, nil) require.NoError(t, err) - persistentData, err = loadPersistent[string, uint, uint, uint]( + persistentData, err = loadPersistent[string, uint, dummyAuthID, uint]( store, genesisHash, genesisNumber, genesisAuthorities(genesisAuths, nil)) require.NoError(t, err) - newState, err := NewLiveVoterSetState[string, uint, uint, uint](genesisSet.SetID, *genesisSet, *base) + newState, err := NewLiveVoterSetState[string, uint, dummyAuthID, uint](genesisSet.SetID, *genesisSet, *base) require.NoError(t, err) require.Equal(t, *genesisSet, persistentData.authoritySet.inner) @@ -204,20 +203,20 @@ func TestLoadPersistentNotGenesis(t *testing.T) { func TestUpdateAuthoritySet(t *testing.T) { // Test no new set case store := newDummyStore(t) - authorities := AuthoritySet[string, uint, uint]{ + authorities := AuthoritySet[string, uint, dummyAuthID]{ SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), } - err := UpdateAuthoritySet[string, uint, uint, uint](authorities, nil, write(store)) + err := UpdateAuthoritySet[string, uint, dummyAuthID, uint](authorities, nil, write(store)) require.NoError(t, err) encData, err := store.Get(authoritySetKey) require.NoError(t, err) require.NotNil(t, encData) - newAuthorities := AuthoritySet[string, uint, uint]{ - PendingStandardChanges: NewChangeTree[string, uint, uint](), + newAuthorities := AuthoritySet[string, uint, dummyAuthID]{ + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), } err = scale.Unmarshal(*encData, &newAuthorities) require.NoError(t, err) @@ -225,25 +224,25 @@ func TestUpdateAuthoritySet(t *testing.T) { // New set case store = newDummyStore(t) - authorities = AuthoritySet[string, uint, uint]{ + authorities = AuthoritySet[string, uint, dummyAuthID]{ SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), } - newAuthSet := &NewAuthoritySetStruct[string, uint, uint]{ + newAuthSet := &NewAuthoritySetStruct[string, uint, dummyAuthID]{ CanonNumber: 4, SetId: 2, } - err = UpdateAuthoritySet[string, uint, uint, uint](authorities, newAuthSet, write(store)) + err = UpdateAuthoritySet[string, uint, dummyAuthID, uint](authorities, newAuthSet, write(store)) require.NoError(t, err) encData, err = store.Get(authoritySetKey) require.NoError(t, err) require.NotNil(t, encData) - newAuthorities = AuthoritySet[string, uint, uint]{ - PendingStandardChanges: NewChangeTree[string, uint, uint](), + newAuthorities = AuthoritySet[string, uint, dummyAuthID]{ + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), } err = scale.Unmarshal(*encData, &newAuthorities) require.NoError(t, err) @@ -257,7 +256,11 @@ func TestUpdateAuthoritySet(t *testing.T) { Number: newAuthSet.CanonNumber, } - setState, err := NewLiveVoterSetState[string, uint, uint, uint](uint64(newAuthSet.SetId), authorities, genesisState) + setState, err := NewLiveVoterSetState[string, uint, dummyAuthID, uint]( + uint64(newAuthSet.SetId), + authorities, + genesisState, + ) require.NoError(t, err) encodedVoterSet, err := scale.Marshal(setState) @@ -267,11 +270,11 @@ func TestUpdateAuthoritySet(t *testing.T) { func TestWriteVoterSetState(t *testing.T) { store := newDummyStore(t) - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -280,7 +283,7 @@ func TestWriteVoterSetState(t *testing.T) { Number: 1, } - completedRound := completedRound[string, uint, uint, uint]{ + completedRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: finalityGrandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -292,14 +295,14 @@ func TestWriteVoterSetState(t *testing.T) { } completedRounds := NewCompletedRounds[string, uint](completedRound, 1, authorities) - currentRounds := make(map[uint64]hasVoted[string, uint, uint]) + currentRounds := make(map[uint64]hasVoted[string, uint, dummyAuthID]) - liveState := voterSetStateLive[string, uint, uint, uint]{ + liveState := voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, CurrentRounds: currentRounds, } - voterSetState := NewVoterSetState[string, uint, uint, uint]() + voterSetState := NewVoterSetState[string, uint, dummyAuthID, uint]() err := voterSetState.Set(liveState) require.NoError(t, err) require.NotNil(t, voterSetState) @@ -318,11 +321,11 @@ func TestWriteVoterSetState(t *testing.T) { func TestWriteConcludedRound(t *testing.T) { store := newDummyStore(t) - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -331,7 +334,7 @@ func TestWriteConcludedRound(t *testing.T) { Number: 1, } - completedRound := completedRound[string, uint, uint, uint]{ + completedRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: finalityGrandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -343,14 +346,14 @@ func TestWriteConcludedRound(t *testing.T) { } completedRounds := NewCompletedRounds[string, uint](completedRound, 1, authorities) - currentRounds := make(map[uint64]hasVoted[string, uint, uint]) + currentRounds := make(map[uint64]hasVoted[string, uint, dummyAuthID]) - liveState := voterSetStateLive[string, uint, uint, uint]{ + liveState := voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, CurrentRounds: currentRounds, } - voterSetState := NewVoterSetState[string, uint, uint, uint]() + voterSetState := NewVoterSetState[string, uint, dummyAuthID, uint]() err := voterSetState.Set(liveState) require.NoError(t, err) require.NotNil(t, voterSetState) @@ -369,3 +372,38 @@ func TestWriteConcludedRound(t *testing.T) { require.NotNil(t, val) require.Equal(t, encRoundData, *val) } + +func TestWriteJustification(t *testing.T) { + store := newDummyStore(t) + + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + expAncestries := make([]testHeader[string, uint], 0) + expAncestries = append(expAncestries, testHeader[string, uint]{ + NumberField: 100, + ParentHashField: "a", + }) + + justification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Round: 2, + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: expAncestries, + } + + _, err := BestJustification[string, uint, string, dummyAuthID, testHeader[string, uint]](store) + require.ErrorIs(t, err, errValueNotFound) + + err = updateBestJustification[string, uint, string, dummyAuthID, testHeader[string, uint]](justification, write(store)) + require.NoError(t, err) + + bestJust, err := BestJustification[string, uint, string, dummyAuthID, testHeader[string, uint]](store) + require.NoError(t, err) + require.NotNil(t, bestJust) + require.Equal(t, justification, *bestJust) +} diff --git a/client/consensus/grandpa/change_tree_test.go b/client/consensus/grandpa/change_tree_test.go index 95d7c8e79a..9e42468953 100644 --- a/client/consensus/grandpa/change_tree_test.go +++ b/client/consensus/grandpa/change_tree_test.go @@ -10,53 +10,53 @@ import ( ) func TestSwapRemove(t *testing.T) { - change1 := &PendingChange[string, uint, uint]{ + change1 := &PendingChange[string, uint, dummyAuthID]{ CanonHash: "a", } - change2 := &PendingChange[string, uint, uint]{ + change2 := &PendingChange[string, uint, dummyAuthID]{ CanonHash: "b", } - change3 := &PendingChange[string, uint, uint]{ + change3 := &PendingChange[string, uint, dummyAuthID]{ CanonHash: "c", } - pendingChangeNode1 := &PendingChangeNode[string, uint, uint]{ + pendingChangeNode1 := &PendingChangeNode[string, uint, dummyAuthID]{ Change: change1, } - pendingChangeNode2 := &PendingChangeNode[string, uint, uint]{ + pendingChangeNode2 := &PendingChangeNode[string, uint, dummyAuthID]{ Change: change2, } - pendingChangeNode3 := &PendingChangeNode[string, uint, uint]{ + pendingChangeNode3 := &PendingChangeNode[string, uint, dummyAuthID]{ Change: change3, } - changeNodes1 := []*PendingChangeNode[string, uint, uint]{ + changeNodes1 := []*PendingChangeNode[string, uint, dummyAuthID]{ pendingChangeNode1, pendingChangeNode2, } - changeNodes2 := []*PendingChangeNode[string, uint, uint]{ + changeNodes2 := []*PendingChangeNode[string, uint, dummyAuthID]{ pendingChangeNode1, pendingChangeNode2, pendingChangeNode3, } type args struct { - ct ChangeTree[string, uint, uint] + ct ChangeTree[string, uint, dummyAuthID] index uint } tests := []struct { name string args args - exp PendingChangeNode[string, uint, uint] + exp PendingChangeNode[string, uint, dummyAuthID] }{ { name: "2ElemSliceDeletingLastElement", args: args{ - ct: ChangeTree[string, uint, uint]{ + ct: ChangeTree[string, uint, dummyAuthID]{ TreeRoots: changeNodes1, }, index: 1, @@ -66,7 +66,7 @@ func TestSwapRemove(t *testing.T) { { name: "3ElemSliceDeletingFirstElement", args: args{ - ct: ChangeTree[string, uint, uint]{ + ct: ChangeTree[string, uint, dummyAuthID]{ TreeRoots: changeNodes2, }, index: 0, diff --git a/client/consensus/grandpa/environment_test.go b/client/consensus/grandpa/environment_test.go index 7a922a01aa..e51a2c821a 100644 --- a/client/consensus/grandpa/environment_test.go +++ b/client/consensus/grandpa/environment_test.go @@ -13,11 +13,11 @@ import ( func TestSharedVoterSetState_hasVoted(t *testing.T) { // Has Not Voted - hasNotVoted := hasVoted[string, uint, uint]{} + hasNotVoted := hasVoted[string, uint, dummyAuthID]{} hasNotVoted = hasNotVoted.New() err := hasNotVoted.Set(no{}) require.NoError(t, err) - voterSetState := *NewVoterSetState[string, uint, uint, uint]() + voterSetState := *NewVoterSetState[string, uint, dummyAuthID, uint]() sharedVoterSetState := NewSharedVoterSetState[string, uint](voterSetState) voted, err := sharedVoterSetState.hasVoted(0) require.NoError(t, err) @@ -29,26 +29,26 @@ func TestSharedVoterSetState_hasVoted(t *testing.T) { err = vote.Set(propose[string, uint]{}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ AuthId: key, Vote: vote, } - hasIndeedVoted := hasVoted[string, uint, uint]{} + hasIndeedVoted := hasVoted[string, uint, dummyAuthID]{} hasIndeedVoted = hasIndeedVoted.New() err = hasIndeedVoted.Set(yes) require.NoError(t, err) - example := make(map[uint64]hasVoted[string, uint, uint]) + example := make(map[uint64]hasVoted[string, uint, dummyAuthID]) example[1] = hasIndeedVoted - newCurrentRounds := CurrentRounds[string, uint, uint]( + newCurrentRounds := CurrentRounds[string, uint, dummyAuthID]( example, ) - liveState := voterSetStateLive[string, uint, uint, uint]{ + liveState := voterSetStateLive[string, uint, dummyAuthID, uint]{ CurrentRounds: newCurrentRounds, } - newVoterSetState := *NewVoterSetState[string, uint, uint, uint]() + newVoterSetState := *NewVoterSetState[string, uint, dummyAuthID, uint]() err = newVoterSetState.Set(liveState) require.NoError(t, err) @@ -64,7 +64,7 @@ func TestCompleteRoundEncoding(t *testing.T) { Number: 1, } - compRound := completedRound[string, uint, uint, uint]{ + compRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -78,18 +78,18 @@ func TestCompleteRoundEncoding(t *testing.T) { enc, err := scale.Marshal(compRound) require.NoError(t, err) - newCompletedRound := completedRound[string, uint, uint, uint]{} + newCompletedRound := completedRound[string, uint, dummyAuthID, uint]{} err = scale.Unmarshal(enc, &newCompletedRound) require.NoError(t, err) require.Equal(t, compRound, newCompletedRound) } func TestCompletedRoundsEncoding(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -98,7 +98,7 @@ func TestCompletedRoundsEncoding(t *testing.T) { Number: 1, } - completedRound := completedRound[string, uint, uint, uint]{ + completedRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -113,7 +113,7 @@ func TestCompletedRoundsEncoding(t *testing.T) { enc, err := scale.Marshal(compRounds) require.NoError(t, err) - var newCompletedRounds completedRounds[string, uint, uint, uint] + var newCompletedRounds completedRounds[string, uint, dummyAuthID, uint] err = scale.Unmarshal(enc, &newCompletedRounds) require.NoError(t, err) require.Equal(t, compRounds, newCompletedRounds) @@ -125,7 +125,7 @@ func TestCompletedRounds_Iter(t *testing.T) { Number: 1, } - completedRound0 := completedRound[string, uint, uint, uint]{ + completedRound0 := completedRound[string, uint, dummyAuthID, uint]{ Number: 0, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -136,7 +136,7 @@ func TestCompletedRounds_Iter(t *testing.T) { Base: dummyHashNumber, } - completedRound1 := completedRound[string, uint, uint, uint]{ + completedRound1 := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -147,7 +147,7 @@ func TestCompletedRounds_Iter(t *testing.T) { Base: dummyHashNumber, } - completedRound2 := completedRound[string, uint, uint, uint]{ + completedRound2 := completedRound[string, uint, dummyAuthID, uint]{ Number: 2, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -157,17 +157,17 @@ func TestCompletedRounds_Iter(t *testing.T) { }, Base: dummyHashNumber, } - rounds := make([]completedRound[string, uint, uint, uint], 0, 3) + rounds := make([]completedRound[string, uint, dummyAuthID, uint], 0, 3) rounds = append(rounds, completedRound0) rounds = append(rounds, completedRound1) rounds = append(rounds, completedRound2) - expRounds := make([]completedRound[string, uint, uint, uint], 0, 3) + expRounds := make([]completedRound[string, uint, dummyAuthID, uint], 0, 3) expRounds = append(expRounds, completedRound2) expRounds = append(expRounds, completedRound1) expRounds = append(expRounds, completedRound0) - compRounds := completedRounds[string, uint, uint, uint]{ + compRounds := completedRounds[string, uint, dummyAuthID, uint]{ Rounds: rounds, } @@ -176,11 +176,11 @@ func TestCompletedRounds_Iter(t *testing.T) { } func TestCompletedRounds_Last(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -189,7 +189,7 @@ func TestCompletedRounds_Last(t *testing.T) { Number: 1, } - compRound := completedRound[string, uint, uint, uint]{ + compRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -203,16 +203,16 @@ func TestCompletedRounds_Last(t *testing.T) { lastCompletedRound := compRounds.last() require.Equal(t, compRound, lastCompletedRound) - emptyCompletedRounds := completedRounds[string, uint, uint, uint]{} + emptyCompletedRounds := completedRounds[string, uint, dummyAuthID, uint]{} require.Panics(t, func() { emptyCompletedRounds.last() }, "last did not panic") } func TestCompletedRounds_Push(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -221,7 +221,7 @@ func TestCompletedRounds_Push(t *testing.T) { Number: 1, } - completedRound0 := completedRound[string, uint, uint, uint]{ + completedRound0 := completedRound[string, uint, dummyAuthID, uint]{ Number: 0, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -232,7 +232,7 @@ func TestCompletedRounds_Push(t *testing.T) { Base: dummyHashNumber, } - completedRound1 := completedRound[string, uint, uint, uint]{ + completedRound1 := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -243,7 +243,7 @@ func TestCompletedRounds_Push(t *testing.T) { Base: dummyHashNumber, } - completedRound2 := completedRound[string, uint, uint, uint]{ + completedRound2 := completedRound[string, uint, dummyAuthID, uint]{ Number: 2, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -265,11 +265,11 @@ func TestCompletedRounds_Push(t *testing.T) { } func TestCurrentRoundsEncoding(t *testing.T) { - currentRounds := CurrentRounds[string, uint, uint]( - make(map[uint64]hasVoted[string, uint, uint]), + currentRounds := CurrentRounds[string, uint, dummyAuthID]( + make(map[uint64]hasVoted[string, uint, dummyAuthID]), ) - hv := hasVoted[string, uint, uint]{} + hv := hasVoted[string, uint, dummyAuthID]{} hv = hv.New() err := hv.Set(no{}) require.NoError(t, err) @@ -278,11 +278,11 @@ func TestCurrentRoundsEncoding(t *testing.T) { enc, err := scale.Marshal(currentRounds) require.NoError(t, err) - hasVotedNew := hasVoted[string, uint, uint]{} + hasVotedNew := hasVoted[string, uint, dummyAuthID]{} hasVotedNew = hv.New() - example := make(map[uint64]hasVoted[string, uint, uint]) + example := make(map[uint64]hasVoted[string, uint, dummyAuthID]) example[1] = hasVotedNew - newCurrentRounds := CurrentRounds[string, uint, uint]( + newCurrentRounds := CurrentRounds[string, uint, dummyAuthID]( example, ) err = scale.Unmarshal(enc, &newCurrentRounds) @@ -291,14 +291,14 @@ func TestCurrentRoundsEncoding(t *testing.T) { } func TestVoterSetStateEncoding(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{} + authorities := AuthoritySet[string, uint, dummyAuthID]{} dummyHashNumber := grandpa.HashNumber[string, uint]{ Hash: "a", Number: 1, } - compRound := completedRound[string, uint, uint, uint]{ + compRound := completedRound[string, uint, dummyAuthID, uint]{ Number: 1, State: grandpa.RoundState[string, uint]{ PrevoteGHOST: &dummyHashNumber, @@ -309,24 +309,24 @@ func TestVoterSetStateEncoding(t *testing.T) { Base: dummyHashNumber, } - completedRounds := NewCompletedRounds[string, uint, uint, uint](compRound, 1, authorities) - currentRounds := CurrentRounds[string, uint, uint]( - make(map[uint64]hasVoted[string, uint, uint]), + completedRounds := NewCompletedRounds[string, uint, dummyAuthID, uint](compRound, 1, authorities) + currentRounds := CurrentRounds[string, uint, dummyAuthID]( + make(map[uint64]hasVoted[string, uint, dummyAuthID]), ) - liveState := voterSetStateLive[string, uint, uint, uint]{ + liveState := voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, CurrentRounds: currentRounds, } - voterSetState := *NewVoterSetState[string, uint, uint, uint]() + voterSetState := *NewVoterSetState[string, uint, dummyAuthID, uint]() err := voterSetState.Set(liveState) require.NoError(t, err) enc, err := scale.Marshal(voterSetState) require.NoError(t, err) - newVoterSetState := *NewVoterSetState[string, uint, uint, uint]() + newVoterSetState := *NewVoterSetState[string, uint, dummyAuthID, uint]() err = scale.Unmarshal(enc, &newVoterSetState) require.NoError(t, err) @@ -335,16 +335,16 @@ func TestVoterSetStateEncoding(t *testing.T) { newVal, err := newVoterSetState.Value() require.NoError(t, err) - require.Equal(t, oldVal.(voterSetStateLive[string, uint, uint, uint]), - newVal.(voterSetStateLive[string, uint, uint, uint])) + require.Equal(t, oldVal.(voterSetStateLive[string, uint, dummyAuthID, uint]), + newVal.(voterSetStateLive[string, uint, dummyAuthID, uint])) } func TestVoterSetState_Live(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } @@ -353,24 +353,24 @@ func TestVoterSetState_Live(t *testing.T) { Number: 1, } - liveSetState, err := NewLiveVoterSetState[string, uint, uint, uint](5, authorities, dummyHashNumber) + liveSetState, err := NewLiveVoterSetState[string, uint, dummyAuthID, uint](5, authorities, dummyHashNumber) require.NoError(t, err) live, err := liveSetState.Value() require.NoError(t, err) - val, ok := live.(voterSetStateLive[string, uint, uint, uint]) + val, ok := live.(voterSetStateLive[string, uint, dummyAuthID, uint]) require.True(t, ok) require.Equal(t, uint64(5), val.CompletedRounds.SetId) require.Equal(t, uint64(0), val.CompletedRounds.Rounds[0].Number) } func TestVoterSetState_CompletedRounds(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } dummyHashNumber := grandpa.HashNumber[string, uint]{ @@ -378,20 +378,20 @@ func TestVoterSetState_CompletedRounds(t *testing.T) { Number: 1, } state := grandpa.NewRoundState[string, uint](dummyHashNumber) - completedRounds := NewCompletedRounds[string, uint, uint, uint]( - completedRound[string, uint, uint, uint]{ + completedRounds := NewCompletedRounds[string, uint, dummyAuthID, uint]( + completedRound[string, uint, dummyAuthID, uint]{ 10, state, dummyHashNumber, - []grandpa.SignedMessage[string, uint, uint, uint]{}, + []grandpa.SignedMessage[string, uint, dummyAuthID, uint]{}, }, 5, authorities, ) - voterSetState := NewVoterSetState[string, uint, uint, uint]() + voterSetState := NewVoterSetState[string, uint, dummyAuthID, uint]() - err := voterSetState.Set(voterSetStateLive[string, uint, uint, uint]{ + err := voterSetState.Set(voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, }) require.NoError(t, err) @@ -402,11 +402,11 @@ func TestVoterSetState_CompletedRounds(t *testing.T) { } func TestVoterSetState_LastCompletedRound(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } dummyHashNumber := grandpa.HashNumber[string, uint]{ @@ -415,29 +415,29 @@ func TestVoterSetState_LastCompletedRound(t *testing.T) { } state := grandpa.NewRoundState[string, uint](dummyHashNumber) - originalCompletedRound := completedRound[string, uint, uint, uint]{ + originalCompletedRound := completedRound[string, uint, dummyAuthID, uint]{ 8, state, dummyHashNumber, - []grandpa.SignedMessage[string, uint, uint, uint]{}, + []grandpa.SignedMessage[string, uint, dummyAuthID, uint]{}, } - completedRounds := NewCompletedRounds[string, uint, uint, uint]( + completedRounds := NewCompletedRounds[string, uint, dummyAuthID, uint]( originalCompletedRound, 5, authorities, ) - addedCompletedRound := completedRound[string, uint, uint, uint]{ + addedCompletedRound := completedRound[string, uint, dummyAuthID, uint]{ 8, state, dummyHashNumber, - []grandpa.SignedMessage[string, uint, uint, uint]{}, + []grandpa.SignedMessage[string, uint, dummyAuthID, uint]{}, } completedRounds.push(addedCompletedRound) - voterSetState := NewVoterSetState[string, uint, uint, uint]() - err := voterSetState.Set(voterSetStatePaused[string, uint, uint, uint]{ + voterSetState := NewVoterSetState[string, uint, dummyAuthID, uint]() + err := voterSetState.Set(voterSetStatePaused[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, }) require.NoError(t, err) @@ -448,11 +448,11 @@ func TestVoterSetState_LastCompletedRound(t *testing.T) { } func TestVoterSetState_WithCurrentRound(t *testing.T) { - authorities := AuthoritySet[string, uint, uint]{ - CurrentAuthorities: []Authority[uint]{}, + authorities := AuthoritySet[string, uint, dummyAuthID]{ + CurrentAuthorities: []Authority[dummyAuthID]{}, SetID: 1, - PendingStandardChanges: NewChangeTree[string, uint, uint](), - PendingForcedChanges: []PendingChange[string, uint, uint]{}, + PendingStandardChanges: NewChangeTree[string, uint, dummyAuthID](), + PendingForcedChanges: []PendingChange[string, uint, dummyAuthID]{}, AuthoritySetChanges: AuthoritySetChanges[uint]{}, } dummyHashNumber := grandpa.HashNumber[string, uint]{ @@ -460,21 +460,21 @@ func TestVoterSetState_WithCurrentRound(t *testing.T) { Number: 1, } state := grandpa.NewRoundState[string, uint](dummyHashNumber) - completedRounds := NewCompletedRounds[string, uint, uint, uint]( - completedRound[string, uint, uint, uint]{ + completedRounds := NewCompletedRounds[string, uint, dummyAuthID, uint]( + completedRound[string, uint, dummyAuthID, uint]{ 10, state, dummyHashNumber, - []grandpa.SignedMessage[string, uint, uint, uint]{}, + []grandpa.SignedMessage[string, uint, dummyAuthID, uint]{}, }, 5, authorities, ) - voterSetState := NewVoterSetState[string, uint, uint, uint]() + voterSetState := NewVoterSetState[string, uint, dummyAuthID, uint]() // voterSetStatePaused - err := voterSetState.Set(voterSetStatePaused[string, uint, uint, uint]{ + err := voterSetState.Set(voterSetStatePaused[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, }) require.NoError(t, err) @@ -483,7 +483,7 @@ func TestVoterSetState_WithCurrentRound(t *testing.T) { require.Equal(t, "voter acting while in paused state", err.Error()) // voterSetStateLive: invalid round - err = voterSetState.Set(voterSetStateLive[string, uint, uint, uint]{ + err = voterSetState.Set(voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, }) require.NoError(t, err) @@ -492,16 +492,16 @@ func TestVoterSetState_WithCurrentRound(t *testing.T) { require.Equal(t, "voter acting on a live round we are not tracking", err.Error()) // Valid - currentRounds := CurrentRounds[string, uint, uint]( - make(map[uint64]hasVoted[string, uint, uint]), + currentRounds := CurrentRounds[string, uint, dummyAuthID]( + make(map[uint64]hasVoted[string, uint, dummyAuthID]), ) - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(no{}) require.NoError(t, err) currentRounds[1] = hasVoted - err = voterSetState.Set(voterSetStateLive[string, uint, uint, uint]{ + err = voterSetState.Set(voterSetStateLive[string, uint, dummyAuthID, uint]{ CompletedRounds: completedRounds, CurrentRounds: currentRounds, }) @@ -518,11 +518,11 @@ func TestHasVotedEncoding(t *testing.T) { err := vote.Set(propose[string, uint]{}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ AuthId: key, Vote: vote, } - hv := hasVoted[string, uint, uint]{} + hv := hasVoted[string, uint, dummyAuthID]{} hv = hv.New() err = hv.Set(yes) require.NoError(t, err) @@ -530,7 +530,7 @@ func TestHasVotedEncoding(t *testing.T) { res, err := scale.Marshal(hv) require.NoError(t, err) - newHasVoted := hasVoted[string, uint, uint]{} + newHasVoted := hasVoted[string, uint, dummyAuthID]{} newHasVoted = hv.New() err = scale.Unmarshal(res, &newHasVoted) require.NoError(t, err) @@ -547,10 +547,10 @@ func TestHasVoted_Propose(t *testing.T) { err := vote.Set(propose[string, uint]{*primaryPropose}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ Vote: vote, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(yes) require.NoError(t, err) @@ -569,10 +569,10 @@ func TestHasVoted_Prevote(t *testing.T) { err := voteVal.Set(prevote[string, uint]{&grandpa.PrimaryPropose[string, uint]{}, *prevoteVal}) require.NoError(t, err) - y := yes[string, uint, uint]{ + y := yes[string, uint, dummyAuthID]{ Vote: voteVal, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(y) require.NoError(t, err) @@ -589,7 +589,7 @@ func TestHasVoted_Prevote(t *testing.T) { err = proposeVote.Set(propose[string, uint]{PrimaryPropose: *primaryPropose}) require.NoError(t, err) - y = yes[string, uint, uint]{ + y = yes[string, uint, dummyAuthID]{ Vote: proposeVote, } hasVoted = hasVoted.New() @@ -613,10 +613,10 @@ func TestHasVoted_Precommit(t *testing.T) { *precommitVal}) require.NoError(t, err) - y := yes[string, uint, uint]{ + y := yes[string, uint, dummyAuthID]{ Vote: voteVal, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(y) require.NoError(t, err) @@ -633,7 +633,7 @@ func TestHasVoted_Precommit(t *testing.T) { err = proposeVote.Set(propose[string, uint]{PrimaryPropose: *primaryPropose}) require.NoError(t, err) - y = yes[string, uint, uint]{ + y = yes[string, uint, dummyAuthID]{ Vote: proposeVote, } hasVoted = hasVoted.New() @@ -654,10 +654,10 @@ func TestHasVoted_CanPropose(t *testing.T) { err := voteVal.Set(propose[string, uint]{*primaryPropose}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ Vote: voteVal, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(yes) require.NoError(t, err) @@ -679,10 +679,10 @@ func TestHasVoted_CanPrevote(t *testing.T) { err := voteVal.Set(prevote[string, uint]{&grandpa.PrimaryPropose[string, uint]{}, *prevoteVal}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ Vote: voteVal, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(yes) require.NoError(t, err) @@ -707,10 +707,10 @@ func TestHasVoted_CanPrecommit(t *testing.T) { *precommitVal}) require.NoError(t, err) - yes := yes[string, uint, uint]{ + yes := yes[string, uint, dummyAuthID]{ Vote: vote, } - hasVoted := hasVoted[string, uint, uint]{} + hasVoted := hasVoted[string, uint, dummyAuthID]{} hasVoted = hasVoted.New() err = hasVoted.Set(yes) require.NoError(t, err) diff --git a/client/consensus/grandpa/interfaces.go b/client/consensus/grandpa/interfaces.go index 8bf2e01fdf..b772a26efa 100644 --- a/client/consensus/grandpa/interfaces.go +++ b/client/consensus/grandpa/interfaces.go @@ -3,5 +3,20 @@ package grandpa +import ( + "golang.org/x/exp/constraints" +) + // Telemetry TODO issue #3474 type Telemetry interface{} + +type Header[Hash constraints.Ordered, N constraints.Unsigned] interface { + ParentHash() Hash + Hash() Hash + Number() N +} + +type HeaderBackend[Hash constraints.Ordered, N constraints.Unsigned, H Header[Hash, N]] interface { + // Header Get block header. Returns None if block is not found. + Header(Hash) (*H, error) +} diff --git a/client/consensus/grandpa/justification.go b/client/consensus/grandpa/justification.go new file mode 100644 index 0000000000..31473e4c6e --- /dev/null +++ b/client/consensus/grandpa/justification.go @@ -0,0 +1,313 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +import ( + "errors" + "fmt" + "reflect" + + finalityGrandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa" + "github.com/ChainSafe/gossamer/pkg/scale" + "golang.org/x/exp/constraints" +) + +var ( + errInvalidAuthoritiesSet = errors.New("current state of blockchain has invalid authorities set") + errBadJustification = errors.New("bad justification for header") + errBlockNotDescendentOfBase = errors.New("block not descendent of base") +) + +// Justification is a GRANDPA justification for block finality, it includes a commit message and +// an ancestry proof including all headers routing all precommit target blocks +// to the commit target block. Due to the current voting strategy the precommit +// targets should be the same as the commit target, since honest voters don't +// vote past authority set change blocks. +// +// This is meant to be stored in the db and passed around the network to other +// nodes, and are used by syncing nodes to prove authority set handoffs. +type Justification[ + Hash constraints.Ordered, + N constraints.Unsigned, + S comparable, + ID AuthorityID, + H Header[Hash, N]] struct { + Round uint64 + Commit finalityGrandpa.Commit[Hash, N, S, ID] + VotesAncestries []H +} + +// NewJustificationFromCommit Create a GRANDPA justification from the given commit. This method +// assumes the commit is valid and well-formed. +func NewJustificationFromCommit[ + Hash constraints.Ordered, + N constraints.Unsigned, + S comparable, + ID AuthorityID, + H Header[Hash, N]]( + client HeaderBackend[Hash, N, H], + round uint64, + commit finalityGrandpa.Commit[Hash, N, S, ID]) (Justification[Hash, N, S, ID, H], error) { + votesAncestriesHashes := make(map[Hash]struct{}) + voteAncestries := make([]H, 0) + + // we pick the precommit for the lowest block as the base that + // should serve as the root block for populating ancestry (i.e. + // collect all headers from all precommit blocks to the base) + var minPrecommit *hashNumber[Hash, N] + for _, signed := range commit.Precommits { + precommit := signed.Precommit + if minPrecommit == nil { + minPrecommit = &hashNumber[Hash, N]{ + hash: precommit.TargetHash, + number: precommit.TargetNumber, + } + } else if precommit.TargetNumber < minPrecommit.number { + minPrecommit = &hashNumber[Hash, N]{ + hash: precommit.TargetHash, + number: precommit.TargetNumber, + } + } + } + if minPrecommit == nil { + return Justification[Hash, N, S, ID, H]{}, + fmt.Errorf("%w: invalid precommits for target commit", errBadJustification) + } + + baseNumber := minPrecommit.number + baseHash := minPrecommit.hash + for _, signed := range commit.Precommits { + currentHash := signed.Precommit.TargetHash + for { + if currentHash == baseHash { + break + } + + header, err := client.Header(currentHash) + if err != nil || header == nil { + return Justification[Hash, N, S, ID, H]{}, + fmt.Errorf("%w: invalid precommits for target commit", errBadJustification) + } + + currentHeader := *header + + // NOTE: this should never happen as we pick the lowest block + // as base and only traverse backwards from the other blocks + // in the commit. but better be safe to avoid an unbound loop. + if currentHeader.Number() <= baseNumber { + return Justification[Hash, N, S, ID, H]{}, + fmt.Errorf("%w: invalid precommits for target commit", errBadJustification) + } + parentHash := currentHeader.ParentHash() + + _, ok := votesAncestriesHashes[currentHash] + if !ok { + voteAncestries = append(voteAncestries, currentHeader) + } + + votesAncestriesHashes[currentHash] = struct{}{} + currentHash = parentHash + } + } + + return Justification[Hash, N, S, ID, H]{ + Round: round, + Commit: commit, + VotesAncestries: voteAncestries, + }, nil +} + +// Decode a GRANDPA justification and validate the commit and the votes' +// ancestry proofs finalize the given block. +func decodeAndVerifyFinalizes[Hash constraints.Ordered, + N constraints.Unsigned, + S comparable, + ID AuthorityID, + H Header[Hash, N]]( + encoded []byte, + finalizedTarget hashNumber[Hash, N], + setID uint64, + voters finalityGrandpa.VoterSet[ID]) (Justification[Hash, N, S, ID, H], error) { + justification := Justification[Hash, N, S, ID, H]{ + VotesAncestries: make([]H, 0), + } + err := scale.Unmarshal(encoded, &justification) + if err != nil { + return Justification[Hash, N, S, ID, H]{}, fmt.Errorf("error decoding justification for header: %s", err) + } + + decodedTarget := hashNumber[Hash, N]{ + hash: justification.Commit.TargetHash, + number: justification.Commit.TargetNumber, + } + + if decodedTarget != finalizedTarget { + return Justification[Hash, N, S, ID, H]{}, fmt.Errorf("invalid commit target in grandpa justification") + } + + return justification, justification.verifyWithVoterSet(setID, voters) +} + +// Verify Validate the commit and the votes' ancestry proofs. +func (j *Justification[Hash, N, S, ID, H]) Verify(setID uint64, authorities AuthorityList[ID]) error { + var weights []finalityGrandpa.IDWeight[ID] + for _, authority := range authorities { + weight := finalityGrandpa.IDWeight[ID]{ + ID: authority.Key, + Weight: finalityGrandpa.VoterWeight(authority.Weight), + } + weights = append(weights, weight) + } + + voters := finalityGrandpa.NewVoterSet[ID](weights) + if voters != nil { + err := j.verifyWithVoterSet(setID, *voters) + return err + } + return fmt.Errorf("%w", errInvalidAuthoritiesSet) +} + +// Validate the commit and the votes' ancestry proofs. +func (j *Justification[Hash, N, S, ID, H]) verifyWithVoterSet( + setID uint64, + voters finalityGrandpa.VoterSet[ID]) error { + ancestryChain := newAncestryChain[Hash, N](j.VotesAncestries) + commitValidationResult, err := finalityGrandpa.ValidateCommit[Hash, N, S, ID](j.Commit, voters, ancestryChain) + if err != nil { + return fmt.Errorf("%w: invalid commit in grandpa justification", errBadJustification) + } + + if !commitValidationResult.Valid() { + return fmt.Errorf("%w: invalid commit in grandpa justification", errBadJustification) + } + + // we pick the precommit for the lowest block as the base that + // should serve as the root block for populating ancestry (i.e. + // collect all headers from all precommit blocks to the base) + precommits := j.Commit.Precommits + var minPrecommit *finalityGrandpa.SignedPrecommit[Hash, N, S, ID] + if len(precommits) == 0 { + panic("can only fail if precommits is empty; commit has been validated above; " + + "valid commits must include precommits") + } + for _, precommit := range precommits { + currPrecommit := precommit + if minPrecommit == nil { + minPrecommit = &currPrecommit + } else if currPrecommit.Precommit.TargetNumber <= minPrecommit.Precommit.TargetNumber { + minPrecommit = &currPrecommit + } + } + + baseHash := minPrecommit.Precommit.TargetHash + visitedHashes := make(map[Hash]struct{}) + for _, signed := range precommits { + mgs := finalityGrandpa.Message[Hash, N]{Value: signed.Precommit} + isValidSignature, err := checkMessageSignature[Hash, N, ID](mgs, signed.ID, signed.Signature, j.Round, setID) + if err != nil { + return err + } + + if !isValidSignature { + return fmt.Errorf("%w: invalid signature for precommit in grandpa justification", + errBadJustification) + } + + if baseHash == signed.Precommit.TargetHash { + continue + } + + route, err := ancestryChain.Ancestry(baseHash, signed.Precommit.TargetHash) + if err != nil { + return fmt.Errorf("%w: invalid precommit ancestry proof in grandpa justification", + errBadJustification) + } + + // ancestry starts from parent HashField but the precommit target HashField has been + // visited + visitedHashes[signed.Precommit.TargetHash] = struct{}{} + for _, hash := range route { + visitedHashes[hash] = struct{}{} + } + } + + ancestryHashes := make(map[Hash]struct{}) + for _, header := range j.VotesAncestries { + hash := header.Hash() + ancestryHashes[hash] = struct{}{} + } + + if len(visitedHashes) != len(ancestryHashes) { + return fmt.Errorf("%w: invalid precommit ancestries in grandpa justification with unused headers", + errBadJustification) + } + + // Check if maps are equal + if !reflect.DeepEqual(ancestryHashes, visitedHashes) { + return fmt.Errorf("%w: invalid precommit ancestries in grandpa justification with unused headers", + errBadJustification) + } + + return nil +} + +// Target The target block NumberField and HashField that this justifications proves finality for +func (j *Justification[Hash, N, S, ID, H]) Target() hashNumber[Hash, N] { + return hashNumber[Hash, N]{ + number: j.Commit.TargetNumber, + hash: j.Commit.TargetHash, + } +} + +// ancestryChain A utility trait implementing `finality_grandpa::Chain` using a given set of headers. +// This is useful when validating commits, using the given set of headers to +// verify a valid ancestry route to the target commit block. +type ancestryChain[Hash constraints.Ordered, N constraints.Unsigned, H Header[Hash, N]] struct { + ancestry map[Hash]H +} + +func newAncestryChain[Hash constraints.Ordered, N constraints.Unsigned, H Header[Hash, N]]( + headers []H) ancestryChain[Hash, N, H] { + ancestry := make(map[Hash]H) + for _, header := range headers { + hash := header.Hash() + ancestry[hash] = header + } + return ancestryChain[Hash, N, H]{ + ancestry: ancestry, + } +} + +func (ac ancestryChain[Hash, N, H]) Ancestry(base Hash, block Hash) ([]Hash, error) { + route := make([]Hash, 0) + currentHash := block + + for { + if currentHash == base { + break + } + + br, ok := ac.ancestry[currentHash] + if !ok { + return nil, fmt.Errorf("%w", errBlockNotDescendentOfBase) + } + block = br.ParentHash() + currentHash = block + route = append(route, currentHash) + } + + if len(route) != 0 { + route = route[:len(route)-1] + } + return route, nil +} + +func (ac ancestryChain[Hash, N, H]) IsEqualOrDescendantOf(base Hash, block Hash) bool { + if base == block { + return true + } + + _, err := ac.Ancestry(base, block) + return err == nil +} diff --git a/client/consensus/grandpa/justification_test.go b/client/consensus/grandpa/justification_test.go new file mode 100644 index 0000000000..f860de1ebd --- /dev/null +++ b/client/consensus/grandpa/justification_test.go @@ -0,0 +1,540 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +import ( + "reflect" + "testing" + + finalityGrandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/constraints" +) + +// Fulfils Header interface +type testHeader[H constraints.Ordered, N constraints.Unsigned] struct { + ParentHashField H + NumberField N + StateRoot H + ExtrinsicsRoot H + HashField H +} + +func (s testHeader[H, N]) ParentHash() H { + return s.ParentHashField +} + +func (s testHeader[H, N]) Hash() H { + return s.HashField +} + +func (s testHeader[H, N]) Number() N { + return s.NumberField +} + +// Fulfils HeaderBackend interface +type testBackend[H constraints.Ordered, N constraints.Unsigned, Header testHeader[H, N]] struct { + header *testHeader[H, N] +} + +func (backend testBackend[H, N, Header]) Header(hash H) (*testHeader[H, N], error) { + return backend.header, nil +} + +func makePrecommit(t *testing.T, + targetHash string, + targetNumber uint, id dummyAuthID) finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] { + t.Helper() + return finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID]{ + Precommit: finalityGrandpa.Precommit[string, uint]{ + TargetHash: targetHash, + TargetNumber: targetNumber, + }, + ID: id, + } +} + +func TestJustificationEncoding(t *testing.T) { + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + expAncestries := make([]testHeader[string, uint], 0) + expAncestries = append(expAncestries, testHeader[string, uint]{ + NumberField: 100, + ParentHashField: "a", + }) + + justification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Round: 2, + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: expAncestries, + } + + encodedJustification, err := scale.Marshal(justification) + require.NoError(t, err) + + newJustificaiton := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{} + err = scale.Unmarshal(encodedJustification, &newJustificaiton) + require.NoError(t, err) + require.Equal(t, justification, newJustificaiton) +} + +func TestJustification_fromCommit(t *testing.T) { + commit := finalityGrandpa.Commit[string, uint, string, dummyAuthID]{} + client := testBackend[string, uint, testHeader[string, uint]]{} + _, err := NewJustificationFromCommit[string, uint, string, dummyAuthID, testHeader[string, uint]](client, 2, commit) + require.NotNil(t, err) + require.ErrorIs(t, err, errBadJustification) + require.Equal(t, "bad justification for header: invalid precommits for target commit", err.Error()) + + // nil header + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "b", 2, 3) + precommits = append(precommits, precommit) + + validCommit := finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + } + + clientNil := testBackend[string, uint, testHeader[string, uint]]{} + + _, err = NewJustificationFromCommit[string, uint, string, dummyAuthID, testHeader[string, uint]]( + clientNil, + 2, + validCommit, + ) + require.NotNil(t, err) + require.ErrorIs(t, err, errBadJustification) + require.Equal(t, "bad justification for header: invalid precommits for target commit", err.Error()) + + // currentHeader.Number() <= baseNumber + _, err = NewJustificationFromCommit[string, uint, string, dummyAuthID, testHeader[string, uint]]( + client, + 2, + validCommit, + ) + require.NotNil(t, err) + require.ErrorIs(t, err, errBadJustification) + require.Equal(t, "bad justification for header: invalid precommits for target commit", err.Error()) + + // happy path + clientLargeNum := testBackend[string, uint, testHeader[string, uint]]{ + header: &testHeader[string, uint]{ + NumberField: 100, + ParentHashField: "a", + }, + } + expAncestries := make([]testHeader[string, uint], 0) + expAncestries = append(expAncestries, testHeader[string, uint]{ + NumberField: 100, + ParentHashField: "a", + }) + expJustification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Round: 2, + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: expAncestries, + } + justification, err := NewJustificationFromCommit[string, uint, string, dummyAuthID, testHeader[string, uint]]( + clientLargeNum, + 2, + validCommit) + require.NoError(t, err) + require.Equal(t, expJustification, justification) +} + +func TestJustification_decodeAndVerifyFinalizes(t *testing.T) { + // Invalid Encoding + invalidEncoding := []byte{21} + _, err := decodeAndVerifyFinalizes[string, uint, string, dummyAuthID, testHeader[string, uint]]( + invalidEncoding, + hashNumber[string, uint]{}, + 2, + finalityGrandpa.VoterSet[dummyAuthID]{}) + require.NotNil(t, err) + + // Invalid target + justification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + }, + } + + encWrongTarget, err := scale.Marshal(justification) + require.NoError(t, err) + _, err = decodeAndVerifyFinalizes[string, uint, string, dummyAuthID, testHeader[string, uint]]( + encWrongTarget, + hashNumber[string, uint]{}, + 2, + finalityGrandpa.VoterSet[dummyAuthID]{}) + require.NotNil(t, err) + require.Equal(t, "invalid commit target in grandpa justification", err.Error()) + + // Happy path + headerB := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "a", + } + + headerList := []testHeader[string, uint]{ + headerB, + } + + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "a", 1, 2) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "b", 2, 3) + precommits = append(precommits, precommit) + + validJustification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: headerList, + } + + encValid, err := scale.Marshal(validJustification) + require.NoError(t, err) + + target := hashNumber[string, uint]{ + hash: "a", + number: 1, + } + + IDWeights := make([]finalityGrandpa.IDWeight[dummyAuthID], 0) + for i := 1; i <= 4; i++ { + IDWeights = append(IDWeights, finalityGrandpa.IDWeight[dummyAuthID]{dummyAuthID(i), 1}) //nolint + } + voters := finalityGrandpa.NewVoterSet(IDWeights) + + newJustification, err := decodeAndVerifyFinalizes[string, uint, string, dummyAuthID, testHeader[string, uint]]( + encValid, + target, + 2, + *voters) + require.NoError(t, err) + require.Equal(t, validJustification, newJustification) +} + +func TestJustification_verify(t *testing.T) { + // Nil voter case + auths := make(AuthorityList[dummyAuthID], 0) + justification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{} + err := justification.Verify(2, auths) + require.ErrorIs(t, err, errInvalidAuthoritiesSet) + + // happy path + for i := 1; i <= 4; i++ { + auths = append(auths, Authority[dummyAuthID]{ + dummyAuthID(i), + 1, + }) + } + + headerB := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "a", + } + + headerList := []testHeader[string, uint]{ + headerB, + } + + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "a", 1, 2) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "b", 2, 3) + precommits = append(precommits, precommit) + + validJustification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: headerList, + } + + err = validJustification.Verify(2, auths) + require.NoError(t, err) +} + +func TestJustification_verifyWithVoterSet(t *testing.T) { + // 1) invalid commit + IDWeights := make([]finalityGrandpa.IDWeight[dummyAuthID], 0) + for i := 1; i <= 4; i++ { + IDWeights = append(IDWeights, finalityGrandpa.IDWeight[dummyAuthID]{dummyAuthID(i), 1}) //nolint + } + voters := finalityGrandpa.NewVoterSet(IDWeights) + + invalidJustification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "B", + TargetNumber: 2, + Precommits: []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID]{}, + }, + } + + err := invalidJustification.verifyWithVoterSet(2, *voters) + require.ErrorIs(t, err, errBadJustification) + require.Equal(t, err.Error(), "bad justification for header: invalid commit in grandpa justification") + + // 2) visitedHashes != ancestryHashes + headerA := testHeader[string, uint]{ + HashField: "a", + } + + headerB := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "a", + } + + headerList := []testHeader[string, uint]{ + headerA, + headerB, + } + + var precommits []finalityGrandpa.SignedPrecommit[string, uint, string, dummyAuthID] + precommit := makePrecommit(t, "a", 1, 1) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "a", 1, 2) + precommits = append(precommits, precommit) + + precommit = makePrecommit(t, "b", 2, 3) + precommits = append(precommits, precommit) + + validJustification := Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: headerList, + } + + err = validJustification.verifyWithVoterSet(2, *voters) + require.ErrorIs(t, err, errBadJustification) + require.Equal(t, err.Error(), "bad justification for header: "+ + "invalid precommit ancestries in grandpa justification with unused headers") + + // Valid case + headerList = []testHeader[string, uint]{ + headerB, + } + + validJustification = Justification[string, uint, string, dummyAuthID, testHeader[string, uint]]{ + Commit: finalityGrandpa.Commit[string, uint, string, dummyAuthID]{ + TargetHash: "a", + TargetNumber: 1, + Precommits: precommits, + }, + VotesAncestries: headerList, + } + + err = validJustification.verifyWithVoterSet(2, *voters) + require.NoError(t, err) +} + +func Test_newAncestryChain(t *testing.T) { + dummyHeader := testHeader[string, uint]{ + HashField: "a", + } + expAncestryMap := make(map[string]testHeader[string, uint]) + hash := dummyHeader.Hash() + expAncestryMap[hash] = dummyHeader + type testCase[H constraints.Ordered, N constraints.Unsigned] struct { + name string + headers []testHeader[H, N] + want ancestryChain[H, N, testHeader[H, N]] + } + tests := []testCase[string, uint]{ + { + name: "noInputHeaders", + headers: []testHeader[string, uint]{}, + want: ancestryChain[string, uint, testHeader[string, uint]]{ + ancestry: make(map[string]testHeader[string, uint]), + }, + }, + { + name: "validInput", + headers: []testHeader[string, uint]{ + dummyHeader, + }, + want: ancestryChain[string, uint, testHeader[string, uint]]{ + ancestry: expAncestryMap, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newAncestryChain[string, uint](tt.headers); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newAncestryChain() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAncestryChain_Ancestry(t *testing.T) { + headerA := testHeader[string, uint]{ + HashField: "a", + } + + headerB := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "a", + } + + headerC := testHeader[string, uint]{ + HashField: "c", + ParentHashField: "b", + } + + invalidParentHeader := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "", + } + + headerList := []testHeader[string, uint]{ + headerA, + headerB, + headerC, + } + invalidHeaderList := []testHeader[string, uint]{ + invalidParentHeader, + } + validAncestryMap := newAncestryChain[string, uint](headerList) + invalidAncestryMap := newAncestryChain[string, uint](invalidHeaderList) + type testCase[H constraints.Ordered, N constraints.Unsigned] struct { + name string + chain ancestryChain[H, N, testHeader[H, N]] + base H + block H + want []H + expErr error + } + tests := []testCase[string, uint]{ + { + name: "baseEqualsBlock", + chain: validAncestryMap, + base: "a", + block: "a", + want: []string{}, + }, + { + name: "baseEqualsBlock", + chain: validAncestryMap, + base: "a", + block: "d", + expErr: errBlockNotDescendentOfBase, + }, + { + name: "invalidParentHashField", + chain: invalidAncestryMap, + base: "a", + block: "b", + expErr: errBlockNotDescendentOfBase, + }, + { + name: "validRoute", + chain: validAncestryMap, + base: "a", + block: "c", + want: []string{"b"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.chain.Ancestry(tt.base, tt.block) + assert.ErrorIs(t, err, tt.expErr) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAncestryChain_IsEqualOrDescendantOf(t *testing.T) { + headerA := testHeader[string, uint]{ + HashField: "a", + } + + headerB := testHeader[string, uint]{ + HashField: "b", + ParentHashField: "a", + } + + headerC := testHeader[string, uint]{ + HashField: "c", + ParentHashField: "b", + } + + headerList := []testHeader[string, uint]{ + headerA, + headerB, + headerC, + } + + validAncestryMap := newAncestryChain[string, uint](headerList) + type testCase[H constraints.Ordered, N constraints.Unsigned] struct { + name string + chain ancestryChain[H, N, testHeader[H, N]] + base H + block H + want bool + } + tests := []testCase[string, uint]{ + { + name: "baseEqualsBlock", + chain: validAncestryMap, + base: "a", + block: "a", + want: true, + }, + { + name: "baseEqualsBlock", + chain: validAncestryMap, + base: "a", + block: "d", + want: false, + }, + { + name: "validRoute", + chain: validAncestryMap, + base: "a", + block: "c", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.chain.IsEqualOrDescendantOf(tt.base, tt.block) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/client/consensus/grandpa/lib.go b/client/consensus/grandpa/lib.go index aff92d1f21..f90c872857 100644 --- a/client/consensus/grandpa/lib.go +++ b/client/consensus/grandpa/lib.go @@ -5,12 +5,18 @@ package grandpa import ( "github.com/ChainSafe/gossamer/internal/log" + finalityGrandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa" + "github.com/ChainSafe/gossamer/pkg/scale" + "golang.org/x/exp/constraints" ) var logger = log.NewFromGlobal(log.AddContext("consensus", "grandpa")) -type AuthorityID any +type AuthorityID interface { + constraints.Ordered + Verify(msg []byte, sig []byte) (bool, error) +} type AuthoritySignature any @@ -20,6 +26,8 @@ type Authority[ID AuthorityID] struct { Weight uint64 } +type AuthorityList[ID AuthorityID] []Authority[ID] + // NewAuthoritySetStruct A new authority set along with the canonical block it changed at. type NewAuthoritySetStruct[H comparable, N constraints.Unsigned, ID AuthorityID] struct { CanonNumber N @@ -27,3 +35,46 @@ type NewAuthoritySetStruct[H comparable, N constraints.Unsigned, ID AuthorityID] SetId N Authorities []Authority[ID] } + +type messageData[H comparable, N constraints.Unsigned] struct { + Round uint64 + SetID uint64 + Message finalityGrandpa.Message[H, N] +} + +// Check a message signature by encoding the message as a localised payload and +// verifying the provided signature using the expected authority id. +// The encoding necessary to verify the signature will be done using the given +// buffer, the original content of the buffer will be cleared. +func checkMessageSignature[H comparable, N constraints.Unsigned, ID AuthorityID]( + message finalityGrandpa.Message[H, N], + id ID, + signature any, + round uint64, + setID uint64) (bool, error) { + + sig, ok := signature.([]byte) + + // Verify takes []byte, but string is a valid signature type, + // so if signature is not already type []byte, check if it is a string + sigString, okString := signature.(string) + if !okString && !ok { + sig = []byte(sigString) + } + + m := messageData[H, N]{ + round, + setID, + message, + } + + enc, err := scale.Marshal(m) + if err != nil { + return false, err + } + valid, err := id.Verify(enc, sig[:]) + if err != nil { + return false, err + } + return valid, nil +} diff --git a/client/consensus/grandpa/lib_test.go b/client/consensus/grandpa/lib_test.go new file mode 100644 index 0000000000..e7775a1826 --- /dev/null +++ b/client/consensus/grandpa/lib_test.go @@ -0,0 +1,51 @@ +// Copyright 2023 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package grandpa + +import ( + "testing" + + finalityGrandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa" + "github.com/ChainSafe/gossamer/pkg/scale" + "github.com/stretchr/testify/require" +) + +type dummyAuthID uint + +func (dummyAuthID) Verify(_ []byte, _ []byte) (bool, error) { + return true, nil +} + +type dummyInvalidAuthID uint + +func (dummyInvalidAuthID) Verify(_ []byte, _ []byte) (bool, error) { + return false, nil +} + +func Test_checkMessageSignature(t *testing.T) { + pubKeyValid := dummyAuthID(1) + pubKeyInvalid := dummyInvalidAuthID(1) + + message := finalityGrandpa.Message[string, uint]{ + Value: 4, + } + + msg := messageData[string, uint]{ + 1, + 2, + message, + } + + // Dummy signature + encMsg, err := scale.Marshal(msg) + require.NoError(t, err) + + valid, err := checkMessageSignature[string, uint, dummyAuthID](message, pubKeyValid, encMsg, 1, 2) + require.NoError(t, err) + require.True(t, valid) + + invalid, err := checkMessageSignature[string, uint, dummyInvalidAuthID](message, pubKeyInvalid, encMsg, 1, 2) + require.NoError(t, err) + require.False(t, invalid) +} diff --git a/pkg/finality-grandpa/lib.go b/pkg/finality-grandpa/lib.go index 39246c4985..2226182f14 100644 --- a/pkg/finality-grandpa/lib.go +++ b/pkg/finality-grandpa/lib.go @@ -54,12 +54,12 @@ type Equivocation[ID constraints.Ordered, Vote, Signature comparable] struct { // Message is a protocol message or vote. type Message[Hash, Number any] struct { - value any + Value any } // Target returns the target block of the vote. func (m Message[H, N]) Target() HashNumber[H, N] { - switch message := m.value.(type) { + switch message := m.Value.(type) { case Prevote[H, N]: return HashNumber[H, N]{ message.TargetHash, @@ -80,18 +80,13 @@ func (m Message[H, N]) Target() HashNumber[H, N] { } } -// Value returns the message constrained by `Messages` -func (m Message[H, N]) Value() any { - return m.value -} - // Messages is the interface constraint for `Message` type Messages[Hash, Number any] interface { Prevote[Hash, Number] | Precommit[Hash, Number] | PrimaryPropose[Hash, Number] } func setMessage[Hash, Number any, T Messages[Hash, Number]](m *Message[Hash, Number], val T) { - m.value = val + m.Value = val } func newMessage[Hash, Number any, T Messages[Hash, Number]](val T) (m Message[Hash, Number]) { diff --git a/pkg/finality-grandpa/round_test.go b/pkg/finality-grandpa/round_test.go index 5c60cf2cf6..19b3a39cb4 100644 --- a/pkg/finality-grandpa/round_test.go +++ b/pkg/finality-grandpa/round_test.go @@ -278,7 +278,7 @@ func TestRound_HistoricalVotesWorks(t *testing.T) { seen: []SignedMessage[string, uint32, string, string]{ { Message: Message[string, uint32]{ - value: Prevote[string, uint32]{ + Value: Prevote[string, uint32]{ TargetHash: "FC", TargetNumber: 10, }, @@ -288,7 +288,7 @@ func TestRound_HistoricalVotesWorks(t *testing.T) { }, { Message: Message[string, uint32]{ - value: Prevote[string, uint32]{ + Value: Prevote[string, uint32]{ TargetHash: "EA", TargetNumber: 7, }, @@ -298,7 +298,7 @@ func TestRound_HistoricalVotesWorks(t *testing.T) { }, { Message: Message[string, uint32]{ - value: Precommit[string, uint32]{ + Value: Precommit[string, uint32]{ TargetHash: "EA", TargetNumber: 7, }, @@ -308,7 +308,7 @@ func TestRound_HistoricalVotesWorks(t *testing.T) { }, { Message: Message[string, uint32]{ - value: Prevote[string, uint32]{ + Value: Prevote[string, uint32]{ TargetHash: "EC", TargetNumber: 10, }, diff --git a/pkg/finality-grandpa/voter_test.go b/pkg/finality-grandpa/voter_test.go index fd92f9bdc9..e8acb2a6f9 100644 --- a/pkg/finality-grandpa/voter_test.go +++ b/pkg/finality-grandpa/voter_test.go @@ -296,7 +296,7 @@ func TestVoter_BroadcastCommitOnlyIfNewer(t *testing.T) { item := <-roundIn // wait for a prevote assert.NoError(t, item.Error) - assert.IsType(t, Prevote[string, uint32]{}, item.SignedMessage.Message.value) + assert.IsType(t, Prevote[string, uint32]{}, item.SignedMessage.Message.Value) assert.Equal(t, localID, item.SignedMessage.ID) // send our prevote and precommit @@ -310,7 +310,7 @@ waitForPrecommit: item = <-roundIn // wait for a precommit assert.NoError(t, item.Error) - switch item.SignedMessage.Message.value.(type) { + switch item.SignedMessage.Message.Value.(type) { case Precommit[string, uint32]: if item.SignedMessage.ID == localID { break waitForPrecommit @@ -649,7 +649,7 @@ waitForPrevote: t.Errorf("wtf?") } - msg := sme.SignedMessage.Message.Value() + msg := sme.SignedMessage.Message.Value switch msg.(type) { case Prevote[string, uint32]: if sme.SignedMessage.ID == localID { diff --git a/pkg/finality-grandpa/voting_round.go b/pkg/finality-grandpa/voting_round.go index 47998f3134..92d94516a3 100644 --- a/pkg/finality-grandpa/voting_round.go +++ b/pkg/finality-grandpa/voting_round.go @@ -396,7 +396,7 @@ func (vr *votingRound[Hash, Number, Signature, ID, E]) handleVote(vote SignedMes return nil } - switch message := message.Value().(type) { + switch message := message.Value.(type) { case Prevote[Hash, Number]: prevote := message importResult, err := vr.votes.importPrevote(vr.env, prevote, vote.ID, vote.Signature)