diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index 90bd8479053b..5fc016773431 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -99,7 +99,7 @@ go_library( go_test( name = "go_default_test", - size = "small", + size = "medium", srcs = [ "aggregate_test.go", "attest_protect_test.go", diff --git a/validator/client/aggregate_test.go b/validator/client/aggregate_test.go index c08cda6a008a..1100f6e9339c 100644 --- a/validator/client/aggregate_test.go +++ b/validator/client/aggregate_test.go @@ -3,6 +3,7 @@ package client import ( "context" "errors" + "fmt" "testing" "github.com/golang/mock/gomock" @@ -21,151 +22,175 @@ import ( ) func TestSubmitAggregateAndProof_GetDutiesRequestFailure(t *testing.T) { - hook := logTest.NewGlobal() - validator, _, validatorKey, finish := setup(t) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) - - require.LogsContain(t, hook, "Could not fetch validator assignment") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) + + require.LogsContain(t, hook, "Could not fetch validator assignment") + }) + } } func TestSubmitAggregateAndProof_SignFails(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{ - Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - }, - }, + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{ + Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + }, + }, + } + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().SubmitAggregateSelectionProof( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AggregateSelectionRequest{}), + ).Return(ðpb.AggregateSelectionResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProof{ + AggregatorIndex: 0, + Aggregate: util.HydrateAttestation(ðpb.Attestation{ + AggregationBits: make([]byte, 1), + }), + SelectionProof: make([]byte, 96), + }, + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: nil}, errors.New("bad domain root")) + + validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) + }) } - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().SubmitAggregateSelectionProof( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AggregateSelectionRequest{}), - ).Return(ðpb.AggregateSelectionResponse{ - AggregateAndProof: ðpb.AggregateAttestationAndProof{ - AggregatorIndex: 0, - Aggregate: util.HydrateAttestation(ðpb.Attestation{ - AggregationBits: make([]byte, 1), - }), - SelectionProof: make([]byte, 96), - }, - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: nil}, errors.New("bad domain root")) - - validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) } func TestSubmitAggregateAndProof_Ok(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{ - Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - }, - }, + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{ + Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + }, + }, + } + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().SubmitAggregateSelectionProof( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AggregateSelectionRequest{}), + ).Return(ðpb.AggregateSelectionResponse{ + AggregateAndProof: ðpb.AggregateAttestationAndProof{ + AggregatorIndex: 0, + Aggregate: util.HydrateAttestation(ðpb.Attestation{ + AggregationBits: make([]byte, 1), + }), + SelectionProof: make([]byte, 96), + }, + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().SubmitSignedAggregateSelectionProof( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.SignedAggregateSubmitRequest{}), + ).Return(ðpb.SignedAggregateSubmitResponse{AttestationDataRoot: make([]byte, 32)}, nil) + + validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) + }) } - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().SubmitAggregateSelectionProof( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AggregateSelectionRequest{}), - ).Return(ðpb.AggregateSelectionResponse{ - AggregateAndProof: ðpb.AggregateAttestationAndProof{ - AggregatorIndex: 0, - Aggregate: util.HydrateAttestation(ðpb.Attestation{ - AggregationBits: make([]byte, 1), - }), - SelectionProof: make([]byte, 96), - }, - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().SubmitSignedAggregateSelectionProof( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.SignedAggregateSubmitRequest{}), - ).Return(ðpb.SignedAggregateSubmitResponse{AttestationDataRoot: make([]byte, 32)}, nil) - - validator.SubmitAggregateAndProof(context.Background(), 0, pubKey) } func TestWaitForSlotTwoThird_WaitCorrectly(t *testing.T) { - validator, _, _, finish := setup(t) - defer finish() - currentTime := time.Now() - numOfSlots := primitives.Slot(4) - validator.genesisTime = uint64(currentTime.Unix()) - uint64(numOfSlots.Mul(params.BeaconConfig().SecondsPerSlot)) - oneThird := slots.DivideSlotBy(3 /* one third of slot duration */) - timeToSleep := oneThird + oneThird - - twoThirdTime := currentTime.Add(timeToSleep) - validator.waitToSlotTwoThirds(context.Background(), numOfSlots) - currentTime = time.Now() - assert.Equal(t, twoThirdTime.Unix(), currentTime.Unix()) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, _, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + currentTime := time.Now() + numOfSlots := primitives.Slot(4) + validator.genesisTime = uint64(currentTime.Unix()) - uint64(numOfSlots.Mul(params.BeaconConfig().SecondsPerSlot)) + oneThird := slots.DivideSlotBy(3 /* one third of slot duration */) + timeToSleep := oneThird + oneThird + + twoThirdTime := currentTime.Add(timeToSleep) + validator.waitToSlotTwoThirds(context.Background(), numOfSlots) + currentTime = time.Now() + assert.Equal(t, twoThirdTime.Unix(), currentTime.Unix()) + }) + } } func TestWaitForSlotTwoThird_DoneContext_ReturnsImmediately(t *testing.T) { - validator, _, _, finish := setup(t) - defer finish() - currentTime := time.Now() - numOfSlots := primitives.Slot(4) - validator.genesisTime = uint64(currentTime.Unix()) - uint64(numOfSlots.Mul(params.BeaconConfig().SecondsPerSlot)) - - expectedTime := time.Now() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - validator.waitToSlotTwoThirds(ctx, numOfSlots) - currentTime = time.Now() - assert.Equal(t, expectedTime.Unix(), currentTime.Unix()) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, _, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + currentTime := time.Now() + numOfSlots := primitives.Slot(4) + validator.genesisTime = uint64(currentTime.Unix()) - uint64(numOfSlots.Mul(params.BeaconConfig().SecondsPerSlot)) + + expectedTime := time.Now() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + validator.waitToSlotTwoThirds(ctx, numOfSlots) + currentTime = time.Now() + assert.Equal(t, expectedTime.Unix(), currentTime.Unix()) + }) + } } func TestAggregateAndProofSignature_CanSignValidSignature(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - ðpb.DomainRequest{Epoch: 0, Domain: params.BeaconConfig().DomainAggregateAndProof[:]}, - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - agg := ðpb.AggregateAttestationAndProof{ - AggregatorIndex: 0, - Aggregate: util.HydrateAttestation(ðpb.Attestation{ - AggregationBits: bitfield.NewBitlist(1), - }), - SelectionProof: make([]byte, 96), + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + ðpb.DomainRequest{Epoch: 0, Domain: params.BeaconConfig().DomainAggregateAndProof[:]}, + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + agg := ðpb.AggregateAttestationAndProof{ + AggregatorIndex: 0, + Aggregate: util.HydrateAttestation(ðpb.Attestation{ + AggregationBits: bitfield.NewBitlist(1), + }), + SelectionProof: make([]byte, 96), + } + sig, err := validator.aggregateAndProofSig(context.Background(), pubKey, agg, 0 /* slot */) + require.NoError(t, err) + _, err = bls.SignatureFromBytes(sig) + require.NoError(t, err) + }) } - sig, err := validator.aggregateAndProofSig(context.Background(), pubKey, agg, 0 /* slot */) - require.NoError(t, err) - _, err = bls.SignatureFromBytes(sig) - require.NoError(t, err) } diff --git a/validator/client/attest_protect_test.go b/validator/client/attest_protect_test.go index e6185c8fb2a4..d90f3216b189 100644 --- a/validator/client/attest_protect_test.go +++ b/validator/client/attest_protect_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "fmt" "testing" "github.com/golang/mock/gomock" @@ -13,135 +14,151 @@ import ( ) func Test_slashableAttestationCheck(t *testing.T) { - validator, _, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - att := ðpb.IndexedAttestation{ - AttestingIndices: []uint64{1, 2}, - Data: ðpb.AttestationData{ - Slot: 5, - CommitteeIndex: 2, - BeaconBlockRoot: bytesutil.PadTo([]byte("great block"), 32), - Source: ðpb.Checkpoint{ - Epoch: 4, - Root: bytesutil.PadTo([]byte("good source"), 32), - }, - Target: ðpb.Checkpoint{ - Epoch: 10, - Root: bytesutil.PadTo([]byte("good target"), 32), - }, - }, - } + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + att := ðpb.IndexedAttestation{ + AttestingIndices: []uint64{1, 2}, + Data: ðpb.AttestationData{ + Slot: 5, + CommitteeIndex: 2, + BeaconBlockRoot: bytesutil.PadTo([]byte("great block"), 32), + Source: ðpb.Checkpoint{ + Epoch: 4, + Root: bytesutil.PadTo([]byte("good source"), 32), + }, + Target: ðpb.Checkpoint{ + Epoch: 10, + Root: bytesutil.PadTo([]byte("good target"), 32), + }, + }, + } - err := validator.slashableAttestationCheck(context.Background(), att, pubKey, [32]byte{1}) - require.NoError(t, err, "Expected allowed attestation not to throw error") + err := validator.slashableAttestationCheck(context.Background(), att, pubKey, [32]byte{1}) + require.NoError(t, err, "Expected allowed attestation not to throw error") + }) + } } func Test_slashableAttestationCheck_UpdatesLowestSignedEpochs(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - ctx := context.Background() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - att := ðpb.IndexedAttestation{ - AttestingIndices: []uint64{1, 2}, - Data: ðpb.AttestationData{ - Slot: 5, - CommitteeIndex: 2, - BeaconBlockRoot: bytesutil.PadTo([]byte("great block"), 32), - Source: ðpb.Checkpoint{ - Epoch: 4, - Root: bytesutil.PadTo([]byte("good source"), 32), - }, - Target: ðpb.Checkpoint{ - Epoch: 10, - Root: bytesutil.PadTo([]byte("good target"), 32), - }, - }, - } + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + ctx := context.Background() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + att := ðpb.IndexedAttestation{ + AttestingIndices: []uint64{1, 2}, + Data: ðpb.AttestationData{ + Slot: 5, + CommitteeIndex: 2, + BeaconBlockRoot: bytesutil.PadTo([]byte("great block"), 32), + Source: ðpb.Checkpoint{ + Epoch: 4, + Root: bytesutil.PadTo([]byte("good source"), 32), + }, + Target: ðpb.Checkpoint{ + Epoch: 10, + Root: bytesutil.PadTo([]byte("good target"), 32), + }, + }, + } - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - ðpb.DomainRequest{Epoch: 10, Domain: []byte{1, 0, 0, 0}}, - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - _, sr, err := validator.getDomainAndSigningRoot(ctx, att.Data) - require.NoError(t, err) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + ðpb.DomainRequest{Epoch: 10, Domain: []byte{1, 0, 0, 0}}, + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + _, sr, err := validator.getDomainAndSigningRoot(ctx, att.Data) + require.NoError(t, err) - err = validator.slashableAttestationCheck(context.Background(), att, pubKey, sr) - require.NoError(t, err) - differentSigningRoot := [32]byte{2} + err = validator.slashableAttestationCheck(context.Background(), att, pubKey, sr) + require.NoError(t, err) + differentSigningRoot := [32]byte{2} - err = validator.slashableAttestationCheck(context.Background(), att, pubKey, differentSigningRoot) - require.ErrorContains(t, "could not sign attestation", err) + err = validator.slashableAttestationCheck(context.Background(), att, pubKey, differentSigningRoot) + require.ErrorContains(t, "could not sign attestation", err) - e, exists, err := validator.db.LowestSignedSourceEpoch(context.Background(), pubKey) - require.NoError(t, err) - require.Equal(t, true, exists) - require.Equal(t, primitives.Epoch(4), e) - e, exists, err = validator.db.LowestSignedTargetEpoch(context.Background(), pubKey) - require.NoError(t, err) - require.Equal(t, true, exists) - require.Equal(t, primitives.Epoch(10), e) + e, exists, err := validator.db.LowestSignedSourceEpoch(context.Background(), pubKey) + require.NoError(t, err) + require.Equal(t, true, exists) + require.Equal(t, primitives.Epoch(4), e) + e, exists, err = validator.db.LowestSignedTargetEpoch(context.Background(), pubKey) + require.NoError(t, err) + require.Equal(t, true, exists) + require.Equal(t, primitives.Epoch(10), e) + }) + } } func Test_slashableAttestationCheck_OK(t *testing.T) { - ctx := context.Background() - validator, _, _, finish := setup(t) - defer finish() - att := ðpb.IndexedAttestation{ - AttestingIndices: []uint64{1, 2}, - Data: ðpb.AttestationData{ - Slot: 5, - CommitteeIndex: 2, - BeaconBlockRoot: []byte("great block"), - Source: ðpb.Checkpoint{ - Epoch: 4, - Root: []byte("good source"), - }, - Target: ðpb.Checkpoint{ - Epoch: 10, - Root: []byte("good target"), - }, - }, - } - sr := [32]byte{1} - fakePubkey := bytesutil.ToBytes48([]byte("test")) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + ctx := context.Background() + validator, _, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + att := ðpb.IndexedAttestation{ + AttestingIndices: []uint64{1, 2}, + Data: ðpb.AttestationData{ + Slot: 5, + CommitteeIndex: 2, + BeaconBlockRoot: []byte("great block"), + Source: ðpb.Checkpoint{ + Epoch: 4, + Root: []byte("good source"), + }, + Target: ðpb.Checkpoint{ + Epoch: 10, + Root: []byte("good target"), + }, + }, + } + sr := [32]byte{1} + fakePubkey := bytesutil.ToBytes48([]byte("test")) - err := validator.slashableAttestationCheck(ctx, att, fakePubkey, sr) - require.NoError(t, err, "Expected allowed attestation not to throw error") + err := validator.slashableAttestationCheck(ctx, att, fakePubkey, sr) + require.NoError(t, err, "Expected allowed attestation not to throw error") + }) + } } func Test_slashableAttestationCheck_GenesisEpoch(t *testing.T) { - ctx := context.Background() - validator, _, _, finish := setup(t) - defer finish() - att := ðpb.IndexedAttestation{ - AttestingIndices: []uint64{1, 2}, - Data: ðpb.AttestationData{ - Slot: 5, - CommitteeIndex: 2, - BeaconBlockRoot: bytesutil.PadTo([]byte("great block root"), 32), - Source: ðpb.Checkpoint{ - Epoch: 0, - Root: bytesutil.PadTo([]byte("great root"), 32), - }, - Target: ðpb.Checkpoint{ - Epoch: 0, - Root: bytesutil.PadTo([]byte("great root"), 32), - }, - }, - } + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + ctx := context.Background() + validator, _, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + att := ðpb.IndexedAttestation{ + AttestingIndices: []uint64{1, 2}, + Data: ðpb.AttestationData{ + Slot: 5, + CommitteeIndex: 2, + BeaconBlockRoot: bytesutil.PadTo([]byte("great block root"), 32), + Source: ðpb.Checkpoint{ + Epoch: 0, + Root: bytesutil.PadTo([]byte("great root"), 32), + }, + Target: ðpb.Checkpoint{ + Epoch: 0, + Root: bytesutil.PadTo([]byte("great root"), 32), + }, + }, + } - fakePubkey := bytesutil.ToBytes48([]byte("test")) - err := validator.slashableAttestationCheck(ctx, att, fakePubkey, [32]byte{}) - require.NoError(t, err, "Expected allowed attestation not to throw error") - e, exists, err := validator.db.LowestSignedSourceEpoch(context.Background(), fakePubkey) - require.NoError(t, err) - require.Equal(t, true, exists) - require.Equal(t, primitives.Epoch(0), e) - e, exists, err = validator.db.LowestSignedTargetEpoch(context.Background(), fakePubkey) - require.NoError(t, err) - require.Equal(t, true, exists) - require.Equal(t, primitives.Epoch(0), e) + fakePubkey := bytesutil.ToBytes48([]byte("test")) + err := validator.slashableAttestationCheck(ctx, att, fakePubkey, [32]byte{}) + require.NoError(t, err, "Expected allowed attestation not to throw error") + e, exists, err := validator.db.LowestSignedSourceEpoch(context.Background(), fakePubkey) + require.NoError(t, err) + require.Equal(t, true, exists) + require.Equal(t, primitives.Epoch(0), e) + e, exists, err = validator.db.LowestSignedTargetEpoch(context.Background(), fakePubkey) + require.NoError(t, err) + require.Equal(t, true, exists) + require.Equal(t, primitives.Epoch(0), e) + }) + } } diff --git a/validator/client/attest_test.go b/validator/client/attest_test.go index 290fd51c3e9d..8d4d0b8b7c5b 100644 --- a/validator/client/attest_test.go +++ b/validator/client/attest_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "reflect" "sync" "testing" @@ -29,449 +30,493 @@ import ( ) func TestRequestAttestation_ValidatorDutiesRequestFailure(t *testing.T) { - hook := logTest.NewGlobal() - validator, _, validatorKey, finish := setup(t) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsContain(t, hook, "Could not fetch validator assignment") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsContain(t, hook, "Could not fetch validator assignment") + }) + } } func TestAttestToBlockHead_SubmitAttestation_EmptyCommittee(t *testing.T) { - hook := logTest.NewGlobal() - - validator, _, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 0, - Committee: make([]primitives.ValidatorIndex, 0), - ValidatorIndex: 0, - }}} - validator.SubmitAttestation(context.Background(), 0, pubKey) - require.LogsContain(t, hook, "Empty committee") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 0, + Committee: make([]primitives.ValidatorIndex, 0), + ValidatorIndex: 0, + }}} + validator.SubmitAttestation(context.Background(), 0, pubKey) + require.LogsContain(t, hook, "Empty committee") + }) + } } func TestAttestToBlockHead_SubmitAttestation_RequestFailure(t *testing.T) { - hook := logTest.NewGlobal() - - validator, m, validatorKey, finish := setup(t) - defer finish() - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: make([]primitives.ValidatorIndex, 111), - ValidatorIndex: 0, - }}} - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: make([]byte, fieldparams.RootLength), - Target: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - Source: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - }, nil) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch2 - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Return(nil, errors.New("something went wrong")) - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsContain(t, hook, "Could not submit attestation to beacon node") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: make([]primitives.ValidatorIndex, 111), + ValidatorIndex: 0, + }}} + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + Target: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, + Source: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, + }, nil) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch2 + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Return(nil, errors.New("something went wrong")) + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsContain(t, hook, "Could not submit attestation to beacon node") + }) + } } func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - hook := logTest.NewGlobal() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - - beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) - targetRoot := bytesutil.ToBytes32([]byte("B")) - sourceRoot := bytesutil.ToBytes32([]byte("C")) - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:]}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - var generatedAttestation *ethpb.Attestation - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Do(func(_ context.Context, att *ethpb.Attestation) { - generatedAttestation = att - }).Return(ðpb.AttestResponse{}, nil /* error */) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - - aggregationBitfield := bitfield.NewBitlist(uint64(len(committee))) - aggregationBitfield.SetBitAt(4, true) - expectedAttestation := ðpb.Attestation{ - Data: ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:]}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, - }, - AggregationBits: aggregationBitfield, - Signature: make([]byte, 96), - } - - root, err := signing.ComputeSigningRoot(expectedAttestation.Data, make([]byte, 32)) - require.NoError(t, err) - - sig, err := validator.keyManager.Sign(context.Background(), &validatorpb.SignRequest{ - PublicKey: validatorKey.PublicKey().Marshal(), - SigningRoot: root[:], - }) - require.NoError(t, err) - expectedAttestation.Signature = sig.Marshal() - if !reflect.DeepEqual(generatedAttestation, expectedAttestation) { - t.Errorf("Incorrectly attested head, wanted %v, received %v", expectedAttestation, generatedAttestation) - diff, _ := messagediff.PrettyDiff(expectedAttestation, generatedAttestation) - t.Log(diff) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + hook := logTest.NewGlobal() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + + beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) + targetRoot := bytesutil.ToBytes32([]byte("B")) + sourceRoot := bytesutil.ToBytes32([]byte("C")) + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:]}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + var generatedAttestation *ethpb.Attestation + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Do(func(_ context.Context, att *ethpb.Attestation) { + generatedAttestation = att + }).Return(ðpb.AttestResponse{}, nil /* error */) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + + aggregationBitfield := bitfield.NewBitlist(uint64(len(committee))) + aggregationBitfield.SetBitAt(4, true) + expectedAttestation := ðpb.Attestation{ + Data: ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:]}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, + }, + AggregationBits: aggregationBitfield, + Signature: make([]byte, 96), + } + + root, err := signing.ComputeSigningRoot(expectedAttestation.Data, make([]byte, 32)) + require.NoError(t, err) + + sig, err := validator.keyManager.Sign(context.Background(), &validatorpb.SignRequest{ + PublicKey: validatorKey.PublicKey().Marshal(), + SigningRoot: root[:], + }) + require.NoError(t, err) + expectedAttestation.Signature = sig.Marshal() + if !reflect.DeepEqual(generatedAttestation, expectedAttestation) { + t.Errorf("Incorrectly attested head, wanted %v, received %v", expectedAttestation, generatedAttestation) + diff, _ := messagediff.PrettyDiff(expectedAttestation, generatedAttestation) + t.Log(diff) + } + require.LogsDoNotContain(t, hook, "Could not") + }) } - require.LogsDoNotContain(t, hook, "Could not") } func TestAttestToBlockHead_BlocksDoubleAtt(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) - targetRoot := bytesutil.ToBytes32([]byte("B")) - sourceRoot := bytesutil.ToBytes32([]byte("C")) - beaconBlockRoot2 := bytesutil.ToBytes32([]byte("D")) - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 4}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, - }, nil) - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot2[:], - Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 4}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, - }, nil) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Return(ðpb.AttestResponse{AttestationDataRoot: make([]byte, 32)}, nil /* error */) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsContain(t, hook, "Failed attestation slashing protection") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) + targetRoot := bytesutil.ToBytes32([]byte("B")) + sourceRoot := bytesutil.ToBytes32([]byte("C")) + beaconBlockRoot2 := bytesutil.ToBytes32([]byte("D")) + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 4}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, + }, nil) + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot2[:], + Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 4}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 3}, + }, nil) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Return(ðpb.AttestResponse{AttestationDataRoot: make([]byte, 32)}, nil /* error */) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsContain(t, hook, "Failed attestation slashing protection") + }) + } } func TestAttestToBlockHead_BlocksSurroundAtt(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) - targetRoot := bytesutil.ToBytes32([]byte("B")) - sourceRoot := bytesutil.ToBytes32([]byte("C")) - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 2}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 1}, - }, nil) - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 3}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 0}, - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Return(ðpb.AttestResponse{}, nil /* error */) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsContain(t, hook, "Failed attestation slashing protection") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) + targetRoot := bytesutil.ToBytes32([]byte("B")) + sourceRoot := bytesutil.ToBytes32([]byte("C")) + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 2}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 1}, + }, nil) + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 3}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 0}, + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Return(ðpb.AttestResponse{}, nil /* error */) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsContain(t, hook, "Failed attestation slashing protection") + }) + } } func TestAttestToBlockHead_BlocksSurroundedAtt(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - validatorIndex := primitives.ValidatorIndex(7) - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) - targetRoot := bytesutil.ToBytes32([]byte("B")) - sourceRoot := bytesutil.ToBytes32([]byte("C")) - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: beaconBlockRoot[:], - Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 3}, - Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 0}, - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Return(ðpb.AttestResponse{}, nil /* error */) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsDoNotContain(t, hook, failedAttLocalProtectionErr) - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: bytesutil.PadTo([]byte("A"), 32), - Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32), Epoch: 2}, - Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 1}, - }, nil) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - require.LogsContain(t, hook, "Failed attestation slashing protection") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + validatorIndex := primitives.ValidatorIndex(7) + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + beaconBlockRoot := bytesutil.ToBytes32([]byte("A")) + targetRoot := bytesutil.ToBytes32([]byte("B")) + sourceRoot := bytesutil.ToBytes32([]byte("C")) + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: beaconBlockRoot[:], + Target: ðpb.Checkpoint{Root: targetRoot[:], Epoch: 3}, + Source: ðpb.Checkpoint{Root: sourceRoot[:], Epoch: 0}, + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(4).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Return(ðpb.AttestResponse{}, nil /* error */) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsDoNotContain(t, hook, failedAttLocalProtectionErr) + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("A"), 32), + Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32), Epoch: 2}, + Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 1}, + }, nil) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + require.LogsContain(t, hook, "Failed attestation slashing protection") + }) + } } func TestAttestToBlockHead_DoesNotAttestBeforeDelay(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.genesisTime = uint64(prysmTime.Now().Unix()) - m.validatorClient.EXPECT().GetDuties( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.DutiesRequest{}), - ).Times(0) - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Times(0) - - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Return(ðpb.AttestResponse{}, nil /* error */).Times(0) - - timer := time.NewTimer(1 * time.Second) - go validator.SubmitAttestation(context.Background(), 0, pubKey) - <-timer.C + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.genesisTime = uint64(prysmTime.Now().Unix()) + m.validatorClient.EXPECT().GetDuties( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.DutiesRequest{}), + ).Times(0) + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Times(0) + + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Return(ðpb.AttestResponse{}, nil /* error */).Times(0) + + timer := time.NewTimer(1 * time.Second) + go validator.SubmitAttestation(context.Background(), 0, pubKey) + <-timer.C + }) + } } func TestAttestToBlockHead_DoesAttestAfterDelay(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - - var wg sync.WaitGroup - wg.Add(1) - defer wg.Wait() - - validator.genesisTime = uint64(prysmTime.Now().Unix()) - validatorIndex := primitives.ValidatorIndex(5) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }}} - - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - BeaconBlockRoot: bytesutil.PadTo([]byte("A"), 32), - Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32)}, - Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 3}, - }, nil).Do(func(arg0, arg1 interface{}) { - wg.Done() - }) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.Any(), - ).Return(ðpb.AttestResponse{}, nil).Times(1) - - validator.SubmitAttestation(context.Background(), 0, pubKey) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + var wg sync.WaitGroup + wg.Add(1) + defer wg.Wait() + + validator.genesisTime = uint64(prysmTime.Now().Unix()) + validatorIndex := primitives.ValidatorIndex(5) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }}} + + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + BeaconBlockRoot: bytesutil.PadTo([]byte("A"), 32), + Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32)}, + Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 3}, + }, nil).Do(func(arg0, arg1 interface{}) { + wg.Done() + }) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.Any(), + ).Return(ðpb.AttestResponse{}, nil).Times(1) + + validator.SubmitAttestation(context.Background(), 0, pubKey) + }) + } } func TestAttestToBlockHead_CorrectBitfieldLength(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - validatorIndex := primitives.ValidatorIndex(2) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - CommitteeIndex: 5, - Committee: committee, - ValidatorIndex: validatorIndex, - }}} - m.validatorClient.EXPECT().GetAttestationData( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), - ).Return(ðpb.AttestationData{ - Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32)}, - Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 3}, - BeaconBlockRoot: make([]byte, fieldparams.RootLength), - }, nil) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - var generatedAttestation *ethpb.Attestation - m.validatorClient.EXPECT().ProposeAttestation( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.Attestation{}), - ).Do(func(_ context.Context, att *ethpb.Attestation) { - generatedAttestation = att - }).Return(ðpb.AttestResponse{}, nil /* error */) - - validator.SubmitAttestation(context.Background(), 30, pubKey) - - assert.Equal(t, 2, len(generatedAttestation.AggregationBits)) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + validatorIndex := primitives.ValidatorIndex(2) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + CommitteeIndex: 5, + Committee: committee, + ValidatorIndex: validatorIndex, + }}} + m.validatorClient.EXPECT().GetAttestationData( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.AttestationDataRequest{}), + ).Return(ðpb.AttestationData{ + Target: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("B"), 32)}, + Source: ðpb.Checkpoint{Root: bytesutil.PadTo([]byte("C"), 32), Epoch: 3}, + BeaconBlockRoot: make([]byte, fieldparams.RootLength), + }, nil) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + var generatedAttestation *ethpb.Attestation + m.validatorClient.EXPECT().ProposeAttestation( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.Attestation{}), + ).Do(func(_ context.Context, att *ethpb.Attestation) { + generatedAttestation = att + }).Return(ðpb.AttestResponse{}, nil /* error */) + + validator.SubmitAttestation(context.Background(), 30, pubKey) + + assert.Equal(t, 2, len(generatedAttestation.AggregationBits)) + }) + } } func TestSignAttestation(t *testing.T) { - validator, m, _, finish := setup(t) - defer finish() - - wantedFork := ðpb.Fork{ - PreviousVersion: []byte{'a', 'b', 'c', 'd'}, - CurrentVersion: []byte{'d', 'e', 'f', 'f'}, - Epoch: 0, + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + wantedFork := ðpb.Fork{ + PreviousVersion: []byte{'a', 'b', 'c', 'd'}, + CurrentVersion: []byte{'d', 'e', 'f', 'f'}, + Epoch: 0, + } + genesisValidatorsRoot := [32]byte{0x01, 0x02} + attesterDomain, err := signing.Domain(wantedFork, 0, params.BeaconConfig().DomainBeaconAttester, genesisValidatorsRoot[:]) + require.NoError(t, err) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: attesterDomain}, nil) + ctx := context.Background() + att := util.NewAttestation() + att.Data.Source.Epoch = 100 + att.Data.Target.Epoch = 200 + att.Data.Slot = 999 + att.Data.BeaconBlockRoot = bytesutil.PadTo([]byte("blockRoot"), 32) + + pk := testKeyFromBytes(t, []byte{1}) + validator.keyManager = newMockKeymanager(t, pk) + sig, sr, err := validator.signAtt(ctx, pk.pub, att.Data, att.Data.Slot) + require.NoError(t, err, "%x,%x,%v", sig, sr, err) + require.Equal(t, "b6a60f8497bd328908be83634d045"+ + "dd7a32f5e246b2c4031fc2f316983f362e36fc27fd3d6d5a2b15"+ + "b4dbff38804ffb10b1719b7ebc54e9cbf3293fd37082bc0fc91f"+ + "79d70ce5b04ff13de3c8e10bb41305bfdbe921a43792c12624f2"+ + "25ee865", hex.EncodeToString(sig)) + // proposer domain + require.DeepEqual(t, "02bbdb88056d6cbafd6e94575540"+ + "e74b8cf2c0f2c1b79b8e17e7b21ed1694305", hex.EncodeToString(sr[:])) + }) } - genesisValidatorsRoot := [32]byte{0x01, 0x02} - attesterDomain, err := signing.Domain(wantedFork, 0, params.BeaconConfig().DomainBeaconAttester, genesisValidatorsRoot[:]) - require.NoError(t, err) - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: attesterDomain}, nil) - ctx := context.Background() - att := util.NewAttestation() - att.Data.Source.Epoch = 100 - att.Data.Target.Epoch = 200 - att.Data.Slot = 999 - att.Data.BeaconBlockRoot = bytesutil.PadTo([]byte("blockRoot"), 32) - - pk := testKeyFromBytes(t, []byte{1}) - validator.keyManager = newMockKeymanager(t, pk) - sig, sr, err := validator.signAtt(ctx, pk.pub, att.Data, att.Data.Slot) - require.NoError(t, err, "%x,%x,%v", sig, sr, err) - require.Equal(t, "b6a60f8497bd328908be83634d045"+ - "dd7a32f5e246b2c4031fc2f316983f362e36fc27fd3d6d5a2b15"+ - "b4dbff38804ffb10b1719b7ebc54e9cbf3293fd37082bc0fc91f"+ - "79d70ce5b04ff13de3c8e10bb41305bfdbe921a43792c12624f2"+ - "25ee865", hex.EncodeToString(sig)) - // proposer domain - require.DeepEqual(t, "02bbdb88056d6cbafd6e94575540"+ - "e74b8cf2c0f2c1b79b8e17e7b21ed1694305", hex.EncodeToString(sr[:])) } func TestServer_WaitToSlotOneThird_CanWait(t *testing.T) { diff --git a/validator/client/propose_protect.go b/validator/client/propose_protect.go index 49d253c778be..1c9ec3d42612 100644 --- a/validator/client/propose_protect.go +++ b/validator/client/propose_protect.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" ) -var failedBlockSignLocalErr = "attempted to sign a double proposal, block rejected by local protection" +var failedBlockSignLocalErr = "block rejected by local protection" // slashableProposalCheck checks if a block proposal is slashable by comparing it with the // block proposals history for the given public key in our DB. If it is not, we then update the history @@ -136,7 +136,7 @@ func (v *validator) slashableProposalCheckMinimal( // Check if the proposal is potentially slashable regarding EIP-3076 minimal conditions. // If not, save the new proposal into the database. if err := v.db.SaveProposalHistoryForSlot(ctx, pubKey, signedBlock.Block().Slot(), signingRoot[:]); err != nil { - if strings.Contains(err.Error(), "could not sign block") { + if strings.Contains(err.Error(), "could not sign proposal") { return errors.Wrapf(err, failedBlockSignLocalErr) } diff --git a/validator/client/propose_protect_test.go b/validator/client/propose_protect_test.go index d613dde7408e..d35228287d42 100644 --- a/validator/client/propose_protect_test.go +++ b/validator/client/propose_protect_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "fmt" "testing" fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams" @@ -14,142 +15,176 @@ import ( ) func Test_slashableProposalCheck_PreventsLowerThanMinProposal(t *testing.T) { - ctx := context.Background() - validator, _, validatorKey, finish := setup(t) - defer finish() - lowestSignedSlot := primitives.Slot(10) - var pubKeyBytes [fieldparams.BLSPubkeyLength]byte - copy(pubKeyBytes[:], validatorKey.PublicKey().Marshal()) - - // We save a proposal at the lowest signed slot in the DB. - err := validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, lowestSignedSlot, []byte{1}) - require.NoError(t, err) - require.NoError(t, err) - - // We expect the same block with a slot lower than the lowest - // signed slot to fail validation. - blk := ðpb.SignedBeaconBlock{ - Block: ðpb.BeaconBlock{ - Slot: lowestSignedSlot - 1, - ProposerIndex: 0, - Body: ðpb.BeaconBlockBody{}, - }, - Signature: params.BeaconConfig().EmptySignature[:], + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + ctx := context.Background() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + lowestSignedSlot := primitives.Slot(10) + var pubKeyBytes [fieldparams.BLSPubkeyLength]byte + copy(pubKeyBytes[:], validatorKey.PublicKey().Marshal()) + + // We save a proposal at the lowest signed slot in the DB. + err := validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, lowestSignedSlot, []byte{1}) + require.NoError(t, err) + + // We expect the same block with a slot lower than the lowest + // signed slot to fail validation. + blk := ðpb.SignedBeaconBlock{ + Block: ðpb.BeaconBlock{ + Slot: lowestSignedSlot - 1, + ProposerIndex: 0, + Body: ðpb.BeaconBlockBody{}, + }, + Signature: params.BeaconConfig().EmptySignature[:], + } + wsb, err := blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{4}) + if isSlashingProtectionMinimal { + require.ErrorContains(t, failedBlockSignLocalErr, err) + } else { + require.ErrorContains(t, "could not sign block with slot < lowest signed", err) + } + // We expect the same block with a slot equal to the lowest + // signed slot to pass validation if signing roots are equal. + blk = ðpb.SignedBeaconBlock{ + Block: ðpb.BeaconBlock{ + Slot: lowestSignedSlot, + ProposerIndex: 0, + Body: ðpb.BeaconBlockBody{}, + }, + Signature: params.BeaconConfig().EmptySignature[:], + } + wsb, err = blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{1}) + + if isSlashingProtectionMinimal { + require.ErrorContains(t, failedBlockSignLocalErr, err) + } else { + require.NoError(t, err) + } + + // We expect the same block with a slot equal to the lowest + // signed slot to fail validation if signing roots are different. + wsb, err = blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{4}) + + if isSlashingProtectionMinimal { + require.ErrorContains(t, failedBlockSignLocalErr, err) + } else { + require.ErrorContains(t, "could not sign block with slot == lowest signed", err) + } + + // We expect the same block with a slot > than the lowest + // signed slot to pass validation. + blk = ðpb.SignedBeaconBlock{ + Block: ðpb.BeaconBlock{ + Slot: lowestSignedSlot + 1, + ProposerIndex: 0, + Body: ðpb.BeaconBlockBody{}, + }, + Signature: params.BeaconConfig().EmptySignature[:], + } + + wsb, err = blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{3}) + require.NoError(t, err) + }) } - wsb, err := blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{4}) - require.ErrorContains(t, "could not sign block with slot < lowest signed", err) - - // We expect the same block with a slot equal to the lowest - // signed slot to pass validation if signing roots are equal. - blk = ðpb.SignedBeaconBlock{ - Block: ðpb.BeaconBlock{ - Slot: lowestSignedSlot, - ProposerIndex: 0, - Body: ðpb.BeaconBlockBody{}, - }, - Signature: params.BeaconConfig().EmptySignature[:], - } - wsb, err = blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{1}) - require.NoError(t, err) - - // We expect the same block with a slot equal to the lowest - // signed slot to fail validation if signing roots are different. - wsb, err = blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{4}) - require.ErrorContains(t, "could not sign block with slot == lowest signed", err) - - // We expect the same block with a slot > than the lowest - // signed slot to pass validation. - blk = ðpb.SignedBeaconBlock{ - Block: ðpb.BeaconBlock{ - Slot: lowestSignedSlot + 1, - ProposerIndex: 0, - Body: ðpb.BeaconBlockBody{}, - }, - Signature: params.BeaconConfig().EmptySignature[:], - } - - wsb, err = blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wsb, [32]byte{3}) - require.NoError(t, err) } func Test_slashableProposalCheck(t *testing.T) { - ctx := context.Background() - validator, _, validatorKey, finish := setup(t) - defer finish() - - blk := util.HydrateSignedBeaconBlock(ðpb.SignedBeaconBlock{ - Block: ðpb.BeaconBlock{ - Slot: 10, - ProposerIndex: 0, - Body: ðpb.BeaconBlockBody{}, - }, - Signature: params.BeaconConfig().EmptySignature[:], - }) - - var pubKeyBytes [fieldparams.BLSPubkeyLength]byte - copy(pubKeyBytes[:], validatorKey.PublicKey().Marshal()) - - // We save a proposal at slot 1 as our lowest proposal. - err := validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, 1, []byte{1}) - require.NoError(t, err) - - // We save a proposal at slot 10 with a dummy signing root. - dummySigningRoot := [32]byte{1} - err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, 10, dummySigningRoot[:]) - require.NoError(t, err) - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - sBlock, err := blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - - // We expect the same block sent out with the same root should not be slasahble. - err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, dummySigningRoot) - require.NoError(t, err) - - // We expect the same block sent out with a different signing root should be slasahble. - err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) - require.ErrorContains(t, failedBlockSignLocalErr, err) - - // We save a proposal at slot 11 with a nil signing root. - blk.Block.Slot = 11 - sBlock, err = blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, blk.Block.Slot, nil) - require.NoError(t, err) - - // We expect the same block sent out should return slashable error even - // if we had a nil signing root stored in the database. - err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) - require.ErrorContains(t, failedBlockSignLocalErr, err) - - // A block with a different slot for which we do not have a proposing history - // should not be failing validation. - blk.Block.Slot = 9 - sBlock, err = blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{3}) - require.NoError(t, err, "Expected allowed block not to throw error") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + ctx := context.Background() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + blk := util.HydrateSignedBeaconBlock(ðpb.SignedBeaconBlock{ + Block: ðpb.BeaconBlock{ + Slot: 10, + ProposerIndex: 0, + Body: ðpb.BeaconBlockBody{}, + }, + Signature: params.BeaconConfig().EmptySignature[:], + }) + + var pubKeyBytes [fieldparams.BLSPubkeyLength]byte + copy(pubKeyBytes[:], validatorKey.PublicKey().Marshal()) + + // We save a proposal at slot 1 as our lowest proposal. + err := validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, 1, []byte{1}) + require.NoError(t, err) + + // We save a proposal at slot 10 with a dummy signing root. + dummySigningRoot := [32]byte{1} + err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, 10, dummySigningRoot[:]) + require.NoError(t, err) + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + sBlock, err := blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + + err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, dummySigningRoot) + + if isSlashingProtectionMinimal { + // We expect the same block sent out should be slasahble. + require.ErrorContains(t, failedBlockSignLocalErr, err) + } else { + // We expect the same block sent out with the same root should not be slasahble. + require.NoError(t, err) + } + // We expect the same block sent out with a different signing root should be slashable. + err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) + require.ErrorContains(t, failedBlockSignLocalErr, err) + + // We save a proposal at slot 11 with a nil signing root. + blk.Block.Slot = 11 + sBlock, err = blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, blk.Block.Slot, nil) + require.NoError(t, err) + + // We expect the same block sent out should return slashable error even + // if we had a nil signing root stored in the database. + err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) + require.ErrorContains(t, failedBlockSignLocalErr, err) + + // A block with a different slot for which we do not have a proposing history + // should not be failing validation. + blk.Block.Slot = 9 + sBlock, err = blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{3}) + + if isSlashingProtectionMinimal { + require.ErrorContains(t, failedBlockSignLocalErr, err) + } else { + require.NoError(t, err, "Expected allowed block not to throw error") + } + }) + } } func Test_slashableProposalCheck_RemoteProtection(t *testing.T) { - validator, _, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - blk := util.NewBeaconBlock() - blk.Block.Slot = 10 - sBlock, err := blocks.NewSignedBeaconBlock(blk) - require.NoError(t, err) - - err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) - require.NoError(t, err, "Expected allowed block not to throw error") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + blk := util.NewBeaconBlock() + blk.Block.Slot = 10 + sBlock, err := blocks.NewSignedBeaconBlock(blk) + require.NoError(t, err) + + err = validator.slashableProposalCheck(context.Background(), pubKey, sBlock, [32]byte{2}) + require.NoError(t, err, "Expected allowed block not to throw error") + }) + } } diff --git a/validator/client/propose_test.go b/validator/client/propose_test.go index 89984cec6cdf..e762626fabdd 100644 --- a/validator/client/propose_test.go +++ b/validator/client/propose_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "strings" "testing" @@ -63,10 +64,13 @@ func testKeyFromBytes(t *testing.T, b []byte) keypair { return keypair{pub: bytesutil.ToBytes48(pri.PublicKey().Marshal()), pri: pri} } -func setup(t *testing.T) (*validator, *mocks, bls.SecretKey, func()) { +// setup sets up a validator instance. +// The `isSlashingProtectionMinimal` flag indicates whether the DB should be instantiated with minimal, filesystem +// slashing protection database. +func setup(t *testing.T, isSlashingProtectionMinimal bool) (*validator, *mocks, bls.SecretKey, func()) { validatorKey, err := bls.RandKey() require.NoError(t, err) - return setupWithKey(t, validatorKey, false) + return setupWithKey(t, validatorKey, isSlashingProtectionMinimal) } // setupWithKey sets up a validator instance with a given key. @@ -99,46 +103,58 @@ func setupWithKey(t *testing.T, validatorKey bls.SecretKey, isSlashingProtection } func TestProposeBlock_DoesNotProposeGenesisBlock(t *testing.T) { - hook := logTest.NewGlobal() - validator, _, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.ProposeBlock(context.Background(), 0, pubKey) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.ProposeBlock(context.Background(), 0, pubKey) - require.LogsContain(t, hook, "Assigned to genesis slot, skipping proposal") + require.LogsContain(t, hook, "Assigned to genesis slot, skipping proposal") + }) + } } func TestProposeBlock_DomainDataFailed(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(nil /*response*/, errors.New("uh oh")) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(nil /*response*/, errors.New("uh oh")) - validator.ProposeBlock(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Failed to sign randao reveal") + validator.ProposeBlock(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Failed to sign randao reveal") + }) + } } func TestProposeBlock_DomainDataIsNil(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(nil /*response*/, nil) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(nil /*response*/, nil) - validator.ProposeBlock(context.Background(), 1, pubKey) - require.LogsContain(t, hook, domainDataErr) + validator.ProposeBlock(context.Background(), 1, pubKey) + require.LogsContain(t, hook, domainDataErr) + }) + } } func TestProposeBlock_RequestBlockFailed(t *testing.T) { @@ -167,26 +183,28 @@ func TestProposeBlock_RequestBlockFailed(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(nil /*response*/, errors.New("uh oh")) - - validator.ProposeBlock(context.Background(), tt.slot, pubKey) - require.LogsContain(t, hook, "Failed to request block from beacon node") - }) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("%s/SlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(nil /*response*/, errors.New("uh oh")) + + validator.ProposeBlock(context.Background(), tt.slot, pubKey) + require.LogsContain(t, hook, "Failed to request block from beacon node") + }) + } } } @@ -220,38 +238,41 @@ func TestProposeBlock_ProposeBlockFailed(t *testing.T) { }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + for _, isSlashingProtectionMinimal := range [...]bool{true, true} { + t.Run(fmt.Sprintf("%s/SlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(tt.block, nil /*err*/) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), + ).Return(nil /*response*/, errors.New("uh oh")) + + validator.ProposeBlock(context.Background(), 1, pubKey) + + require.LogsContain(t, hook, "Failed to propose block") - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(tt.block, nil /*err*/) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), - ).Return(nil /*response*/, errors.New("uh oh")) - - validator.ProposeBlock(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Failed to propose block") - }) + }) + } } } @@ -320,16 +341,66 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("%s/SlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + var dummyRoot [32]byte + // Save a dummy proposal history at slot 1. + err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 1, dummyRoot[:]) + require.NoError(t, err) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(1).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(tt.blocks[0], nil /*err*/) + + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(tt.blocks[1], nil /*err*/) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(3).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().ProposeBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), + ).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/) + + validator.ProposeBlock(context.Background(), slot, pubKey) + require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) + + validator.ProposeBlock(context.Background(), slot, pubKey) + require.LogsContain(t, hook, failedBlockSignLocalErr) + }) + } + } +} + +func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) { + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) defer finish() var pubKey [fieldparams.BLSPubkeyLength]byte copy(pubKey[:], validatorKey.PublicKey().Marshal()) var dummyRoot [32]byte - // Save a dummy proposal history at slot 0. - err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 0, dummyRoot[:]) + // Save a dummy proposal history at slot 1. + err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 1, dummyRoot[:]) require.NoError(t, err) m.validatorClient.EXPECT().DomainData( @@ -337,16 +408,31 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) { gomock.Any(), // epoch ).Times(1).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + testBlock := util.NewBeaconBlock() + farFuture := params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().WeakSubjectivityPeriod + 9)) + testBlock.Block.Slot = farFuture m.validatorClient.EXPECT().GetBeaconBlock( gomock.Any(), // ctx gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(tt.blocks[0], nil /*err*/) + ).Return(ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Phase0{ + Phase0: testBlock.Block, + }, + }, nil /*err*/) + secondTestBlock := util.NewBeaconBlock() + secondTestBlock.Block.Slot = farFuture + var blockGraffiti [32]byte + copy(blockGraffiti[:], "someothergraffiti") + secondTestBlock.Block.Body.Graffiti = blockGraffiti[:] m.validatorClient.EXPECT().GetBeaconBlock( gomock.Any(), // ctx gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(tt.blocks[1], nil /*err*/) - + ).Return(ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Phase0{ + Phase0: secondTestBlock.Block, + }, + }, nil /*err*/) m.validatorClient.EXPECT().DomainData( gomock.Any(), // ctx gomock.Any(), // epoch @@ -357,75 +443,16 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) { gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), ).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/) - validator.ProposeBlock(context.Background(), slot, pubKey) + validator.ProposeBlock(context.Background(), farFuture, pubKey) require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) - validator.ProposeBlock(context.Background(), slot, pubKey) + validator.ProposeBlock(context.Background(), farFuture, pubKey) require.LogsContain(t, hook, failedBlockSignLocalErr) }) } } -func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - var dummyRoot [32]byte - // Save a dummy proposal history at slot 0. - err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 0, dummyRoot[:]) - require.NoError(t, err) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(1).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - testBlock := util.NewBeaconBlock() - farFuture := params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().WeakSubjectivityPeriod + 9)) - testBlock.Block.Slot = farFuture - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(ðpb.GenericBeaconBlock{ - Block: ðpb.GenericBeaconBlock_Phase0{ - Phase0: testBlock.Block, - }, - }, nil /*err*/) - - secondTestBlock := util.NewBeaconBlock() - secondTestBlock.Block.Slot = farFuture - var blockGraffiti [32]byte - copy(blockGraffiti[:], "someothergraffiti") - secondTestBlock.Block.Body.Graffiti = blockGraffiti[:] - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(ðpb.GenericBeaconBlock{ - Block: ðpb.GenericBeaconBlock_Phase0{ - Phase0: secondTestBlock.Block, - }, - }, nil /*err*/) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(3).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().ProposeBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), - ).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/) - - validator.ProposeBlock(context.Background(), farFuture, pubKey) - require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) - - validator.ProposeBlock(context.Background(), farFuture, pubKey) - require.LogsContain(t, hook, failedBlockSignLocalErr) -} - -func TestProposeBlock_AllowsPastProposals(t *testing.T) { +func TestProposeBlock_AllowsOrNotPastProposals(t *testing.T) { slot := params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().WeakSubjectivityPeriod + 9)) tests := []struct { @@ -442,59 +469,70 @@ func TestProposeBlock_AllowsPastProposals(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - // Save a dummy proposal history at slot 0. - err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 0, []byte{}) - require.NoError(t, err) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - blk := util.NewBeaconBlock() - blk.Block.Slot = slot - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(ðpb.GenericBeaconBlock{ - Block: ðpb.GenericBeaconBlock_Phase0{ - Phase0: blk.Block, - }, - }, nil /*err*/) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("%s/SlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + // Save a dummy proposal history at slot 0. + err := validator.db.SaveProposalHistoryForSlot(context.Background(), pubKey, 0, []byte{}) + require.NoError(t, err) - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + blk := util.NewBeaconBlock() + blk.Block.Slot = slot + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Phase0{ + Phase0: blk.Block, + }, + }, nil /*err*/) - m.validatorClient.EXPECT().ProposeBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), - ).Times(2).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Times(2).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - validator.ProposeBlock(context.Background(), slot, pubKey) - require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) + proposeBeaconBlockCount := 2 + if isSlashingProtectionMinimal { + proposeBeaconBlockCount = 1 + } - blk2 := util.NewBeaconBlock() - blk2.Block.Slot = tt.pastSlot - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).Return(ðpb.GenericBeaconBlock{ - Block: ðpb.GenericBeaconBlock_Phase0{ - Phase0: blk2.Block, - }, - }, nil /*err*/) - validator.ProposeBlock(context.Background(), tt.pastSlot, pubKey) - require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) - }) + m.validatorClient.EXPECT().ProposeBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), + ).Times(proposeBeaconBlockCount).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/) + + validator.ProposeBlock(context.Background(), slot, pubKey) + require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) + + blk2 := util.NewBeaconBlock() + blk2.Block.Slot = tt.pastSlot + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).Return(ðpb.GenericBeaconBlock{ + Block: ðpb.GenericBeaconBlock_Phase0{ + Phase0: blk2.Block, + }, + }, nil /*err*/) + validator.ProposeBlock(context.Background(), tt.pastSlot, pubKey) + if isSlashingProtectionMinimal { + require.LogsContain(t, hook, failedBlockSignLocalErr) + } else { + require.LogsDoNotContain(t, hook, failedBlockSignLocalErr) + } + }) + } } } @@ -614,266 +652,300 @@ func testProposeBlock(t *testing.T, graffiti []byte) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - defer finish() - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - - validator.graffiti = graffiti + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("%s/SlashingProtectionMinimal:%v", tt.name, isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + + validator.graffiti = graffiti + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().GetBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.BlockRequest{}), + ).DoAndReturn(func(ctx context.Context, req *ethpb.BlockRequest) (*ethpb.GenericBeaconBlock, error) { + assert.DeepEqual(t, graffiti, req.Graffiti, "Unexpected graffiti in request") + + return tt.block, nil + }) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + var sentBlock interfaces.ReadOnlySignedBeaconBlock + var err error + + m.validatorClient.EXPECT().ProposeBeaconBlock( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), + ).DoAndReturn(func(ctx context.Context, block *ethpb.GenericSignedBeaconBlock) (*ethpb.ProposeResponse, error) { + sentBlock, err = blocktest.NewSignedBeaconBlockFromGeneric(block) + require.NoError(t, err) + return ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil + }) + + validator.ProposeBlock(context.Background(), 1, pubKey) + g := sentBlock.Block().Body().Graffiti() + assert.Equal(t, string(validator.graffiti), string(g[:])) + require.LogsContain(t, hook, "Submitted new block") - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().GetBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.BlockRequest{}), - ).DoAndReturn(func(ctx context.Context, req *ethpb.BlockRequest) (*ethpb.GenericBeaconBlock, error) { - assert.DeepEqual(t, graffiti, req.Graffiti, "Unexpected graffiti in request") - - return tt.block, nil }) + } + } +} - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) +func TestProposeExit_ValidatorIndexFailed(t *testing.T) { + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - var sentBlock interfaces.ReadOnlySignedBeaconBlock - var err error + m.validatorClient.EXPECT().ValidatorIndex( + gomock.Any(), + gomock.Any(), + ).Return(nil, errors.New("uh oh")) + + err := ProposeExit( + context.Background(), + m.validatorClient, + m.signfunc, + validatorKey.PublicKey().Marshal(), + params.BeaconConfig().GenesisEpoch, + ) + assert.NotNil(t, err) + assert.ErrorContains(t, "uh oh", err) + assert.ErrorContains(t, "gRPC call to get validator index failed", err) + }) + } +} - m.validatorClient.EXPECT().ProposeBeaconBlock( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.GenericSignedBeaconBlock{}), - ).DoAndReturn(func(ctx context.Context, block *ethpb.GenericSignedBeaconBlock) (*ethpb.ProposeResponse, error) { - sentBlock, err = blocktest.NewSignedBeaconBlockFromGeneric(block) - require.NoError(t, err) - return ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil - }) +func TestProposeExit_DomainDataFailed(t *testing.T) { + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - validator.ProposeBlock(context.Background(), 1, pubKey) - g := sentBlock.Block().Body().Graffiti() - assert.Equal(t, string(validator.graffiti), string(g[:])) - require.LogsContain(t, hook, "Submitted new block") + m.validatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), gomock.Any()). + Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(nil, errors.New("uh oh")) + + err := ProposeExit( + context.Background(), + m.validatorClient, + m.signfunc, + validatorKey.PublicKey().Marshal(), + params.BeaconConfig().GenesisEpoch, + ) + assert.NotNil(t, err) + assert.ErrorContains(t, domainDataErr, err) + assert.ErrorContains(t, "uh oh", err) + assert.ErrorContains(t, "failed to sign voluntary exit", err) }) } } -func TestProposeExit_ValidatorIndexFailed(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - - m.validatorClient.EXPECT().ValidatorIndex( - gomock.Any(), - gomock.Any(), - ).Return(nil, errors.New("uh oh")) - - err := ProposeExit( - context.Background(), - m.validatorClient, - m.signfunc, - validatorKey.PublicKey().Marshal(), - params.BeaconConfig().GenesisEpoch, - ) - assert.NotNil(t, err) - assert.ErrorContains(t, "uh oh", err) - assert.ErrorContains(t, "gRPC call to get validator index failed", err) -} +func TestProposeExit_DomainDataIsNil(t *testing.T) { + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() -func TestProposeExit_DomainDataFailed(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - - m.validatorClient.EXPECT(). - ValidatorIndex(gomock.Any(), gomock.Any()). - Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(nil, errors.New("uh oh")) - - err := ProposeExit( - context.Background(), - m.validatorClient, - m.signfunc, - validatorKey.PublicKey().Marshal(), - params.BeaconConfig().GenesisEpoch, - ) - assert.NotNil(t, err) - assert.ErrorContains(t, domainDataErr, err) - assert.ErrorContains(t, "uh oh", err) - assert.ErrorContains(t, "failed to sign voluntary exit", err) -} + m.validatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), gomock.Any()). + Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) -func TestProposeExit_DomainDataIsNil(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - - m.validatorClient.EXPECT(). - ValidatorIndex(gomock.Any(), gomock.Any()). - Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(nil, nil) - - err := ProposeExit( - context.Background(), - m.validatorClient, - m.signfunc, - validatorKey.PublicKey().Marshal(), - params.BeaconConfig().GenesisEpoch, - ) - assert.NotNil(t, err) - assert.ErrorContains(t, domainDataErr, err) - assert.ErrorContains(t, "failed to sign voluntary exit", err) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(nil, nil) + + err := ProposeExit( + context.Background(), + m.validatorClient, + m.signfunc, + validatorKey.PublicKey().Marshal(), + params.BeaconConfig().GenesisEpoch, + ) + assert.NotNil(t, err) + assert.ErrorContains(t, domainDataErr, err) + assert.ErrorContains(t, "failed to sign voluntary exit", err) + }) + } } func TestProposeBlock_ProposeExitFailed(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - - m.validatorClient.EXPECT(). - ValidatorIndex(gomock.Any(), gomock.Any()). - Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil) - - m.validatorClient.EXPECT(). - ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(ðpb.SignedVoluntaryExit{})). - Return(nil, errors.New("uh oh")) - - err := ProposeExit( - context.Background(), - m.validatorClient, - m.signfunc, - validatorKey.PublicKey().Marshal(), - params.BeaconConfig().GenesisEpoch, - ) - assert.NotNil(t, err) - assert.ErrorContains(t, "uh oh", err) - assert.ErrorContains(t, "failed to propose voluntary exit", err) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + m.validatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), gomock.Any()). + Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil) + + m.validatorClient.EXPECT(). + ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(ðpb.SignedVoluntaryExit{})). + Return(nil, errors.New("uh oh")) + + err := ProposeExit( + context.Background(), + m.validatorClient, + m.signfunc, + validatorKey.PublicKey().Marshal(), + params.BeaconConfig().GenesisEpoch, + ) + assert.NotNil(t, err) + assert.ErrorContains(t, "uh oh", err) + assert.ErrorContains(t, "failed to propose voluntary exit", err) + }) + } } func TestProposeExit_BroadcastsBlock(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - - m.validatorClient.EXPECT(). - ValidatorIndex(gomock.Any(), gomock.Any()). - Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil) - - m.validatorClient.EXPECT(). - ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(ðpb.SignedVoluntaryExit{})). - Return(ðpb.ProposeExitResponse{}, nil) - - assert.NoError(t, ProposeExit( - context.Background(), - m.validatorClient, - m.signfunc, - validatorKey.PublicKey().Marshal(), - params.BeaconConfig().GenesisEpoch, - )) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + m.validatorClient.EXPECT(). + ValidatorIndex(gomock.Any(), gomock.Any()). + Return(ðpb.ValidatorIndexResponse{Index: 1}, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil) + + m.validatorClient.EXPECT(). + ProposeExit(gomock.Any(), gomock.AssignableToTypeOf(ðpb.SignedVoluntaryExit{})). + Return(ðpb.ProposeExitResponse{}, nil) + + assert.NoError(t, ProposeExit( + context.Background(), + m.validatorClient, + m.signfunc, + validatorKey.PublicKey().Marshal(), + params.BeaconConfig().GenesisEpoch, + )) + }) + } } func TestSignBlock(t *testing.T) { - validator, m, _, finish := setup(t) - defer finish() - - proposerDomain := make([]byte, 32) - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) - ctx := context.Background() - blk := util.NewBeaconBlock() - blk.Block.Slot = 1 - blk.Block.ProposerIndex = 100 - - kp := testKeyFromBytes(t, []byte{1}) - - validator.keyManager = newMockKeymanager(t, kp) - b, err := blocks.NewBeaconBlock(blk.Block) - require.NoError(t, err) - sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, b) - require.NoError(t, err, "%x,%v", sig, err) - require.Equal(t, "a049e1dc723e5a8b5bd14f292973572dffd53785ddb337"+ - "82f20bf762cbe10ee7b9b4f5ae1ad6ff2089d352403750bed402b94b58469c072536"+ - "faa9a09a88beaff697404ca028b1c7052b0de37dbcff985dfa500459783370312bdd"+ - "36d6e0f224", hex.EncodeToString(sig)) - - // Verify the returned block root matches the expected root using the proposer signature - // domain. - wantedBlockRoot, err := signing.ComputeSigningRoot(b, proposerDomain) - if err != nil { - require.NoError(t, err) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + proposerDomain := make([]byte, 32) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) + ctx := context.Background() + blk := util.NewBeaconBlock() + blk.Block.Slot = 1 + blk.Block.ProposerIndex = 100 + + kp := testKeyFromBytes(t, []byte{1}) + + validator.keyManager = newMockKeymanager(t, kp) + b, err := blocks.NewBeaconBlock(blk.Block) + require.NoError(t, err) + sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, b) + require.NoError(t, err, "%x,%v", sig, err) + require.Equal(t, "a049e1dc723e5a8b5bd14f292973572dffd53785ddb337"+ + "82f20bf762cbe10ee7b9b4f5ae1ad6ff2089d352403750bed402b94b58469c072536"+ + "faa9a09a88beaff697404ca028b1c7052b0de37dbcff985dfa500459783370312bdd"+ + "36d6e0f224", hex.EncodeToString(sig)) + + // Verify the returned block root matches the expected root using the proposer signature + // domain. + wantedBlockRoot, err := signing.ComputeSigningRoot(b, proposerDomain) + if err != nil { + require.NoError(t, err) + } + require.DeepEqual(t, wantedBlockRoot, blockRoot) + }) } - require.DeepEqual(t, wantedBlockRoot, blockRoot) } func TestSignAltairBlock(t *testing.T) { - validator, m, _, finish := setup(t) - defer finish() - - kp := testKeyFromBytes(t, []byte{1}) - proposerDomain := make([]byte, 32) - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) - ctx := context.Background() - blk := util.NewBeaconBlockAltair() - blk.Block.Slot = 1 - blk.Block.ProposerIndex = 100 - validator.keyManager = newMockKeymanager(t, kp) - wb, err := blocks.NewBeaconBlock(blk.Block) - require.NoError(t, err) - sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, wb) - require.NoError(t, err, "%x,%v", sig, err) - // Verify the returned block root matches the expected root using the proposer signature - // domain. - wantedBlockRoot, err := signing.ComputeSigningRoot(wb, proposerDomain) - if err != nil { - require.NoError(t, err) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + kp := testKeyFromBytes(t, []byte{1}) + proposerDomain := make([]byte, 32) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) + ctx := context.Background() + blk := util.NewBeaconBlockAltair() + blk.Block.Slot = 1 + blk.Block.ProposerIndex = 100 + validator.keyManager = newMockKeymanager(t, kp) + wb, err := blocks.NewBeaconBlock(blk.Block) + require.NoError(t, err) + sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, wb) + require.NoError(t, err, "%x,%v", sig, err) + // Verify the returned block root matches the expected root using the proposer signature + // domain. + wantedBlockRoot, err := signing.ComputeSigningRoot(wb, proposerDomain) + if err != nil { + require.NoError(t, err) + } + require.DeepEqual(t, wantedBlockRoot, blockRoot) + }) } - require.DeepEqual(t, wantedBlockRoot, blockRoot) } func TestSignBellatrixBlock(t *testing.T) { - validator, m, _, finish := setup(t) - defer finish() - - proposerDomain := make([]byte, 32) - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) - - ctx := context.Background() - blk := util.NewBeaconBlockBellatrix() - blk.Block.Slot = 1 - blk.Block.ProposerIndex = 100 - - kp := randKeypair(t) - validator.keyManager = newMockKeymanager(t, kp) - wb, err := blocks.NewBeaconBlock(blk.Block) - require.NoError(t, err) - sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, wb) - require.NoError(t, err, "%x,%v", sig, err) - // Verify the returned block root matches the expected root using the proposer signature - // domain. - wantedBlockRoot, err := signing.ComputeSigningRoot(wb, proposerDomain) - if err != nil { - require.NoError(t, err) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, _, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + proposerDomain := make([]byte, 32) + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(ðpb.DomainResponse{SignatureDomain: proposerDomain}, nil) + + ctx := context.Background() + blk := util.NewBeaconBlockBellatrix() + blk.Block.Slot = 1 + blk.Block.ProposerIndex = 100 + + kp := randKeypair(t) + validator.keyManager = newMockKeymanager(t, kp) + wb, err := blocks.NewBeaconBlock(blk.Block) + require.NoError(t, err) + sig, blockRoot, err := validator.signBlock(ctx, kp.pub, 0, 0, wb) + require.NoError(t, err, "%x,%v", sig, err) + // Verify the returned block root matches the expected root using the proposer signature + // domain. + wantedBlockRoot, err := signing.ComputeSigningRoot(wb, proposerDomain) + if err != nil { + require.NoError(t, err) + } + require.DeepEqual(t, wantedBlockRoot, blockRoot) + }) } - require.DeepEqual(t, wantedBlockRoot, blockRoot) } func TestGetGraffiti_Ok(t *testing.T) { diff --git a/validator/client/registration_test.go b/validator/client/registration_test.go index 18fcc7faf7e8..fbcf15727b8b 100644 --- a/validator/client/registration_test.go +++ b/validator/client/registration_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "fmt" "testing" "time" @@ -17,38 +18,67 @@ import ( ) func TestSubmitValidatorRegistrations(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - ctx := context.Background() - validatorRegsBatchSize := 2 - require.NoError(t, nil, SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{}, validatorRegsBatchSize)) + ctx := context.Background() + validatorRegsBatchSize := 2 + require.NoError(t, nil, SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{}, validatorRegsBatchSize)) - regs := [...]*ethpb.ValidatorRegistrationV1{ - { - FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), - GasLimit: 123, - Timestamp: uint64(time.Now().Unix()), - Pubkey: validatorKey.PublicKey().Marshal(), - }, - { - FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), - GasLimit: 456, - Timestamp: uint64(time.Now().Unix()), - Pubkey: validatorKey.PublicKey().Marshal(), - }, - { - FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), - GasLimit: 789, - Timestamp: uint64(time.Now().Unix()), - Pubkey: validatorKey.PublicKey().Marshal(), - }, - } + regs := [...]*ethpb.ValidatorRegistrationV1{ + { + FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), + GasLimit: 123, + Timestamp: uint64(time.Now().Unix()), + Pubkey: validatorKey.PublicKey().Marshal(), + }, + { + FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), + GasLimit: 456, + Timestamp: uint64(time.Now().Unix()), + Pubkey: validatorKey.PublicKey().Marshal(), + }, + { + FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), + GasLimit: 789, + Timestamp: uint64(time.Now().Unix()), + Pubkey: validatorKey.PublicKey().Marshal(), + }, + } - gomock.InOrder( - m.validatorClient.EXPECT(). - SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ - Messages: []*ethpb.SignedValidatorRegistrationV1{ + gomock.InOrder( + m.validatorClient.EXPECT(). + SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ + Messages: []*ethpb.SignedValidatorRegistrationV1{ + { + Message: regs[0], + Signature: params.BeaconConfig().ZeroHash[:], + }, + { + Message: regs[1], + Signature: params.BeaconConfig().ZeroHash[:], + }, + }, + }). + Return(nil, nil), + + m.validatorClient.EXPECT(). + SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ + Messages: []*ethpb.SignedValidatorRegistrationV1{ + { + Message: regs[2], + Signature: params.BeaconConfig().ZeroHash[:], + }, + }, + }). + Return(nil, nil), + ) + + require.NoError(t, nil, SubmitValidatorRegistrations( + ctx, m.validatorClient, + []*ethpb.SignedValidatorRegistrationV1{ { Message: regs[0], Signature: params.BeaconConfig().ZeroHash[:], @@ -57,222 +87,206 @@ func TestSubmitValidatorRegistrations(t *testing.T) { Message: regs[1], Signature: params.BeaconConfig().ZeroHash[:], }, - }, - }). - Return(nil, nil), - - m.validatorClient.EXPECT(). - SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ - Messages: []*ethpb.SignedValidatorRegistrationV1{ { Message: regs[2], Signature: params.BeaconConfig().ZeroHash[:], }, }, - }). - Return(nil, nil), - ) - - require.NoError(t, nil, SubmitValidatorRegistrations( - ctx, m.validatorClient, - []*ethpb.SignedValidatorRegistrationV1{ - { - Message: regs[0], - Signature: params.BeaconConfig().ZeroHash[:], - }, - { - Message: regs[1], - Signature: params.BeaconConfig().ZeroHash[:], - }, - { - Message: regs[2], - Signature: params.BeaconConfig().ZeroHash[:], - }, - }, - validatorRegsBatchSize, - )) + validatorRegsBatchSize, + )) + }) + } } func TestSubmitValidatorRegistration_CantSign(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - ctx := context.Background() - validatorRegsBatchSize := 500 - reg := ðpb.ValidatorRegistrationV1{ - FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), - GasLimit: 123456, - Timestamp: uint64(time.Now().Unix()), - Pubkey: validatorKey.PublicKey().Marshal(), - } + ctx := context.Background() + validatorRegsBatchSize := 500 + reg := ðpb.ValidatorRegistrationV1{ + FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), + GasLimit: 123456, + Timestamp: uint64(time.Now().Unix()), + Pubkey: validatorKey.PublicKey().Marshal(), + } - m.validatorClient.EXPECT(). - SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ - Messages: []*ethpb.SignedValidatorRegistrationV1{ + m.validatorClient.EXPECT(). + SubmitValidatorRegistrations(gomock.Any(), ðpb.SignedValidatorRegistrationsV1{ + Messages: []*ethpb.SignedValidatorRegistrationV1{ + {Message: reg, + Signature: params.BeaconConfig().ZeroHash[:]}, + }, + }). + Return(nil, errors.New("could not sign")) + require.ErrorContains(t, "could not sign", SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{ {Message: reg, Signature: params.BeaconConfig().ZeroHash[:]}, - }, - }). - Return(nil, errors.New("could not sign")) - require.ErrorContains(t, "could not sign", SubmitValidatorRegistrations(ctx, m.validatorClient, []*ethpb.SignedValidatorRegistrationV1{ - {Message: reg, - Signature: params.BeaconConfig().ZeroHash[:]}, - }, validatorRegsBatchSize)) + }, validatorRegsBatchSize)) + }) + } } func Test_signValidatorRegistration(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - ctx := context.Background() - reg := ðpb.ValidatorRegistrationV1{ - FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), - GasLimit: 123456, - Timestamp: uint64(time.Now().Unix()), - Pubkey: validatorKey.PublicKey().Marshal(), + ctx := context.Background() + reg := ðpb.ValidatorRegistrationV1{ + FeeRecipient: bytesutil.PadTo([]byte("fee"), 20), + GasLimit: 123456, + Timestamp: uint64(time.Now().Unix()), + Pubkey: validatorKey.PublicKey().Marshal(), + } + _, err := signValidatorRegistration(ctx, m.signfunc, reg) + require.NoError(t, err) + }) } - _, err := signValidatorRegistration(ctx, m.signfunc, reg) - require.NoError(t, err) - } func TestValidator_SignValidatorRegistrationRequest(t *testing.T) { - _, m, validatorKey, finish := setup(t) - defer finish() - ctx := context.Background() - byteval, err := hexutil.Decode("0x878705ba3f8bc32fcf7f4caa1a35e72af65cf766") - require.NoError(t, err) - tests := []struct { - name string - arg *ethpb.ValidatorRegistrationV1 - validatorSetter func(t *testing.T) *validator - isCached bool - err string - }{ - { - name: " Happy Path cached", - arg: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), - GasLimit: 30000000, - Timestamp: uint64(time.Now().Unix()), - }, - validatorSetter: func(t *testing.T) *validator { - v := validator{ - pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), - signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), - useWeb: false, - genesisTime: 0, - } - v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ - Message: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - GasLimit: 30000000, - FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), - Timestamp: uint64(time.Now().Unix()), - }, - Signature: make([]byte, 0), - } - return &v - }, - isCached: true, - }, - { - name: " Happy Path not cached gas updated", - arg: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), - GasLimit: 30000000, - Timestamp: uint64(time.Now().Unix()), - }, - validatorSetter: func(t *testing.T) *validator { - v := validator{ - pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), - signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), - useWeb: false, - genesisTime: 0, - } - v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ - Message: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - GasLimit: 35000000, - FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), - Timestamp: uint64(time.Now().Unix() - 1), - }, - Signature: make([]byte, 0), - } - return &v - }, - isCached: false, - }, - { - name: " Happy Path not cached feerecipient updated", - arg: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - FeeRecipient: byteval, - GasLimit: 30000000, - Timestamp: uint64(time.Now().Unix()), + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + _, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + ctx := context.Background() + byteval, err := hexutil.Decode("0x878705ba3f8bc32fcf7f4caa1a35e72af65cf766") + require.NoError(t, err) + tests := []struct { + name string + arg *ethpb.ValidatorRegistrationV1 + validatorSetter func(t *testing.T) *validator + isCached bool + err string + }{ + { + name: " Happy Path cached", + arg: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + GasLimit: 30000000, + Timestamp: uint64(time.Now().Unix()), + }, + validatorSetter: func(t *testing.T) *validator { + v := validator{ + pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), + signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), + useWeb: false, + genesisTime: 0, + } + v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ + Message: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + GasLimit: 30000000, + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + Timestamp: uint64(time.Now().Unix()), + }, + Signature: make([]byte, 0), + } + return &v + }, + isCached: true, }, - validatorSetter: func(t *testing.T) *validator { - v := validator{ - pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), - signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), - useWeb: false, - genesisTime: 0, - } - v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ - Message: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - GasLimit: 30000000, - FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), - Timestamp: uint64(time.Now().Unix() - 1), - }, - Signature: make([]byte, 0), - } - return &v + { + name: " Happy Path not cached gas updated", + arg: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + GasLimit: 30000000, + Timestamp: uint64(time.Now().Unix()), + }, + validatorSetter: func(t *testing.T) *validator { + v := validator{ + pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), + signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), + useWeb: false, + genesisTime: 0, + } + v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ + Message: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + GasLimit: 35000000, + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + Timestamp: uint64(time.Now().Unix() - 1), + }, + Signature: make([]byte, 0), + } + return &v + }, + isCached: false, }, - isCached: false, - }, - { - name: " Happy Path not cached first Entry", - arg: ðpb.ValidatorRegistrationV1{ - Pubkey: validatorKey.PublicKey().Marshal(), - FeeRecipient: byteval, - GasLimit: 30000000, - Timestamp: uint64(time.Now().Unix()), + { + name: " Happy Path not cached feerecipient updated", + arg: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + FeeRecipient: byteval, + GasLimit: 30000000, + Timestamp: uint64(time.Now().Unix()), + }, + validatorSetter: func(t *testing.T) *validator { + v := validator{ + pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), + signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), + useWeb: false, + genesisTime: 0, + } + v.signedValidatorRegistrations[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())] = ðpb.SignedValidatorRegistrationV1{ + Message: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + GasLimit: 30000000, + FeeRecipient: make([]byte, fieldparams.FeeRecipientLength), + Timestamp: uint64(time.Now().Unix() - 1), + }, + Signature: make([]byte, 0), + } + return &v + }, + isCached: false, }, - validatorSetter: func(t *testing.T) *validator { - v := validator{ - pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), - signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), - useWeb: false, - genesisTime: 0, - } - return &v + { + name: " Happy Path not cached first Entry", + arg: ðpb.ValidatorRegistrationV1{ + Pubkey: validatorKey.PublicKey().Marshal(), + FeeRecipient: byteval, + GasLimit: 30000000, + Timestamp: uint64(time.Now().Unix()), + }, + validatorSetter: func(t *testing.T) *validator { + v := validator{ + pubkeyToValidatorIndex: make(map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex), + signedValidatorRegistrations: make(map[[fieldparams.BLSPubkeyLength]byte]*ethpb.SignedValidatorRegistrationV1), + useWeb: false, + genesisTime: 0, + } + return &v + }, + isCached: false, }, - isCached: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := tt.validatorSetter(t) + } + for _, tt := range tests { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + v := tt.validatorSetter(t) - startingReq, ok := v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)] + startingReq, ok := v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)] - got, err := v.SignValidatorRegistrationRequest(ctx, m.signfunc, tt.arg) - require.NoError(t, err) - if tt.isCached { - require.DeepEqual(t, got, v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)]) - } else { - if ok { - require.NotEqual(t, got.Message.Timestamp, startingReq.Message.Timestamp) + got, err := v.SignValidatorRegistrationRequest(ctx, m.signfunc, tt.arg) + require.NoError(t, err) + if tt.isCached { + require.DeepEqual(t, got, v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)]) + } else { + if ok { + require.NotEqual(t, got.Message.Timestamp, startingReq.Message.Timestamp) + } + require.Equal(t, got.Message.Timestamp, tt.arg.Timestamp) + require.Equal(t, got.Message.GasLimit, tt.arg.GasLimit) + require.Equal(t, hexutil.Encode(got.Message.FeeRecipient), hexutil.Encode(tt.arg.FeeRecipient)) + require.DeepEqual(t, got, v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)]) } - require.Equal(t, got.Message.Timestamp, tt.arg.Timestamp) - require.Equal(t, got.Message.GasLimit, tt.arg.GasLimit) - require.Equal(t, hexutil.Encode(got.Message.FeeRecipient), hexutil.Encode(tt.arg.FeeRecipient)) - require.DeepEqual(t, got, v.signedValidatorRegistrations[bytesutil.ToBytes48(tt.arg.Pubkey)]) - } - }) + }) + } } } diff --git a/validator/client/slashing_protection_interchange_test.go b/validator/client/slashing_protection_interchange_test.go index ae21b686a137..ad86aef68f86 100644 --- a/validator/client/slashing_protection_interchange_test.go +++ b/validator/client/slashing_protection_interchange_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "encoding/json" + "fmt" "strings" "testing" @@ -46,6 +47,7 @@ type eip3076TestCase struct { Pubkey string `json:"pubkey"` Slot string `json:"slot"` SigningRoot string `json:"signing_root"` + ShouldSucceedMinimal bool `json:"should_succeed"` ShouldSucceedComplete bool `json:"should_succeed_complete"` } `json:"blocks"` Attestations []struct { @@ -53,6 +55,7 @@ type eip3076TestCase struct { SourceEpoch string `json:"source_epoch"` TargetEpoch string `json:"target_epoch"` SigningRoot string `json:"signing_root"` + ShouldSucceedMinimal bool `json:"should_succeed"` ShouldSucceedComplete bool `json:"should_succeed_complete"` } `json:"attestations"` } `json:"steps"` @@ -76,99 +79,115 @@ func setupEIP3076SpecTests(t *testing.T) []*eip3076TestCase { } func TestEIP3076SpecTests(t *testing.T) { - testCases := setupEIP3076SpecTests(t) - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - if tt.Name == "" { - t.Skip("Skipping eip3076TestCase with empty name") - } - - // Set up validator client, one new validator client per eip3076TestCase. - // This ensures we initialize a new (empty) slashing protection database. - validator, _, _, _ := setup(t) - - for _, step := range tt.Steps { - if tt.GenesisValidatorsRoot != "" { - r, err := history.RootFromHex(tt.GenesisValidatorsRoot) - require.NoError(t, validator.db.SaveGenesisValidatorsRoot(context.Background(), r[:])) - require.NoError(t, err) - } + for _, isMinimal := range []bool{false, true} { + slashingProtectionType := "complete" + if isMinimal { + slashingProtectionType = "minimal" + } - // The eip3076TestCase config contains the interchange config in json. - // This loads the interchange data via ImportStandardProtectionJSON. - interchangeBytes, err := json.Marshal(step.Interchange) - if err != nil { - t.Fatal(err) - } - b := bytes.NewBuffer(interchangeBytes) - if err := history.ImportStandardProtectionJSON(context.Background(), validator.db, b); err != nil { - if step.ShouldSucceed { - t.Fatal(err) - } - } else if !step.ShouldSucceed { - require.NotNil(t, err, "import standard protection json should have failed") + for _, tt := range setupEIP3076SpecTests(t) { + t.Run(fmt.Sprintf("%s-%s", slashingProtectionType, tt.Name), func(t *testing.T) { + if tt.Name == "" { + t.Skip("Skipping eip3076TestCase with empty name") } - // This loops through a list of block signings to attempt after importing the interchange data above. - for _, sb := range step.Blocks { - bSlot, err := history.SlotFromString(sb.Slot) - require.NoError(t, err) - pk, err := history.PubKeyFromHex(sb.Pubkey) - require.NoError(t, err) - b := util.NewBeaconBlock() - b.Block.Slot = bSlot - - var signingRoot [32]byte - if sb.SigningRoot != "" { - signingRootBytes, err := hex.DecodeString(strings.TrimPrefix(sb.SigningRoot, "0x")) - require.NoError(t, err) - copy(signingRoot[:], signingRootBytes) - } + // Set up validator client, one new validator client per eip3076TestCase. + // This ensures we initialize a new (empty) slashing protection database. + validator, _, _, _ := setup(t, isMinimal) - wsb, err := blocks.NewSignedBeaconBlock(b) - require.NoError(t, err) - err = validator.slashableProposalCheck(context.Background(), pk, wsb, signingRoot) - if sb.ShouldSucceedComplete { + for _, step := range tt.Steps { + if tt.GenesisValidatorsRoot != "" { + r, err := history.RootFromHex(tt.GenesisValidatorsRoot) + require.NoError(t, validator.db.SaveGenesisValidatorsRoot(context.Background(), r[:])) require.NoError(t, err) - } else { - require.NotEqual(t, nil, err, "pre validation should have failed for block") } - } - // This loops through a list of attestation signings to attempt after importing the interchange data above. - for _, sa := range step.Attestations { - target, err := history.EpochFromString(sa.TargetEpoch) - require.NoError(t, err) - source, err := history.EpochFromString(sa.SourceEpoch) - require.NoError(t, err) - pk, err := history.PubKeyFromHex(sa.Pubkey) - require.NoError(t, err) - ia := ðpb.IndexedAttestation{ - Data: ðpb.AttestationData{ - BeaconBlockRoot: make([]byte, 32), - Target: ðpb.Checkpoint{Epoch: target, Root: make([]byte, 32)}, - Source: ðpb.Checkpoint{Epoch: source, Root: make([]byte, 32)}, - }, - Signature: make([]byte, fieldparams.BLSSignatureLength), + // The eip3076TestCase config contains the interchange config in json. + // This loads the interchange data via ImportStandardProtectionJSON. + interchangeBytes, err := json.Marshal(step.Interchange) + if err != nil { + t.Fatal(err) } + b := bytes.NewBuffer(interchangeBytes) + if err := history.ImportStandardProtectionJSON(context.Background(), validator.db, b); err != nil { + if step.ShouldSucceed { + t.Fatal(err) + } + } else if !step.ShouldSucceed { + require.NotNil(t, err, "import standard protection json should have failed") + } + + // This loops through a list of block signings to attempt after importing the interchange data above. + for _, sb := range step.Blocks { + shouldSucceed := sb.ShouldSucceedComplete + if isMinimal { + shouldSucceed = sb.ShouldSucceedMinimal + } - var signingRoot [32]byte - if sa.SigningRoot != "" { - signingRootBytes, err := hex.DecodeString(strings.TrimPrefix(sa.SigningRoot, "0x")) + bSlot, err := history.SlotFromString(sb.Slot) require.NoError(t, err) - copy(signingRoot[:], signingRootBytes) + pk, err := history.PubKeyFromHex(sb.Pubkey) + require.NoError(t, err) + b := util.NewBeaconBlock() + b.Block.Slot = bSlot + + var signingRoot [32]byte + if sb.SigningRoot != "" { + signingRootBytes, err := hex.DecodeString(strings.TrimPrefix(sb.SigningRoot, "0x")) + require.NoError(t, err) + copy(signingRoot[:], signingRootBytes) + } + + wsb, err := blocks.NewSignedBeaconBlock(b) + require.NoError(t, err) + err = validator.slashableProposalCheck(context.Background(), pk, wsb, signingRoot) + if shouldSucceed { + require.NoError(t, err) + } else { + require.NotEqual(t, nil, err, "pre validation should have failed for block") + } } - err = validator.slashableAttestationCheck(context.Background(), ia, pk, signingRoot) - if sa.ShouldSucceedComplete { + // This loops through a list of attestation signings to attempt after importing the interchange data above. + for _, sa := range step.Attestations { + shouldSucceed := sa.ShouldSucceedComplete + if isMinimal { + shouldSucceed = sa.ShouldSucceedMinimal + } + + target, err := history.EpochFromString(sa.TargetEpoch) require.NoError(t, err) - } else { - require.NotNil(t, err, "pre validation should have failed for attestation") + source, err := history.EpochFromString(sa.SourceEpoch) + require.NoError(t, err) + pk, err := history.PubKeyFromHex(sa.Pubkey) + require.NoError(t, err) + ia := ðpb.IndexedAttestation{ + Data: ðpb.AttestationData{ + BeaconBlockRoot: make([]byte, 32), + Target: ðpb.Checkpoint{Epoch: target, Root: make([]byte, 32)}, + Source: ðpb.Checkpoint{Epoch: source, Root: make([]byte, 32)}, + }, + Signature: make([]byte, fieldparams.BLSSignatureLength), + } + + var signingRoot [32]byte + if sa.SigningRoot != "" { + signingRootBytes, err := hex.DecodeString(strings.TrimPrefix(sa.SigningRoot, "0x")) + require.NoError(t, err) + copy(signingRoot[:], signingRootBytes) + } + + err = validator.slashableAttestationCheck(context.Background(), ia, pk, signingRoot) + if shouldSucceed { + require.NoError(t, err) + } else { + require.NotNil(t, err, "pre validation should have failed for attestation") + } } } - } - require.NoError(t, validator.db.Close(), "failed to close slashing protection database") - }) + require.NoError(t, validator.db.Close(), "failed to close slashing protection database") + }) + } } } diff --git a/validator/client/sync_committee_test.go b/validator/client/sync_committee_test.go index 3cc4c71ad973..544f05de985d 100644 --- a/validator/client/sync_committee_test.go +++ b/validator/client/sync_committee_test.go @@ -3,6 +3,7 @@ package client import ( "context" "encoding/hex" + "fmt" "testing" "github.com/golang/mock/gomock" @@ -20,246 +21,278 @@ import ( ) func TestSubmitSyncCommitteeMessage_ValidatorDutiesRequestFailure(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} - defer finish() - - m.validatorClient.EXPECT().GetSyncMessageBlockRoot( - gomock.Any(), // ctx - &emptypb.Empty{}, - ).Return(ðpb.SyncMessageBlockRootResponse{ - Root: bytesutil.PadTo([]byte{}, 32), - }, nil) - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Could not fetch validator assignment") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} + defer finish() + + m.validatorClient.EXPECT().GetSyncMessageBlockRoot( + gomock.Any(), // ctx + &emptypb.Empty{}, + ).Return(ðpb.SyncMessageBlockRootResponse{ + Root: bytesutil.PadTo([]byte{}, 32), + }, nil) + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Could not fetch validator assignment") + }) + } } func TestSubmitSyncCommitteeMessage_BadDomainData(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - hook := logTest.NewGlobal() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - - r := []byte{'a'} - m.validatorClient.EXPECT().GetSyncMessageBlockRoot( - gomock.Any(), // ctx - &emptypb.Empty{}, - ).Return(ðpb.SyncMessageBlockRootResponse{ - Root: bytesutil.PadTo(r, 32), - }, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), gomock.Any()). - Return(nil, errors.New("uh oh")) - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Could not get sync committee domain data") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + hook := logTest.NewGlobal() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + + r := []byte{'a'} + m.validatorClient.EXPECT().GetSyncMessageBlockRoot( + gomock.Any(), // ctx + &emptypb.Empty{}, + ).Return(ðpb.SyncMessageBlockRootResponse{ + Root: bytesutil.PadTo(r, 32), + }, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), gomock.Any()). + Return(nil, errors.New("uh oh")) + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Could not get sync committee domain data") + }) + } } func TestSubmitSyncCommitteeMessage_CouldNotSubmit(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - hook := logTest.NewGlobal() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - - r := []byte{'a'} - m.validatorClient.EXPECT().GetSyncMessageBlockRoot( - gomock.Any(), // ctx - &emptypb.Empty{}, - ).Return(ðpb.SyncMessageBlockRootResponse{ - Root: bytesutil.PadTo(r, 32), - }, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), // ctx - gomock.Any()). // epoch - Return(ðpb.DomainResponse{ - SignatureDomain: make([]byte, 32), - }, nil) - - m.validatorClient.EXPECT().SubmitSyncMessage( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.SyncCommitteeMessage{}), - ).Return(&emptypb.Empty{}, errors.New("uh oh") /* error */) - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) - - require.LogsContain(t, hook, "Could not submit sync committee message") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + hook := logTest.NewGlobal() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + + r := []byte{'a'} + m.validatorClient.EXPECT().GetSyncMessageBlockRoot( + gomock.Any(), // ctx + &emptypb.Empty{}, + ).Return(ðpb.SyncMessageBlockRootResponse{ + Root: bytesutil.PadTo(r, 32), + }, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), // ctx + gomock.Any()). // epoch + Return(ðpb.DomainResponse{ + SignatureDomain: make([]byte, 32), + }, nil) + + m.validatorClient.EXPECT().SubmitSyncMessage( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.SyncCommitteeMessage{}), + ).Return(&emptypb.Empty{}, errors.New("uh oh") /* error */) + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) + + require.LogsContain(t, hook, "Could not submit sync committee message") + }) + } } func TestSubmitSyncCommitteeMessage_OK(t *testing.T) { - validator, m, validatorKey, finish := setup(t) - defer finish() - hook := logTest.NewGlobal() - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - - r := []byte{'a'} - m.validatorClient.EXPECT().GetSyncMessageBlockRoot( - gomock.Any(), // ctx - &emptypb.Empty{}, - ).Return(ðpb.SyncMessageBlockRootResponse{ - Root: bytesutil.PadTo(r, 32), - }, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), // ctx - gomock.Any()). // epoch - Return(ðpb.DomainResponse{ - SignatureDomain: make([]byte, 32), - }, nil) - - var generatedMsg *ethpb.SyncCommitteeMessage - m.validatorClient.EXPECT().SubmitSyncMessage( - gomock.Any(), // ctx - gomock.AssignableToTypeOf(ðpb.SyncCommitteeMessage{}), - ).Do(func(_ context.Context, msg *ethpb.SyncCommitteeMessage) { - generatedMsg = msg - }).Return(&emptypb.Empty{}, nil /* error */) - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) - - require.LogsDoNotContain(t, hook, "Could not") - require.Equal(t, primitives.Slot(1), generatedMsg.Slot) - require.Equal(t, validatorIndex, generatedMsg.ValidatorIndex) - require.DeepEqual(t, bytesutil.PadTo(r, 32), generatedMsg.BlockRoot) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + hook := logTest.NewGlobal() + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + + r := []byte{'a'} + m.validatorClient.EXPECT().GetSyncMessageBlockRoot( + gomock.Any(), // ctx + &emptypb.Empty{}, + ).Return(ðpb.SyncMessageBlockRootResponse{ + Root: bytesutil.PadTo(r, 32), + }, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), // ctx + gomock.Any()). // epoch + Return(ðpb.DomainResponse{ + SignatureDomain: make([]byte, 32), + }, nil) + + var generatedMsg *ethpb.SyncCommitteeMessage + m.validatorClient.EXPECT().SubmitSyncMessage( + gomock.Any(), // ctx + gomock.AssignableToTypeOf(ðpb.SyncCommitteeMessage{}), + ).Do(func(_ context.Context, msg *ethpb.SyncCommitteeMessage) { + generatedMsg = msg + }).Return(&emptypb.Empty{}, nil /* error */) + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitSyncCommitteeMessage(context.Background(), 1, pubKey) + + require.LogsDoNotContain(t, hook, "Could not") + require.Equal(t, primitives.Slot(1), generatedMsg.Slot) + require.Equal(t, validatorIndex, generatedMsg.ValidatorIndex) + require.DeepEqual(t, bytesutil.PadTo(r, 32), generatedMsg.BlockRoot) + }) + } } func TestSubmitSignedContributionAndProof_ValidatorDutiesRequestFailure(t *testing.T) { - hook := logTest.NewGlobal() - validator, _, validatorKey, finish := setup(t) - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Could not fetch validator assignment") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, _, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Could not fetch validator assignment") + }) + } } func TestSubmitSignedContributionAndProof_GetSyncSubcommitteeIndexFailure(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - Slot: 1, - PublicKey: pubKey[:], - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{}, errors.New("Bad index")) - - validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Could not get sync subcommittee index") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + Slot: 1, + PublicKey: pubKey[:], + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{}, errors.New("Bad index")) + + validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Could not get sync subcommittee index") + }) + } } func TestSubmitSignedContributionAndProof_NothingToDo(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - Slot: 1, - PublicKey: pubKey[:], - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{}}, nil) - - validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Empty subcommittee index list, do nothing") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + Slot: 1, + PublicKey: pubKey[:], + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{}}, nil) + + validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Empty subcommittee index list, do nothing") + }) + } } func TestSubmitSignedContributionAndProof_BadDomain(t *testing.T) { - hook := logTest.NewGlobal() - validator, m, validatorKey, finish := setup(t) - validatorIndex := primitives.ValidatorIndex(7) - committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} - validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ - { - PublicKey: validatorKey.PublicKey().Marshal(), - Committee: committee, - ValidatorIndex: validatorIndex, - }, - }} - defer finish() - - var pubKey [fieldparams.BLSPubkeyLength]byte - copy(pubKey[:], validatorKey.PublicKey().Marshal()) - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - Slot: 1, - PublicKey: pubKey[:], - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{1}}, nil) - - m.validatorClient.EXPECT(). - DomainData(gomock.Any(), // ctx - gomock.Any()). // epoch - Return(ðpb.DomainResponse{ - SignatureDomain: make([]byte, 32), - }, errors.New("bad domain response")) - - validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) - require.LogsContain(t, hook, "Could not get selection proofs") - require.LogsContain(t, hook, "bad domain response") + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + hook := logTest.NewGlobal() + validator, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + validatorIndex := primitives.ValidatorIndex(7) + committee := []primitives.ValidatorIndex{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} + validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ + { + PublicKey: validatorKey.PublicKey().Marshal(), + Committee: committee, + ValidatorIndex: validatorIndex, + }, + }} + defer finish() + + var pubKey [fieldparams.BLSPubkeyLength]byte + copy(pubKey[:], validatorKey.PublicKey().Marshal()) + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + Slot: 1, + PublicKey: pubKey[:], + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{1}}, nil) + + m.validatorClient.EXPECT(). + DomainData(gomock.Any(), // ctx + gomock.Any()). // epoch + Return(ðpb.DomainResponse{ + SignatureDomain: make([]byte, 32), + }, errors.New("bad domain response")) + + validator.SubmitSignedContributionAndProof(context.Background(), 1, pubKey) + require.LogsContain(t, hook, "Could not get selection proofs") + require.LogsContain(t, hook, "bad domain response") + }) + } } func TestSubmitSignedContributionAndProof_CouldNotGetContribution(t *testing.T) { diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index b5826c6c93d5..4a205ebe9566 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -685,105 +685,113 @@ func TestUpdateDuties_AllValidatorsExited(t *testing.T) { } func TestRolesAt_OK(t *testing.T) { - v, m, validatorKey, finish := setup(t) - defer finish() + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - v.duties = ðpb.DutiesResponse{ - Duties: []*ethpb.DutiesResponse_Duty{ - { - CommitteeIndex: 1, - AttesterSlot: 1, - PublicKey: validatorKey.PublicKey().Marshal(), - IsSyncCommittee: true, - }, - }, - NextEpochDuties: []*ethpb.DutiesResponse_Duty{ - { - CommitteeIndex: 1, - AttesterSlot: 1, - PublicKey: validatorKey.PublicKey().Marshal(), - IsSyncCommittee: true, - }, - }, - } + v.duties = ðpb.DutiesResponse{ + Duties: []*ethpb.DutiesResponse_Duty{ + { + CommitteeIndex: 1, + AttesterSlot: 1, + PublicKey: validatorKey.PublicKey().Marshal(), + IsSyncCommittee: true, + }, + }, + NextEpochDuties: []*ethpb.DutiesResponse_Duty{ + { + CommitteeIndex: 1, + AttesterSlot: 1, + PublicKey: validatorKey.PublicKey().Marshal(), + IsSyncCommittee: true, + }, + }, + } - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - PublicKey: validatorKey.PublicKey().Marshal(), - Slot: 1, - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + PublicKey: validatorKey.PublicKey().Marshal(), + Slot: 1, + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) - roleMap, err := v.RolesAt(context.Background(), 1) - require.NoError(t, err) + roleMap, err := v.RolesAt(context.Background(), 1) + require.NoError(t, err) - assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) - assert.Equal(t, iface.RoleAggregator, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][1]) - assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][2]) + assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) + assert.Equal(t, iface.RoleAggregator, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][1]) + assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][2]) - // Test sync committee role at epoch boundary. - v.duties = ðpb.DutiesResponse{ - Duties: []*ethpb.DutiesResponse_Duty{ - { - CommitteeIndex: 1, - AttesterSlot: 1, - PublicKey: validatorKey.PublicKey().Marshal(), - IsSyncCommittee: false, - }, - }, - NextEpochDuties: []*ethpb.DutiesResponse_Duty{ - { - CommitteeIndex: 1, - AttesterSlot: 1, - PublicKey: validatorKey.PublicKey().Marshal(), - IsSyncCommittee: true, - }, - }, - } + // Test sync committee role at epoch boundary. + v.duties = ðpb.DutiesResponse{ + Duties: []*ethpb.DutiesResponse_Duty{ + { + CommitteeIndex: 1, + AttesterSlot: 1, + PublicKey: validatorKey.PublicKey().Marshal(), + IsSyncCommittee: false, + }, + }, + NextEpochDuties: []*ethpb.DutiesResponse_Duty{ + { + CommitteeIndex: 1, + AttesterSlot: 1, + PublicKey: validatorKey.PublicKey().Marshal(), + IsSyncCommittee: true, + }, + }, + } - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - PublicKey: validatorKey.PublicKey().Marshal(), - Slot: 31, - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + PublicKey: validatorKey.PublicKey().Marshal(), + Slot: 31, + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) - roleMap, err = v.RolesAt(context.Background(), params.BeaconConfig().SlotsPerEpoch-1) - require.NoError(t, err) - assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) + roleMap, err = v.RolesAt(context.Background(), params.BeaconConfig().SlotsPerEpoch-1) + require.NoError(t, err) + assert.Equal(t, iface.RoleSyncCommittee, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) + }) + } } func TestRolesAt_DoesNotAssignProposer_Slot0(t *testing.T) { - v, m, validatorKey, finish := setup(t) - defer finish() + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() - v.duties = ðpb.DutiesResponse{ - Duties: []*ethpb.DutiesResponse_Duty{ - { - CommitteeIndex: 1, - AttesterSlot: 0, - ProposerSlots: []primitives.Slot{0}, - PublicKey: validatorKey.PublicKey().Marshal(), - }, - }, - } + v.duties = ðpb.DutiesResponse{ + Duties: []*ethpb.DutiesResponse_Duty{ + { + CommitteeIndex: 1, + AttesterSlot: 0, + ProposerSlots: []primitives.Slot{0}, + PublicKey: validatorKey.PublicKey().Marshal(), + }, + }, + } - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - roleMap, err := v.RolesAt(context.Background(), 0) - require.NoError(t, err) + roleMap, err := v.RolesAt(context.Background(), 0) + require.NoError(t, err) - assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) + assert.Equal(t, iface.RoleAttester, roleMap[bytesutil.ToBytes48(validatorKey.PublicKey().Marshal())][0]) + }) + } } func TestCheckAndLogValidatorStatus_OK(t *testing.T) { @@ -1238,45 +1246,49 @@ func createAttestation(source, target primitives.Epoch) *ethpb.IndexedAttestatio } func TestIsSyncCommitteeAggregator_OK(t *testing.T) { - params.SetupTestConfigCleanup(t) - v, m, validatorKey, finish := setup(t) - defer finish() - - slot := primitives.Slot(1) - pubKey := validatorKey.PublicKey().Marshal() - - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - PublicKey: validatorKey.PublicKey().Marshal(), - Slot: 1, - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) + for _, isSlashingProtectionMinimal := range [...]bool{false, true} { + t.Run(fmt.Sprintf("SlashingProtectionMinimal:%v", isSlashingProtectionMinimal), func(t *testing.T) { + params.SetupTestConfigCleanup(t) + v, m, validatorKey, finish := setup(t, isSlashingProtectionMinimal) + defer finish() + + slot := primitives.Slot(1) + pubKey := validatorKey.PublicKey().Marshal() + + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + PublicKey: validatorKey.PublicKey().Marshal(), + Slot: 1, + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{}, nil /*err*/) - aggregator, err := v.isSyncCommitteeAggregator(context.Background(), slot, bytesutil.ToBytes48(pubKey)) - require.NoError(t, err) - require.Equal(t, false, aggregator) - - c := params.BeaconConfig().Copy() - c.TargetAggregatorsPerSyncSubcommittee = math.MaxUint64 - params.OverrideBeaconConfig(c) - - m.validatorClient.EXPECT().DomainData( - gomock.Any(), // ctx - gomock.Any(), // epoch - ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) - - m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( - gomock.Any(), // ctx - ðpb.SyncSubcommitteeIndexRequest{ - PublicKey: validatorKey.PublicKey().Marshal(), - Slot: 1, - }, - ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{0}}, nil /*err*/) + aggregator, err := v.isSyncCommitteeAggregator(context.Background(), slot, bytesutil.ToBytes48(pubKey)) + require.NoError(t, err) + require.Equal(t, false, aggregator) + + c := params.BeaconConfig().Copy() + c.TargetAggregatorsPerSyncSubcommittee = math.MaxUint64 + params.OverrideBeaconConfig(c) + + m.validatorClient.EXPECT().DomainData( + gomock.Any(), // ctx + gomock.Any(), // epoch + ).Return(ðpb.DomainResponse{SignatureDomain: make([]byte, 32)}, nil /*err*/) + + m.validatorClient.EXPECT().GetSyncSubcommitteeIndex( + gomock.Any(), // ctx + ðpb.SyncSubcommitteeIndexRequest{ + PublicKey: validatorKey.PublicKey().Marshal(), + Slot: 1, + }, + ).Return(ðpb.SyncSubcommitteeIndexResponse{Indices: []primitives.CommitteeIndex{0}}, nil /*err*/) - aggregator, err = v.isSyncCommitteeAggregator(context.Background(), slot, bytesutil.ToBytes48(pubKey)) - require.NoError(t, err) - require.Equal(t, true, aggregator) + aggregator, err = v.isSyncCommitteeAggregator(context.Background(), slot, bytesutil.ToBytes48(pubKey)) + require.NoError(t, err) + require.Equal(t, true, aggregator) + }) + } } func TestValidator_WaitForKeymanagerInitialization_web3Signer(t *testing.T) {