Skip to content

Commit a6e2d81

Browse files
authored
feat(client/consensus/grandpa): BlockImport implementation for GRANDPA (#4762)
1 parent 0bc442c commit a6e2d81

File tree

14 files changed

+1299
-10
lines changed

14 files changed

+1299
-10
lines changed

internal/client/api/backend.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ type AuxStore interface {
254254
GetAux(key []byte) ([]byte, error)
255255
}
256256

257+
// StorageProvider provides access to storage primitives
258+
type StorageProvider[H runtime.Hash, N runtime.Number, Hasher runtime.Hasher[H]] interface {
259+
// Given a block hash and a key, return the value under the key in that block.
260+
Storage(hash H, key storage.StorageKey) (storage.StorageData, error)
261+
}
262+
257263
// Backend is the client backend.
258264
//
259265
// Manages the data layer.

internal/client/consensus/common/block_import.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,29 @@ type BlockImportParams[H runtime.Hash, N runtime.Number, E runtime.Extrinsic, He
183183
// Cached full header hash (with post-digests applied).
184184
PostHash *H
185185
}
186+
187+
// GetPostHash retrieves the full header hash (with post-digests applied).
188+
func (b *BlockImportParams[H, N, E, Header]) GetPostHash() H {
189+
if b.PostHash != nil {
190+
return *b.PostHash
191+
}
192+
return b.GetPostHeader().Hash()
193+
}
194+
195+
// GetPostHeader retrieves the post header.
196+
func (b *BlockImportParams[H, N, E, Header]) GetPostHeader() Header {
197+
if len(b.PostDigests) == 0 {
198+
return b.Header.Clone().(Header)
199+
}
200+
hdr := b.Header.Clone().(Header)
201+
for _, digestItem := range b.PostDigests {
202+
hdr.DigestMut().Push(digestItem)
203+
}
204+
return hdr
205+
}
206+
207+
// WithState checks if this block contains state import action
208+
func (b *BlockImportParams[H, N, E, Header]) WithState() bool {
209+
_, ok := b.StateAction.(StateActionApplyChanges)
210+
return ok
211+
}

internal/client/consensus/grandpa/authorities.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ func (sas *SharedAuthoritySet[H, N]) applyForcedChanges( //nolint:unused
144144
bestHash H,
145145
bestNumber N,
146146
isDescendentOf IsDescendentOf[H],
147+
initialSync bool,
147148
// TODO: telemtry,
148149
) (newSet *appliedChanges[H, N], err error) {
149150
authSet := sas.inner.Data()
150-
return authSet.applyForcedChanges(bestHash, bestNumber, isDescendentOf)
151+
return authSet.applyForcedChanges(bestHash, bestNumber, isDescendentOf, initialSync)
151152
}
152153

153154
// applyStandardChanges will apply or prune any pending transitions based on a finality trigger. This method ensures
@@ -505,6 +506,7 @@ func (authSet *AuthoritySet[H, N]) currentLimit(min N) (limit *N) {
505506
func (authSet *AuthoritySet[H, N]) applyForcedChanges(bestHash H, //skipcq: RVV-B0001
506507
bestNumber N,
507508
isDescendentOf IsDescendentOf[H],
509+
initialSync bool,
508510
// TODO: telemetry
509511
) (newSet *appliedChanges[H, N], err error) {
510512

@@ -541,7 +543,11 @@ func (authSet *AuthoritySet[H, N]) applyForcedChanges(bestHash H, //skipcq: RVV
541543
}
542544

543545
// apply this change: make the set canonical
544-
logger.Infof("👴 Applying authority set change forced at block #%d", change.CanonHeight)
546+
l := logger.Infof
547+
if initialSync {
548+
l = logger.Debugf
549+
}
550+
l("👴 Applying authority set change forced at block #%d", change.CanonHeight)
545551

546552
// TODO telemetry
547553

internal/client/consensus/grandpa/authorities_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,12 +577,13 @@ func TestForceChanges(t *testing.T) {
577577
"hash_a10",
578578
10,
579579
staticIsDescendentOf[string](true),
580+
false,
580581
)
581582
require.NoError(t, err)
582583
require.Nil(t, resForced)
583584

584585
// too late
585-
resForced, err = authorities.applyForcedChanges("hash_a16", 16, isDescOfA)
586+
resForced, err = authorities.applyForcedChanges("hash_a16", 16, isDescOfA, false)
586587
require.NoError(t, err)
587588
require.Nil(t, resForced)
588589

@@ -602,7 +603,7 @@ func TestForceChanges(t *testing.T) {
602603
},
603604
},
604605
}
605-
resForced, err = authorities.applyForcedChanges("hash_a15", 15, isDescOfA)
606+
resForced, err = authorities.applyForcedChanges("hash_a15", 15, isDescOfA, false)
606607
require.NoError(t, err)
607608
require.NotNil(t, resForced)
608609
require.Equal(t, exp, *resForced)
@@ -644,6 +645,7 @@ func TestForceChangesWithNoDelay(t *testing.T) {
644645
hashA,
645646
5,
646647
staticIsDescendentOf[string](false),
648+
false,
647649
)
648650
require.NoError(t, err)
649651
require.NotNil(t, resForced)
@@ -723,6 +725,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) {
723725
"hash_d45",
724726
45,
725727
staticIsDescendentOf[string](true),
728+
false,
726729
)
727730
require.ErrorIs(t, err, errForcedAuthoritySetChangeDependencyUnsatisfied)
728731
require.Equal(t, 0, len(authorities.AuthoritySetChanges))
@@ -748,6 +751,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) {
748751
"hash_d45",
749752
45,
750753
staticIsDescendentOf[string](true),
754+
false,
751755
)
752756
require.ErrorIs(t, err, errForcedAuthoritySetChangeDependencyUnsatisfied)
753757
require.Equal(t, expChanges, authorities.AuthoritySetChanges)
@@ -787,6 +791,7 @@ func TestForceChangesBlockedByStandardChanges(t *testing.T) {
787791
hashD,
788792
45,
789793
staticIsDescendentOf[string](true),
794+
false,
790795
)
791796
require.NoError(t, err)
792797
require.NotNil(t, resForced)

