diff --git a/beacon-chain/blockchain/process_attestation.go b/beacon-chain/blockchain/process_attestation.go index b28be5e40dcc..7787bda31614 100644 --- a/beacon-chain/blockchain/process_attestation.go +++ b/beacon-chain/blockchain/process_attestation.go @@ -8,7 +8,10 @@ import ( ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" stateTrie "github.com/prysmaticlabs/prysm/beacon-chain/state" + "github.com/prysmaticlabs/prysm/shared/attestationutil" + "github.com/prysmaticlabs/prysm/shared/bls" "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/roughtime" "go.opencensus.io/trace" @@ -66,6 +69,53 @@ func (s *Service) onAttestation(ctx context.Context, a *ethpb.Attestation) ([]ui return nil, ErrTargetRootNotInDB } + if featureconfig.Get().UseCheckPointInfoCache { + c, err := s.AttestationCheckPtInfo(ctx, a) + if err != nil { + return nil, err + } + if err := s.verifyAttTargetEpoch(ctx, params.BeaconConfig().GenesisTime, uint64(roughtime.Now().Unix()), tgt); err != nil { + return nil, err + } + if err := s.verifyBeaconBlock(ctx, a.Data); err != nil { + return nil, errors.Wrap(err, "could not verify attestation beacon block") + } + if err := s.verifyLMDFFGConsistent(ctx, a.Data.Target.Epoch, a.Data.Target.Root, a.Data.BeaconBlockRoot); err != nil { + return nil, errors.Wrap(err, "could not verify attestation beacon block") + } + if err := helpers.VerifySlotTime(params.BeaconConfig().GenesisTime, a.Data.Slot+1, params.BeaconNetworkConfig().MaximumGossipClockDisparity); err != nil { + return nil, err + } + committee, err := helpers.BeaconCommittee(c.ActiveIndices, bytesutil.ToBytes32(c.Seed), a.Data.Slot, a.Data.CommitteeIndex) + if err != nil { + return nil, err + } + indexedAtt := attestationutil.ConvertToIndexed(ctx, a, committee) + if err := attestationutil.IsValidAttestationIndices(ctx, indexedAtt); err != nil { + return nil, err + } + domain, err := helpers.Domain(c.Fork, indexedAtt.Data.Target.Epoch, params.BeaconConfig().DomainBeaconAttester, c.GenesisRoot) + if err != nil { + return nil, err + } + indices := indexedAtt.AttestingIndices + pubkeys := []bls.PublicKey{} + for i := 0; i < len(indices); i++ { + pubkeyAtIdx := c.PubKeys[indices[i]] + pk, err := bls.PublicKeyFromBytes(pubkeyAtIdx) + if err != nil { + return nil, err + } + pubkeys = append(pubkeys, pk) + } + if err := attestationutil.VerifyIndexedAttestationSig(ctx, indexedAtt, pubkeys, domain); err != nil { + return nil, err + } + s.forkChoiceStore.ProcessAttestation(ctx, indexedAtt.AttestingIndices, bytesutil.ToBytes32(a.Data.BeaconBlockRoot), a.Data.Target.Epoch) + + return indexedAtt.AttestingIndices, nil + } + // Retrieve attestation's data beacon block pre state. Advance pre state to latest epoch if necessary and // save it to the cache. baseState, err := s.getAttPreState(ctx, tgt) diff --git a/beacon-chain/blockchain/process_attestation_helpers.go b/beacon-chain/blockchain/process_attestation_helpers.go index 3ba51ee69a5d..8dbeed9aa584 100644 --- a/beacon-chain/blockchain/process_attestation_helpers.go +++ b/beacon-chain/blockchain/process_attestation_helpers.go @@ -11,6 +11,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/beacon-chain/core/state" stateTrie "github.com/prysmaticlabs/prysm/beacon-chain/state" + pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/attestationutil" "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/params" @@ -59,6 +60,63 @@ func (s *Service) getAttPreState(ctx context.Context, c *ethpb.Checkpoint) (*sta } +// getAttCheckPtInfo retrieves the check point info given a check point. Check point info enables the node +// to efficiently verify attestation signature without using beacon state. This function utilizes +// the checkpoint info cache and will update the check point info cache on miss. +func (s *Service) getAttCheckPtInfo(ctx context.Context, c *ethpb.Checkpoint, e uint64) (*pb.CheckPtInfo, error) { + // Return checkpoint info if exists in cache. + info, err := s.checkPtInfoCache.get(c) + if err != nil { + return nil, errors.Wrap(err, "could not get cached checkpoint state") + } + if info != nil { + return info, nil + } + + // Retrieve checkpoint state to compute checkpoint info. + baseState, err := s.stateGen.StateByRoot(ctx, bytesutil.ToBytes32(c.Root)) + if err != nil { + return nil, errors.Wrapf(err, "could not get pre state for slot %d", helpers.StartSlot(c.Epoch)) + } + if helpers.StartSlot(c.Epoch) > baseState.Slot() { + baseState = baseState.Copy() + baseState, err = state.ProcessSlots(ctx, baseState, helpers.StartSlot(c.Epoch)) + if err != nil { + return nil, errors.Wrapf(err, "could not process slots up to %d", helpers.StartSlot(c.Epoch)) + } + } + f := baseState.Fork() + g := bytesutil.ToBytes32(baseState.GenesisValidatorRoot()) + seed, err := helpers.Seed(baseState, e, params.BeaconConfig().DomainBeaconAttester) + if err != nil { + return nil, err + } + indices, err := helpers.ActiveValidatorIndices(baseState, e) + if err != nil { + return nil, err + } + validators := baseState.ValidatorsReadOnly() + pks := make([][]byte, len(validators)) + for i := 0; i < len(pks); i++ { + pk := validators[i].PublicKey() + pks[i] = pk[:] + } + + // Cache and return the checkpoint info. + info = &pb.CheckPtInfo{ + Fork: f, + GenesisRoot: g[:], + Seed: seed[:], + ActiveIndices: indices, + PubKeys: pks, + } + if err := s.checkPtInfoCache.put(c, info); err != nil { + return nil, err + } + + return info, nil +} + // verifyAttTargetEpoch validates attestation is from the current or previous epoch. func (s *Service) verifyAttTargetEpoch(ctx context.Context, genesisTime uint64, nowTime uint64, c *ethpb.Checkpoint) error { currentSlot := (nowTime - genesisTime) / params.BeaconConfig().SecondsPerSlot diff --git a/beacon-chain/blockchain/process_attestation_test.go b/beacon-chain/blockchain/process_attestation_test.go index 9382aec85a84..be4b54fa81cc 100644 --- a/beacon-chain/blockchain/process_attestation_test.go +++ b/beacon-chain/blockchain/process_attestation_test.go @@ -7,6 +7,7 @@ import ( "github.com/gogo/protobuf/proto" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/prysmaticlabs/prysm/beacon-chain/cache" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/beacon-chain/core/state" testDB "github.com/prysmaticlabs/prysm/beacon-chain/db/testing" @@ -16,6 +17,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/testutil" "github.com/prysmaticlabs/prysm/shared/testutil/assert" @@ -139,6 +141,129 @@ func TestStore_OnAttestation(t *testing.T) { } } +func TestStore_OnAttestationUsingCheckptCache(t *testing.T) { + resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{UseCheckPointInfoCache: true}) + defer resetCfg() + + ctx := context.Background() + db, sc := testDB.SetupDB(t) + + cfg := &Config{ + BeaconDB: db, + ForkChoiceStore: protoarray.New(0, 0, [32]byte{}), + StateGen: stategen.New(db, sc), + } + service, err := NewService(ctx, cfg) + require.NoError(t, err) + + _, err = blockTree1(db, []byte{'g'}) + require.NoError(t, err) + + BlkWithOutState := testutil.NewBeaconBlock() + BlkWithOutState.Block.Slot = 0 + require.NoError(t, db.SaveBlock(ctx, BlkWithOutState)) + BlkWithOutStateRoot, err := stateutil.BlockRoot(BlkWithOutState.Block) + require.NoError(t, err) + + BlkWithStateBadAtt := testutil.NewBeaconBlock() + BlkWithStateBadAtt.Block.Slot = 1 + require.NoError(t, db.SaveBlock(ctx, BlkWithStateBadAtt)) + BlkWithStateBadAttRoot, err := stateutil.BlockRoot(BlkWithStateBadAtt.Block) + require.NoError(t, err) + + s := testutil.NewBeaconState() + require.NoError(t, s.SetSlot(100*params.BeaconConfig().SlotsPerEpoch)) + require.NoError(t, service.beaconDB.SaveState(ctx, s, BlkWithStateBadAttRoot)) + + BlkWithValidState := testutil.NewBeaconBlock() + BlkWithValidState.Block.Slot = 2 + require.NoError(t, db.SaveBlock(ctx, BlkWithValidState)) + + BlkWithValidStateRoot, err := stateutil.BlockRoot(BlkWithValidState.Block) + require.NoError(t, err) + s = testutil.NewBeaconState() + if err := s.SetFork(&pb.Fork{ + Epoch: 0, + CurrentVersion: params.BeaconConfig().GenesisForkVersion, + PreviousVersion: params.BeaconConfig().GenesisForkVersion, + }); err != nil { + t.Fatal(err) + } + require.NoError(t, service.beaconDB.SaveState(ctx, s, BlkWithValidStateRoot)) + + tests := []struct { + name string + a *ethpb.Attestation + wantErr bool + wantErrString string + }{ + { + name: "attestation's data slot not aligned with target vote", + a: ðpb.Attestation{Data: ðpb.AttestationData{Slot: params.BeaconConfig().SlotsPerEpoch, Target: ðpb.Checkpoint{Root: make([]byte, 32)}}}, + wantErr: true, + wantErrString: "data slot is not in the same epoch as target 1 != 0", + }, + { + name: "attestation's target root not in db", + a: ðpb.Attestation{Data: ðpb.AttestationData{Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte{'A'}, 32)}}}, + wantErr: true, + wantErrString: "target root does not exist in db", + }, + { + name: "no pre state for attestations's target block", + a: ðpb.Attestation{Data: ðpb.AttestationData{Target: ðpb.Checkpoint{Root: BlkWithOutStateRoot[:]}}}, + wantErr: true, + wantErrString: "could not get pre state for slot 0", + }, + { + name: "process attestation doesn't match current epoch", + a: ðpb.Attestation{Data: ðpb.AttestationData{Slot: 100 * params.BeaconConfig().SlotsPerEpoch, Target: ðpb.Checkpoint{Epoch: 100, + Root: BlkWithStateBadAttRoot[:]}}}, + wantErr: true, + wantErrString: "target epoch 100 does not match current epoch", + }, + { + name: "process nil attestation", + a: nil, + wantErr: true, + wantErrString: "nil attestation", + }, + { + name: "process nil field (a.Data) in attestation", + a: ðpb.Attestation{}, + wantErr: true, + wantErrString: "nil attestation.Data field", + }, + { + name: "process nil field (a.Target) in attestation", + a: ðpb.Attestation{ + Data: ðpb.AttestationData{ + BeaconBlockRoot: make([]byte, 32), + Target: nil, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + AggregationBits: make([]byte, 1), + Signature: make([]byte, 96), + }, + wantErr: true, + wantErrString: "nil attestation.Data.Target field", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := service.onAttestation(ctx, tt.a) + if tt.wantErr { + if err == nil || !strings.Contains(err.Error(), tt.wantErrString) { + t.Errorf("Store.onAttestation() error = %v, wantErr = %v", err, tt.wantErrString) + } + } else { + t.Error(err) + } + }) + } +} + func TestStore_SaveCheckpointState(t *testing.T) { ctx := context.Background() db, sc := testDB.SetupDB(t) @@ -395,3 +520,47 @@ func TestVerifyLMDFFGConsistent_OK(t *testing.T) { err = service.verifyLMDFFGConsistent(context.Background(), 1, r32[:], r33[:]) assert.NoError(t, err, "Could not verify LMD and FFG votes to be consistent") } + +func TestGetAttCheckptInfo(t *testing.T) { + ctx := context.Background() + db, _ := testDB.SetupDB(t) + cfg := &Config{BeaconDB: db, StateGen: stategen.New(db, cache.NewStateSummaryCache())} + service, err := NewService(ctx, cfg) + require.NoError(t, err) + + baseState, _ := testutil.DeterministicGenesisState(t, 128) + b := testutil.NewBeaconBlock() + r, err := stateutil.BlockRoot(b.Block) + require.NoError(t, err) + require.NoError(t, service.beaconDB.SaveState(ctx, baseState, r)) + require.NoError(t, service.beaconDB.SaveBlock(ctx, b)) + require.NoError(t, service.beaconDB.SaveGenesisBlockRoot(ctx, r)) + checkpoint := ðpb.Checkpoint{Root: r[:]} + + returned, err := service.getAttCheckPtInfo(ctx, checkpoint, 0) + require.NoError(t, err) + + seed, err := helpers.Seed(baseState, 0, params.BeaconConfig().DomainBeaconAttester) + require.NoError(t, err) + indices, err := helpers.ActiveValidatorIndices(baseState, 0) + require.NoError(t, err) + validators := baseState.ValidatorsReadOnly() + pks := make([][]byte, len(validators)) + for i := 0; i < len(pks); i++ { + pk := validators[i].PublicKey() + pks[i] = pk[:] + } + + wanted := &pb.CheckPtInfo{ + Fork: baseState.Fork(), + GenesisRoot: baseState.GenesisValidatorRoot(), + Seed: seed[:], + ActiveIndices: indices, + PubKeys: pks, + } + require.DeepEqual(t, wanted, returned) + + cached, err := service.checkPtInfoCache.get(checkpoint) + require.NoError(t, err) + require.DeepEqual(t, wanted, cached) +} diff --git a/beacon-chain/blockchain/receive_attestation.go b/beacon-chain/blockchain/receive_attestation.go index 5eb91fa2ce31..78b49cede6f4 100644 --- a/beacon-chain/blockchain/receive_attestation.go +++ b/beacon-chain/blockchain/receive_attestation.go @@ -10,6 +10,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/core/feed" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/beacon-chain/state" + pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" @@ -24,6 +25,7 @@ type AttestationReceiver interface { ReceiveAttestationNoPubsub(ctx context.Context, att *ethpb.Attestation) error IsValidAttestation(ctx context.Context, att *ethpb.Attestation) bool AttestationPreState(ctx context.Context, att *ethpb.Attestation) (*state.BeaconState, error) + AttestationCheckPtInfo(ctx context.Context, att *ethpb.Attestation) (*pb.CheckPtInfo, error) } // ReceiveAttestationNoPubsub is a function that defines the operations that are performed on @@ -74,6 +76,12 @@ func (s *Service) AttestationPreState(ctx context.Context, att *ethpb.Attestatio return s.getAttPreState(ctx, att.Data.Target) } +// AttestationCheckPtInfo returns the check point info of attestation that can be used to verify the attestation +// contents and signatures. +func (s *Service) AttestationCheckPtInfo(ctx context.Context, att *ethpb.Attestation) (*pb.CheckPtInfo, error) { + return s.getAttCheckPtInfo(ctx, att.Data.Target, helpers.SlotToEpoch(att.Data.Slot)) +} + // This processes attestations from the attestation pool to account for validator votes and fork choice. func (s *Service) processAttestation(subscribedToStateEvents chan struct{}) { // Wait for state to be initialized. diff --git a/beacon-chain/blockchain/service.go b/beacon-chain/blockchain/service.go index 9081fe893c2e..d4cbc0f270c9 100644 --- a/beacon-chain/blockchain/service.go +++ b/beacon-chain/blockchain/service.go @@ -75,6 +75,7 @@ type Service struct { recentCanonicalBlocksLock sync.RWMutex justifiedBalances []uint64 justifiedBalancesLock sync.RWMutex + checkPtInfoCache *checkPtInfoCache } // Config options for the service. @@ -119,6 +120,7 @@ func NewService(ctx context.Context, cfg *Config) (*Service, error) { initSyncBlocks: make(map[[32]byte]*ethpb.SignedBeaconBlock), recentCanonicalBlocks: make(map[[32]byte]bool), justifiedBalances: make([]uint64, 0), + checkPtInfoCache: newCheckPointInfoCache(), }, nil } diff --git a/beacon-chain/blockchain/testing/BUILD.bazel b/beacon-chain/blockchain/testing/BUILD.bazel index 7591c127b485..48e3609d2a47 100644 --- a/beacon-chain/blockchain/testing/BUILD.bazel +++ b/beacon-chain/blockchain/testing/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "//beacon-chain/state:go_default_library", "//beacon-chain/state/stateutil:go_default_library", "//proto/beacon/p2p/v1:go_default_library", + "//shared/bytesutil:go_default_library", "//shared/event:go_default_library", "//shared/params:go_default_library", "@com_github_pkg_errors//:go_default_library", diff --git a/beacon-chain/blockchain/testing/mock.go b/beacon-chain/blockchain/testing/mock.go index e662defbb484..320b51fe9da4 100644 --- a/beacon-chain/blockchain/testing/mock.go +++ b/beacon-chain/blockchain/testing/mock.go @@ -21,6 +21,7 @@ import ( stateTrie "github.com/prysmaticlabs/prysm/beacon-chain/state" "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" + "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/event" "github.com/prysmaticlabs/prysm/shared/params" "github.com/sirupsen/logrus" @@ -353,3 +354,33 @@ func (ms *ChainService) HeadGenesisValidatorRoot() [32]byte { func (ms *ChainService) VerifyBlkDescendant(ctx context.Context, root [32]byte) error { return ms.VerifyBlkDescendantErr } + +// AttestationCheckPtInfo mocks AttestationCheckPtInfo and always returns nil. +func (ms *ChainService) AttestationCheckPtInfo(ctx context.Context, att *ethpb.Attestation) (*pb.CheckPtInfo, error) { + f := ms.State.Fork() + g := bytesutil.ToBytes32(ms.State.GenesisValidatorRoot()) + seed, err := helpers.Seed(ms.State, helpers.SlotToEpoch(att.Data.Slot), params.BeaconConfig().DomainBeaconAttester) + if err != nil { + return nil, err + } + indices, err := helpers.ActiveValidatorIndices(ms.State, helpers.SlotToEpoch(att.Data.Slot)) + if err != nil { + return nil, err + } + validators := ms.State.ValidatorsReadOnly() + pks := make([][]byte, len(validators)) + for i := 0; i < len(pks); i++ { + pk := validators[i].PublicKey() + pks[i] = pk[:] + } + + info := &pb.CheckPtInfo{ + Fork: f, + GenesisRoot: g[:], + Seed: seed[:], + ActiveIndices: indices, + PubKeys: pks, + } + + return info, nil +} diff --git a/beacon-chain/cache/hot_state_cache.go b/beacon-chain/cache/hot_state_cache.go index 32e8b2cb134e..846891ba1e90 100644 --- a/beacon-chain/cache/hot_state_cache.go +++ b/beacon-chain/cache/hot_state_cache.go @@ -11,7 +11,7 @@ import ( var ( // hotStateCacheSize defines the max number of hot state this can cache. - hotStateCacheSize = 32 + hotStateCacheSize = 16 // Metrics hotStateCacheHit = promauto.NewCounter(prometheus.CounterOpts{ Name: "hot_state_cache_hit", diff --git a/beacon-chain/core/blocks/attestation.go b/beacon-chain/core/blocks/attestation.go index d30f00096a2b..96b1574b9164 100644 --- a/beacon-chain/core/blocks/attestation.go +++ b/beacon-chain/core/blocks/attestation.go @@ -12,6 +12,7 @@ import ( pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/attestationutil" "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/params" "go.opencensus.io/trace" ) @@ -325,3 +326,35 @@ func verifyAttestationsSigWithDomain(ctx context.Context, beaconState *stateTrie } return nil } + +// VerifyAttSigUseCheckPt uses the checkpoint info object to verify attestation signature. +func VerifyAttSigUseCheckPt(ctx context.Context, c *pb.CheckPtInfo, att *ethpb.Attestation) error { + if att == nil || att.Data == nil || att.AggregationBits.Count() == 0 { + return fmt.Errorf("nil or missing attestation data: %v", att) + } + seed := bytesutil.ToBytes32(c.Seed) + committee, err := helpers.BeaconCommittee(c.ActiveIndices, seed, att.Data.Slot, att.Data.CommitteeIndex) + if err != nil { + return err + } + indexedAtt := attestationutil.ConvertToIndexed(ctx, att, committee) + if err := attestationutil.IsValidAttestationIndices(ctx, indexedAtt); err != nil { + return err + } + domain, err := helpers.Domain(c.Fork, indexedAtt.Data.Target.Epoch, params.BeaconConfig().DomainBeaconAttester, c.GenesisRoot) + if err != nil { + return err + } + indices := indexedAtt.AttestingIndices + pubkeys := []bls.PublicKey{} + for i := 0; i < len(indices); i++ { + pubkeyAtIdx := c.PubKeys[indices[i]] + pk, err := bls.PublicKeyFromBytes(pubkeyAtIdx) + if err != nil { + return errors.Wrap(err, "could not deserialize validator public key") + } + pubkeys = append(pubkeys, pk) + } + + return attestationutil.VerifyIndexedAttestationSig(ctx, indexedAtt, pubkeys, domain) +} diff --git a/beacon-chain/core/helpers/BUILD.bazel b/beacon-chain/core/helpers/BUILD.bazel index de52ace98fe0..91eb59dd8194 100644 --- a/beacon-chain/core/helpers/BUILD.bazel +++ b/beacon-chain/core/helpers/BUILD.bazel @@ -36,6 +36,7 @@ go_library( "//proto/beacon/p2p/v1:go_default_library", "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", + "//shared/featureconfig:go_default_library", "//shared/hashutil:go_default_library", "//shared/params:go_default_library", "//shared/roughtime:go_default_library", diff --git a/beacon-chain/core/helpers/committee.go b/beacon-chain/core/helpers/committee.go index 245551ad20d5..21a4d19c3a9d 100644 --- a/beacon-chain/core/helpers/committee.go +++ b/beacon-chain/core/helpers/committee.go @@ -12,6 +12,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/cache" stateTrie "github.com/prysmaticlabs/prysm/beacon-chain/state" "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/hashutil" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/sliceutil" @@ -143,7 +144,30 @@ func ComputeCommittee( // for fast computation of committees. // Reference implementation: https://github.com/protolambda/eth2-shuffle shuffledList, err := UnshuffleList(shuffledIndices, seed) - return shuffledList[start:end], err + if err != nil { + return nil, err + } + + // This updates the cache on a miss. + if featureconfig.Get().UseCheckPointInfoCache { + sortedIndices := make([]uint64, len(indices)) + copy(sortedIndices, indices) + sort.Slice(sortedIndices, func(i, j int) bool { + return sortedIndices[i] < sortedIndices[j] + }) + + count = SlotCommitteeCount(uint64(len(shuffledIndices))) + if err := committeeCache.AddCommitteeShuffledList(&cache.Committees{ + ShuffledIndices: shuffledList, + CommitteeCount: count * params.BeaconConfig().SlotsPerEpoch, + Seed: seed, + SortedIndices: sortedIndices, + }); err != nil { + return nil, err + } + } + + return shuffledList[start:end], nil } // CommitteeAssignmentContainer represents a committee, index, and attester slot for a given epoch. diff --git a/beacon-chain/sync/validate_aggregate_proof.go b/beacon-chain/sync/validate_aggregate_proof.go index 81a7c30fe565..555df6788e19 100644 --- a/beacon-chain/sync/validate_aggregate_proof.go +++ b/beacon-chain/sync/validate_aggregate_proof.go @@ -95,6 +95,61 @@ func (s *Service) validateAggregatedAtt(ctx context.Context, signed *ethpb.Signe attSlot := signed.Message.Aggregate.Data.Slot + if featureconfig.Get().UseCheckPointInfoCache { + // Use check point info to validate aggregated attestation. + c, err := s.chain.AttestationCheckPtInfo(ctx, signed.Message.Aggregate) + if err != nil { + traceutil.AnnotateError(span, err) + return pubsub.ValidationIgnore + } + a := signed.Message.Aggregate + committee, err := helpers.BeaconCommittee(c.ActiveIndices, bytesutil.ToBytes32(c.Seed), a.Data.Slot, a.Data.CommitteeIndex) + if err != nil { + return pubsub.ValidationIgnore + } + // Is the aggregator part of the committee. + var withinCommittee bool + for _, i := range committee { + if signed.Message.AggregatorIndex == i { + withinCommittee = true + break + } + } + if !withinCommittee { + return pubsub.ValidationReject + } + // Is the selection proof signed by the aggregator. + aggregator, err := helpers.IsAggregator(uint64(len(committee)), signed.Message.SelectionProof) + if err != nil { + return pubsub.ValidationReject + } + if !aggregator { + return pubsub.ValidationReject + } + // Are the aggregate and proof by the aggregator. + d, err := helpers.Domain(c.Fork, helpers.SlotToEpoch(a.Data.Slot), params.BeaconConfig().DomainSelectionProof, c.GenesisRoot) + if err != nil { + return pubsub.ValidationReject + } + pk := c.PubKeys[signed.Message.AggregatorIndex] + if err := helpers.VerifySigningRoot(a.Data.Slot, pk[:], signed.Message.SelectionProof, d); err != nil { + return pubsub.ValidationReject + } + // Is the attestation signature correct. + d, err = helpers.Domain(c.Fork, helpers.SlotToEpoch(a.Data.Slot), params.BeaconConfig().DomainAggregateAndProof, c.GenesisRoot) + if err != nil { + return pubsub.ValidationReject + } + if err := helpers.VerifySigningRoot(signed.Message, pk[:], signed.Signature, d); err != nil { + return pubsub.ValidationReject + } + if err := blocks.VerifyAttSigUseCheckPt(ctx, c, signed.Message.Aggregate); err != nil { + return pubsub.ValidationReject + } + + return pubsub.ValidationAccept + } + bs, err := s.chain.AttestationPreState(ctx, signed.Message.Aggregate) if err != nil { traceutil.AnnotateError(span, err) diff --git a/beacon-chain/sync/validate_aggregate_proof_test.go b/beacon-chain/sync/validate_aggregate_proof_test.go index 69aef1f085b0..1e008b6b4bb2 100644 --- a/beacon-chain/sync/validate_aggregate_proof_test.go +++ b/beacon-chain/sync/validate_aggregate_proof_test.go @@ -314,7 +314,7 @@ func TestValidateAggregateAndProof_ExistedInPool(t *testing.T) { } } -func TestValidateAggregateAndProofWithNewStateMgmt_CanValidate(t *testing.T) { +func TestValidateAggregateAndProof_CanValidate(t *testing.T) { resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{NewStateMgmt: true}) defer resetCfg() @@ -405,6 +405,97 @@ func TestValidateAggregateAndProofWithNewStateMgmt_CanValidate(t *testing.T) { assert.NotNil(t, msg.ValidatorData, "Did not set validator data") } +func TestValidateAggregateAndProofUseCheckptCache_CanValidate(t *testing.T) { + resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{UseCheckPointInfoCache: true}) + defer resetCfg() + + db, _ := dbtest.SetupDB(t) + p := p2ptest.NewTestP2P(t) + + validators := uint64(256) + beaconState, privKeys := testutil.DeterministicGenesisState(t, validators) + + b := testutil.NewBeaconBlock() + require.NoError(t, db.SaveBlock(context.Background(), b)) + root, err := stateutil.BlockRoot(b.Block) + require.NoError(t, err) + s := testutil.NewBeaconState() + require.NoError(t, db.SaveState(context.Background(), s, root)) + + aggBits := bitfield.NewBitlist(3) + aggBits.SetBitAt(0, true) + att := ðpb.Attestation{ + Data: ðpb.AttestationData{ + BeaconBlockRoot: root[:], + Source: ðpb.Checkpoint{Epoch: 0, Root: bytesutil.PadTo([]byte("hello-world"), 32)}, + Target: ðpb.Checkpoint{Epoch: 0, Root: bytesutil.PadTo([]byte("hello-world"), 32)}, + }, + AggregationBits: aggBits, + } + + committee, err := helpers.BeaconCommitteeFromState(beaconState, att.Data.Slot, att.Data.CommitteeIndex) + assert.NoError(t, err) + attestingIndices := attestationutil.AttestingIndices(att.AggregationBits, committee) + assert.NoError(t, err) + attesterDomain, err := helpers.Domain(beaconState.Fork(), 0, params.BeaconConfig().DomainBeaconAttester, beaconState.GenesisValidatorRoot()) + assert.NoError(t, err) + hashTreeRoot, err := helpers.ComputeSigningRoot(att.Data, attesterDomain) + assert.NoError(t, err) + sigs := make([]bls.Signature, len(attestingIndices)) + for i, indice := range attestingIndices { + sig := privKeys[indice].Sign(hashTreeRoot[:]) + sigs[i] = sig + } + att.Signature = bls.AggregateSignatures(sigs).Marshal()[:] + ai := committee[0] + sig, err := helpers.ComputeDomainAndSign(beaconState, 0, att.Data.Slot, params.BeaconConfig().DomainSelectionProof, privKeys[ai]) + require.NoError(t, err) + aggregateAndProof := ðpb.AggregateAttestationAndProof{ + SelectionProof: sig, + Aggregate: att, + AggregatorIndex: ai, + } + signedAggregateAndProof := ðpb.SignedAggregateAttestationAndProof{Message: aggregateAndProof} + signedAggregateAndProof.Signature, err = helpers.ComputeDomainAndSign(beaconState, 0, signedAggregateAndProof.Message, params.BeaconConfig().DomainAggregateAndProof, privKeys[ai]) + require.NoError(t, err) + + require.NoError(t, beaconState.SetGenesisTime(uint64(time.Now().Unix()))) + c, err := lru.New(10) + require.NoError(t, err) + r := &Service{ + p2p: p, + db: db, + initialSync: &mockSync.Sync{IsSyncing: false}, + chain: &mock.ChainService{Genesis: time.Now(), + State: beaconState, + ValidAttestation: true, + FinalizedCheckPoint: ðpb.Checkpoint{ + Epoch: 0, + }}, + attPool: attestations.NewPool(), + seenAttestationCache: c, + stateSummaryCache: cache.NewStateSummaryCache(), + } + err = r.initCaches() + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = p.Encoding().EncodeGossip(buf, signedAggregateAndProof) + require.NoError(t, err) + + msg := &pubsub.Message{ + Message: &pubsubpb.Message{ + Data: buf.Bytes(), + TopicIDs: []string{ + p2p.GossipTypeMapping[reflect.TypeOf(signedAggregateAndProof)], + }, + }, + } + + assert.Equal(t, pubsub.ValidationAccept, r.validateAggregateAndProof(context.Background(), "", msg), "Validated status is false") + assert.NotNil(t, msg.ValidatorData, "Did not set validator data") +} + func TestVerifyIndexInCommittee_SeenAggregatorEpoch(t *testing.T) { db, _ := dbtest.SetupDB(t) p := p2ptest.NewTestP2P(t) diff --git a/beacon-chain/sync/validate_beacon_attestation.go b/beacon-chain/sync/validate_beacon_attestation.go index e9a33d92d661..b7f0c96182ee 100644 --- a/beacon-chain/sync/validate_beacon_attestation.go +++ b/beacon-chain/sync/validate_beacon_attestation.go @@ -98,6 +98,37 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p traceutil.AnnotateError(span, err) return pubsub.ValidationIgnore } + + if featureconfig.Get().UseCheckPointInfoCache { + // Use check point info to validate unaggregated attestation. + c, err := s.chain.AttestationCheckPtInfo(ctx, att) + if err != nil { + return pubsub.ValidationIgnore + } + // Is the attestation subnet correct. + indices := c.ActiveIndices + subnet := helpers.ComputeSubnetForAttestation(uint64(len(indices)), att) + if !strings.HasPrefix(originalTopic, fmt.Sprintf(format, digest, subnet)) { + return pubsub.ValidationReject + } + committee, err := helpers.BeaconCommittee(indices, bytesutil.ToBytes32(c.Seed), att.Data.Slot, att.Data.CommitteeIndex) + if err != nil { + return pubsub.ValidationIgnore + } + // Is the attestation bitfield correct. + if att.AggregationBits.Count() != 1 || att.AggregationBits.BitIndices()[0] >= len(committee) { + return pubsub.ValidationReject + } + // Is the attestation signature correct. + if err := blocks.VerifyAttSigUseCheckPt(ctx, c, att); err != nil { + return pubsub.ValidationReject + } + + s.setSeenCommitteeIndicesSlot(att.Data.Slot, att.Data.CommitteeIndex, att.AggregationBits) + msg.ValidatorData = att + return pubsub.ValidationAccept + } + preState, err := s.chain.AttestationPreState(ctx, att) if err != nil { log.Error("Failed to retrieve pre state") diff --git a/beacon-chain/sync/validate_beacon_attestation_test.go b/beacon-chain/sync/validate_beacon_attestation_test.go index c973682b7b51..4e5904bfacef 100644 --- a/beacon-chain/sync/validate_beacon_attestation_test.go +++ b/beacon-chain/sync/validate_beacon_attestation_test.go @@ -20,6 +20,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" mockSync "github.com/prysmaticlabs/prysm/beacon-chain/sync/initial-sync/testing" "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/testutil" "github.com/prysmaticlabs/prysm/shared/testutil/require" @@ -227,3 +228,214 @@ func TestService_validateCommitteeIndexBeaconAttestation(t *testing.T) { }) } } + +func TestService_validateCommitteeIndexBeaconAttestationUseCheckptCache(t *testing.T) { + ctx := context.Background() + p := p2ptest.NewTestP2P(t) + db, _ := dbtest.SetupDB(t) + chain := &mockChain.ChainService{ + // 1 slot ago. + Genesis: time.Now().Add(time.Duration(-1*int64(params.BeaconConfig().SecondsPerSlot)) * time.Second), + ValidatorsRoot: [32]byte{'A'}, + ValidAttestation: true, + } + + c, err := lru.New(10) + require.NoError(t, err) + s := &Service{ + initialSync: &mockSync.Sync{IsSyncing: false}, + p2p: p, + db: db, + chain: chain, + blkRootToPendingAtts: make(map[[32]byte][]*ethpb.SignedAggregateAttestationAndProof), + seenAttestationCache: c, + stateSummaryCache: cache.NewStateSummaryCache(), + } + err = s.initCaches() + require.NoError(t, err) + + invalidRoot := [32]byte{'A', 'B', 'C', 'D'} + s.setBadBlock(ctx, invalidRoot) + + digest, err := s.forkDigest() + require.NoError(t, err) + + blk := testutil.NewBeaconBlock() + blk.Block.Slot = 1 + require.NoError(t, db.SaveBlock(ctx, blk)) + + validBlockRoot, err := stateutil.BlockRoot(blk.Block) + require.NoError(t, err) + + validators := uint64(64) + savedState, keys := testutil.DeterministicGenesisState(t, validators) + require.NoError(t, savedState.SetSlot(1)) + require.NoError(t, db.SaveState(context.Background(), savedState, validBlockRoot)) + chain.State = savedState + + tests := []struct { + name string + msg *ethpb.Attestation + topic string + validAttestationSignature bool + want bool + }{ + { + name: "valid attestation signature", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: validBlockRoot[:], + CommitteeIndex: 0, + Slot: 1, + Target: ðpb.Checkpoint{ + Epoch: 0, + Root: validBlockRoot[:], + }, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: true, + want: true, + }, + { + name: "already seen", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: validBlockRoot[:], + CommitteeIndex: 0, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: true, + want: false, + }, + { + name: "invalid beacon block", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: invalidRoot[:], + CommitteeIndex: 0, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: true, + want: false, + }, + + { + name: "wrong committee index", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: validBlockRoot[:], + CommitteeIndex: 2, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_2", digest), + validAttestationSignature: true, + want: false, + }, + { + name: "already aggregated", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1011}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: validBlockRoot[:], + CommitteeIndex: 1, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: true, + want: false, + }, + { + name: "missing block", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("missing"), 32), + CommitteeIndex: 1, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: true, + want: false, + }, + { + name: "invalid attestation", + msg: ðpb.Attestation{ + AggregationBits: bitfield.Bitlist{0b1010}, + Data: ðpb.AttestationData{ + BeaconBlockRoot: validBlockRoot[:], + CommitteeIndex: 1, + Slot: 1, + Target: ðpb.Checkpoint{Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Root: make([]byte, 32)}, + }, + }, + topic: fmt.Sprintf("/eth2/%x/beacon_attestation_1", digest), + validAttestationSignature: false, + want: false, + }, + } + + resetCfg := featureconfig.InitWithReset(&featureconfig.Flags{UseCheckPointInfoCache: true}) + defer resetCfg() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chain.ValidAttestation = tt.validAttestationSignature + if tt.validAttestationSignature { + com, err := helpers.BeaconCommitteeFromState(savedState, tt.msg.Data.Slot, tt.msg.Data.CommitteeIndex) + require.NoError(t, err) + domain, err := helpers.Domain(savedState.Fork(), tt.msg.Data.Target.Epoch, params.BeaconConfig().DomainBeaconAttester, savedState.GenesisValidatorRoot()) + require.NoError(t, err) + attRoot, err := helpers.ComputeSigningRoot(tt.msg.Data, domain) + require.NoError(t, err) + for i := 0; ; i++ { + if tt.msg.AggregationBits.BitAt(uint64(i)) { + tt.msg.Signature = keys[com[i]].Sign(attRoot[:]).Marshal() + break + } + } + } else { + tt.msg.Signature = make([]byte, 96) + } + buf := new(bytes.Buffer) + _, err := p.Encoding().EncodeGossip(buf, tt.msg) + require.NoError(t, err) + m := &pubsub.Message{ + Message: &pubsubpb.Message{ + Data: buf.Bytes(), + TopicIDs: []string{tt.topic}, + }, + } + received := s.validateCommitteeIndexBeaconAttestation(ctx, "" /*peerID*/, m) == pubsub.ValidationAccept + if received != tt.want { + t.Fatalf("Did not received wanted validation. Got %v, wanted %v", !tt.want, tt.want) + } + if tt.want && m.ValidatorData == nil { + t.Error("Expected validator data to be set") + } + }) + } +} diff --git a/shared/featureconfig/config.go b/shared/featureconfig/config.go index 3b5f997cd1a0..83acbea39893 100644 --- a/shared/featureconfig/config.go +++ b/shared/featureconfig/config.go @@ -82,6 +82,7 @@ type Flags struct { EnableEth1DataVoteCache bool // EnableEth1DataVoteCache; see https://github.com/prysmaticlabs/prysm/issues/3106. EnableSlasherConnection bool // EnableSlasher enable retrieval of slashing events from a slasher instance. EnableBlockTreeCache bool // EnableBlockTreeCache enable fork choice service to maintain latest filtered block tree. + UseCheckPointInfoCache bool // UseCheckPointInfoCache uses check point info cache to efficiently verify attestation signatures. KafkaBootstrapServers string // KafkaBootstrapServers to find kafka servers to stream blocks, attestations, etc. AttestationAggregationStrategy string // AttestationAggregationStrategy defines aggregation strategy to be used when aggregating. @@ -265,6 +266,10 @@ func ConfigureBeaconChain(ctx *cli.Context) { log.Warn("Enabling roughtime sync") cfg.EnableRoughtime = true } + if ctx.Bool(checkPtInfoCache.Name) { + log.Warn("Using advance check point info cache") + cfg.UseCheckPointInfoCache = true + } Init(cfg) } diff --git a/shared/featureconfig/flags.go b/shared/featureconfig/flags.go index 648139a1c352..779e9b7dec79 100644 --- a/shared/featureconfig/flags.go +++ b/shared/featureconfig/flags.go @@ -174,10 +174,15 @@ var ( Name: "enable-roughtime", Usage: "Enables periodic roughtime syncs.", } + checkPtInfoCache = &cli.BoolFlag{ + Name: "use-check-point-cache", + Usage: "Enables check point info caching", + } ) // devModeFlags holds list of flags that are set when development mode is on. var devModeFlags = []cli.Flag{ + checkPtInfoCache, batchBlockVerify, enableAttBroadcastDiscoveryAttempts, enablePeerScorer, @@ -665,6 +670,7 @@ var BeaconChainFlags = append(deprecatedFlags, []cli.Flag{ enableAttBroadcastDiscoveryAttempts, enablePeerScorer, enableRoughtime, + checkPtInfoCache, }...) // E2EBeaconChainFlags contains a list of the beacon chain feature flags to be tested in E2E. @@ -675,5 +681,6 @@ var E2EBeaconChainFlags = []string{ "--attestation-aggregation-strategy=max_cover", "--dev", "--enable-finalized-deposits-cache", + "--use-check-point-cache", // "--enable-eth1-data-majority-vote", // TODO(6786): This flag fails long running e2e tests. }