diff --git a/dot/services.go b/dot/services.go index 23688eec836..5674fe753f4 100644 --- a/dot/services.go +++ b/dot/services.go @@ -175,6 +175,7 @@ func (nb nodeBuilder) createBABEService(cfg *Config, st *state.Service, ks keyst cs *core.Service, telemetryMailer telemetry.Client) (babe.ServiceIFace, error) { return nb.createBABEServiceWithBuilder(cfg, st, ks, cs, telemetryMailer, babe.Builder{}) } + func (nodeBuilder) createBABEServiceWithBuilder(cfg *Config, st *state.Service, ks keystore.Keystore, cs *core.Service, telemetryMailer telemetry.Client, newBabeService ServiceBuilder) (babe. ServiceIFace, error) { diff --git a/dot/types/authority.go b/dot/types/authority.go index a75f6437df9..2dff7a44340 100644 --- a/dot/types/authority.go +++ b/dot/types/authority.go @@ -16,7 +16,9 @@ import ( // Authority struct to hold authority data type Authority struct { - Key crypto.PublicKey + Key crypto.PublicKey + // Weight exists for potential improvements in the protocol and could + // have a use-case in the future. In polkadot all authorities have the weight = 1. Weight uint64 } diff --git a/lib/babe/babe.go b/lib/babe/babe.go index 929e77315b4..14d65e7feea 100644 --- a/lib/babe/babe.go +++ b/lib/babe/babe.go @@ -467,6 +467,30 @@ func (b *Service) handleSlot(epoch, slotNum uint64, return errNilParentHeader } + atGenesisBlock := b.blockState.GenesisHash().Equal(parentHeader.Hash()) + if !atGenesisBlock { + bestBlockSlotNum, err := b.blockState.GetSlotForBlock(parentHeader.Hash()) + if err != nil { + return fmt.Errorf("could not get slot for block %s: %w", parentHeader.Hash(), err) + } + + if bestBlockSlotNum > slotNum { + return errLaggingSlot + } + + if bestBlockSlotNum == slotNum { + // pick parent of best block instead to handle slot + newParentHeader, err := b.blockState.GetHeader(parentHeader.ParentHash) + if err != nil { + return fmt.Errorf("could not get header for hash %s: %w", parentHeader.ParentHash, err) + } + if newParentHeader == nil { + return errNilParentHeader + } + parentHeader = newParentHeader + } + } + // there is a chance that the best block header may change in the course of building the block, // so let's copy it first. parent, err := parentHeader.DeepCopy() diff --git a/lib/babe/babe_integration_test.go b/lib/babe/babe_integration_test.go index a3033853088..1249d389018 100644 --- a/lib/babe/babe_integration_test.go +++ b/lib/babe/babe_integration_test.go @@ -15,6 +15,7 @@ import ( "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/internal/log" "github.com/ChainSafe/gossamer/lib/babe/mocks" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/crypto/sr25519" "github.com/ChainSafe/gossamer/lib/genesis" "github.com/ChainSafe/gossamer/lib/runtime" @@ -293,3 +294,173 @@ func TestService_PauseAndResume(t *testing.T) { err = bs.Stop() require.NoError(t, err) } + +func TestService_HandleSlotWithLaggingSlot(t *testing.T) { + cfg := &ServiceConfig{ + Authority: true, + Lead: true, + } + babeService := createTestService(t, cfg) + + err := babeService.Start() + require.NoError(t, err) + defer func() { + _ = babeService.Stop() + }() + + // add a block + parentHash := babeService.blockState.GenesisHash() + rt, err := babeService.blockState.GetRuntime(nil) + require.NoError(t, err) + + epochData, err := babeService.initiateEpoch(testEpochIndex) + require.NoError(t, err) + + ext := runtime.NewTestExtrinsic(t, rt, parentHash, parentHash, 0, "System.remark", []byte{0xab, 0xcd}) + block := createTestBlock(t, babeService, emptyHeader, [][]byte{common.MustHexToBytes(ext)}, + 1, testEpochIndex, epochData) + + babeService.blockState.AddBlock(block) + time.Sleep(babeService.constants.slotDuration * 1) + + header, err := babeService.blockState.BestBlockHeader() + require.NoError(t, err) + + bestBlockSlotNum, err := babeService.blockState.GetSlotForBlock(header.Hash()) + require.NoError(t, err) + + slotnum := uint64(1) + slot := Slot{ + start: time.Now(), + duration: 1 * time.Second, + number: slotnum, + } + testVRFOutputAndProof := &VrfOutputAndProof{} + preRuntimeDigest, err := types.NewBabePrimaryPreDigest( + 0, slot.number, + testVRFOutputAndProof.output, + testVRFOutputAndProof.proof, + ).ToPreRuntimeDigest() + + require.NoError(t, err) + + err = babeService.handleSlot( + babeService.epochHandler.epochNumber, + bestBlockSlotNum-1, + babeService.epochHandler.epochData.authorityIndex, + preRuntimeDigest) + + require.ErrorIs(t, err, errLaggingSlot) +} + +func TestService_HandleSlotWithSameSlot(t *testing.T) { + alice := keyring.Alice().(*sr25519.Keypair) + bob := keyring.Bob().(*sr25519.Keypair) + + // Create babe service for alice + cfgAlice := &ServiceConfig{ + Authority: true, + Lead: true, + Keypair: alice, + } + cfgAlice.AuthData = []types.Authority{ + { + Key: alice.Public().(*sr25519.PublicKey), + Weight: 1, + }, + { + Key: bob.Public().(*sr25519.PublicKey), + Weight: 1, + }, + } + + // Create babe service for bob + cfgBob := &ServiceConfig{ + Authority: true, + Lead: true, + Keypair: bob, + } + cfgBob.AuthData = []types.Authority{ + { + Key: alice.Public().(*sr25519.PublicKey), + Weight: 1, + }, + { + Key: bob.Public().(*sr25519.PublicKey), + Weight: 1, + }, + } + + babeServiceBob := createTestService(t, cfgBob) + + err := babeServiceBob.Start() + require.NoError(t, err) + defer func() { + _ = babeServiceBob.Stop() + }() + + time.Sleep(babeServiceBob.constants.slotDuration * 5) + + // create a block using bob + parentHash := babeServiceBob.blockState.GenesisHash() + rt, err := babeServiceBob.blockState.GetRuntime(nil) + require.NoError(t, err) + + epochData, err := babeServiceBob.initiateEpoch(testEpochIndex) + require.NoError(t, err) + + ext := runtime.NewTestExtrinsic(t, rt, parentHash, parentHash, 0, "System.remark", []byte{0xab, 0xcd}) + block := createTestBlock(t, babeServiceBob, emptyHeader, [][]byte{common.MustHexToBytes(ext)}, + 1, testEpochIndex, epochData) + + err = babeServiceBob.Stop() + require.NoError(t, err) + + babeServiceAlice := createTestService(t, cfgAlice) + + err = babeServiceAlice.Start() + require.NoError(t, err) + defer func() { + _ = babeServiceAlice.Stop() + }() + time.Sleep(babeServiceAlice.constants.slotDuration * 1) + + // Add block created by Bob to Alice + babeServiceAlice.blockState.AddBlock(block) + + time.Sleep(babeServiceAlice.constants.slotDuration * 1) + + bestBlockHeader, err := babeServiceAlice.blockState.BestBlockHeader() + require.NoError(t, err) + + require.Equal(t, block.Header.Hash().String(), bestBlockHeader.Hash().String()) + + // If the slot we are claiming is same as slot in best header, test that we don't + // through any error and can claim slot. + bestBlockSlotNum, err := babeServiceAlice.blockState.GetSlotForBlock(bestBlockHeader.Hash()) + require.NoError(t, err) + + err = babeServiceAlice.Stop() + require.NoError(t, err) + + slot := Slot{ + start: time.Now(), + duration: 1 * time.Second, + number: bestBlockSlotNum, + } + testVRFOutputAndProof := &VrfOutputAndProof{} + preRuntimeDigest, err := types.NewBabePrimaryPreDigest( + 0, slot.number, + testVRFOutputAndProof.output, + testVRFOutputAndProof.proof, + ).ToPreRuntimeDigest() + require.NoError(t, err) + + err = babeServiceAlice.handleSlot( + babeServiceAlice.epochHandler.epochNumber, + bestBlockSlotNum, + babeServiceAlice.epochHandler.epochData.authorityIndex, + preRuntimeDigest) + require.NoError(t, err) + +} diff --git a/lib/babe/errors.go b/lib/babe/errors.go index 463832ba0fe..15a572b6f72 100644 --- a/lib/babe/errors.go +++ b/lib/babe/errors.go @@ -71,6 +71,7 @@ var ( errInvalidSlotTechnique = errors.New("invalid slot claiming technique") errNoBABEAuthorityKeyProvided = errors.New("cannot create BABE service as authority; no keypair provided") errLastDigestItemNotSeal = errors.New("last digest item is not seal") + errLaggingSlot = errors.New("cannot claim slot, current slot is smaller than slot of best block") other Other invalidCustom InvalidCustom diff --git a/lib/genesis/test_utils.go b/lib/genesis/test_utils.go index f0bfe01ec37..4d63675ca45 100644 --- a/lib/genesis/test_utils.go +++ b/lib/genesis/test_utils.go @@ -13,6 +13,7 @@ import ( "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/trie" "github.com/ChainSafe/gossamer/lib/utils" + "github.com/ChainSafe/gossamer/pkg/scale" "github.com/stretchr/testify/require" ) @@ -100,8 +101,22 @@ func newGenesisTrieAndHeader(t *testing.T, gen *Genesis) (*trie.Trie, *types.Hea genTrie, err := NewTrieFromGenesis(gen) require.NoError(t, err) + babeDigest := types.NewBabeDigest() + err = babeDigest.Set(types.BabePrimaryPreDigest{AuthorityIndex: 0}) + require.NoError(t, err) + + bdEnc, err := scale.Marshal(babeDigest) + require.NoError(t, err) + + digest := types.NewDigest() + err = digest.Add(types.PreRuntimeDigest{ + ConsensusEngineID: types.BabeEngineID, + Data: bdEnc, + }) + require.NoError(t, err) + genesisHeader, err := types.NewHeader(common.NewHash([]byte{0}), - genTrie.MustHash(), trie.EmptyHash, 0, types.NewDigest()) + genTrie.MustHash(), trie.EmptyHash, 0, digest) require.NoError(t, err) return genTrie, genesisHeader