From 55446ab7d85c1c1094a645712a8d3c8d0ea9a514 Mon Sep 17 00:00:00 2001 From: Diego Date: Wed, 29 Jan 2025 12:55:04 -0300 Subject: [PATCH] feat(dot/sync): Implement warp sync strategy (#4275) --- chain/kusama/defaults.go | 1 + chain/paseo/defaults.go | 1 + chain/polkadot/defaults.go | 1 + chain/westend-dev/defaults.go | 1 + chain/westend-local/default.go | 1 + chain/westend/defaults.go | 1 + cmd/gossamer/commands/root.go | 8 + config/config.go | 7 + dot/network/host_integration_test.go | 12 +- dot/network/messages/warp_sync.go | 6 + dot/network/mock_warp_sync_provider_test.go | 5 +- dot/network/service.go | 16 +- dot/network/warp_sync.go | 11 +- dot/peerset/constants.go | 10 + dot/services.go | 34 +- dot/sync/configuration.go | 11 +- dot/sync/fullsync.go | 13 +- dot/sync/fullsync_test.go | 24 +- dot/sync/message_integration_test.go | 2 +- dot/sync/mocks_generate_test.go | 2 +- dot/sync/mocks_test.go | 59 +++- dot/sync/peer_view.go | 8 +- dot/sync/service.go | 23 +- dot/sync/warp_sync.go | 303 ++++++++++++++++++ dot/sync/warp_sync_test.go | 174 ++++++++++ lib/common/hash.go | 4 +- lib/grandpa/warpsync/mocks_generate_test.go | 6 + lib/grandpa/warpsync/mocks_test.go | 169 ++++++++++ .../warpsync/testdata/warp_sync_proofs.yaml | 1 + lib/grandpa/{ => warpsync}/warp_sync.go | 112 +++++-- lib/grandpa/{ => warpsync}/warp_sync_test.go | 49 ++- 31 files changed, 1004 insertions(+), 71 deletions(-) create mode 100644 dot/sync/warp_sync.go create mode 100644 dot/sync/warp_sync_test.go create mode 100644 lib/grandpa/warpsync/mocks_generate_test.go create mode 100644 lib/grandpa/warpsync/mocks_test.go create mode 100644 lib/grandpa/warpsync/testdata/warp_sync_proofs.yaml rename lib/grandpa/{ => warpsync}/warp_sync.go (77%) rename lib/grandpa/{ => warpsync}/warp_sync_test.go (89%) diff --git a/chain/kusama/defaults.go b/chain/kusama/defaults.go index 589b666084..26054fa83a 100644 --- a/chain/kusama/defaults.go +++ b/chain/kusama/defaults.go @@ -25,6 +25,7 @@ func DefaultConfig() *cfg.Config { config.Core.GrandpaAuthority = false config.Core.Role = 1 config.Network.NoMDNS = false + config.Core.Sync = "full" return config } diff --git a/chain/paseo/defaults.go b/chain/paseo/defaults.go index aafee6e882..30bdc77e3f 100644 --- a/chain/paseo/defaults.go +++ b/chain/paseo/defaults.go @@ -24,6 +24,7 @@ func DefaultConfig() *cfg.Config { config.Core.GrandpaAuthority = false config.Core.Role = 1 config.Network.NoMDNS = false + config.Core.Sync = "full" return config } diff --git a/chain/polkadot/defaults.go b/chain/polkadot/defaults.go index 48ce349c67..c0e405f69c 100644 --- a/chain/polkadot/defaults.go +++ b/chain/polkadot/defaults.go @@ -24,6 +24,7 @@ func DefaultConfig() *cfg.Config { config.Core.GrandpaAuthority = false config.Core.Role = 1 config.Network.NoMDNS = false + config.Core.Sync = "full" return config } diff --git a/chain/westend-dev/defaults.go b/chain/westend-dev/defaults.go index e171ae3382..912322ba79 100644 --- a/chain/westend-dev/defaults.go +++ b/chain/westend-dev/defaults.go @@ -24,6 +24,7 @@ func DefaultConfig() *cfg.Config { config.RPC.UnsafeRPC = true config.RPC.WSExternal = true config.RPC.UnsafeWSExternal = true + config.Core.Sync = "full" return config } diff --git a/chain/westend-local/default.go b/chain/westend-local/default.go index 0bedec5eae..9e43c5a648 100644 --- a/chain/westend-local/default.go +++ b/chain/westend-local/default.go @@ -29,6 +29,7 @@ func DefaultConfig() *cfg.Config { config.RPC.UnsafeRPC = true config.RPC.WSExternal = true config.RPC.UnsafeWSExternal = true + config.Core.Sync = "full" return config } diff --git a/chain/westend/defaults.go b/chain/westend/defaults.go index 416c30b2a4..1419207084 100644 --- a/chain/westend/defaults.go +++ b/chain/westend/defaults.go @@ -24,6 +24,7 @@ func DefaultConfig() *cfg.Config { config.Core.GrandpaAuthority = false config.Core.Role = 1 config.Network.NoMDNS = false + config.Core.Sync = "full" return config } diff --git a/cmd/gossamer/commands/root.go b/cmd/gossamer/commands/root.go index 0003833ef5..6a8a33416b 100644 --- a/cmd/gossamer/commands/root.go +++ b/cmd/gossamer/commands/root.go @@ -529,6 +529,14 @@ func addCoreFlags(cmd *cobra.Command) error { return fmt.Errorf("failed to add --grandpa-interval flag: %s", err) } + if err := addStringFlagBindViper(cmd, + "sync", + config.Core.Sync, + "sync mode [warp | full]", + "core.sync"); err != nil { + return fmt.Errorf("failed to add --sync flag: %s", err) + } + return nil } diff --git a/config/config.go b/config/config.go index 4c50fe3d67..9ff07eb042 100644 --- a/config/config.go +++ b/config/config.go @@ -61,6 +61,9 @@ const ( DefaultSystemName = "Gossamer" // DefaultSystemVersion is the default system version DefaultSystemVersion = "0.0.0" + + // DefaultSyncMode is the default block sync mode + DefaultSyncMode = "full" ) // DefaultRPCModules the default RPC modules @@ -188,6 +191,7 @@ type CoreConfig struct { GrandpaAuthority bool `mapstructure:"grandpa-authority"` WasmInterpreter string `mapstructure:"wasm-interpreter,omitempty"` GrandpaInterval time.Duration `mapstructure:"grandpa-interval,omitempty"` + Sync string `mapstructure:"sync,omitempty"` } // StateConfig contains the configuration for the state. @@ -363,6 +367,7 @@ func DefaultConfig() *Config { GrandpaAuthority: true, WasmInterpreter: DefaultWasmInterpreter, GrandpaInterval: DefaultDiscoveryInterval, + Sync: DefaultSyncMode, }, Network: &NetworkConfig{ Port: DefaultNetworkPort, @@ -444,6 +449,7 @@ func DefaultConfigFromSpec(nodeSpec *genesis.Genesis) *Config { GrandpaAuthority: true, WasmInterpreter: DefaultWasmInterpreter, GrandpaInterval: DefaultDiscoveryInterval, + Sync: DefaultSyncMode, }, Network: &NetworkConfig{ Port: DefaultNetworkPort, @@ -525,6 +531,7 @@ func Copy(c *Config) Config { GrandpaAuthority: c.Core.GrandpaAuthority, WasmInterpreter: c.Core.WasmInterpreter, GrandpaInterval: c.Core.GrandpaInterval, + Sync: c.Core.Sync, }, Network: &NetworkConfig{ Port: c.Network.Port, diff --git a/dot/network/host_integration_test.go b/dot/network/host_integration_test.go index 17ac938226..2f93c5c25e 100644 --- a/dot/network/host_integration_test.go +++ b/dot/network/host_integration_test.go @@ -7,6 +7,7 @@ package network import ( "fmt" + "strings" "testing" "time" @@ -396,12 +397,21 @@ func Test_PeerSupportsProtocol(t *testing.T) { } require.NoError(t, err) + genesisHash := nodeA.blockState.GenesisHash().String() + genesisHash = strings.TrimPrefix(genesisHash, "0x") + fullSyncProtocolId := fmt.Sprintf("/%s%s", genesisHash, SyncID) + warpSyncProtocolId := fmt.Sprintf("/%s%s", genesisHash, WarpSyncID) + tests := []struct { protocol protocol.ID expect bool }{ { - protocol: protocol.ID("/gossamer/test/0/sync/2"), + protocol: protocol.ID(fullSyncProtocolId), + expect: true, + }, + { + protocol: protocol.ID(warpSyncProtocolId), expect: true, }, { diff --git a/dot/network/messages/warp_sync.go b/dot/network/messages/warp_sync.go index 2e2897b6a1..4307161fb0 100644 --- a/dot/network/messages/warp_sync.go +++ b/dot/network/messages/warp_sync.go @@ -15,6 +15,12 @@ type WarpProofRequest struct { Begin common.Hash } +func NewWarpProofRequest(from common.Hash) *WarpProofRequest { + return &WarpProofRequest{ + Begin: from, + } +} + // Decode decodes the message into a WarpProofRequest func (wpr *WarpProofRequest) Decode(in []byte) error { return scale.Unmarshal(in, wpr) diff --git a/dot/network/mock_warp_sync_provider_test.go b/dot/network/mock_warp_sync_provider_test.go index 04280930e7..4425377b94 100644 --- a/dot/network/mock_warp_sync_provider_test.go +++ b/dot/network/mock_warp_sync_provider_test.go @@ -14,6 +14,7 @@ import ( grandpa "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" common "github.com/ChainSafe/gossamer/lib/common" + warpsync "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" gomock "go.uber.org/mock/gomock" ) @@ -56,10 +57,10 @@ func (mr *MockWarpSyncProviderMockRecorder) Generate(arg0 any) *gomock.Call { } // Verify mocks base method. -func (m *MockWarpSyncProvider) Verify(arg0 []byte, arg1 grandpa.SetID, arg2 grandpa.AuthorityList) (*WarpSyncVerificationResult, error) { +func (m *MockWarpSyncProvider) Verify(arg0 []byte, arg1 grandpa.SetID, arg2 grandpa.AuthorityList) (*warpsync.WarpSyncVerificationResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2) - ret0, _ := ret[0].(*WarpSyncVerificationResult) + ret0, _ := ret[0].(*warpsync.WarpSyncVerificationResult) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/dot/network/service.go b/dot/network/service.go index f916e18359..41cd4fa3a8 100644 --- a/dot/network/service.go +++ b/dot/network/service.go @@ -259,11 +259,14 @@ func (s *Service) Start() error { s.ctx, s.cancel = context.WithCancel(context.Background()) } - genesisHashProtocolId := protocol.ID(s.cfg.BlockState.GenesisHash().String()) + genesisHash := s.blockState.GenesisHash().String() + genesisHash = strings.TrimPrefix(genesisHash, "0x") + fullSyncProtocolId := fmt.Sprintf("/%s%s", genesisHash, SyncID) + warpSyncProtocolId := fmt.Sprintf("/%s%s", genesisHash, WarpSyncID) - s.host.registerStreamHandler(s.host.protocolID+SyncID, s.handleSyncStream) + s.host.registerStreamHandler(protocol.ID(fullSyncProtocolId), s.handleSyncStream) s.host.registerStreamHandler(s.host.protocolID+lightID, s.handleLightStream) - s.host.registerStreamHandler(genesisHashProtocolId+WarpSyncID, s.handleWarpSyncStream) + s.host.registerStreamHandler(protocol.ID(warpSyncProtocolId), s.handleWarpSyncStream) // register block announce protocol err := s.RegisterNotificationsProtocol( @@ -622,13 +625,16 @@ func (s *Service) SendMessage(to peer.ID, msg NotificationsMessage) error { func (s *Service) GetRequestResponseProtocol(subprotocol string, requestTimeout time.Duration, maxResponseSize uint64) *RequestResponseProtocol { - protocolID := s.host.protocolID + protocol.ID(subprotocol) + genesisHash := s.blockState.GenesisHash().String() + genesisHash = strings.TrimPrefix(genesisHash, "0x") + protocolId := fmt.Sprintf("/%s%s", genesisHash, subprotocol) + return &RequestResponseProtocol{ ctx: s.ctx, host: s.host, requestTimeout: requestTimeout, maxResponseSize: maxResponseSize, - protocolID: protocolID, + protocolID: protocol.ID(protocolId), responseBuf: make([]byte, maxResponseSize), responseBufMu: sync.Mutex{}, } diff --git a/dot/network/warp_sync.go b/dot/network/warp_sync.go index 5e5e7b85b5..c72c8333ea 100644 --- a/dot/network/warp_sync.go +++ b/dot/network/warp_sync.go @@ -8,23 +8,16 @@ import ( "fmt" "github.com/ChainSafe/gossamer/dot/network/messages" - "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" primitives "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" libp2pnetwork "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" ) const MaxAllowedSameRequestPerPeer = 5 -type WarpSyncVerificationResult struct { - SetId grandpa.SetID - AuthorityList primitives.AuthorityList - Header types.Header - Completed bool -} - // WarpSyncProvider is an interface for generating warp sync proofs type WarpSyncProvider interface { // Generate proof starting at given block hash. The proof is accumulated until maximum proof @@ -34,7 +27,7 @@ type WarpSyncProvider interface { encodedProof []byte, setId grandpa.SetID, authorities primitives.AuthorityList, - ) (*WarpSyncVerificationResult, error) + ) (*warpsync.WarpSyncVerificationResult, error) } func (s *Service) handleWarpSyncRequest(req messages.WarpProofRequest) ([]byte, error) { diff --git a/dot/peerset/constants.go b/dot/peerset/constants.go index d195b1e0db..095d334ec4 100644 --- a/dot/peerset/constants.go +++ b/dot/peerset/constants.go @@ -76,4 +76,14 @@ const ( // SameBlockSyncRequest used when a peer send us more than the max number of the same request. SameBlockSyncRequest Reputation = math.MinInt32 SameBlockSyncRequestReason = "same block sync request" + + // UnexpectedResponseValue is used when peer send an unexpected response. + UnexpectedResponseValue Reputation = -(1 << 29) + // UnexpectedResponseReason is used when peer send an unexpected response. + UnexpectedResponseReason = "Unexpected response" + + // BadWarpProofValue is used when peer send invalid warp sync proof. + BadWarpProofValue Reputation = -(1 << 29) + // BadWarpProofReason is used when peer send invalid warp sync proof. + BadWarpProofReason = "Bad warp proof" ) diff --git a/dot/services.go b/dot/services.go index 653b400c22..c9dc92169d 100644 --- a/dot/services.go +++ b/dot/services.go @@ -31,6 +31,7 @@ import ( "github.com/ChainSafe/gossamer/lib/crypto/sr25519" "github.com/ChainSafe/gossamer/lib/genesis" "github.com/ChainSafe/gossamer/lib/grandpa" + "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" "github.com/ChainSafe/gossamer/lib/keystore" "github.com/ChainSafe/gossamer/lib/runtime" rtstorage "github.com/ChainSafe/gossamer/lib/runtime/storage" @@ -349,7 +350,7 @@ func (nodeBuilder) createNetworkService(config *cfg.Config, stateSrvc *state.Ser return nil, fmt.Errorf("failed to parse network log level: %w", err) } - warpSyncProvider := grandpa.NewWarpSyncProofProvider( + warpSyncProvider := warpsync.NewWarpSyncProofProvider( stateSrvc.Block, stateSrvc.Grandpa, ) @@ -523,8 +524,28 @@ func (nodeBuilder) newSyncService(config *cfg.Config, st *state.Service, fg sync return nil, fmt.Errorf("failed to parse sync log level: %w", err) } - requestMaker := net.GetRequestResponseProtocol(network.SyncID, - blockRequestTimeout, network.MaxBlockResponseSize) + // Should be shared between all sync strategies + peersView := sync.NewPeerViewSet() + + var warpSyncStrategy sync.Strategy + + if config.Core.Sync == "warp" { + warpSyncProvider := warpsync.NewWarpSyncProofProvider(st.Block, st.Grandpa) + + warpSyncCfg := &sync.WarpSyncConfig{ + Telemetry: telemetryMailer, + BadBlocks: genesisData.BadBlocks, + WarpSyncProvider: warpSyncProvider, + WarpSyncRequestMaker: net.GetRequestResponseProtocol(network.WarpSyncID, + blockRequestTimeout, network.MaxBlockResponseSize), + SyncRequestMaker: net.GetRequestResponseProtocol(network.SyncID, + blockRequestTimeout, network.MaxBlockResponseSize), + BlockState: st.Block, + Peers: peersView, + } + + warpSyncStrategy = sync.NewWarpSyncStrategy(warpSyncCfg) + } syncCfg := &sync.FullSyncConfig{ BlockState: st.Block, @@ -535,7 +556,9 @@ func (nodeBuilder) newSyncService(config *cfg.Config, st *state.Service, fg sync BlockImportHandler: cs, Telemetry: telemetryMailer, BadBlocks: genesisData.BadBlocks, - RequestMaker: requestMaker, + RequestMaker: net.GetRequestResponseProtocol(network.SyncID, + blockRequestTimeout, network.MaxBlockResponseSize), + Peers: peersView, } fullSync := sync.NewFullSyncStrategy(syncCfg) @@ -544,7 +567,8 @@ func (nodeBuilder) newSyncService(config *cfg.Config, st *state.Service, fg sync sync.WithNetwork(net), sync.WithBlockState(st.Block), sync.WithSlotDuration(slotDuration), - sync.WithStrategies(fullSync, nil), + sync.WithWarpSyncStrategy(warpSyncStrategy), + sync.WithFullSyncStrategy(fullSync), sync.WithMinPeers(config.Network.MinPeers), ), nil } diff --git a/dot/sync/configuration.go b/dot/sync/configuration.go index e144a87cbc..9e01c3996d 100644 --- a/dot/sync/configuration.go +++ b/dot/sync/configuration.go @@ -7,10 +7,15 @@ import "time" type ServiceConfig func(svc *SyncService) -func WithStrategies(currentStrategy, defaultStrategy Strategy) ServiceConfig { +func WithWarpSyncStrategy(warpSyncStrategy Strategy) ServiceConfig { return func(svc *SyncService) { - svc.currentStrategy = currentStrategy - svc.defaultStrategy = defaultStrategy + svc.warpSyncStrategy = warpSyncStrategy + } +} + +func WithFullSyncStrategy(fullSyncStrategy Strategy) ServiceConfig { + return func(svc *SyncService) { + svc.fullSyncStrategy = fullSyncStrategy } } diff --git a/dot/sync/fullsync.go b/dot/sync/fullsync.go index 5ae57ea2c8..248f3bd67a 100644 --- a/dot/sync/fullsync.go +++ b/dot/sync/fullsync.go @@ -43,6 +43,7 @@ type FullSyncConfig struct { BadBlocks []string NumOfTasks int RequestMaker network.RequestMaker + Peers *peerViewSet } type importer interface { @@ -75,15 +76,12 @@ func NewFullSyncStrategy(cfg *FullSyncConfig) *FullSyncStrategy { reqMaker: cfg.RequestMaker, blockState: cfg.BlockState, numOfTasks: cfg.NumOfTasks, + peers: cfg.Peers, blockImporter: newBlockImporter(cfg), unreadyBlocks: newUnreadyBlocks(), requestQueue: &requestsQueue[*messages.BlockRequestMessage]{ queue: list.New(), }, - peers: &peerViewSet{ - view: make(map[peer.ID]peerView), - target: 0, - }, } } @@ -109,7 +107,7 @@ func (f *FullSyncStrategy) NextActions() ([]*SyncTask, error) { } // our best block is equal or ahead of current target. - // in the node's pov we are not legging behind so there's nothing to do + // in the node's pov we are not lagging behind so there's nothing to do // or we didn't receive block announces, so lets ask for more blocks if uint32(bestBlockHeader.Number) >= currentTarget { return f.createTasks(reqsFromQueue), nil @@ -405,6 +403,11 @@ func (f *FullSyncStrategy) IsSynced() bool { return uint32(highestBlock)+messages.MaxBlocksInResponse >= f.peers.getTarget() } +func (f *FullSyncStrategy) Result() any { + logger.Debug("trying to get a result from full sync strategy which is supposed to run forever") + return nil +} + type RequestResponseData struct { req *messages.BlockRequestMessage responseData []*types.BlockData diff --git a/dot/sync/fullsync_test.go b/dot/sync/fullsync_test.go index 04536f5125..dc2b7c6e31 100644 --- a/dot/sync/fullsync_test.go +++ b/dot/sync/fullsync_test.go @@ -30,7 +30,10 @@ type WestendBlocks struct { } func TestFullSyncNextActions(t *testing.T) { + t.Parallel() + t.Run("best_block_greater_or_equal_current_target", func(t *testing.T) { + t.Parallel() // current target is 0 and best block is 0, then we should // get an empty set of tasks @@ -40,6 +43,7 @@ func TestFullSyncNextActions(t *testing.T) { cfg := &FullSyncConfig{ BlockState: mockBlockState, + Peers: NewPeerViewSet(), } fs := NewFullSyncStrategy(cfg) @@ -49,12 +53,15 @@ func TestFullSyncNextActions(t *testing.T) { }) t.Run("target_block_greater_than_best_block", func(t *testing.T) { + t.Parallel() + mockBlockState := NewMockBlockState(gomock.NewController(t)) mockBlockState.EXPECT().BestBlockHeader().Return( types.NewEmptyHeader(), nil) cfg := &FullSyncConfig{ BlockState: mockBlockState, + Peers: NewPeerViewSet(), } fs := NewFullSyncStrategy(cfg) @@ -76,6 +83,8 @@ func TestFullSyncNextActions(t *testing.T) { }) t.Run("having_requests_in_the_queue", func(t *testing.T) { + t.Parallel() + refTo := func(v uint32) *uint32 { return &v } @@ -148,7 +157,11 @@ func TestFullSyncNextActions(t *testing.T) { for tname, tt := range cases { tt := tt t.Run(tname, func(t *testing.T) { - fs := NewFullSyncStrategy(&FullSyncConfig{}) + t.Parallel() + + fs := NewFullSyncStrategy(&FullSyncConfig{ + Peers: NewPeerViewSet(), + }) fs.requestQueue = tt.setupRequestQueue(t) fs.numOfTasks = 1 @@ -179,6 +192,8 @@ func TestFullSyncNextActions(t *testing.T) { } func TestFullSyncProcess(t *testing.T) { + t.Parallel() + westendBlocks := &WestendBlocks{} err := yaml.Unmarshal(rawWestendBlocks, westendBlocks) require.NoError(t, err) @@ -192,6 +207,8 @@ func TestFullSyncProcess(t *testing.T) { require.NoError(t, err) t.Run("requested_max_but_received_less_blocks", func(t *testing.T) { + t.Parallel() + syncTaskResults := []*SyncTaskResult{ // first task // 1 -> 10 @@ -293,7 +310,10 @@ func TestFullSyncProcess(t *testing.T) { } func TestFullSyncBlockAnnounce(t *testing.T) { + t.Parallel() t.Run("announce_a_far_block_without_any_commom_ancestor", func(t *testing.T) { + t.Parallel() + highestFinalizedHeader := &types.Header{ ParentHash: common.BytesToHash([]byte{0}), StateRoot: common.BytesToHash([]byte{3, 3, 3, 3}), @@ -315,6 +335,7 @@ func TestFullSyncBlockAnnounce(t *testing.T) { fsCfg := &FullSyncConfig{ BlockState: mockBlockState, + Peers: NewPeerViewSet(), } fs := NewFullSyncStrategy(fsCfg) @@ -372,6 +393,7 @@ func TestFullSyncBlockAnnounce(t *testing.T) { Return(false, nil) fsCfg := &FullSyncConfig{ BlockState: mockBlockState, + Peers: NewPeerViewSet(), } fs := NewFullSyncStrategy(fsCfg) diff --git a/dot/sync/message_integration_test.go b/dot/sync/message_integration_test.go index 55e560a3be..f597c13789 100644 --- a/dot/sync/message_integration_test.go +++ b/dot/sync/message_integration_test.go @@ -153,7 +153,7 @@ func newFullSyncService(t *testing.T) *SyncService { WithBlockState(stateSrvc.Block), WithNetwork(mockNetwork), WithSlotDuration(6 * time.Second), - WithStrategies(fullSync, nil), + WithFullSyncStrategy(fullSync), } syncLogLvl := log.Info diff --git a/dot/sync/mocks_generate_test.go b/dot/sync/mocks_generate_test.go index a8f52d172f..35da5b7961 100644 --- a/dot/sync/mocks_generate_test.go +++ b/dot/sync/mocks_generate_test.go @@ -3,6 +3,6 @@ package sync -//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network +//go:generate mockgen -destination=mocks_test.go -package=$GOPACKAGE . Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network,WarpSyncProofProvider //go:generate mockgen -destination=mock_request_maker.go -package $GOPACKAGE github.com/ChainSafe/gossamer/dot/network RequestMaker //go:generate mockgen -destination=mock_importer.go -source=fullsync.go -package=sync diff --git a/dot/sync/mocks_test.go b/dot/sync/mocks_test.go index ef04c575d7..88b694a70d 100644 --- a/dot/sync/mocks_test.go +++ b/dot/sync/mocks_test.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/ChainSafe/gossamer/dot/sync (interfaces: Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network) +// Source: github.com/ChainSafe/gossamer/dot/sync (interfaces: Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network,WarpSyncProofProvider) // // Generated by this command: // -// mockgen -destination=mocks_test.go -package=sync . Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network +// mockgen -destination=mocks_test.go -package=sync . Telemetry,BlockState,StorageState,TransactionState,BabeVerifier,FinalityGadget,BlockImportHandler,Network,WarpSyncProofProvider // // Package sync is a generated GoMock package. @@ -17,7 +17,9 @@ import ( network "github.com/ChainSafe/gossamer/dot/network" peerset "github.com/ChainSafe/gossamer/dot/peerset" types "github.com/ChainSafe/gossamer/dot/types" + grandpa "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" common "github.com/ChainSafe/gossamer/lib/common" + warpsync "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" runtime "github.com/ChainSafe/gossamer/lib/runtime" storage "github.com/ChainSafe/gossamer/lib/runtime/storage" peer "github.com/libp2p/go-libp2p/core/peer" @@ -731,3 +733,56 @@ func (mr *MockNetworkMockRecorder) ReportPeer(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportPeer", reflect.TypeOf((*MockNetwork)(nil).ReportPeer), arg0, arg1) } + +// MockWarpSyncProofProvider is a mock of WarpSyncProofProvider interface. +type MockWarpSyncProofProvider struct { + ctrl *gomock.Controller + recorder *MockWarpSyncProofProviderMockRecorder +} + +// MockWarpSyncProofProviderMockRecorder is the mock recorder for MockWarpSyncProofProvider. +type MockWarpSyncProofProviderMockRecorder struct { + mock *MockWarpSyncProofProvider +} + +// NewMockWarpSyncProofProvider creates a new mock instance. +func NewMockWarpSyncProofProvider(ctrl *gomock.Controller) *MockWarpSyncProofProvider { + mock := &MockWarpSyncProofProvider{ctrl: ctrl} + mock.recorder = &MockWarpSyncProofProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWarpSyncProofProvider) EXPECT() *MockWarpSyncProofProviderMockRecorder { + return m.recorder +} + +// CurrentAuthorities mocks base method. +func (m *MockWarpSyncProofProvider) CurrentAuthorities() (grandpa.AuthorityList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CurrentAuthorities") + ret0, _ := ret[0].(grandpa.AuthorityList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CurrentAuthorities indicates an expected call of CurrentAuthorities. +func (mr *MockWarpSyncProofProviderMockRecorder) CurrentAuthorities() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentAuthorities", reflect.TypeOf((*MockWarpSyncProofProvider)(nil).CurrentAuthorities)) +} + +// Verify mocks base method. +func (m *MockWarpSyncProofProvider) Verify(arg0 []byte, arg1 grandpa.SetID, arg2 grandpa.AuthorityList) (*warpsync.WarpSyncVerificationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2) + ret0, _ := ret[0].(*warpsync.WarpSyncVerificationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Verify indicates an expected call of Verify. +func (mr *MockWarpSyncProofProviderMockRecorder) Verify(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockWarpSyncProofProvider)(nil).Verify), arg0, arg1, arg2) +} diff --git a/dot/sync/peer_view.go b/dot/sync/peer_view.go index 241a078b25..5e60c55b22 100644 --- a/dot/sync/peer_view.go +++ b/dot/sync/peer_view.go @@ -22,6 +22,12 @@ type peerViewSet struct { target uint32 } +func NewPeerViewSet() *peerViewSet { + return &peerViewSet{ + view: make(map[peer.ID]peerView), + } +} + func (p *peerViewSet) update(peerID peer.ID, bestHash common.Hash, bestNumber uint32) { p.mtx.Lock() defer p.mtx.Unlock() @@ -36,7 +42,7 @@ func (p *peerViewSet) update(peerID peer.ID, bestHash common.Hash, bestNumber ui return } - logger.Infof("updating peer %s view to #%d (%s)", peerID.String(), bestNumber, bestHash.Short()) + logger.Debugf("updating peer %s view to #%d (%s)", peerID.String(), bestNumber, bestHash.Short()) p.view[peerID] = newView } diff --git a/dot/sync/service.go b/dot/sync/service.go index a290e1f322..94e8970b65 100644 --- a/dot/sync/service.go +++ b/dot/sync/service.go @@ -92,6 +92,7 @@ type Strategy interface { Process(results []*SyncTaskResult) (done bool, repChanges []Change, blocks []peer.ID, err error) ShowMetrics() IsSynced() bool + Result() any } type SyncService struct { @@ -100,8 +101,9 @@ type SyncService struct { network Network blockState BlockState - currentStrategy Strategy - defaultStrategy Strategy + currentStrategy Strategy + fullSyncStrategy Strategy + warpSyncStrategy Strategy workerPool *syncWorkerPool waitPeersDuration time.Duration @@ -127,6 +129,13 @@ func NewSyncService(logLvl log.Level, cfgs ...ServiceConfig) *SyncService { cfg(svc) } + // Set initial strategy + if svc.warpSyncStrategy != nil { + svc.currentStrategy = svc.warpSyncStrategy + } else { + svc.currentStrategy = svc.fullSyncStrategy + } + return svc } @@ -172,7 +181,7 @@ func (s *SyncService) Stop() error { } func (s *SyncService) HandleBlockAnnounceHandshake(from peer.ID, msg *network.BlockAnnounceHandshake) error { - logger.Infof("receiving a block announce handshake from %s", from.String()) + logger.Debugf("receiving a block announce handshake from %s", from.String()) if err := s.workerPool.fromBlockAnnounceHandshake(from); err != nil { return err } @@ -283,6 +292,7 @@ func (s *SyncService) runStrategy() { results := s.workerPool.submitRequests(tasks) done, repChanges, peersToIgnore, err := s.currentStrategy.Process(results) + if err != nil { logger.Criticalf("current sync strategy failed with: %s", err.Error()) return @@ -297,9 +307,12 @@ func (s *SyncService) runStrategy() { } s.currentStrategy.ShowMetrics() - logger.Trace("finish process to acquire more blocks") + // TODO: why not use s.currentStrategy.IsSynced()? if done { - s.currentStrategy = s.defaultStrategy + // Switch to full sync when warp sync finishes + if s.warpSyncStrategy != nil { + s.currentStrategy = s.fullSyncStrategy + } } } diff --git a/dot/sync/warp_sync.go b/dot/sync/warp_sync.go new file mode 100644 index 0000000000..b886b5e817 --- /dev/null +++ b/dot/sync/warp_sync.go @@ -0,0 +1,303 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package sync + +import ( + "fmt" + "slices" + "time" + + "github.com/ChainSafe/gossamer/dot/network" + "github.com/ChainSafe/gossamer/dot/network/messages" + "github.com/ChainSafe/gossamer/dot/peerset" + "github.com/ChainSafe/gossamer/dot/types" + primitives "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" + "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" + "github.com/libp2p/go-libp2p/core/peer" +) + +type WarpSyncPhase uint + +const ( + WarpProof = iota + TargetBlock + Completed +) + +type WarpSyncProofProvider interface { + CurrentAuthorities() (primitives.AuthorityList, error) + Verify(encodedProof []byte, setId primitives.SetID, authorities primitives.AuthorityList) ( + *warpsync.WarpSyncVerificationResult, error) +} + +type WarpSyncStrategy struct { + // Strategy dependencies and config + peers *peerViewSet + badBlocks []string + warpSyncReqMaker network.RequestMaker + syncReqMaker network.RequestMaker + warpSyncProvider WarpSyncProofProvider + blockState BlockState + + // Warp sync state + startedAt time.Time + phase WarpSyncPhase + syncedFragments int + setId primitives.SetID + authorities primitives.AuthorityList + lastBlock *types.Header + result types.BlockData +} + +type WarpSyncConfig struct { + Telemetry Telemetry + BadBlocks []string + WarpSyncRequestMaker network.RequestMaker + SyncRequestMaker network.RequestMaker + WarpSyncProvider WarpSyncProofProvider + BlockState BlockState + Peers *peerViewSet +} + +// NewWarpSyncStrategy returns a new warp sync strategy +func NewWarpSyncStrategy(cfg *WarpSyncConfig) *WarpSyncStrategy { + authorities, err := cfg.WarpSyncProvider.CurrentAuthorities() + if err != nil { + panic(fmt.Sprintf("failed to get current authorities %s", err)) + } + + return &WarpSyncStrategy{ + warpSyncProvider: cfg.WarpSyncProvider, + blockState: cfg.BlockState, + badBlocks: cfg.BadBlocks, + warpSyncReqMaker: cfg.WarpSyncRequestMaker, + syncReqMaker: cfg.SyncRequestMaker, + peers: cfg.Peers, + setId: 0, + authorities: authorities, + } +} + +// OnBlockAnnounce on every new block announce received. +// Since it is a warp sync strategy, we are going to only update the peerset reputation +// And peers target block. +func (w *WarpSyncStrategy) OnBlockAnnounce(from peer.ID, msg *network.BlockAnnounceMessage) ( + repChange *Change, err error) { + + blockAnnounceHeaderHash, err := msg.Hash() + if err != nil { + return nil, err + } + + logger.Debugf("received block announce from %s: #%d (%s) best block: %v", + from, + msg.Number, + blockAnnounceHeaderHash, + msg.BestBlock, + ) + + if slices.Contains(w.badBlocks, blockAnnounceHeaderHash.String()) { + logger.Debugf("bad block received from %s: #%d (%s) is a bad block", + from, msg.Number, blockAnnounceHeaderHash) + + return &Change{ + who: from, + rep: peerset.ReputationChange{ + Value: peerset.BadBlockAnnouncementValue, + Reason: peerset.BadBlockAnnouncementReason, + }, + }, errBadBlockReceived + } + + if msg.BestBlock { + w.peers.update(from, blockAnnounceHeaderHash, uint32(msg.Number)) + } + + return &Change{ + who: from, + rep: peerset.ReputationChange{ + Value: peerset.GossipSuccessValue, + Reason: peerset.GossipSuccessReason, + }, + }, nil +} + +func (w *WarpSyncStrategy) OnBlockAnnounceHandshake(from peer.ID, msg *network.BlockAnnounceHandshake) error { + w.peers.update(from, msg.BestBlockHash, msg.BestBlockNumber) + return nil +} + +// NextActions returns the next actions to be taken by the sync service +func (w *WarpSyncStrategy) NextActions() ([]*SyncTask, error) { + w.startedAt = time.Now() + + lastBlock, err := w.lastBlockHeader() + if err != nil { + return nil, err + } + + var task SyncTask + switch w.phase { + case WarpProof: + task = SyncTask{ + request: messages.NewWarpProofRequest(lastBlock.Hash()), + response: &warpsync.WarpSyncProof{}, + requestMaker: w.warpSyncReqMaker, + } + case TargetBlock: + req := messages.NewBlockRequest( + *messages.NewFromBlock(lastBlock.Hash()), + 1, + messages.RequestedDataHeader+ + messages.RequestedDataBody+ + messages.RequestedDataJustification, + messages.Ascending, + ) + task = SyncTask{ + request: req, + response: &messages.BlockResponseMessage{}, + requestMaker: w.syncReqMaker, + } + } + + return []*SyncTask{&task}, nil +} + +// Process processes the results of the sync tasks, getting the best warp sync response and +// Updating our block state +func (w *WarpSyncStrategy) Process(results []*SyncTaskResult) ( + done bool, repChanges []Change, bans []peer.ID, err error) { + + switch w.phase { + case WarpProof: + logger.Debug("processing warp sync proof results") + + var warpProofResult *warpsync.WarpSyncVerificationResult + + repChanges, bans, warpProofResult = w.validateWarpSyncResults(results) + + if warpProofResult != nil { + w.lastBlock = &warpProofResult.Header + + if !warpProofResult.Completed { + logger.Debug("partial warp sync proof received") + + w.setId = warpProofResult.SetId + w.authorities = warpProofResult.AuthorityList + } else { + logger.Debugf("⏩ Warping, finish processing proofs, downloading target block #%d (%s)", + w.lastBlock.Number, w.lastBlock.Hash().String()) + w.phase = TargetBlock + } + } + + case TargetBlock: + logger.Debug("processing warp sync target block results") + + var validRes []RequestResponseData + + // Reuse same validator than in fullsync + repChanges, bans, validRes = validateResults(results, w.badBlocks) + + if len(validRes) > 0 && validRes[0].responseData != nil && len(validRes[0].responseData) > 0 { + w.result = *validRes[0].responseData[0] + w.phase = Completed + } + } + + return w.IsSynced(), repChanges, bans, nil +} + +func (w *WarpSyncStrategy) validateWarpSyncResults(results []*SyncTaskResult) ( + repChanges []Change, peersToBlock []peer.ID, result *warpsync.WarpSyncVerificationResult) { + + repChanges = make([]Change, 0) + peersToBlock = make([]peer.ID, 0) + bestProof := &warpsync.WarpSyncProof{} + var bestResult *warpsync.WarpSyncVerificationResult + + for _, result := range results { + switch response := result.response.(type) { + case *warpsync.WarpSyncProof: + if !result.completed { + continue + } + + // If invalid warp sync proof, then we should block the peer and update its reputation + encodedProof, err := response.Encode() + if err != nil { + // This should never happen since the proof is already decoded without issues + panic("fail to encode warp proof") + } + + res, err := w.warpSyncProvider.Verify(encodedProof, w.setId, w.authorities) + + if err != nil { + logger.Warnf("bad warp proof response: %s", err) + + repChanges = append(repChanges, Change{ + who: result.who, + rep: peerset.ReputationChange{ + Value: peerset.BadWarpProofValue, + Reason: peerset.BadWarpProofReason, + }}) + peersToBlock = append(peersToBlock, result.who) + continue + } + + if response.IsFinished || len(response.Proofs) > len(bestProof.Proofs) { + bestProof = response + bestResult = res + } + default: + repChanges = append(repChanges, Change{ + who: result.who, + rep: peerset.ReputationChange{ + Value: peerset.UnexpectedResponseValue, + Reason: peerset.UnexpectedResponseReason, + }}) + peersToBlock = append(peersToBlock, result.who) + continue + } + } + + w.syncedFragments += len(bestProof.Proofs) + + return repChanges, peersToBlock, bestResult +} + +func (w *WarpSyncStrategy) ShowMetrics() { + switch w.phase { + case WarpProof: + totalSyncSeconds := time.Since(w.startedAt).Seconds() + + logger.Infof("⏩ Warping, downloading finality proofs, fragments %d, best #%d (%s) "+ + "took: %.2f seconds", + w.syncedFragments, w.lastBlock.Number, w.lastBlock.Hash().Short(), totalSyncSeconds) + case TargetBlock: + logger.Infof("⏩ Warping, downloading target block #%d (%s)", + w.lastBlock.Number, w.lastBlock.Hash().String()) + } + +} + +func (w *WarpSyncStrategy) IsSynced() bool { + return w.phase == Completed +} + +func (w *WarpSyncStrategy) Result() any { + return w.result +} + +func (w *WarpSyncStrategy) lastBlockHeader() (header *types.Header, err error) { + if w.lastBlock == nil { + w.lastBlock, err = w.blockState.GetHighestFinalisedHeader() + if err != nil { + return nil, err + } + } + return w.lastBlock, nil +} + +var _ Strategy = (*WarpSyncStrategy)(nil) diff --git a/dot/sync/warp_sync_test.go b/dot/sync/warp_sync_test.go new file mode 100644 index 0000000000..e3d704a11b --- /dev/null +++ b/dot/sync/warp_sync_test.go @@ -0,0 +1,174 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package sync + +import ( + "testing" + + "github.com/ChainSafe/gossamer/dot/network" + "github.com/ChainSafe/gossamer/dot/network/messages" + "github.com/ChainSafe/gossamer/dot/peerset" + "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/grandpa/warpsync" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestWarpSyncBlockAnnounce(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + warpSyncProvider := NewMockWarpSyncProofProvider(ctrl) + warpSyncProvider.EXPECT().CurrentAuthorities().Return(nil, nil).AnyTimes() + + peer := peer.ID("peer") + + t.Run("successful_block_announce", func(t *testing.T) { + t.Parallel() + + peersView := NewPeerViewSet() + + strategy := NewWarpSyncStrategy(&WarpSyncConfig{ + Peers: peersView, + WarpSyncProvider: warpSyncProvider, + }) + + blockAnnounce := &network.BlockAnnounceMessage{ + ParentHash: common.BytesToHash([]byte{0, 1, 2}), + Number: 1024, + StateRoot: common.BytesToHash([]byte{3, 3, 3, 3}), + ExtrinsicsRoot: common.BytesToHash([]byte{4, 4, 4, 4}), + Digest: types.NewDigest(), + BestBlock: true, + } + + expectedRepChange := &Change{ + who: peer, + rep: peerset.ReputationChange{ + Value: peerset.GossipSuccessValue, + Reason: peerset.GossipSuccessReason, + }, + } + + rep, err := strategy.OnBlockAnnounce(peer, blockAnnounce) + require.NoError(t, err) + require.NotNil(t, rep) + require.Equal(t, expectedRepChange, rep) + require.Equal(t, blockAnnounce.Number, uint(peersView.getTarget())) + }) + + t.Run("successful_block_announce_handshake", func(t *testing.T) { + t.Parallel() + + peersView := NewPeerViewSet() + + strategy := NewWarpSyncStrategy(&WarpSyncConfig{ + Peers: peersView, + WarpSyncProvider: warpSyncProvider, + }) + + handshake := &network.BlockAnnounceHandshake{ + Roles: 1, + BestBlockNumber: 17, + BestBlockHash: common.BytesToHash([]byte{0, 1, 2}), + GenesisHash: common.BytesToHash([]byte{1, 1, 1, 1}), + } + err := strategy.OnBlockAnnounceHandshake(peer, handshake) + require.NoError(t, err) + require.Equal(t, handshake.BestBlockNumber, peersView.getTarget()) + }) + + t.Run("successful_block_announce", func(t *testing.T) { + t.Parallel() + + peersView := NewPeerViewSet() + + blockAnnounce := &network.BlockAnnounceMessage{ + ParentHash: common.BytesToHash([]byte{0, 1, 2}), + Number: 1024, + StateRoot: common.BytesToHash([]byte{3, 3, 3, 3}), + ExtrinsicsRoot: common.BytesToHash([]byte{4, 4, 4, 4}), + Digest: types.NewDigest(), + BestBlock: true, + } + + blockAnnounceHash, err := blockAnnounce.Hash() + require.NoError(t, err) + + strategy := NewWarpSyncStrategy(&WarpSyncConfig{ + Peers: peersView, + WarpSyncProvider: warpSyncProvider, + BadBlocks: []string{blockAnnounceHash.String()}, + }) + + expectedRepChange := &Change{ + who: peer, + rep: peerset.ReputationChange{ + Value: peerset.BadBlockAnnouncementValue, + Reason: peerset.BadBlockAnnouncementReason, + }, + } + + rep, err := strategy.OnBlockAnnounce(peer, blockAnnounce) + require.NotNil(t, err) + require.ErrorIs(t, err, errBadBlockReceived) + require.NotNil(t, rep) + require.Equal(t, expectedRepChange, rep) + require.Equal(t, 0, int(peersView.getTarget())) + }) +} + +func TestWarpSyncNextActions(t *testing.T) { + ctrl := gomock.NewController(t) + mockBlockState := NewMockBlockState(ctrl) + + warpSyncProvider := NewMockWarpSyncProofProvider(ctrl) + warpSyncProvider.EXPECT().CurrentAuthorities().Return(nil, nil).AnyTimes() + + genesisHeader := &types.Header{ + Number: 1, + } + mockBlockState.EXPECT().GetHighestFinalisedHeader().Return(genesisHeader, nil).AnyTimes() + + tc := map[string]struct { + phase WarpSyncPhase + lastBlock *types.Header + expectedRequestType interface{} + expectedResponseType interface{} + }{ + "warp_sync_phase": { + phase: WarpProof, + expectedRequestType: &messages.WarpProofRequest{}, + expectedResponseType: &warpsync.WarpSyncProof{}, + }, + "target_block_phase": { + phase: TargetBlock, + expectedRequestType: &messages.BlockRequestMessage{}, + expectedResponseType: &messages.BlockResponseMessage{}, + }, + } + + for name, c := range tc { + t.Run(name, func(t *testing.T) { + strategy := NewWarpSyncStrategy(&WarpSyncConfig{ + BlockState: mockBlockState, + WarpSyncProvider: warpSyncProvider, + }) + + strategy.phase = c.phase + + tasks, err := strategy.NextActions() + require.NoError(t, err) + require.Equal(t, 1, len(tasks), "expected 1 task") + + task := tasks[0] + require.IsType(t, c.expectedRequestType, task.request) + require.IsType(t, c.expectedResponseType, task.response) + }) + } +} diff --git a/lib/common/hash.go b/lib/common/hash.go index 28798afb46..6603d7bfec 100644 --- a/lib/common/hash.go +++ b/lib/common/hash.go @@ -60,9 +60,9 @@ func (h Hash) String() string { //skipcq: GO-W1029 return fmt.Sprintf("0x%x", h[:]) } -// Short returns the first 4 bytes and the last 4 bytes of the hex string for the hash +// Short returns the first 2 bytes and the last 2 bytes of the hex string for the hash func (h Hash) Short() string { //skipcq: GO-W1029 - const nBytes = 4 + const nBytes = 2 return fmt.Sprintf("0x%x...%x", h[:nBytes], h[len(h)-nBytes:]) } diff --git a/lib/grandpa/warpsync/mocks_generate_test.go b/lib/grandpa/warpsync/mocks_generate_test.go new file mode 100644 index 0000000000..82185f8d08 --- /dev/null +++ b/lib/grandpa/warpsync/mocks_generate_test.go @@ -0,0 +1,6 @@ +// Copyright 2021 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package warpsync + +//go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . BlockState,GrandpaState diff --git a/lib/grandpa/warpsync/mocks_test.go b/lib/grandpa/warpsync/mocks_test.go new file mode 100644 index 0000000000..5d3ed95394 --- /dev/null +++ b/lib/grandpa/warpsync/mocks_test.go @@ -0,0 +1,169 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ChainSafe/gossamer/lib/grandpa/warpsync (interfaces: BlockState,GrandpaState) +// +// Generated by this command: +// +// mockgen -destination=mocks_test.go -package warpsync . BlockState,GrandpaState +// + +// Package warpsync is a generated GoMock package. +package warpsync + +import ( + reflect "reflect" + + types "github.com/ChainSafe/gossamer/dot/types" + common "github.com/ChainSafe/gossamer/lib/common" + gomock "go.uber.org/mock/gomock" +) + +// MockBlockState is a mock of BlockState interface. +type MockBlockState struct { + ctrl *gomock.Controller + recorder *MockBlockStateMockRecorder +} + +// MockBlockStateMockRecorder is the mock recorder for MockBlockState. +type MockBlockStateMockRecorder struct { + mock *MockBlockState +} + +// NewMockBlockState creates a new mock instance. +func NewMockBlockState(ctrl *gomock.Controller) *MockBlockState { + mock := &MockBlockState{ctrl: ctrl} + mock.recorder = &MockBlockStateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockState) EXPECT() *MockBlockStateMockRecorder { + return m.recorder +} + +// GetHeader mocks base method. +func (m *MockBlockState) GetHeader(arg0 common.Hash) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeader", arg0) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeader indicates an expected call of GetHeader. +func (mr *MockBlockStateMockRecorder) GetHeader(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeader", reflect.TypeOf((*MockBlockState)(nil).GetHeader), arg0) +} + +// GetHeaderByNumber mocks base method. +func (m *MockBlockState) GetHeaderByNumber(arg0 uint) (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeaderByNumber", arg0) + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeaderByNumber indicates an expected call of GetHeaderByNumber. +func (mr *MockBlockStateMockRecorder) GetHeaderByNumber(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeaderByNumber", reflect.TypeOf((*MockBlockState)(nil).GetHeaderByNumber), arg0) +} + +// GetHighestFinalisedHeader mocks base method. +func (m *MockBlockState) GetHighestFinalisedHeader() (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHighestFinalisedHeader") + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHighestFinalisedHeader indicates an expected call of GetHighestFinalisedHeader. +func (mr *MockBlockStateMockRecorder) GetHighestFinalisedHeader() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHighestFinalisedHeader", reflect.TypeOf((*MockBlockState)(nil).GetHighestFinalisedHeader)) +} + +// GetJustification mocks base method. +func (m *MockBlockState) GetJustification(arg0 common.Hash) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetJustification", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetJustification indicates an expected call of GetJustification. +func (mr *MockBlockStateMockRecorder) GetJustification(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJustification", reflect.TypeOf((*MockBlockState)(nil).GetJustification), arg0) +} + +// MockGrandpaState is a mock of GrandpaState interface. +type MockGrandpaState struct { + ctrl *gomock.Controller + recorder *MockGrandpaStateMockRecorder +} + +// MockGrandpaStateMockRecorder is the mock recorder for MockGrandpaState. +type MockGrandpaStateMockRecorder struct { + mock *MockGrandpaState +} + +// NewMockGrandpaState creates a new mock instance. +func NewMockGrandpaState(ctrl *gomock.Controller) *MockGrandpaState { + mock := &MockGrandpaState{ctrl: ctrl} + mock.recorder = &MockGrandpaStateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGrandpaState) EXPECT() *MockGrandpaStateMockRecorder { + return m.recorder +} + +// GetAuthorities mocks base method. +func (m *MockGrandpaState) GetAuthorities(arg0 uint64) ([]types.GrandpaVoter, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthorities", arg0) + ret0, _ := ret[0].([]types.GrandpaVoter) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthorities indicates an expected call of GetAuthorities. +func (mr *MockGrandpaStateMockRecorder) GetAuthorities(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorities", reflect.TypeOf((*MockGrandpaState)(nil).GetAuthorities), arg0) +} + +// GetAuthoritiesChangesFromBlock mocks base method. +func (m *MockGrandpaState) GetAuthoritiesChangesFromBlock(arg0 uint) ([]uint, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthoritiesChangesFromBlock", arg0) + ret0, _ := ret[0].([]uint) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthoritiesChangesFromBlock indicates an expected call of GetAuthoritiesChangesFromBlock. +func (mr *MockGrandpaStateMockRecorder) GetAuthoritiesChangesFromBlock(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthoritiesChangesFromBlock", reflect.TypeOf((*MockGrandpaState)(nil).GetAuthoritiesChangesFromBlock), arg0) +} + +// GetCurrentSetID mocks base method. +func (m *MockGrandpaState) GetCurrentSetID() (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCurrentSetID") + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCurrentSetID indicates an expected call of GetCurrentSetID. +func (mr *MockGrandpaStateMockRecorder) GetCurrentSetID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentSetID", reflect.TypeOf((*MockGrandpaState)(nil).GetCurrentSetID)) +} diff --git a/lib/grandpa/warpsync/testdata/warp_sync_proofs.yaml b/lib/grandpa/warpsync/testdata/warp_sync_proofs.yaml new file mode 100644 index 0000000000..16a7058823 --- /dev/null +++ b/lib/grandpa/warpsync/testdata/warp_sync_proofs.yaml @@ -0,0 +1 @@ +substrate_warp_sync_proof_1: 0x04e995dd4dddbdfc54bd8126e0b5b394a47da71ca4a6510e7bd132d103470bf9b9be748b05d4026dd4bd8821893ce9e82a3a8c324ca454dd7510ee9ab05df3df82e87a1eada450bc10f223a562a12962e8914f436fd0991319d662310298fa47e5ff0dffb50c0642414245b501031100000009ba301100000000a6815e866aebee63d23cefa60964fa88b4814264fd35703c0c8a5f8192d8f41aa15bc5d991d1cad65ffccb73dbacd542a32286ec051190166ec00bfb4304b6049a882d0858cdbfb8d37a07269ea01bad1b55fb1174d6dedbfac56787adfa700004424545468403a6c0199af958a3ae8a3928b0802b709589bcbb1ec5b7041cf91644dd696ad99f05424142450101f8b400261b84cc580a8947abe4caf3b4ee51b7aeb26afc9a3bcdd2b3cd634f2df03cda49368e5e56a5b2427f6088cdc4144d75e1985f5e0a7e8f527290f47b8dad10000000000000376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd620138376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd62015037bddf7847fd40d7e6235c17203c8b70e661ef25aa44cbbaed19d5bb455bec3b20f59a720253b7c536171e1e68a91dcbf6b88fd077e28a2125cc9ea4fb430819b6ba718c28b1078474dc63e9d8a6f4d794da79f661da070fcca13b3bba50e7376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201243478b0465e1c13e9cda8479d42958422ab0acd69a1dbdc8d14fa95d2b15e96e6c1a5e58df7ff148977ba68418a7d6f6cf2ab959056c59cd768e490bc90ea0c1a619f90a9c96ec3e1c57211613247e8c0efe123188263da1dc8796d6a018c16376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd620151010e386ecf4cb60e88acf75d7859547584a8e4258af8ca3fcde8d79dedf9ed1b6b0bf7df2704c3dfbc2db4a785833f0a70e30ec3c6ad9f35cae93e6ebd990a1d50350ac619dc36a0fe05cdd7a1654e73858586d55265f3524cc27e005be0f2376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201ca80d95b84408c14cc2c0cca1f3d2e094ebe056b5816959c887ac40b7311edf8c87e0f77a06ed398323b684e09c6d784e5333136dbae5a1d0627bf774e633408229366210b4b20adf7741fb6bc8fa669ea2f7470e1440b38969d2a47d752ffeb376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201bda1ac4056f6e6210d6b2dd8f69323cb7f2729f49ad9d42059ceecdffecebef04ce478a3cfde9bceadaf9be978443f0c3a5aea19d04b538b087cc0ca614e6b0a2bf793d9fb4251da094a2dd33bb0f68a46e60d799b58a78a4a5cc369a92ee2ec376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd620156dd1808cb6044b9d6762bbd5a0284b32e51809010300fa8b4a6f2bb3072e9e632041c6ad74ba989c3e18f3874ab4be95c2e8f51ab275d943a839c12d5b73c0a3ab4559bf186773ede8cf3ddc78a93439c8e82ead090de7d3c2fe4e75591fcf1376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd620176857f28231e3b3a8eaf85eb07b835f5f7f5d379e854debd3f325693118db758b2765fa1c4a959d41f7d0b1650f61cffe1d607eee7fb12de81777cf725cd8e073fee6e433392b821f11c4cf8fe8d17b7c08dbbc32381895955ee27448f8869e1376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201688b6d2c8640491fe4db52979dfaa2947ebded2b4ba5e020257fe29ffdb2a6c7017102fa6c9d6f0744bba28a05c34c377108bf73ee04982d6db0656208ec620c7e287589ac74a46b3a74a8ae66c3e2554857419bd00f9bfeb55957fffccd98b9376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd62012256e07d9c9f3c76356fa7946b8233591a4d9b91ed3e81b2a09b1d3ae6b69c8906a299e8245221c8efd46bc55966f1de6b2b4bd0b2b3256527d59e7b738056009e8adb538dacdd20c86c4eca9a158895cef125dfd118e883cd55454f8d59f3ed376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd62011479e687ce7b0594637250707491cbec69a909aad5dab78e00aae8c3d8a530d8de4c6a1faad6ef416aea872988d50227e35e2dfb5b73d4b3b897acaa4cf56f0ea674568468af9fef031f033166880eec9f5ddd05d797d5abf1a9b9a957c77820376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201a0e495db4830b2a4e80d045871b5f0f8541091fdb7707d5e6c8f085bc255f0a5069a0ae153d9a83f6fa3f208cd1d701fbd7e852449f026f8566c7d8826cf5b0aa99c0755eec29f2be753fb701762d1e7cc841323bac49576b9ea2e124c4b7b9f376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd62018c4f486b7b87c5735e42249c15f5ec6d7b2b50a16aa0ba3bfbe666f99537e81a6a121659a06d58eacb9594201b6644f18e54c1c6ca457a4fcfa9d15e3bfea201be0bbf54aad305682afc6dc7cd051b5dce154e7d0056e877562407872cdb1242376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201763fba0da4fef393f59f1fd9eb9a646a2a1106f404681840fa846ec56c1a3443223b8a147990c6fd3e2af11300531168be94f091931f6ac07724f3ce75a06600d3ccc285f3c648f34ca9ce476651495b9a6c4aa7607d83737bd871de56a50ed9376d697522a9a42b9c6952e771fd74be9cb19fe6dd0d41c362f753ae2686547a2fdd6201d9b7c0218e498e5930fc4ad9a8c139f774de3fda36ad408008c3957f1e822070ceda5ca810ef3fb7fda2c48576051107235edba92a3be39b38b9ac9aba8c2305f12e7ddae48873b7dcd614b8ccc8ea2e5ec000f3cc398f20801cf9047bc909b20001 \ No newline at end of file diff --git a/lib/grandpa/warp_sync.go b/lib/grandpa/warpsync/warp_sync.go similarity index 77% rename from lib/grandpa/warp_sync.go rename to lib/grandpa/warpsync/warp_sync.go index 0774433aed..9759556b8e 100644 --- a/lib/grandpa/warp_sync.go +++ b/lib/grandpa/warpsync/warp_sync.go @@ -1,15 +1,15 @@ // Copyright 2024 ChainSafe Systems (ON) // SPDX-License-Identifier: LGPL-3.0-only -package grandpa +package warpsync import ( "bytes" "fmt" - "github.com/ChainSafe/gossamer/dot/network" "github.com/ChainSafe/gossamer/dot/types" consensus_grandpa "github.com/ChainSafe/gossamer/internal/client/consensus/grandpa" + "github.com/ChainSafe/gossamer/internal/log" "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa" "github.com/ChainSafe/gossamer/internal/primitives/consensus/grandpa/app" "github.com/ChainSafe/gossamer/internal/primitives/core/hash" @@ -22,17 +22,39 @@ import ( const MaxWarpSyncProofSize = 8 * 1024 * 1024 var ( + logger = log.NewFromGlobal(log.AddContext("pkg", "warpsync")) + errMissingStartBlock = fmt.Errorf("missing start block") errStartBlockNotFinalized = fmt.Errorf("start block is not finalized") ) +type BlockState interface { + GetHeader(common.Hash) (*types.Header, error) + GetHeaderByNumber(uint) (*types.Header, error) + GetJustification(common.Hash) ([]byte, error) + GetHighestFinalisedHeader() (*types.Header, error) +} + +type GrandpaState interface { + GetCurrentSetID() (uint64, error) + GetAuthorities(uint64) ([]types.GrandpaVoter, error) + GetAuthoritiesChangesFromBlock(uint) ([]uint, error) +} + +type WarpSyncVerificationResult struct { + SetId grandpa.SetID + AuthorityList grandpa.AuthorityList + Header types.Header + Completed bool +} + type WarpSyncFragment struct { // The last block that the given authority set finalized. This block should contain a digest // signalling an authority set change from which we can fetch the next authority set. Header types.Header // A justification for the header above which proves its finality. In order to validate it the // verifier must be aware of the authorities and set id for which the justification refers to. - Justification consensus_grandpa.GrandpaJustification[hash.H256, uint64] + Justification consensus_grandpa.GrandpaJustification[hash.H256, uint32] } type WarpSyncProof struct { @@ -50,6 +72,26 @@ func NewWarpSyncProof() WarpSyncProof { } } +func (wsp *WarpSyncProof) Decode(in []byte) error { + return scale.Unmarshal(in, wsp) +} + +func (wsp *WarpSyncProof) Encode() ([]byte, error) { + if wsp == nil { + return nil, fmt.Errorf("cannot encode nil WarpSyncProof") + } + return scale.Marshal(*wsp) +} + +func (wsp *WarpSyncProof) String() string { + if wsp == nil { + return "WarpSyncProof=nil" + } + + return fmt.Sprintf("WarpSyncProof proofs=%v isFinished=%v proofsLength=%v", + wsp.Proofs, wsp.IsFinished, wsp.proofsLength) +} + func (w *WarpSyncProof) addFragment(fragment WarpSyncFragment) (limitReached bool, err error) { encodedFragment, err := scale.Marshal(fragment) if err != nil { @@ -66,7 +108,7 @@ func (w *WarpSyncProof) addFragment(fragment WarpSyncFragment) (limitReached boo return false, nil } -func (w *WarpSyncProof) lastProofBlockNumber() uint64 { +func (w *WarpSyncProof) lastProofBlockNumber() uint32 { if len(w.Proofs) == 0 { return 0 } @@ -82,8 +124,10 @@ func (w *WarpSyncProof) verify( authorities grandpa.AuthorityList, hardForks map[string]SetIdAuthorityList, ) (*SetIdAuthorityList, error) { - currentSetId := setId - currentAuthorities := authorities + setIdAuth := &SetIdAuthorityList{ + SetID: setId, + AuthorityList: authorities, + } for fragmentNumber, proof := range w.Proofs { headerHash := proof.Header.Hash() @@ -91,11 +135,12 @@ func (w *WarpSyncProof) verify( hardForkKey := fmt.Sprintf("%v-%v", headerHash, number) if fork, ok := hardForks[hardForkKey]; ok { - currentSetId = fork.SetID - currentAuthorities = fork.AuthorityList + setIdAuth.SetID = fork.SetID + setIdAuth.AuthorityList = fork.AuthorityList } else { - err := proof.Justification.Verify(uint64(currentSetId), currentAuthorities) + err := proof.Justification.Verify(uint64(setIdAuth.SetID), setIdAuth.AuthorityList) if err != nil { + logger.Debugf("failed to verify justification %s", err) return nil, err } @@ -114,15 +159,15 @@ func (w *WarpSyncProof) verify( return nil, fmt.Errorf("cannot parse GRANPDA raw authorities: %w", err) } - currentSetId += 1 - currentAuthorities = auths + setIdAuth.SetID += 1 + setIdAuth.AuthorityList = auths } else if fragmentNumber != len(w.Proofs)-1 || !w.IsFinished { - return nil, fmt.Errorf("Header is missing authority set change digest") + return nil, fmt.Errorf("header is missing authority set change digest") } } } - return &SetIdAuthorityList{currentSetId, currentAuthorities}, nil + return setIdAuth, nil } type WarpSyncProofProvider struct { @@ -143,6 +188,33 @@ type SetIdAuthorityList struct { grandpa.AuthorityList } +func (p *WarpSyncProofProvider) CurrentAuthorities() (grandpa.AuthorityList, error) { + currentSetid, err := p.grandpaState.GetCurrentSetID() + if err != nil { + return nil, err + } + + authorities, err := p.grandpaState.GetAuthorities(currentSetid) + if err != nil { + return nil, err + } + + var authorityList grandpa.AuthorityList + for _, auth := range authorities { + key, err := app.NewPublic(auth.Key[:]) + if err != nil { + return nil, err + } + + authorityList = append(authorityList, grandpa.AuthorityIDWeight{ + AuthorityID: key, + AuthorityWeight: grandpa.AuthorityWeight(auth.ID), + }) + } + + return authorityList, nil +} + // Generate build a warp sync encoded proof starting from the given block hash func (p *WarpSyncProofProvider) Generate(start common.Hash) ([]byte, error) { // Get and traverse all GRANDPA authorities changes from the given block hash @@ -192,7 +264,7 @@ func (p *WarpSyncProofProvider) Generate(start common.Hash) ([]byte, error) { return nil, err } - justification, err := consensus_grandpa.DecodeJustification[hash.H256, uint64, runtime.BlakeTwo256](encJustification) + justification, err := consensus_grandpa.DecodeJustification[hash.H256, uint32, runtime.BlakeTwo256](encJustification) if err != nil { return nil, err } @@ -227,7 +299,7 @@ func (p *WarpSyncProofProvider) Generate(start common.Hash) ([]byte, error) { return nil, err } - justification, err := consensus_grandpa.DecodeJustification[hash.H256, uint64, runtime.BlakeTwo256]( + justification, err := consensus_grandpa.DecodeJustification[hash.H256, uint32, runtime.BlakeTwo256]( latestJustification, ) if err != nil { @@ -254,7 +326,7 @@ func (p *WarpSyncProofProvider) Verify( encodedProof []byte, setId grandpa.SetID, authorities grandpa.AuthorityList, -) (*network.WarpSyncVerificationResult, error) { +) (*WarpSyncVerificationResult, error) { var proof WarpSyncProof err := scale.Unmarshal(encodedProof, &proof) if err != nil { @@ -273,7 +345,7 @@ func (p *WarpSyncProofProvider) Verify( return nil, fmt.Errorf("verifying warp sync proof: %w", err) } - return &network.WarpSyncVerificationResult{ + return &WarpSyncVerificationResult{ SetId: nextSetAndAuthorities.SetID, AuthorityList: nextSetAndAuthorities.AuthorityList, Header: lastHeader, @@ -304,8 +376,10 @@ func findScheduledChange( return nil, err } - parsedScheduledChange, _ := scheduledChange.(types.GrandpaScheduledChange) - return &parsedScheduledChange, nil + parsedScheduledChange, ok := scheduledChange.(types.GrandpaScheduledChange) + if ok { + return &parsedScheduledChange, nil + } } } } diff --git a/lib/grandpa/warp_sync_test.go b/lib/grandpa/warpsync/warp_sync_test.go similarity index 89% rename from lib/grandpa/warp_sync_test.go rename to lib/grandpa/warpsync/warp_sync_test.go index 8f26c2e9a5..86212f256c 100644 --- a/lib/grandpa/warp_sync_test.go +++ b/lib/grandpa/warpsync/warp_sync_test.go @@ -1,10 +1,11 @@ // Copyright 2024 ChainSafe Systems (ON) // SPDX-License-Identifier: LGPL-3.0-only -package grandpa +package warpsync import ( "errors" + "log" "math/rand" "slices" "testing" @@ -21,8 +22,38 @@ import ( "github.com/ChainSafe/gossamer/pkg/scale" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "gopkg.in/yaml.v3" + + _ "embed" ) +//go:embed testdata/warp_sync_proofs.yaml +var rawWarpSyncProofs []byte + +type WarpSyncProofs struct { + SubstrateWarpSyncProof1 string `yaml:"substrate_warp_sync_proof_1"` +} + +func TestDecodeWarpSyncProof(t *testing.T) { + warpSyncProofs := &WarpSyncProofs{} + err := yaml.Unmarshal(rawWarpSyncProofs, warpSyncProofs) + require.NoError(t, err) + + // Generated using substrate + expected := common.MustHexToBytes(warpSyncProofs.SubstrateWarpSyncProof1) + if err != nil { + log.Fatal(err) + } + + var proof WarpSyncProof + + err = proof.Decode(expected) + require.NoError(t, err) + + encoded, err := proof.Encode() + require.NoError(t, err) + require.Equal(t, expected, encoded) +} func TestGenerateWarpSyncProofBlockNotFound(t *testing.T) { t.Parallel() @@ -81,8 +112,8 @@ func TestGenerateWarpSyncProofBlockNotFinalized(t *testing.T) { func TestGenerateAndVerifyWarpSyncProofOk(t *testing.T) { t.Parallel() - type signedPrecommit = grandpa.SignedPrecommit[hash.H256, uint64, primitives.AuthoritySignature, primitives.AuthorityID] - type preCommit = grandpa.Precommit[hash.H256, uint64] + type signedPrecommit = grandpa.SignedPrecommit[hash.H256, uint32, primitives.AuthoritySignature, primitives.AuthorityID] + type preCommit = grandpa.Precommit[hash.H256, uint32] // Initialize mocks ctrl := gomock.NewController(t) @@ -169,7 +200,7 @@ func TestGenerateAndVerifyWarpSyncProofOk(t *testing.T) { // If we have an authority set change, create a justification if len(newAuthorities) > 0 { targetHash := hash.H256(string(header.Hash().ToBytes())) - targetNumber := uint64(header.Number) + targetNumber := uint32(header.Number) // Create precommits for current voters precommits := []signedPrecommit{} @@ -179,7 +210,7 @@ func TestGenerateAndVerifyWarpSyncProofOk(t *testing.T) { TargetNumber: targetNumber, } - msg := grandpa.NewMessage[hash.H256, uint64, preCommit](precommit) + msg := grandpa.NewMessage[hash.H256, uint32, preCommit](precommit) encoded := primitives.NewLocalizedPayload(1, currentSetId, msg) signature := voter.Sign(encoded) @@ -196,9 +227,9 @@ func TestGenerateAndVerifyWarpSyncProofOk(t *testing.T) { } // Create justification - justification := primitives.GrandpaJustification[hash.H256, uint64]{ + justification := primitives.GrandpaJustification[hash.H256, uint32]{ Round: 1, - Commit: primitives.Commit[hash.H256, uint64]{ + Commit: primitives.Commit[hash.H256, uint32]{ TargetHash: targetHash, TargetNumber: targetNumber, Precommits: precommits, @@ -299,10 +330,10 @@ func createGRANDPAConsensusDigest(t *testing.T, digestData any) types.ConsensusD } } -func genericHeadersList(t *testing.T, headers []*types.Header) []runtime.Header[uint64, hash.H256] { +func genericHeadersList(t *testing.T, headers []*types.Header) []runtime.Header[uint32, hash.H256] { t.Helper() - headerList := []runtime.Header[uint64, hash.H256]{} + headerList := []runtime.Header[uint32, hash.H256]{} for _, header := range headers { if header == nil { continue