From 364ad3fbdad2fcab5f1f6ce9fcde53c1f601899a Mon Sep 17 00:00:00 2001 From: terencechain Date: Sun, 29 May 2022 12:32:42 -0700 Subject: [PATCH] Reinsert reorg atts (#10767) * Add common ancestor root for protoarray * More efficient algo * Tests * Fix linting * Fix linting * Fix linting * Fix linting * Fix linting * Fix linting * Apply suggestions from code review Co-authored-by: Potuz * Feedbacks * Revert saveHead changes * Revert "Revert saveHead changes" This reverts commit a15fddc2e68b2e370290b1783eb1ee626af17503. * Fix rest of the tests * Update beacon-chain/blockchain/head.go Co-authored-by: Potuz Co-authored-by: Potuz Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com> --- beacon-chain/blockchain/head.go | 87 +++-- beacon-chain/blockchain/head_test.go | 366 ++++++++++++++++-- .../blockchain/receive_attestation_test.go | 2 + beacon-chain/forkchoice/BUILD.bazel | 2 + .../forkchoice/doubly-linked-tree/BUILD.bazel | 2 + .../doubly-linked-tree/forkchoice.go | 46 +++ .../doubly-linked-tree/forkchoice_test.go | 156 ++++++++ beacon-chain/forkchoice/error.go | 5 + beacon-chain/forkchoice/interfaces.go | 1 + .../forkchoice/protoarray/BUILD.bazel | 2 + beacon-chain/forkchoice/protoarray/store.go | 47 +++ .../forkchoice/protoarray/store_test.go | 154 ++++++++ 12 files changed, 808 insertions(+), 62 deletions(-) create mode 100644 beacon-chain/forkchoice/error.go diff --git a/beacon-chain/blockchain/head.go b/beacon-chain/blockchain/head.go index 4feaccfe71..00b4b5b85d 100644 --- a/beacon-chain/blockchain/head.go +++ b/beacon-chain/blockchain/head.go @@ -9,6 +9,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/core/feed" statefeed "github.com/prysmaticlabs/prysm/beacon-chain/core/feed/state" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice" doublylinkedtree "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/doubly-linked-tree" "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/protoarray" "github.com/prysmaticlabs/prysm/beacon-chain/state" @@ -107,16 +108,16 @@ func (s *Service) updateHead(ctx context.Context, balances []uint64) ([32]byte, // This saves head info to the local service cache, it also saves the // new head root to the DB. -func (s *Service) saveHead(ctx context.Context, headRoot [32]byte, headBlock interfaces.SignedBeaconBlock, headState state.BeaconState) error { +func (s *Service) saveHead(ctx context.Context, newHeadRoot [32]byte, headBlock interfaces.SignedBeaconBlock, headState state.BeaconState) error { ctx, span := trace.StartSpan(ctx, "blockChain.saveHead") defer span.End() // Do nothing if head hasn't changed. - r, err := s.HeadRoot(ctx) + oldHeadroot, err := s.HeadRoot(ctx) if err != nil { return err } - if headRoot == bytesutil.ToBytes32(r) { + if newHeadRoot == bytesutil.ToBytes32(oldHeadroot) { return nil } if err := wrapper.BeaconBlockIsNil(headBlock); err != nil { @@ -128,7 +129,7 @@ func (s *Service) saveHead(ctx context.Context, headRoot [32]byte, headBlock int // If the head state is not available, just return nil. // There's nothing to cache - if !s.cfg.BeaconDB.HasStateSummary(ctx, headRoot) { + if !s.cfg.BeaconDB.HasStateSummary(ctx, newHeadRoot) { return nil } @@ -140,7 +141,7 @@ func (s *Service) saveHead(ctx context.Context, headRoot [32]byte, headBlock int headSlot := s.HeadSlot() newHeadSlot := headBlock.Block().Slot() newStateRoot := headBlock.Block().StateRoot() - if bytesutil.ToBytes32(headBlock.Block().ParentRoot()) != bytesutil.ToBytes32(r) { + if bytesutil.ToBytes32(headBlock.Block().ParentRoot()) != bytesutil.ToBytes32(oldHeadroot) { log.WithFields(logrus.Fields{ "newSlot": fmt.Sprintf("%d", newHeadSlot), "oldSlot": fmt.Sprintf("%d", headSlot), @@ -156,7 +157,7 @@ func (s *Service) saveHead(ctx context.Context, headRoot [32]byte, headBlock int Slot: newHeadSlot, Depth: absoluteSlotDifference, OldHeadBlock: oldHeadRoot[:], - NewHeadBlock: headRoot[:], + NewHeadBlock: newHeadRoot[:], OldHeadState: oldStateRoot, NewHeadState: newStateRoot, Epoch: slots.ToEpoch(newHeadSlot), @@ -164,25 +165,24 @@ func (s *Service) saveHead(ctx context.Context, headRoot [32]byte, headBlock int }, }) - if err := s.saveOrphanedAtts(ctx, bytesutil.ToBytes32(r)); err != nil { + if err := s.saveOrphanedAtts(ctx, bytesutil.ToBytes32(oldHeadroot), newHeadRoot); err != nil { return err } - reorgCount.Inc() } // Cache the new head info. - s.setHead(headRoot, headBlock, headState) + s.setHead(newHeadRoot, headBlock, headState) // Save the new head root to DB. - if err := s.cfg.BeaconDB.SaveHeadBlockRoot(ctx, headRoot); err != nil { + if err := s.cfg.BeaconDB.SaveHeadBlockRoot(ctx, newHeadRoot); err != nil { return errors.Wrap(err, "could not save head root in DB") } // Forward an event capturing a new chain head over a common event feed // done in a goroutine to avoid blocking the critical runtime main routine. go func() { - if err := s.notifyNewHeadEvent(ctx, newHeadSlot, headState, newStateRoot, headRoot[:]); err != nil { + if err := s.notifyNewHeadEvent(ctx, newHeadSlot, headState, newStateRoot, newHeadRoot[:]); err != nil { log.WithError(err).Error("Could not notify event feed of new chain head") } }() @@ -355,35 +355,48 @@ func (s *Service) notifyNewHeadEvent( return nil } -// This saves the attestations inside the beacon block with respect to root `orphanedRoot` back into the -// attestation pool. It also filters out the attestations that is one epoch older as a -// defense so invalid attestations don't flow into the attestation pool. -func (s *Service) saveOrphanedAtts(ctx context.Context, orphanedRoot [32]byte) error { - orphanedBlk, err := s.getBlock(ctx, orphanedRoot) - if err != nil { +// This saves the attestations between `orphanedRoot` and the common ancestor root that is derived using `newHeadRoot`. +// It also filters out the attestations that is one epoch older as a defense so invalid attestations don't flow into the attestation pool. +func (s *Service) saveOrphanedAtts(ctx context.Context, orphanedRoot [32]byte, newHeadRoot [32]byte) error { + commonAncestorRoot, err := s.ForkChoicer().CommonAncestorRoot(ctx, newHeadRoot, orphanedRoot) + switch { + // Exit early if there's no common ancestor as there would be nothing to save. + case errors.Is(err, forkchoice.ErrUnknownCommonAncestor): + return nil + case err != nil: return err } - - if orphanedBlk == nil || orphanedBlk.IsNil() { - return errors.New("orphaned block can't be nil") - } - - for _, a := range orphanedBlk.Block().Body().Attestations() { - // Is the attestation one epoch older. - if a.Data.Slot+params.BeaconConfig().SlotsPerEpoch < s.CurrentSlot() { - continue + for orphanedRoot != commonAncestorRoot { + if ctx.Err() != nil { + return ctx.Err() } - if helpers.IsAggregated(a) { - if err := s.cfg.AttPool.SaveAggregatedAttestation(a); err != nil { - return err - } - } else { - if err := s.cfg.AttPool.SaveUnaggregatedAttestation(a); err != nil { - return err - } - } - saveOrphanedAttCount.Inc() - } + orphanedBlk, err := s.getBlock(ctx, orphanedRoot) + if err != nil { + return err + } + // If the block is an epoch older, break out of the loop since we can't include atts anyway. + // This prevents stuck within this for loop longer than necessary. + if orphanedBlk.Block().Slot()+params.BeaconConfig().SlotsPerEpoch <= s.CurrentSlot() { + break + } + for _, a := range orphanedBlk.Block().Body().Attestations() { + // if the attestation is one epoch older, it wouldn't been useful to save it. + if a.Data.Slot+params.BeaconConfig().SlotsPerEpoch < s.CurrentSlot() { + continue + } + if helpers.IsAggregated(a) { + if err := s.cfg.AttPool.SaveAggregatedAttestation(a); err != nil { + return err + } + } else { + if err := s.cfg.AttPool.SaveUnaggregatedAttestation(a); err != nil { + return err + } + } + saveOrphanedAttCount.Inc() + } + orphanedRoot = bytesutil.ToBytes32(orphanedBlk.Block().ParentRoot()) + } return nil } diff --git a/beacon-chain/blockchain/head_test.go b/beacon-chain/blockchain/head_test.go index 10ff994175..7447fa352b 100644 --- a/beacon-chain/blockchain/head_test.go +++ b/beacon-chain/blockchain/head_test.go @@ -3,6 +3,7 @@ package blockchain import ( "bytes" "context" + "sort" "testing" "time" @@ -10,9 +11,11 @@ import ( testDB "github.com/prysmaticlabs/prysm/beacon-chain/db/testing" doublylinkedtree "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/doubly-linked-tree" "github.com/prysmaticlabs/prysm/beacon-chain/state/stategen" + "github.com/prysmaticlabs/prysm/config/features" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/consensus-types/wrapper" + "github.com/prysmaticlabs/prysm/encoding/bytesutil" ethpbv1 "github.com/prysmaticlabs/prysm/proto/eth/v1" ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/testing/assert" @@ -49,6 +52,8 @@ func TestSaveHead_Different(t *testing.T) { require.NoError(t, service.cfg.BeaconDB.SaveBlock(context.Background(), oldBlock)) oldRoot, err := oldBlock.Block().HashTreeRoot() require.NoError(t, err) + require.NoError(t, service.cfg.ForkChoiceStore.InsertOptimisticBlock( + ctx, oldBlock.Block().Slot(), oldRoot, bytesutil.ToBytes32(oldBlock.Block().ParentRoot()), [32]byte{}, 0, 0)) service.head = &head{ slot: 0, root: oldRoot, @@ -64,6 +69,8 @@ func TestSaveHead_Different(t *testing.T) { require.NoError(t, service.cfg.BeaconDB.SaveBlock(context.Background(), wsb)) newRoot, err := newHeadBlock.HashTreeRoot() require.NoError(t, err) + require.NoError(t, service.cfg.ForkChoiceStore.InsertOptimisticBlock( + ctx, wsb.Block().Slot(), newRoot, bytesutil.ToBytes32(wsb.Block().ParentRoot()), [32]byte{}, 0, 0)) headState, err := util.NewBeaconState() require.NoError(t, err) require.NoError(t, headState.SetSlot(1)) @@ -93,6 +100,8 @@ func TestSaveHead_Different_Reorg(t *testing.T) { require.NoError(t, service.cfg.BeaconDB.SaveBlock(context.Background(), oldBlock)) oldRoot, err := oldBlock.Block().HashTreeRoot() require.NoError(t, err) + require.NoError(t, service.cfg.ForkChoiceStore.InsertOptimisticBlock( + ctx, oldBlock.Block().Slot(), oldRoot, bytesutil.ToBytes32(oldBlock.Block().ParentRoot()), [32]byte{}, 0, 0)) service.head = &head{ slot: 0, root: oldRoot, @@ -110,6 +119,8 @@ func TestSaveHead_Different_Reorg(t *testing.T) { require.NoError(t, service.cfg.BeaconDB.SaveBlock(context.Background(), wsb)) newRoot, err := newHeadBlock.HashTreeRoot() require.NoError(t, err) + require.NoError(t, service.cfg.ForkChoiceStore.InsertOptimisticBlock( + ctx, wsb.Block().Slot(), newRoot, bytesutil.ToBytes32(wsb.Block().ParentRoot()), [32]byte{}, 0, 0)) headState, err := util.NewBeaconState() require.NoError(t, err) require.NoError(t, headState.SetSlot(1)) @@ -229,50 +240,355 @@ func Test_notifyNewHeadEvent(t *testing.T) { }) } -func TestSaveOrphanedAtts(t *testing.T) { - genesis, keys := util.DeterministicGenesisState(t, 64) - b, err := util.GenerateFullBlock(genesis, keys, util.DefaultBlockGenConfig(), 1) - assert.NoError(t, err) - r, err := b.Block.HashTreeRoot() - require.NoError(t, err) - +func TestSaveOrphanedAtts_NoCommonAncestor(t *testing.T) { ctx := context.Background() beaconDB := testDB.SetupDB(t) service := setupBeaconChain(t, beaconDB) - service.genesisTime = time.Now() + service.genesisTime = time.Now().Add(time.Duration(-10*int64(1)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) - wsb, err := wrapper.WrappedSignedBeaconBlock(b) + // Chain setup + // 0 -- 1 -- 2 -- 3 + // -4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) + assert.NoError(t, err) + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() require.NoError(t, err) - require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb)) - require.NoError(t, service.saveOrphanedAtts(ctx, r)) - require.Equal(t, len(b.Block.Body.Attestations), service.cfg.AttPool.AggregatedAttestationCount()) - savedAtts := service.cfg.AttPool.AggregatedAttestations() - atts := b.Block.Body.Attestations - require.DeepSSZEqual(t, atts, savedAtts) + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk3, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 3) + assert.NoError(t, err) + blk3.Block.ParentRoot = r2[:] + r3, err := blk3.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk3, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r3, r4)) + require.Equal(t, 0, service.cfg.AttPool.AggregatedAttestationCount()) +} + +func TestSaveOrphanedAtts(t *testing.T) { + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + service := setupBeaconChain(t, beaconDB) + service.genesisTime = time.Now().Add(time.Duration(-10*int64(1)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) + + // Chain setup + // 0 -- 1 -- 2 -- 3 + // \-4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) + assert.NoError(t, err) + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() + require.NoError(t, err) + + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk3, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 3) + assert.NoError(t, err) + blk3.Block.ParentRoot = r2[:] + r3, err := blk3.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + blk4.Block.ParentRoot = rG[:] + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk3, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r3, r4)) + require.Equal(t, 3, service.cfg.AttPool.AggregatedAttestationCount()) + wantAtts := []*ethpb.Attestation{ + blk3.Block.Body.Attestations[0], + blk2.Block.Body.Attestations[0], + blk1.Block.Body.Attestations[0], + } + atts := service.cfg.AttPool.AggregatedAttestations() + sort.Slice(atts, func(i, j int) bool { + return atts[i].Data.Slot > atts[j].Data.Slot + }) + require.DeepEqual(t, wantAtts, atts) } func TestSaveOrphanedAtts_CanFilter(t *testing.T) { - genesis, keys := util.DeterministicGenesisState(t, 64) - b, err := util.GenerateFullBlock(genesis, keys, util.DefaultBlockGenConfig(), 1) + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + service := setupBeaconChain(t, beaconDB) + service.genesisTime = time.Now().Add(time.Duration(-1*int64(params.BeaconConfig().SlotsPerEpoch+2)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) + + // Chain setup + // 0 -- 1 -- 2 + // \-4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) assert.NoError(t, err) - r, err := b.Block.HashTreeRoot() + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() require.NoError(t, err) + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + blk4.Block.ParentRoot = rG[:] + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r2, r4)) + require.Equal(t, 0, service.cfg.AttPool.AggregatedAttestationCount()) +} + +func TestSaveOrphanedAtts_NoCommonAncestor_DoublyLinkedTrie(t *testing.T) { + resetCfg := features.InitWithReset(&features.Flags{ + EnableForkChoiceDoublyLinkedTree: true, + }) + defer resetCfg() + ctx := context.Background() beaconDB := testDB.SetupDB(t) service := setupBeaconChain(t, beaconDB) - service.genesisTime = time.Now().Add(time.Duration(-1*int64(params.BeaconConfig().SlotsPerEpoch+1)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) + service.genesisTime = time.Now().Add(time.Duration(-10*int64(1)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) - wsb, err := wrapper.WrappedSignedBeaconBlock(b) + // Chain setup + // 0 -- 1 -- 2 -- 3 + // -4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) + assert.NoError(t, err) + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() require.NoError(t, err) - require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wsb)) - require.NoError(t, service.saveOrphanedAtts(ctx, r)) + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk3, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 3) + assert.NoError(t, err) + blk3.Block.ParentRoot = r2[:] + r3, err := blk3.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk3, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r3, r4)) + require.Equal(t, 0, service.cfg.AttPool.AggregatedAttestationCount()) +} + +func TestSaveOrphanedAtts_DoublyLinkedTrie(t *testing.T) { + resetCfg := features.InitWithReset(&features.Flags{ + EnableForkChoiceDoublyLinkedTree: true, + }) + defer resetCfg() + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + service := setupBeaconChain(t, beaconDB) + service.genesisTime = time.Now().Add(time.Duration(-10*int64(1)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) + + // Chain setup + // 0 -- 1 -- 2 -- 3 + // \-4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) + assert.NoError(t, err) + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() + require.NoError(t, err) + + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk3, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 3) + assert.NoError(t, err) + blk3.Block.ParentRoot = r2[:] + r3, err := blk3.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + blk4.Block.ParentRoot = rG[:] + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk3, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r3, r4)) + require.Equal(t, 3, service.cfg.AttPool.AggregatedAttestationCount()) + wantAtts := []*ethpb.Attestation{ + blk3.Block.Body.Attestations[0], + blk2.Block.Body.Attestations[0], + blk1.Block.Body.Attestations[0], + } + atts := service.cfg.AttPool.AggregatedAttestations() + sort.Slice(atts, func(i, j int) bool { + return atts[i].Data.Slot > atts[j].Data.Slot + }) + require.DeepEqual(t, wantAtts, atts) +} + +func TestSaveOrphanedAtts_CanFilter_DoublyLinkedTrie(t *testing.T) { + resetCfg := features.InitWithReset(&features.Flags{ + EnableForkChoiceDoublyLinkedTree: true, + }) + defer resetCfg() + + ctx := context.Background() + beaconDB := testDB.SetupDB(t) + service := setupBeaconChain(t, beaconDB) + service.genesisTime = time.Now().Add(time.Duration(-1*int64(params.BeaconConfig().SlotsPerEpoch+2)*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second) + + // Chain setup + // 0 -- 1 -- 2 + // \-4 + st, keys := util.DeterministicGenesisState(t, 64) + blkG, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 0) + assert.NoError(t, err) + b, err := wrapper.WrappedSignedBeaconBlock(blkG) + assert.NoError(t, err) + require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, b)) + rG, err := blkG.Block.HashTreeRoot() + require.NoError(t, err) + + blk1, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 1) + assert.NoError(t, err) + blk1.Block.ParentRoot = rG[:] + r1, err := blk1.Block.HashTreeRoot() + require.NoError(t, err) + + blk2, err := util.GenerateFullBlock(st, keys, util.DefaultBlockGenConfig(), 2) + assert.NoError(t, err) + blk2.Block.ParentRoot = r1[:] + r2, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + + blk4 := util.NewBeaconBlock() + blk4.Block.Slot = 4 + blk4.Block.ParentRoot = rG[:] + r4, err := blk4.Block.HashTreeRoot() + require.NoError(t, err) + + for _, blk := range []*ethpb.SignedBeaconBlock{blkG, blk1, blk2, blk4} { + r, err := blk.Block.HashTreeRoot() + require.NoError(t, err) + require.NoError(t, service.ForkChoicer().InsertOptimisticBlock(ctx, blk.Block.Slot, r, bytesutil.ToBytes32(blk.Block.ParentRoot), [32]byte{}, 0, 0)) + b, err := wrapper.WrappedSignedBeaconBlock(blk) + require.NoError(t, err) + require.NoError(t, beaconDB.SaveBlock(ctx, b)) + } + + require.NoError(t, service.saveOrphanedAtts(ctx, r2, r4)) require.Equal(t, 0, service.cfg.AttPool.AggregatedAttestationCount()) - savedAtts := service.cfg.AttPool.AggregatedAttestations() - atts := b.Block.Body.Attestations - require.DeepNotSSZEqual(t, atts, savedAtts) } func TestUpdateHead_noSavedChanges(t *testing.T) { diff --git a/beacon-chain/blockchain/receive_attestation_test.go b/beacon-chain/blockchain/receive_attestation_test.go index e3551367e0..521b112496 100644 --- a/beacon-chain/blockchain/receive_attestation_test.go +++ b/beacon-chain/blockchain/receive_attestation_test.go @@ -233,6 +233,8 @@ func TestService_ProcessAttestationsAndUpdateHead(t *testing.T) { r, err := b.Block.HashTreeRoot() require.NoError(t, err) require.NoError(t, service.cfg.BeaconDB.SaveBlock(ctx, wb)) + require.NoError(t, service.cfg.ForkChoiceStore.InsertOptimisticBlock( + ctx, wb.Block().Slot(), r, bytesutil.ToBytes32(wb.Block().ParentRoot()), [32]byte{}, 0, 0)) service.head.root = r // Old head require.Equal(t, 1, len(service.cfg.AttPool.ForkchoiceAttestations())) require.NoError(t, err, service.UpdateHead(ctx)) diff --git a/beacon-chain/forkchoice/BUILD.bazel b/beacon-chain/forkchoice/BUILD.bazel index 2a785ec490..57ce97f928 100644 --- a/beacon-chain/forkchoice/BUILD.bazel +++ b/beacon-chain/forkchoice/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "error.go", "interfaces.go", ], importpath = "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice", @@ -16,5 +17,6 @@ go_library( "//config/fieldparams:go_default_library", "//consensus-types/primitives:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "@com_github_pkg_errors//:go_default_library", ], ) diff --git a/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel b/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel index 47c1170188..c3c91b6a5f 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel +++ b/beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "//testing/spectest:__subpackages__", ], deps = [ + "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", @@ -49,6 +50,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go index e4067dc7f1..24011ab063 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice" fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" @@ -379,3 +380,48 @@ func (f *ForkChoice) UpdateFinalizedCheckpoint(fc *pbrpc.Checkpoint) error { f.store.finalizedEpoch = fc.Epoch return nil } + +// CommonAncestorRoot returns the common ancestor root between the two block roots r1 and r2. +func (f *ForkChoice) CommonAncestorRoot(ctx context.Context, r1 [32]byte, r2 [32]byte) ([32]byte, error) { + ctx, span := trace.StartSpan(ctx, "doublelinkedtree.CommonAncestorRoot") + defer span.End() + + // Do nothing if the input roots are the same. + if r1 == r2 { + return r1, nil + } + + f.store.nodesLock.RLock() + defer f.store.nodesLock.RUnlock() + + n1, ok := f.store.nodeByRoot[r1] + if !ok || n1 == nil { + return [32]byte{}, ErrNilNode + } + n2, ok := f.store.nodeByRoot[r2] + if !ok || n2 == nil { + return [32]byte{}, ErrNilNode + } + + for { + if ctx.Err() != nil { + return [32]byte{}, ctx.Err() + } + if n1.slot > n2.slot { + n1 = n1.parent + // Reaches the end of the tree and unable to find common ancestor. + if n1 == nil { + return [32]byte{}, forkchoice.ErrUnknownCommonAncestor + } + } else { + n2 = n2.parent + // Reaches the end of the tree and unable to find common ancestor. + if n2 == nil { + return [32]byte{}, forkchoice.ErrUnknownCommonAncestor + } + } + if n1 == n2 { + return n1.root, nil + } + } +} diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go index 7f30f9243c..d01a0e8e4c 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "testing" + "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/crypto/hash" @@ -251,3 +252,158 @@ func TestStore_UpdateCheckpoints(t *testing.T) { require.Equal(t, f.store.justifiedEpoch, jc.Epoch) require.Equal(t, f.store.finalizedEpoch, fc.Epoch) } + +func TestStore_CommonAncestor(t *testing.T) { + { + ctx := context.Background() + f := setup(0, 0) + + // /-- b -- d -- e + // a + // \-- c -- f + // \-- g + // \ -- h -- i -- j + require.NoError(t, f.InsertOptimisticBlock(ctx, 0, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'c'}, [32]byte{'a'}, [32]byte{'C'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'d'}, [32]byte{'b'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 4, [32]byte{'e'}, [32]byte{'d'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 5, [32]byte{'f'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 6, [32]byte{'g'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 7, [32]byte{'h'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 8, [32]byte{'i'}, [32]byte{'h'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 9, [32]byte{'j'}, [32]byte{'i'}, [32]byte{}, 1, 1)) + + tests := []struct { + name string + r1 [32]byte + r2 [32]byte + wantRoot [32]byte + }{ + { + name: "Common ancestor between c and b is a", + r1: [32]byte{'c'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between c and d is a", + r1: [32]byte{'c'}, + r2: [32]byte{'d'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between c and e is a", + r1: [32]byte{'c'}, + r2: [32]byte{'e'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between g and f is c", + r1: [32]byte{'g'}, + r2: [32]byte{'f'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between f and h is c", + r1: [32]byte{'f'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between g and h is c", + r1: [32]byte{'g'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between b and h is a", + r1: [32]byte{'b'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between e and h is a", + r1: [32]byte{'e'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between i and f is c", + r1: [32]byte{'i'}, + r2: [32]byte{'f'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between e and h is a", + r1: [32]byte{'j'}, + r2: [32]byte{'g'}, + wantRoot: [32]byte{'c'}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotRoot, err := f.CommonAncestorRoot(ctx, tc.r1, tc.r2) + require.NoError(t, err) + require.Equal(t, tc.wantRoot, gotRoot) + }) + } + + // a -- b -- c -- d + f = setup(0, 0) + require.NoError(t, f.InsertOptimisticBlock(ctx, 0, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + tests = []struct { + name string + r1 [32]byte + r2 [32]byte + wantRoot [32]byte + }{ + { + name: "Common ancestor between a and b is a", + r1: [32]byte{'a'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between b and d is b", + r1: [32]byte{'d'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'b'}, + }, + { + name: "Common ancestor between d and a is a", + r1: [32]byte{'d'}, + r2: [32]byte{'a'}, + wantRoot: [32]byte{'a'}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotRoot, err := f.CommonAncestorRoot(ctx, tc.r1, tc.r2) + require.NoError(t, err) + require.Equal(t, tc.wantRoot, gotRoot) + }) + } + + // Equal inputs should return the same root. + r, err := f.CommonAncestorRoot(ctx, [32]byte{'b'}, [32]byte{'b'}) + require.NoError(t, err) + require.Equal(t, [32]byte{'b'}, r) + // Requesting finalized root (last node) should return the same root. + r, err = f.CommonAncestorRoot(ctx, [32]byte{'a'}, [32]byte{'a'}) + require.NoError(t, err) + require.Equal(t, [32]byte{'a'}, r) + // Requesting unknown root + _, err = f.CommonAncestorRoot(ctx, [32]byte{'a'}, [32]byte{'z'}) + require.ErrorIs(t, err, ErrNilNode) + _, err = f.CommonAncestorRoot(ctx, [32]byte{'z'}, [32]byte{'a'}) + require.ErrorIs(t, err, ErrNilNode) + require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'y'}, [32]byte{'z'}, [32]byte{}, 1, 1)) + // broken link + _, err = f.CommonAncestorRoot(ctx, [32]byte{'y'}, [32]byte{'a'}) + require.ErrorIs(t, err, forkchoice.ErrUnknownCommonAncestor) + } +} diff --git a/beacon-chain/forkchoice/error.go b/beacon-chain/forkchoice/error.go new file mode 100644 index 0000000000..7341d9d89d --- /dev/null +++ b/beacon-chain/forkchoice/error.go @@ -0,0 +1,5 @@ +package forkchoice + +import "github.com/pkg/errors" + +var ErrUnknownCommonAncestor = errors.New("unknown common ancestor") diff --git a/beacon-chain/forkchoice/interfaces.go b/beacon-chain/forkchoice/interfaces.go index db764c21d4..76738bee18 100644 --- a/beacon-chain/forkchoice/interfaces.go +++ b/beacon-chain/forkchoice/interfaces.go @@ -62,6 +62,7 @@ type Getter interface { ProposerBoost() [fieldparams.RootLength]byte HasParent(root [32]byte) bool AncestorRoot(ctx context.Context, root [32]byte, slot types.Slot) ([]byte, error) + CommonAncestorRoot(ctx context.Context, root1 [32]byte, root2 [32]byte) ([32]byte, error) IsCanonical(root [32]byte) bool FinalizedEpoch() types.Epoch JustifiedEpoch() types.Epoch diff --git a/beacon-chain/forkchoice/protoarray/BUILD.bazel b/beacon-chain/forkchoice/protoarray/BUILD.bazel index 1fa101ac3e..6621bceea6 100644 --- a/beacon-chain/forkchoice/protoarray/BUILD.bazel +++ b/beacon-chain/forkchoice/protoarray/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "//testing/spectest:__subpackages__", ], deps = [ + "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", @@ -50,6 +51,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//beacon-chain/forkchoice:go_default_library", "//beacon-chain/forkchoice/types:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", diff --git a/beacon-chain/forkchoice/protoarray/store.go b/beacon-chain/forkchoice/protoarray/store.go index f1518f6353..cf9ed50f2a 100644 --- a/beacon-chain/forkchoice/protoarray/store.go +++ b/beacon-chain/forkchoice/protoarray/store.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice" fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" @@ -195,6 +196,52 @@ func (f *ForkChoice) AncestorRoot(ctx context.Context, root [32]byte, slot types return f.store.nodes[i].root[:], nil } +// CommonAncestorRoot returns the common ancestor root between the two block roots r1 and r2. +func (f *ForkChoice) CommonAncestorRoot(ctx context.Context, r1 [32]byte, r2 [32]byte) ([32]byte, error) { + ctx, span := trace.StartSpan(ctx, "protoArray.CommonAncestorRoot") + defer span.End() + + // Do nothing if the two input roots are the same. + if r1 == r2 { + return r1, nil + } + + i1, ok := f.store.nodesIndices[r1] + if !ok || i1 >= uint64(len(f.store.nodes)) { + return [32]byte{}, errInvalidNodeIndex + } + + i2, ok := f.store.nodesIndices[r2] + if !ok || i2 >= uint64(len(f.store.nodes)) { + return [32]byte{}, errInvalidNodeIndex + } + + for { + if ctx.Err() != nil { + return [32]byte{}, ctx.Err() + } + if i1 > i2 { + n1 := f.store.nodes[i1] + i1 = n1.parent + // Reaches the end of the tree and unable to find common ancestor. + if i1 >= uint64(len(f.store.nodes)) { + return [32]byte{}, forkchoice.ErrUnknownCommonAncestor + } + } else { + n2 := f.store.nodes[i2] + i2 = n2.parent + // Reaches the end of the tree and unable to find common ancestor. + if i2 >= uint64(len(f.store.nodes)) { + return [32]byte{}, forkchoice.ErrUnknownCommonAncestor + } + } + if i1 == i2 { + n1 := f.store.nodes[i1] + return n1.root, nil + } + } +} + // PruneThreshold of fork choice store. func (s *Store) PruneThreshold() uint64 { return s.pruneThreshold diff --git a/beacon-chain/forkchoice/protoarray/store_test.go b/beacon-chain/forkchoice/protoarray/store_test.go index b2d3355457..1ab7b7e1cb 100644 --- a/beacon-chain/forkchoice/protoarray/store_test.go +++ b/beacon-chain/forkchoice/protoarray/store_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/encoding/bytesutil" @@ -560,6 +561,159 @@ func TestStore_PruneBranched(t *testing.T) { } } +func TestStore_CommonAncestor(t *testing.T) { + ctx := context.Background() + f := setup(0, 0) + + // /-- b -- d -- e + // a + // \-- c -- f + // \-- g + // \ -- h -- i -- j + require.NoError(t, f.InsertOptimisticBlock(ctx, 0, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'c'}, [32]byte{'a'}, [32]byte{'C'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'d'}, [32]byte{'b'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 4, [32]byte{'e'}, [32]byte{'d'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 5, [32]byte{'f'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 6, [32]byte{'g'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 7, [32]byte{'h'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 8, [32]byte{'i'}, [32]byte{'h'}, [32]byte{}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 9, [32]byte{'j'}, [32]byte{'i'}, [32]byte{}, 1, 1)) + + tests := []struct { + name string + r1 [32]byte + r2 [32]byte + wantRoot [32]byte + }{ + { + name: "Common ancestor between c and b is a", + r1: [32]byte{'c'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between c and d is a", + r1: [32]byte{'c'}, + r2: [32]byte{'d'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between c and e is a", + r1: [32]byte{'c'}, + r2: [32]byte{'e'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between g and f is c", + r1: [32]byte{'g'}, + r2: [32]byte{'f'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between f and h is c", + r1: [32]byte{'f'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between g and h is c", + r1: [32]byte{'g'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between b and h is a", + r1: [32]byte{'b'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between e and h is a", + r1: [32]byte{'e'}, + r2: [32]byte{'h'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between i and f is c", + r1: [32]byte{'i'}, + r2: [32]byte{'f'}, + wantRoot: [32]byte{'c'}, + }, + { + name: "Common ancestor between e and h is a", + r1: [32]byte{'j'}, + r2: [32]byte{'g'}, + wantRoot: [32]byte{'c'}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotRoot, err := f.CommonAncestorRoot(ctx, tc.r1, tc.r2) + require.NoError(t, err) + require.Equal(t, tc.wantRoot, gotRoot) + }) + } + + // a -- b -- c -- d + f = setup(0, 0) + require.NoError(t, f.InsertOptimisticBlock(ctx, 0, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'d'}, [32]byte{'c'}, [32]byte{}, 1, 1)) + tests = []struct { + name string + r1 [32]byte + r2 [32]byte + wantRoot [32]byte + }{ + { + name: "Common ancestor between a and b is a", + r1: [32]byte{'a'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'a'}, + }, + { + name: "Common ancestor between b and d is b", + r1: [32]byte{'d'}, + r2: [32]byte{'b'}, + wantRoot: [32]byte{'b'}, + }, + { + name: "Common ancestor between d and a is a", + r1: [32]byte{'d'}, + r2: [32]byte{'a'}, + wantRoot: [32]byte{'a'}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + gotRoot, err := f.CommonAncestorRoot(ctx, tc.r1, tc.r2) + require.NoError(t, err) + require.Equal(t, tc.wantRoot, gotRoot) + }) + } + + // Equal inputs should return the same root. + r, err := f.CommonAncestorRoot(ctx, [32]byte{'b'}, [32]byte{'b'}) + require.NoError(t, err) + require.Equal(t, [32]byte{'b'}, r) + // Requesting finalized root (last node) should return the same root. + r, err = f.CommonAncestorRoot(ctx, [32]byte{'a'}, [32]byte{'a'}) + require.NoError(t, err) + require.Equal(t, [32]byte{'a'}, r) + // Requesting unknown root + _, err = f.CommonAncestorRoot(ctx, [32]byte{'a'}, [32]byte{'z'}) + require.ErrorIs(t, err, errInvalidNodeIndex) + _, err = f.CommonAncestorRoot(ctx, [32]byte{'z'}, [32]byte{'a'}) + require.ErrorIs(t, err, errInvalidNodeIndex) + require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'y'}, [32]byte{'z'}, [32]byte{}, 1, 1)) + // broken link + _, err = f.CommonAncestorRoot(ctx, [32]byte{'y'}, [32]byte{'a'}) + require.ErrorIs(t, err, forkchoice.ErrUnknownCommonAncestor) +} + func TestStore_LeadsToViableHead(t *testing.T) { tests := []struct { n *Node