internal/client/consensus/grandpa/aux_schema.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88

99
"github.com/ChainSafe/gossamer/internal/client/api"
10+
shareddata "github.com/ChainSafe/gossamer/internal/client/consensus/common/shared-data"
11+
primitives "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa"
1012
"github.com/ChainSafe/gossamer/internal/primitives/runtime"
1113
grandpa "github.com/ChainSafe/gossamer/pkg/finality-grandpa"
1214
"github.com/ChainSafe/gossamer/pkg/scale"
@@ -21,12 +23,102 @@ var (
2123

2224
type writeAux func(insertions []api.KeyValue) error
2325

26+
type getGenesisAuthorities func() (primitives.AuthorityList, error)
27+
2428
// Persistent data kept between runs.
2529
type persistentData[H runtime.Hash, N runtime.Number] struct {
2630
authoritySet *SharedAuthoritySet[H, N]
2731
setState *SharedVoterSetState[H, N]
2832
}
2933

34+
func loadDecoded[T any](store api.AuxStore, key []byte) (*T, error) {
35+
encodedValue, err := store.GetAux(key)
36+
if err != nil {
37+
return nil, err
38+
}
39+
if encodedValue == nil {
40+
return nil, nil
41+
}
42+
43+
var dst T
44+
err = scale.Unmarshal(encodedValue, &dst)
45+
if err != nil {
46+
return nil, err
47+
}
48+
return &dst, nil
49+
}
50+
51+
func loadPersistent[H runtime.Hash, N runtime.Number](
52+
store api.AuxStore,
53+
genesisHash H,
54+
genesisNumber N,
55+
genesisAuths getGenesisAuthorities,
56+
) (*persistentData[H, N], error) {
57+
genesis := grandpa.HashNumber[H, N]{Hash: genesisHash, Number: genesisNumber}
58+
makeGenesisRound := grandpa.NewRoundState[H, N]
59+
60+
authSet, err := loadDecoded[AuthoritySet[H, N]](store, authoritySetKey)
61+
if err != nil {
62+
return nil, err
63+
}
64+
if authSet != nil {
65+
var setState voterSetState[H, N]
66+
state, err := loadDecoded[voterSetStateVDT[H, N]](store, setStateKey)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
if state != nil && state.inner != nil {
72+
setState = state.inner
73+
} else {
74+
state := makeGenesisRound(genesis)
75+
if state.PrevoteGHOST == nil {
76+
panic("state is for completed round; completed rounds must have a prevote ghost; qed.")
77+
}
78+
base := state.PrevoteGHOST
79+
setState = newVoterSetStateLive(primitives.SetID(authSet.SetID), *authSet, *base)
80+
}
81+
82+
return &persistentData[H, N]{
83+
authoritySet: &SharedAuthoritySet[H, N]{inner: *shareddata.NewSharedData(*authSet)},
84+
setState: &SharedVoterSetState[H, N]{inner: setState},
85+
}, nil
86+
}
87+
88+
logger.Info("👴 Loading GRANDPA authority set from genesis on what appears to be first startup")
89+
genesisAuthorities, err := genesisAuths()
90+
if err != nil {
91+
return nil, err
92+
}
93+
genesisSet, err := NewGenesisAuthoritySet[H, N](genesisAuthorities)
94+
if err != nil {
95+
panic("genesis authorities is non-empty; all weights are non-zero; qed.")
96+
}
97+
98+
state := makeGenesisRound(genesis)
99+
base := state.PrevoteGHOST
100+
if base == nil {
101+
panic("state is for completed round; completed rounds must have a prevote ghost; qed.")
102+
}
103+
104+
genesisState := newVoterSetStateLive(0, *genesisSet, *base)
105+
genesisStateVDT := newVoterSetStateVDT[H, N]()
106+
genesisStateVDT.inner = genesisState
107+
insert := []api.KeyValue{
108+
{Key: authoritySetKey, Value: scale.MustMarshal(*genesisSet)},
109+
{Key: setStateKey, Value: scale.MustMarshal(genesisStateVDT)},
110+
}
111+
err = store.InsertAux(insert, nil)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
return &persistentData[H, N]{
117+
authoritySet: &SharedAuthoritySet[H, N]{inner: *shareddata.NewSharedData(*genesisSet)},
118+
setState: &SharedVoterSetState[H, N]{inner: genesisState},
119+
}, nil
120+
}
121+
30122
// updateAuthoritySet Update the authority set on disk after a change.
31123
//
32124
// If there has just been a handoff, pass a newSet parameter that describes the handoff. set in all cases should

internal/client/consensus/grandpa/grandpa.go

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ type ClientForGrandpa[
110110
papi.ProvideRuntimeAPI[primitives.GrandpaAPI[H, N]]
111111
// api.ExecutorProvider
112112
client_common.BlockImport[H, N, E, Header]
113-
// api.StorageProvider[H, N, Hasher]
113+
api.StorageProvider[H, N, Hasher]
114114
}
115115

116116
// Something that one can ask to do a block sync request.
@@ -165,7 +165,7 @@ type LinkHalf[
165165
persistentData persistentData[H, N]
166166
voterCommandsRx chan voterCommand
167167
justificationSender GrandpaJustificationSender[H, N, Header]
168-
justificationStream GrandpaJustificationStream[H, N, Header] //nolint: unused
168+
justificationStream GrandpaJustificationStream[H, N, Header]
169169
}
170170

171171
// Provider for the Grandpa authority set configured on the genesis block.
@@ -174,6 +174,127 @@ type GenesisAuthoritySetProvider interface {
174174
Get() (primitives.AuthorityList, error)
175175
}
176176

177+
// Make block importer and link half necessary to tie the background voter to it.
178+
//
179+
// The justificationImportPeriod sets the minimum period on which justifications will be imported. When importing
180+
// a block, if it includes a justification it will only be processed if it fits within this period, otherwise it will
181+
// be ignored (and won't be validated). This is to avoid slowing down sync by a peer serving us unnecessary
182+
// justifications which aren't trivial to validate.
183+
func BlockImport[
184+
H runtime.Hash,
185+
N runtime.Number,
186+
Hasher runtime.Hasher[H],
187+
Header runtime.Header[N, H],
188+
E runtime.Extrinsic,
189+
](
190+
client ClientForGrandpa[H, N, Hasher, Header, E],
191+
justificationImportPeriod uint32,
192+
genesisAuthoritySetProvider GenesisAuthoritySetProvider,
193+
selectChain common.SelectChain[H, N, Header],
194+
// TODO: telemetry
195+
) (*GrandpaBlockImport[H, N, Hasher, Header, E], LinkHalf[H, N, Hasher, Header, E], error) {
196+
return blockImportWithAuthoritySetHardForks(
197+
client,
198+
justificationImportPeriod,
199+
genesisAuthoritySetProvider,
200+
selectChain,
201+
nil,
202+
)
203+
}
204+
205+
// A descriptor for an authority set hard fork. These are authority set changes that are not signalled by the runtime
206+
// and instead are defined off-chain (hence the hard fork).
207+
type AuthoritySetHardFork[H, N any] struct {
208+
// The new authority set id.
209+
SetID SetID
210+
// The block hash and number at which the hard fork should be applied.
211+
Block HashNumber[H, N]
212+
// The authorities in the new set.
213+
Authorities primitives.AuthorityList
214+
// The latest block number that was finalized before this authority set hard fork. When defined, the authority set
215+
// change will be forced, i.e. the node won't wait for the block above to be finalized before enacting the change,
216+
// and the given finalized number will be used as a base for voting.
217+
LastFinalized *N
218+
}
219+
220+
// Make block importer and link half necessary to tie the background voter to it. A vector of authority set hard forks
221+
// can be passed, any authority set change signalled at the given block (either already signalled or in a further block
222+
// when importing it) will be replaced by a standard change with the given static authorities.
223+
func blockImportWithAuthoritySetHardForks[
224+
H runtime.Hash,
225+
N runtime.Number,
226+
Hasher runtime.Hasher[H],
227+
Header runtime.Header[N, H],
228+
E runtime.Extrinsic,
229+
](
230+
client ClientForGrandpa[H, N, Hasher, Header, E],
231+
justificationImportPeriod uint32,
232+
genesisAuthoritySetProvider GenesisAuthoritySetProvider,
233+
selectChain common.SelectChain[H, N, Header],
234+
authoritySetHardForks []AuthoritySetHardFork[H, N],
235+
// TODO: telemetry
236+
) (*GrandpaBlockImport[H, N, Hasher, Header, E], LinkHalf[H, N, Hasher, Header, E], error) {
237+
chainInfo := client.Info()
238+
genesisHash := chainInfo.GenesisHash
239+
240+
persistentData, err := loadPersistent[H, N](client, genesisHash, 0, genesisAuthoritySetProvider.Get)
241+
if err != nil {
242+
return nil, LinkHalf[H, N, Hasher, Header, E]{}, err
243+
}
244+
245+
voterCommands := make(chan voterCommand, 100000)
246+
247+
justificationSender, justificationStream := NewGrandpaJustificationSender[H, N, Header]()
248+
249+
// create pending change objects with 0 delay for each authority set hard fork.
250+
hardForks := make([]struct {
251+
SetID
252+
PendingChange[H, N]
253+
}, len(authoritySetHardForks))
254+
for i, fork := range authoritySetHardForks {
255+
var kind delayKind
256+
if fork.LastFinalized != nil {
257+
kind = delayKindBest[N]{MedianLastFinalized: *fork.LastFinalized}
258+
} else {
259+
kind = delayKindFinalized{}
260+
}
261+
262+
hardForks[i] = struct {
263+
SetID
264+
PendingChange[H, N]
265+
}{
266+
SetID: fork.SetID,
267+
PendingChange: PendingChange[H, N]{
268+
NextAuthorities: fork.Authorities,
269+
Delay: 0,
270+
CanonHash: fork.Block.Hash,
271+
CanonHeight: fork.Block.Number,
272+
DelayKind: kind,
273+
},
274+
}
275+
}
276+
277+
blockImport := newGrandpaBlockImport(
278+
client,
279+
justificationImportPeriod,
280+
selectChain,
281+
persistentData.authoritySet,
282+
voterCommands,
283+
hardForks,
284+
justificationSender,
285+
)
286+
287+
linkHalf := LinkHalf[H, N, Hasher, Header, E]{
288+
client: client,
289+
selectChain: selectChain,
290+
persistentData: *persistentData,
291+
voterCommandsRx: voterCommands,
292+
justificationSender: justificationSender,
293+
justificationStream: justificationStream,
294+
}
295+
return blockImport, linkHalf, nil
296+
}
297+
177298
func globalCommunication[
178299
H runtime.Hash,
179300
N runtime.Number,

0 commit comments

Comments
 (0)