mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 21:38:05 -05:00
Compare commits
169 Commits
hack-ssz
...
feature/sl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd1ce957f | ||
|
|
673a845918 | ||
|
|
15a024f171 | ||
|
|
618e7d8f89 | ||
|
|
26de0f1358 | ||
|
|
5f38167cd9 | ||
|
|
a0193ca90c | ||
|
|
0fc5b27195 | ||
|
|
2a9a978fc6 | ||
|
|
9a78ce007a | ||
|
|
16febfe6d9 | ||
|
|
db57c45ec9 | ||
|
|
0d99862e42 | ||
|
|
a4f35f4b09 | ||
|
|
ebb1fe80f9 | ||
|
|
ab071834fb | ||
|
|
e27fb164a3 | ||
|
|
7b807a6c01 | ||
|
|
33e6908c71 | ||
|
|
bac54f53ab | ||
|
|
960d39c859 | ||
|
|
fc951b4416 | ||
|
|
4d2a01e6df | ||
|
|
3121501e80 | ||
|
|
65656045f9 | ||
|
|
59b38ed293 | ||
|
|
963f61f1fb | ||
|
|
1e8720057e | ||
|
|
2fa80c35dc | ||
|
|
96852134ab | ||
|
|
2606935c42 | ||
|
|
c6ca50e766 | ||
|
|
3433002bae | ||
|
|
ce9df503b3 | ||
|
|
b93d925047 | ||
|
|
1df73a698b | ||
|
|
ca2461491c | ||
|
|
83fb66b9d5 | ||
|
|
ab3ffb5d38 | ||
|
|
28fbbbdf7f | ||
|
|
eb612d81b1 | ||
|
|
601fbbfb3a | ||
|
|
f4d3eec431 | ||
|
|
b3ffac459a | ||
|
|
2e1b8190eb | ||
|
|
bf14b28ca9 | ||
|
|
b4b6f25386 | ||
|
|
f5f95d8f99 | ||
|
|
d93369294c | ||
|
|
ad0332ba41 | ||
|
|
18d00724b2 | ||
|
|
a21b98bfd3 | ||
|
|
35b853c958 | ||
|
|
401f73d341 | ||
|
|
403389a0d8 | ||
|
|
8c5d577636 | ||
|
|
f4d54506a0 | ||
|
|
44f6ec48b8 | ||
|
|
2958c84872 | ||
|
|
e9d670f16f | ||
|
|
7194d025ba | ||
|
|
0776639772 | ||
|
|
68d43a9e90 | ||
|
|
e18cca94c9 | ||
|
|
08df279330 | ||
|
|
c80fc63c9f | ||
|
|
1df189339a | ||
|
|
c0246bd82e | ||
|
|
12ed20f341 | ||
|
|
9824e17c83 | ||
|
|
f4e247808a | ||
|
|
8dcfa40807 | ||
|
|
89ee961354 | ||
|
|
ad30274a2d | ||
|
|
91ce227966 | ||
|
|
dd4154aed9 | ||
|
|
6f97ff2219 | ||
|
|
ac44476977 | ||
|
|
6cecb79988 | ||
|
|
67ac7a7455 | ||
|
|
4e6981374e | ||
|
|
21b33d3883 | ||
|
|
ca5527e331 | ||
|
|
cb0450eb23 | ||
|
|
9a75bb9d5a | ||
|
|
71f9b8be87 | ||
|
|
03b1bc12a6 | ||
|
|
3a7ce95dc2 | ||
|
|
dfb0209f01 | ||
|
|
5a22c5c2b0 | ||
|
|
7734ae1006 | ||
|
|
71ac917ba8 | ||
|
|
8836445dff | ||
|
|
661bf55961 | ||
|
|
48d1b8766e | ||
|
|
1127851899 | ||
|
|
bcbfa26fbd | ||
|
|
dd5b19f3de | ||
|
|
c3084a2cfe | ||
|
|
c0e49a4217 | ||
|
|
425816a2b4 | ||
|
|
dd8e4edb41 | ||
|
|
7e3dae42c0 | ||
|
|
d629297b80 | ||
|
|
abbc66e617 | ||
|
|
905f5c3c88 | ||
|
|
980fab5439 | ||
|
|
a2b80eceb6 | ||
|
|
3c4b858c99 | ||
|
|
082bb19c6d | ||
|
|
10f6f06bb8 | ||
|
|
dd31cfb929 | ||
|
|
23cbd504ba | ||
|
|
be711a820e | ||
|
|
376f661f8a | ||
|
|
5cb12099e9 | ||
|
|
a430a4b8ae | ||
|
|
4526483392 | ||
|
|
0efb3ec54e | ||
|
|
254cd6def0 | ||
|
|
4943d635cd | ||
|
|
5184d63801 | ||
|
|
8d5c44ff77 | ||
|
|
8901cbc574 | ||
|
|
8fff0b6ca7 | ||
|
|
e7e62f5234 | ||
|
|
8f7c4cea4e | ||
|
|
d7af14128e | ||
|
|
87bf2296a2 | ||
|
|
963d42567c | ||
|
|
daa575b87c | ||
|
|
346629b205 | ||
|
|
f5f97c3d1f | ||
|
|
5259a4b698 | ||
|
|
746315815e | ||
|
|
5678fbe591 | ||
|
|
a82fc98453 | ||
|
|
30fec47d57 | ||
|
|
fd33c70a5c | ||
|
|
ec678939a8 | ||
|
|
9553b82ed3 | ||
|
|
0bb61cf3fe | ||
|
|
f7cc07c750 | ||
|
|
c56d09dff3 | ||
|
|
8cdbcd9700 | ||
|
|
19ae9b5590 | ||
|
|
2913947c24 | ||
|
|
c342d35bbf | ||
|
|
b285722f5c | ||
|
|
16c6ea9489 | ||
|
|
72bfb94e53 | ||
|
|
976297725a | ||
|
|
9b6f93184d | ||
|
|
a6fde55646 | ||
|
|
a48de9ab69 | ||
|
|
429c94534e | ||
|
|
12defb914d | ||
|
|
ce9cf8cf1f | ||
|
|
5596f8b0d4 | ||
|
|
2864d5f070 | ||
|
|
38d681341c | ||
|
|
90f2ef801a | ||
|
|
d9bbbe5a4e | ||
|
|
24dde8fb82 | ||
|
|
b6b5bb0c51 | ||
|
|
971a15f907 | ||
|
|
3e2c6a9522 | ||
|
|
939a36df58 | ||
|
|
de89d816ad |
@@ -6,6 +6,7 @@
|
||||
mock_path="shared/mock"
|
||||
mocks=(
|
||||
"$mock_path/beacon_service_mock.go BeaconChainClient,BeaconChain_StreamChainHeadClient,BeaconChain_StreamAttestationsClient,BeaconChain_StreamBlocksClient,BeaconChain_StreamValidatorsInfoClient,BeaconChain_StreamIndexedAttestationsClient"
|
||||
"$mock_path/slasher_service_mock.go SlasherClient"
|
||||
"$mock_path/beacon_chain_service_mock.go BeaconChain_StreamChainHeadServer,BeaconChain_StreamAttestationsServer,BeaconChain_StreamBlocksServer,BeaconChain_StreamValidatorsInfoServer,BeaconChain_StreamIndexedAttestationsServer"
|
||||
"$mock_path/beacon_validator_server_mock.go BeaconNodeValidatorServer,BeaconNodeValidator_WaitForActivationServer,BeaconNodeValidator_WaitForChainStartServer,BeaconNodeValidator_StreamDutiesServer"
|
||||
"$mock_path/beacon_validator_client_mock.go BeaconNodeValidatorClient,BeaconNodeValidator_WaitForChainStartClient,BeaconNodeValidator_WaitForActivationClient,BeaconNodeValidator_StreamDutiesClient"
|
||||
@@ -19,7 +20,6 @@ for ((i = 0; i < ${#mocks[@]}; i++)); do
|
||||
interfaces=${mocks[i]#* };
|
||||
echo "generating $file for interfaces: $interfaces";
|
||||
GO11MODULE=on mockgen -package=mock -destination="$file" github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1 "$interfaces"
|
||||
GO11MODULE=on mockgen -package=mock -destination="$file" github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1 "$interfaces"
|
||||
done
|
||||
|
||||
goimports -w "$mock_path/."
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"external/.*": "Third party code",
|
||||
"rules_go_work-.*": "Third party code",
|
||||
"config/params/config.go": "This config struct needs to be organized for now",
|
||||
"shared/featureconfig/config.go": "This config struct needs to be organized for now",
|
||||
"config/features/config.go": "This config struct needs to be organized for now",
|
||||
"proto/.*": "Excluding protobuf objects for now"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ go_library(
|
||||
"//async/event:go_default_library",
|
||||
"//beacon-chain/core:go_default_library",
|
||||
"//beacon-chain/core/altair:go_default_library",
|
||||
"//beacon-chain/core/blocks:go_default_library",
|
||||
"//beacon-chain/core/signing:go_default_library",
|
||||
"//cache/lru:go_default_library",
|
||||
"//config/features:go_default_library",
|
||||
@@ -53,7 +54,6 @@ go_library(
|
||||
"//validator/keymanager:go_default_library",
|
||||
"//validator/keymanager/imported:go_default_library",
|
||||
"//validator/keymanager/remote:go_default_library",
|
||||
"//validator/slashing-protection/iface:go_default_library",
|
||||
"@com_github_dgraph_io_ristretto//:go_default_library",
|
||||
"@com_github_ferranbt_fastssz//:go_default_library",
|
||||
"@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library",
|
||||
|
||||
@@ -81,8 +81,12 @@ func (v *validator) slashableAttestationCheck(
|
||||
return errors.Wrap(err, "could not save attestation history for validator public key")
|
||||
}
|
||||
|
||||
if features.Get().RemoteSlasherProtection && v.protector != nil {
|
||||
if !v.protector.CheckAttestationSafety(ctx, indexedAtt) {
|
||||
if features.Get().RemoteSlasherProtection {
|
||||
slashing, err := v.slashingProtectionClient.IsSlashableAttestation(ctx, indexedAtt)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not check if attestation is slashable")
|
||||
}
|
||||
if slashing != nil && len(slashing.AttesterSlashings) > 0 {
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorAttestFailVecSlasher.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
mockSlasher "github.com/prysmaticlabs/prysm/validator/testing"
|
||||
)
|
||||
|
||||
func Test_slashableAttestationCheck(t *testing.T) {
|
||||
@@ -19,7 +18,7 @@ func Test_slashableAttestationCheck(t *testing.T) {
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
validator, _, validatorKey, finish := setup(t)
|
||||
validator, m, validatorKey, finish := setup(t)
|
||||
defer finish()
|
||||
pubKey := [48]byte{}
|
||||
copy(pubKey[:], validatorKey.PublicKey().Marshal())
|
||||
@@ -39,11 +38,23 @@ func Test_slashableAttestationCheck(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
mockProtector := &mockSlasher.MockProtector{AllowAttestation: false}
|
||||
validator.protector = mockProtector
|
||||
|
||||
m.slasherClient.EXPECT().IsSlashableAttestation(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.AttesterSlashingResponse{AttesterSlashings: []*ethpb.AttesterSlashing{{
|
||||
Attestation_1: ðpb.IndexedAttestation{},
|
||||
Attestation_2: ðpb.IndexedAttestation{},
|
||||
}}}, nil /*err*/)
|
||||
|
||||
err := validator.slashableAttestationCheck(context.Background(), att, pubKey, [32]byte{1})
|
||||
require.ErrorContains(t, failedPostAttSignExternalErr, err)
|
||||
mockProtector.AllowAttestation = true
|
||||
|
||||
m.slasherClient.EXPECT().IsSlashableAttestation(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.AttesterSlashingResponse{}, nil /*err*/)
|
||||
|
||||
err = validator.slashableAttestationCheck(context.Background(), att, pubKey, [32]byte{1})
|
||||
require.NoError(t, err, "Expected allowed attestation not to throw error")
|
||||
}
|
||||
@@ -75,18 +86,23 @@ func Test_slashableAttestationCheck_UpdatesLowestSignedEpochs(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
mockProtector := &mockSlasher.MockProtector{AllowAttestation: false}
|
||||
validator.protector = mockProtector
|
||||
|
||||
m.slasherClient.EXPECT().IsSlashableAttestation(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.AttesterSlashingResponse{}, nil /*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)
|
||||
mockProtector.AllowAttestation = true
|
||||
|
||||
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)
|
||||
|
||||
@@ -102,12 +118,12 @@ func Test_slashableAttestationCheck_UpdatesLowestSignedEpochs(t *testing.T) {
|
||||
|
||||
func Test_slashableAttestationCheck_OK(t *testing.T) {
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: false,
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
ctx := context.Background()
|
||||
validator, _, _, finish := setup(t)
|
||||
validator, mocks, _, finish := setup(t)
|
||||
defer finish()
|
||||
att := ðpb.IndexedAttestation{
|
||||
AttestingIndices: []uint64{1, 2},
|
||||
@@ -127,18 +143,24 @@ func Test_slashableAttestationCheck_OK(t *testing.T) {
|
||||
}
|
||||
sr := [32]byte{1}
|
||||
fakePubkey := bytesutil.ToBytes48([]byte("test"))
|
||||
|
||||
mocks.slasherClient.EXPECT().IsSlashableAttestation(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.AttesterSlashingResponse{}, nil /*err*/)
|
||||
|
||||
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) {
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: false,
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
ctx := context.Background()
|
||||
validator, _, _, finish := setup(t)
|
||||
validator, mocks, _, finish := setup(t)
|
||||
defer finish()
|
||||
att := ðpb.IndexedAttestation{
|
||||
AttestingIndices: []uint64{1, 2},
|
||||
@@ -156,6 +178,12 @@ func Test_slashableAttestationCheck_GenesisEpoch(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mocks.slasherClient.EXPECT().IsSlashableAttestation(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.AttesterSlashingResponse{}, nil /*err*/)
|
||||
|
||||
fakePubkey := bytesutil.ToBytes48([]byte("test"))
|
||||
err := validator.slashableAttestationCheck(ctx, att, fakePubkey, [32]byte{})
|
||||
require.NoError(t, err, "Expected allowed attestation not to throw error")
|
||||
|
||||
@@ -36,7 +36,6 @@ type Validator interface {
|
||||
WaitForChainStart(ctx context.Context) error
|
||||
WaitForSync(ctx context.Context) error
|
||||
WaitForActivation(ctx context.Context, accountsChangedChan chan [][48]byte) error
|
||||
SlasherReady(ctx context.Context) error
|
||||
CanonicalHeadSlot(ctx context.Context) (types.Slot, error)
|
||||
NextSlot() <-chan types.Slot
|
||||
SlotDeadline(slot types.Slot) time.Time
|
||||
|
||||
@@ -119,20 +119,13 @@ func (v *validator) proposeBlockPhase0(ctx context.Context, slot types.Slot, pub
|
||||
return
|
||||
}
|
||||
|
||||
if err := v.preBlockSignValidations(ctx, pubKey, wrapper.WrappedPhase0BeaconBlock(b), signingRoot); err != nil {
|
||||
if err := v.slashableProposalCheck(ctx, pubKey, wrapper.WrappedPhase0SignedBeaconBlock(blk), signingRoot); err != nil {
|
||||
log.WithFields(
|
||||
blockLogFields(pubKey, wrapper.WrappedPhase0BeaconBlock(b), nil),
|
||||
).WithError(err).Error("Failed block slashing protection check")
|
||||
return
|
||||
}
|
||||
|
||||
if err := v.postBlockSignUpdate(ctx, pubKey, wrapper.WrappedPhase0SignedBeaconBlock(blk), signingRoot); err != nil {
|
||||
log.WithFields(
|
||||
blockLogFields(pubKey, wrapper.WrappedPhase0BeaconBlock(b), sig),
|
||||
).WithError(err).Error("Failed block slashing protection check")
|
||||
return
|
||||
}
|
||||
|
||||
// Propose and broadcast block via beacon node
|
||||
blkResp, err := v.validatorClient.ProposeBlock(ctx, blk)
|
||||
if err != nil {
|
||||
@@ -252,16 +245,6 @@ func (v *validator) proposeBlockAltair(ctx context.Context, slot types.Slot, pub
|
||||
return
|
||||
}
|
||||
|
||||
if err := v.preBlockSignValidations(ctx, pubKey, wb, signingRoot); err != nil {
|
||||
log.WithFields(
|
||||
blockLogFields(pubKey, wb, nil),
|
||||
).WithError(err).Error("Failed block slashing protection check")
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wsb, err := wrapper.WrappedAltairSignedBeaconBlock(blk)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to wrap signed block")
|
||||
@@ -270,9 +253,10 @@ func (v *validator) proposeBlockAltair(ctx context.Context, slot types.Slot, pub
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := v.postBlockSignUpdate(ctx, pubKey, wsb, signingRoot); err != nil {
|
||||
|
||||
if err := v.slashableProposalCheck(ctx, pubKey, wsb, signingRoot); err != nil {
|
||||
log.WithFields(
|
||||
blockLogFields(pubKey, wb, sig),
|
||||
blockLogFields(pubKey, wb, nil),
|
||||
).WithError(err).Error("Failed block slashing protection check")
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
|
||||
@@ -5,22 +5,23 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/core/blocks"
|
||||
"github.com/prysmaticlabs/prysm/config/features"
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/block"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var failedPreBlockSignLocalErr = "attempted to sign a double proposal, block rejected by local protection"
|
||||
var failedPreBlockSignExternalErr = "attempted a double proposal, block rejected by remote slashing protection"
|
||||
var failedPostBlockSignErr = "made a double proposal, considered slashable by remote slashing protection"
|
||||
var failedBlockSignLocalErr = "attempted to sign a double proposal, block rejected by local protection"
|
||||
var failedBlockSignExternalErr = "attempted a double proposal, block rejected by remote slashing protection"
|
||||
|
||||
func (v *validator) preBlockSignValidations(
|
||||
ctx context.Context, pubKey [48]byte, blk block.BeaconBlock, signingRoot [32]byte,
|
||||
func (v *validator) slashableProposalCheck(
|
||||
ctx context.Context, pubKey [48]byte, signedBlock block.SignedBeaconBlock, signingRoot [32]byte,
|
||||
) error {
|
||||
fmtKey := fmt.Sprintf("%#x", pubKey[:])
|
||||
|
||||
prevSigningRoot, proposalAtSlotExists, err := v.db.ProposalHistoryForSlot(ctx, pubKey, blk.Slot())
|
||||
block := signedBlock.Block()
|
||||
prevSigningRoot, proposalAtSlotExists, err := v.db.ProposalHistoryForSlot(ctx, pubKey, block.Slot())
|
||||
if err != nil {
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
@@ -42,7 +43,7 @@ func (v *validator) preBlockSignValidations(
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
return errors.New(failedPreBlockSignLocalErr)
|
||||
return errors.New(failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
// Based on EIP3076, validator should refuse to sign any proposal with slot less
|
||||
@@ -56,29 +57,24 @@ func (v *validator) preBlockSignValidations(
|
||||
blk.Slot(),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *validator) postBlockSignUpdate(
|
||||
ctx context.Context,
|
||||
pubKey [48]byte,
|
||||
blk block.SignedBeaconBlock,
|
||||
signingRoot [32]byte,
|
||||
) error {
|
||||
fmtKey := fmt.Sprintf("%#x", pubKey[:])
|
||||
if features.Get().RemoteSlasherProtection && v.protector != nil {
|
||||
blockHdr, err := block.SignedBeaconBlockHeaderFromBlockInterface(blk)
|
||||
if features.Get().RemoteSlasherProtection {
|
||||
blockHdr, err := blocks.SignedBeaconBlockHeaderFromBlockInterface(signedBlock)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get block header from block")
|
||||
}
|
||||
if !v.protector.CheckBlockSafety(ctx, blockHdr) {
|
||||
slashing, err := v.slashingProtectionClient.IsSlashableBlock(ctx, blockHdr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not check if block is slashable")
|
||||
}
|
||||
if slashing != nil && len(slashing.ProposerSlashings) > 0 {
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVecSlasher.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
return errors.New(failedPostBlockSignErr)
|
||||
return errors.New(failedBlockSignExternalErr)
|
||||
}
|
||||
}
|
||||
if err := v.db.SaveProposalHistoryForSlot(ctx, pubKey, blk.Block().Slot(), signingRoot[:]); err != nil {
|
||||
if err := v.db.SaveProposalHistoryForSlot(ctx, pubKey, block.Slot(), signingRoot[:]); err != nil {
|
||||
if v.emitAccountMetrics {
|
||||
ValidatorProposeFailVec.WithLabelValues(fmtKey).Inc()
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
"github.com/prysmaticlabs/prysm/config/features"
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
@@ -13,7 +15,7 @@ import (
|
||||
mockSlasher "github.com/prysmaticlabs/prysm/validator/testing"
|
||||
)
|
||||
|
||||
func TestPreBlockSignLocalValidation_PreventsLowerThanMinProposal(t *testing.T) {
|
||||
func Test_slashableProposalCheck_PreventsLowerThanMinProposal(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
validator, _, validatorKey, finish := setup(t)
|
||||
defer finish()
|
||||
@@ -28,55 +30,64 @@ func TestPreBlockSignLocalValidation_PreventsLowerThanMinProposal(t *testing.T)
|
||||
|
||||
// We expect the same block with a slot lower than the lowest
|
||||
// signed slot to fail validation.
|
||||
block := ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot - 1,
|
||||
ProposerIndex: 0,
|
||||
block := ðpb.SignedBeaconBlock{
|
||||
Block: ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot - 1,
|
||||
ProposerIndex: 0,
|
||||
},
|
||||
Signature: params.BeaconConfig().EmptySignature[:],
|
||||
}
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKeyBytes, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{4})
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wrapper.WrappedPhase0SignedBeaconBlock(block), [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.
|
||||
block = ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot,
|
||||
ProposerIndex: 0,
|
||||
block = ðpb.SignedBeaconBlock{
|
||||
Block: ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot,
|
||||
ProposerIndex: 0,
|
||||
},
|
||||
Signature: params.BeaconConfig().EmptySignature[:],
|
||||
}
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKeyBytes, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{1})
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wrapper.WrappedPhase0SignedBeaconBlock(block), [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.
|
||||
block = ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot,
|
||||
ProposerIndex: 0,
|
||||
}
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKeyBytes, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{4})
|
||||
require.ErrorContains(t, failedPreBlockSignLocalErr, err)
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{4})
|
||||
require.ErrorContains(t, failedBlockSignLocalErr, err)
|
||||
|
||||
// We expect the same block with a slot > than the lowest
|
||||
// signed slot to pass validation.
|
||||
block = ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot + 1,
|
||||
ProposerIndex: 0,
|
||||
block = ðpb.SignedBeaconBlock{
|
||||
Block: ðpb.BeaconBlock{
|
||||
Slot: lowestSignedSlot + 1,
|
||||
ProposerIndex: 0,
|
||||
},
|
||||
Signature: params.BeaconConfig().EmptySignature[:],
|
||||
}
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKeyBytes, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{3})
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKeyBytes, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{3})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPreBlockSignLocalValidation(t *testing.T) {
|
||||
func Test_slashableProposalCheck(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: false,
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
validator, _, validatorKey, finish := setup(t)
|
||||
validator, mocks, validatorKey, finish := setup(t)
|
||||
defer finish()
|
||||
|
||||
block := ðpb.BeaconBlock{
|
||||
Slot: 10,
|
||||
ProposerIndex: 0,
|
||||
}
|
||||
block := util.HydrateSignedBeaconBlock(ðpb.SignedBeaconBlock{
|
||||
Block: ðpb.BeaconBlock{
|
||||
Slot: 10,
|
||||
ProposerIndex: 0,
|
||||
},
|
||||
Signature: params.BeaconConfig().EmptySignature[:],
|
||||
})
|
||||
|
||||
pubKeyBytes := [48]byte{}
|
||||
copy(pubKeyBytes[:], validatorKey.PublicKey().Marshal())
|
||||
|
||||
@@ -91,64 +102,63 @@ func TestPreBlockSignLocalValidation(t *testing.T) {
|
||||
pubKey := [48]byte{}
|
||||
copy(pubKey[:], validatorKey.PublicKey().Marshal())
|
||||
|
||||
mock.slasherClient.EXPECT().IsSlashableBlock(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Times(2).Return(ðpb.ProposerSlashingResponse{}, nil /*err*/)
|
||||
|
||||
// We expect the same block sent out with the same root should not be slasahble.
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKey, wrapper.WrappedPhase0BeaconBlock(block), dummySigningRoot)
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), dummySigningRoot)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We expect the same block sent out with a different signing root should be slasahble.
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKey, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{2})
|
||||
require.ErrorContains(t, failedPreBlockSignLocalErr, err)
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{2})
|
||||
require.ErrorContains(t, failedBlockSignLocalErr, err)
|
||||
|
||||
// We save a proposal at slot 11 with a nil signing root.
|
||||
block.Slot = 11
|
||||
err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, block.Slot, nil)
|
||||
block.Block.Slot = 11
|
||||
err = validator.db.SaveProposalHistoryForSlot(ctx, pubKeyBytes, block.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.preBlockSignValidations(context.Background(), pubKey, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{2})
|
||||
require.ErrorContains(t, failedPreBlockSignLocalErr, err)
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), [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.
|
||||
block.Slot = 9
|
||||
err = validator.preBlockSignValidations(context.Background(), pubKey, wrapper.WrappedPhase0BeaconBlock(block), [32]byte{3})
|
||||
block.Block.Slot = 9
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{3})
|
||||
require.NoError(t, err, "Expected allowed block not to throw error")
|
||||
}
|
||||
|
||||
func TestPreBlockSignValidation(t *testing.T) {
|
||||
validator, _, validatorKey, finish := setup(t)
|
||||
func Test_slashableProposalCheck_RemoteProtection(t *testing.T) {
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
validator, m, validatorKey, finish := setup(t)
|
||||
defer finish()
|
||||
pubKey := [48]byte{}
|
||||
copy(pubKey[:], validatorKey.PublicKey().Marshal())
|
||||
|
||||
block := util.NewBeaconBlock()
|
||||
block.Block.Slot = 10
|
||||
mockProtector := &mockSlasher.MockProtector{AllowBlock: false}
|
||||
validator.protector = mockProtector
|
||||
mockProtector.AllowBlock = true
|
||||
err := validator.preBlockSignValidations(context.Background(), pubKey, wrapper.WrappedPhase0BeaconBlock(block.Block), [32]byte{2})
|
||||
require.NoError(t, err, "Expected allowed block not to throw error")
|
||||
}
|
||||
|
||||
func TestPostBlockSignUpdate(t *testing.T) {
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
validator, _, validatorKey, finish := setup(t)
|
||||
defer finish()
|
||||
pubKey := [48]byte{}
|
||||
copy(pubKey[:], validatorKey.PublicKey().Marshal())
|
||||
emptyBlock := util.NewBeaconBlock()
|
||||
emptyBlock.Block.Slot = 10
|
||||
emptyBlock.Block.ProposerIndex = 0
|
||||
mockProtector := &mockSlasher.MockProtector{AllowBlock: false}
|
||||
validator.protector = mockProtector
|
||||
err := validator.postBlockSignUpdate(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(emptyBlock), [32]byte{})
|
||||
require.ErrorContains(t, failedPostBlockSignErr, err, "Expected error when post signature update is detected as slashable")
|
||||
mockProtector.AllowBlock = true
|
||||
err = validator.postBlockSignUpdate(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(emptyBlock), [32]byte{})
|
||||
m.nodeClient.EXPECT().IsSlashableBlock(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.ProposerSlashingResponse{ProposerSlashings: []*ethpb.ProposerSlashing{{}}}, nil /*err*/)
|
||||
|
||||
err := validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{2})
|
||||
require.ErrorContains(t, failedBlockSignExternalErr, err)
|
||||
|
||||
m.slasherClient.EXPECT().IsSlashableBlock(
|
||||
gomock.Any(), // ctx
|
||||
gomock.Any(),
|
||||
).Return(ðpb.ProposerSlashingResponse{}, nil /*err*/)
|
||||
|
||||
err = validator.slashableProposalCheck(context.Background(), pubKey, wrapper.WrappedPhase0SignedBeaconBlock(block), [32]byte{2})
|
||||
require.NoError(t, err, "Expected allowed block not to throw error")
|
||||
}
|
||||
|
||||
@@ -296,10 +296,10 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) {
|
||||
).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), slot, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
|
||||
validator.ProposeBlock(context.Background(), slot, pubKey)
|
||||
require.LogsContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsContain(t, hook, failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
func TestProposeBlockAltair_BlocksDoubleProposal(t *testing.T) {
|
||||
@@ -356,10 +356,10 @@ func TestProposeBlockAltair_BlocksDoubleProposal(t *testing.T) {
|
||||
).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), slot, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
|
||||
validator.ProposeBlock(context.Background(), slot, pubKey)
|
||||
require.LogsContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsContain(t, hook, failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) {
|
||||
@@ -408,10 +408,10 @@ func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) {
|
||||
).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), farFuture, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
|
||||
validator.ProposeBlock(context.Background(), farFuture, pubKey)
|
||||
require.LogsContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsContain(t, hook, failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
func TestProposeBlock_AllowsPastProposals(t *testing.T) {
|
||||
@@ -449,7 +449,7 @@ func TestProposeBlock_AllowsPastProposals(t *testing.T) {
|
||||
).Times(2).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), farAhead, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
|
||||
past := params.BeaconConfig().SlotsPerEpoch.Mul(uint64(params.BeaconConfig().WeakSubjectivityPeriod - 400))
|
||||
blk2 := util.NewBeaconBlock()
|
||||
@@ -459,7 +459,7 @@ func TestProposeBlock_AllowsPastProposals(t *testing.T) {
|
||||
gomock.Any(),
|
||||
).Return(blk2.Block, nil /*err*/)
|
||||
validator.ProposeBlock(context.Background(), past, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
func TestProposeBlock_AllowsSameEpoch(t *testing.T) {
|
||||
@@ -497,7 +497,7 @@ func TestProposeBlock_AllowsSameEpoch(t *testing.T) {
|
||||
).Times(2).Return(ðpb.ProposeResponse{BlockRoot: make([]byte, 32)}, nil /*error*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), farAhead, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
|
||||
blk2 := util.NewBeaconBlock()
|
||||
blk2.Block.Slot = farAhead - 4
|
||||
@@ -507,7 +507,7 @@ func TestProposeBlock_AllowsSameEpoch(t *testing.T) {
|
||||
).Return(blk2.Block, nil /*err*/)
|
||||
|
||||
validator.ProposeBlock(context.Background(), farAhead-4, pubKey)
|
||||
require.LogsDoNotContain(t, hook, failedPreBlockSignLocalErr)
|
||||
require.LogsDoNotContain(t, hook, failedBlockSignLocalErr)
|
||||
}
|
||||
|
||||
func TestProposeBlock_BroadcastsBlock(t *testing.T) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
"github.com/prysmaticlabs/prysm/beacon-chain/core"
|
||||
"github.com/prysmaticlabs/prysm/config/features"
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
||||
"github.com/prysmaticlabs/prysm/validator/client/iface"
|
||||
@@ -40,11 +39,6 @@ func run(ctx context.Context, v iface.Validator) {
|
||||
cleanup()
|
||||
log.Fatalf("Wallet is not ready: %v", err)
|
||||
}
|
||||
if features.Get().RemoteSlasherProtection {
|
||||
if err := v.SlasherReady(ctx); err != nil {
|
||||
log.Fatalf("Slasher is not ready: %v", err)
|
||||
}
|
||||
}
|
||||
ticker := time.NewTicker(backOffPeriod)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
types "github.com/prysmaticlabs/eth2-types"
|
||||
"github.com/prysmaticlabs/prysm/async/event"
|
||||
"github.com/prysmaticlabs/prysm/config/features"
|
||||
"github.com/prysmaticlabs/prysm/testing/assert"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
"github.com/prysmaticlabs/prysm/validator/client/iface"
|
||||
@@ -62,17 +61,6 @@ func TestCancelledContext_WaitsForActivation(t *testing.T) {
|
||||
assert.Equal(t, 1, v.WaitForActivationCalled, "Expected WaitForActivation() to be called")
|
||||
}
|
||||
|
||||
func TestCancelledContext_ChecksSlasherReady(t *testing.T) {
|
||||
v := &testutil.FakeValidator{Keymanager: &mockKeymanager{accountsChangedFeed: &event.Feed{}}}
|
||||
cfg := &features.Flags{
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(cfg)
|
||||
defer reset()
|
||||
run(cancelledContext(), v)
|
||||
assert.Equal(t, true, v.SlasherReadyCalled, "Expected SlasherReady() to be called")
|
||||
}
|
||||
|
||||
func TestUpdateDuties_NextSlot(t *testing.T) {
|
||||
v := &testutil.FakeValidator{Keymanager: &mockKeymanager{accountsChangedFeed: &event.Feed{}}}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/prysmaticlabs/prysm/validator/graffiti"
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
|
||||
slashingiface "github.com/prysmaticlabs/prysm/validator/slashing-protection/iface"
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
@@ -64,7 +63,6 @@ type ValidatorService struct {
|
||||
withCert string
|
||||
endpoint string
|
||||
validator iface.Validator
|
||||
protector slashingiface.Protector
|
||||
ctx context.Context
|
||||
keyManager keymanager.IKeymanager
|
||||
grpcHeaders []string
|
||||
@@ -82,7 +80,6 @@ type Config struct {
|
||||
GrpcRetriesFlag uint
|
||||
GrpcRetryDelay time.Duration
|
||||
GrpcMaxCallRecvMsgSizeFlag int
|
||||
Protector slashingiface.Protector
|
||||
Endpoint string
|
||||
Validator iface.Validator
|
||||
ValDB db.Database
|
||||
@@ -112,7 +109,6 @@ func NewValidatorService(ctx context.Context, cfg *Config) (*ValidatorService, e
|
||||
grpcRetries: cfg.GrpcRetriesFlag,
|
||||
grpcRetryDelay: cfg.GrpcRetryDelay,
|
||||
grpcHeaders: strings.Split(cfg.GrpcHeadersFlag, ","),
|
||||
protector: cfg.Protector,
|
||||
validator: cfg.Validator,
|
||||
db: cfg.ValDB,
|
||||
walletInitializedFeed: cfg.WalletInitializedFeed,
|
||||
@@ -188,7 +184,6 @@ func (v *ValidatorService) Start() {
|
||||
attLogs: make(map[[32]byte]*attSubmitted),
|
||||
domainDataCache: cache,
|
||||
aggregatedSlotCommitteeIDCache: aggregatedSlotCommitteeIDCache,
|
||||
protector: v.protector,
|
||||
voteStats: voteStats{startEpoch: types.Epoch(^uint64(0))},
|
||||
useWeb: v.useWeb,
|
||||
walletInitializedFeed: v.walletInitializedFeed,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/bazelbuild/rules_go/go/tools/bazel"
|
||||
"github.com/prysmaticlabs/prysm/config/features"
|
||||
"github.com/prysmaticlabs/prysm/io/file"
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/wrapper"
|
||||
@@ -76,12 +75,6 @@ func setupEIP3076SpecTests(t *testing.T) []*eip3076TestCase {
|
||||
}
|
||||
|
||||
func TestEIP3076SpecTests(t *testing.T) {
|
||||
config := &features.Flags{
|
||||
RemoteSlasherProtection: true,
|
||||
}
|
||||
reset := features.InitWithReset(config)
|
||||
defer reset()
|
||||
|
||||
testCases := setupEIP3076SpecTests(t)
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
@@ -130,22 +123,12 @@ func TestEIP3076SpecTests(t *testing.T) {
|
||||
copy(signingRoot[:], signingRootBytes)
|
||||
}
|
||||
|
||||
err = validator.preBlockSignValidations(context.Background(), pk, wrapper.WrappedPhase0BeaconBlock(b.Block), signingRoot)
|
||||
err = validator.slashableProposalCheck(context.Background(), pk, wrapper.WrappedPhase0SignedBeaconBlock(b), signingRoot)
|
||||
if sb.ShouldSucceed {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.NotEqual(t, nil, err, "pre validation should have failed for block")
|
||||
}
|
||||
|
||||
// Only proceed post update if pre validation did not error.
|
||||
if err == nil {
|
||||
err = validator.postBlockSignUpdate(context.Background(), pk, wrapper.WrappedPhase0SignedBeaconBlock(b), signingRoot)
|
||||
if sb.ShouldSucceed {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.NotEqual(t, nil, err, "post validation should have failed for block")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This loops through a list of attestation signings to attempt after importing the interchange data above.
|
||||
|
||||
@@ -36,20 +36,17 @@ import (
|
||||
"github.com/prysmaticlabs/prysm/validator/db/kv"
|
||||
"github.com/prysmaticlabs/prysm/validator/graffiti"
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
||||
slashingiface "github.com/prysmaticlabs/prysm/validator/slashing-protection/iface"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.opencensus.io/trace"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// reconnectPeriod is the frequency that we try to restart our
|
||||
// slasher connection when the slasher client connection is not ready.
|
||||
var reconnectPeriod = 5 * time.Second
|
||||
|
||||
// keyFetchPeriod is the frequency that we try to refetch validating keys
|
||||
// in case no keys were fetched previously.
|
||||
var keyRefetchPeriod = 30 * time.Second
|
||||
var (
|
||||
keyRefetchPeriod = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
msgCouldNotFetchKeys = "could not fetch validating keys"
|
||||
@@ -81,7 +78,7 @@ type validator struct {
|
||||
keyManager keymanager.IKeymanager
|
||||
beaconClient ethpb.BeaconChainClient
|
||||
validatorClient ethpb.BeaconNodeValidatorClient
|
||||
protector slashingiface.Protector
|
||||
slashingProtectionClient ethpb.SlasherClient
|
||||
db vdb.Database
|
||||
graffiti []byte
|
||||
voteStats voteStats
|
||||
@@ -224,6 +221,8 @@ func (v *validator) WaitForSync(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
// SlasherReady checks if slasher that was configured as external protection
|
||||
// is reachable.
|
||||
func (v *validator) SlasherReady(ctx context.Context) error {
|
||||
@@ -255,6 +254,7 @@ func (v *validator) SlasherReady(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
>>>>>>> develop
|
||||
// ReceiveBlocks starts a gRPC client stream listener to obtain
|
||||
// blocks from the beacon node. Upon receiving a block, the service
|
||||
// broadcasts it to a feed for other usages to subscribe to.
|
||||
|
||||
@@ -52,8 +52,6 @@ go_library(
|
||||
"//validator/keymanager:go_default_library",
|
||||
"//validator/keymanager/imported:go_default_library",
|
||||
"//validator/rpc:go_default_library",
|
||||
"//validator/slashing-protection:go_default_library",
|
||||
"//validator/slashing-protection/iface:go_default_library",
|
||||
"//validator/web:go_default_library",
|
||||
"@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
|
||||
@@ -40,8 +40,6 @@ import (
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
||||
"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
|
||||
"github.com/prysmaticlabs/prysm/validator/rpc"
|
||||
slashingprotection "github.com/prysmaticlabs/prysm/validator/slashing-protection"
|
||||
"github.com/prysmaticlabs/prysm/validator/slashing-protection/iface"
|
||||
"github.com/prysmaticlabs/prysm/validator/web"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -251,9 +249,6 @@ func (c *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := c.registerSlasherService(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.registerValidatorService(keyManager); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,9 +333,6 @@ func (c *ValidatorClient) initializeForWeb(cliCtx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := c.registerSlasherService(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.registerValidatorService(keyManager); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -391,12 +383,6 @@ func (c *ValidatorClient) registerValidatorService(
|
||||
maxCallRecvMsgSize := c.cliCtx.Int(cmd.GrpcMaxCallRecvMsgSizeFlag.Name)
|
||||
grpcRetries := c.cliCtx.Uint(flags.GrpcRetriesFlag.Name)
|
||||
grpcRetryDelay := c.cliCtx.Duration(flags.GrpcRetryDelayFlag.Name)
|
||||
var sp *slashingprotection.Service
|
||||
var protector iface.Protector
|
||||
if err := c.services.FetchService(&sp); err == nil {
|
||||
protector = sp
|
||||
}
|
||||
|
||||
gStruct := &g.Graffiti{}
|
||||
var err error
|
||||
if c.cliCtx.IsSet(flags.GraffitiFileFlag.Name) {
|
||||
@@ -419,7 +405,6 @@ func (c *ValidatorClient) registerValidatorService(
|
||||
GrpcRetriesFlag: grpcRetries,
|
||||
GrpcRetryDelay: grpcRetryDelay,
|
||||
GrpcHeadersFlag: c.cliCtx.String(flags.GrpcHeadersFlag.Name),
|
||||
Protector: protector,
|
||||
ValDB: c.db,
|
||||
UseWeb: c.cliCtx.Bool(flags.EnableWebFlag.Name),
|
||||
WalletInitializedFeed: c.walletInitialized,
|
||||
@@ -432,32 +417,6 @@ func (c *ValidatorClient) registerValidatorService(
|
||||
|
||||
return c.services.RegisterService(v)
|
||||
}
|
||||
func (c *ValidatorClient) registerSlasherService() error {
|
||||
if !features.Get().RemoteSlasherProtection {
|
||||
return nil
|
||||
}
|
||||
endpoint := c.cliCtx.String(flags.SlasherRPCProviderFlag.Name)
|
||||
if endpoint == "" {
|
||||
return errors.New("external slasher feature flag is set but no slasher endpoint is configured")
|
||||
|
||||
}
|
||||
cert := c.cliCtx.String(flags.SlasherCertFlag.Name)
|
||||
maxCallRecvMsgSize := c.cliCtx.Int(cmd.GrpcMaxCallRecvMsgSizeFlag.Name)
|
||||
grpcRetries := c.cliCtx.Uint(flags.GrpcRetriesFlag.Name)
|
||||
grpcRetryDelay := c.cliCtx.Duration(flags.GrpcRetryDelayFlag.Name)
|
||||
sp, err := slashingprotection.NewService(c.cliCtx.Context, &slashingprotection.Config{
|
||||
Endpoint: endpoint,
|
||||
CertFlag: cert,
|
||||
GrpcMaxCallRecvMsgSizeFlag: maxCallRecvMsgSize,
|
||||
GrpcRetriesFlag: grpcRetries,
|
||||
GrpcRetryDelay: grpcRetryDelay,
|
||||
GrpcHeadersFlag: c.cliCtx.String(flags.GrpcHeadersFlag.Name),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize slasher service")
|
||||
}
|
||||
return c.services.RegisterService(sp)
|
||||
}
|
||||
|
||||
func (c *ValidatorClient) registerRPCService(cliCtx *cli.Context, km keymanager.IKeymanager) error {
|
||||
var vs *client.ValidatorService
|
||||
|
||||
@@ -5,9 +5,7 @@ go_library(
|
||||
srcs = [
|
||||
"cli_export.go",
|
||||
"cli_import.go",
|
||||
"external.go",
|
||||
"log.go",
|
||||
"slasher_client.go",
|
||||
],
|
||||
importpath = "github.com/prysmaticlabs/prysm/validator/slashing-protection",
|
||||
visibility = [
|
||||
@@ -15,35 +13,21 @@ go_library(
|
||||
"//validator:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
"//api/grpc:go_default_library",
|
||||
"//cmd:go_default_library",
|
||||
"//cmd/validator/flags:go_default_library",
|
||||
"//io/file:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//validator/accounts/userprompt:go_default_library",
|
||||
"//validator/db/kv:go_default_library",
|
||||
"//validator/slashing-protection/local/standard-protection-format:go_default_library",
|
||||
"@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library",
|
||||
"@com_github_grpc_ecosystem_go_grpc_middleware//retry:go_default_library",
|
||||
"@com_github_grpc_ecosystem_go_grpc_middleware//tracing/opentracing:go_default_library",
|
||||
"@com_github_grpc_ecosystem_go_grpc_prometheus//:go_default_library",
|
||||
"@com_github_pkg_errors//:go_default_library",
|
||||
"@com_github_sirupsen_logrus//:go_default_library",
|
||||
"@com_github_urfave_cli_v2//:go_default_library",
|
||||
"@io_opencensus_go//plugin/ocgrpc:go_default_library",
|
||||
"@org_golang_google_grpc//:go_default_library",
|
||||
"@org_golang_google_grpc//connectivity:go_default_library",
|
||||
"@org_golang_google_grpc//credentials:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"cli_import_export_test.go",
|
||||
"external_test.go",
|
||||
"slasher_client_test.go",
|
||||
],
|
||||
srcs = ["cli_import_export_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//cmd:go_default_library",
|
||||
@@ -51,7 +35,6 @@ go_test(
|
||||
"//config/params:go_default_library",
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//io/file:go_default_library",
|
||||
"//proto/prysm/v1alpha1:go_default_library",
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"//validator/db/kv:go_default_library",
|
||||
@@ -59,6 +42,5 @@ go_test(
|
||||
"//validator/slashing-protection/local/standard-protection-format/format:go_default_library",
|
||||
"//validator/testing:go_default_library",
|
||||
"@com_github_urfave_cli_v2//:go_default_library",
|
||||
"@org_golang_google_grpc//metadata:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package slashingprotection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
)
|
||||
|
||||
// CheckBlockSafety for blocks before submitting them to the node.
|
||||
func (s *Service) CheckBlockSafety(ctx context.Context, blockHeader *ethpb.SignedBeaconBlockHeader) bool {
|
||||
ps, err := s.slasherClient.IsSlashableBlock(ctx, blockHeader)
|
||||
if err != nil {
|
||||
log.Errorf("External slashing block protection returned an error: %v", err)
|
||||
return false
|
||||
}
|
||||
if ps != nil && len(ps.ProposerSlashings) != 0 {
|
||||
log.Warn("External slashing proposal protection found the block to be slashable")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CheckAttestationSafety for attestations before submitting them to the node.
|
||||
func (s *Service) CheckAttestationSafety(ctx context.Context, attestation *ethpb.IndexedAttestation) bool {
|
||||
as, err := s.slasherClient.IsSlashableAttestation(ctx, attestation)
|
||||
if err != nil {
|
||||
log.Errorf("External slashing attestation protection returned an error: %v", err)
|
||||
return false
|
||||
}
|
||||
if as != nil && len(as.AttesterSlashings) != 0 {
|
||||
log.Warnf("External slashing attestation protection found the attestation to be slashable: %v", as)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package slashingprotection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
||||
eth "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"github.com/prysmaticlabs/prysm/testing/assert"
|
||||
mockSlasher "github.com/prysmaticlabs/prysm/validator/testing"
|
||||
)
|
||||
|
||||
func TestService_VerifyAttestation(t *testing.T) {
|
||||
s := &Service{slasherClient: mockSlasher.MockSlasher{SlashAttestation: true}}
|
||||
att := ð.IndexedAttestation{
|
||||
AttestingIndices: []uint64{1, 2},
|
||||
Data: ð.AttestationData{
|
||||
Slot: 5,
|
||||
CommitteeIndex: 2,
|
||||
BeaconBlockRoot: []byte("great block"),
|
||||
Source: ð.Checkpoint{
|
||||
Epoch: 4,
|
||||
Root: []byte("good source"),
|
||||
},
|
||||
Target: ð.Checkpoint{
|
||||
Epoch: 10,
|
||||
Root: []byte("good target"),
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, false, s.CheckAttestationSafety(context.Background(), att), "Expected verify attestation to fail verification")
|
||||
s = &Service{slasherClient: mockSlasher.MockSlasher{SlashAttestation: false}}
|
||||
assert.Equal(t, true, s.CheckAttestationSafety(context.Background(), att), "Expected verify attestation to pass verification")
|
||||
}
|
||||
|
||||
func TestService_VerifyBlock(t *testing.T) {
|
||||
s := &Service{slasherClient: mockSlasher.MockSlasher{SlashBlock: true}}
|
||||
blk := ð.BeaconBlockHeader{
|
||||
Slot: 0,
|
||||
ProposerIndex: 0,
|
||||
ParentRoot: bytesutil.PadTo([]byte("parent"), 32),
|
||||
StateRoot: bytesutil.PadTo([]byte("state"), 32),
|
||||
BodyRoot: bytesutil.PadTo([]byte("body"), 32),
|
||||
}
|
||||
sblk := ð.SignedBeaconBlockHeader{Header: blk, Signature: params.BeaconConfig().EmptySignature[:]}
|
||||
assert.Equal(t, false, s.CheckBlockSafety(context.Background(), sblk), "Expected verify block to fail verification")
|
||||
s = &Service{slasherClient: mockSlasher.MockSlasher{SlashBlock: false}}
|
||||
assert.Equal(t, true, s.CheckBlockSafety(context.Background(), sblk), "Expected verify block to pass verification")
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
load("@prysm//tools/go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["protector.go"],
|
||||
importpath = "github.com/prysmaticlabs/prysm/validator/slashing-protection/iface",
|
||||
visibility = ["//validator:__subpackages__"],
|
||||
deps = ["//proto/prysm/v1alpha1:go_default_library"],
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package iface
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eth "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
)
|
||||
|
||||
// Protector interface defines the methods of the service that provides slashing protection.
|
||||
type Protector interface {
|
||||
CheckAttestationSafety(ctx context.Context, attestation *eth.IndexedAttestation) bool
|
||||
CheckBlockSafety(ctx context.Context, blockHeader *eth.SignedBeaconBlockHeader) bool
|
||||
Status() error
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package slashingprotection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
|
||||
grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
|
||||
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
grpcutil "github.com/prysmaticlabs/prysm/api/grpc"
|
||||
ethsl "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// Service represents a service to manage the validator
|
||||
// slashing protection.
|
||||
type Service struct {
|
||||
cfg *Config
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
conn *grpc.ClientConn
|
||||
grpcHeaders []string
|
||||
slasherClient ethsl.SlasherClient
|
||||
}
|
||||
|
||||
// Config for the validator service.
|
||||
type Config struct {
|
||||
Endpoint string
|
||||
CertFlag string
|
||||
GrpcMaxCallRecvMsgSizeFlag int
|
||||
GrpcRetriesFlag uint
|
||||
GrpcRetryDelay time.Duration
|
||||
GrpcHeadersFlag string
|
||||
}
|
||||
|
||||
// NewService creates a new validator service for the service
|
||||
// registry.
|
||||
func NewService(ctx context.Context, cfg *Config) (*Service, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
grpcHeaders: strings.Split(cfg.GrpcHeadersFlag, ","),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start the slasher protection service and grpc client.
|
||||
func (s *Service) Start() {
|
||||
if s.cfg.Endpoint != "" {
|
||||
s.slasherClient = s.startSlasherClient()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) startSlasherClient() ethsl.SlasherClient {
|
||||
var dialOpt grpc.DialOption
|
||||
|
||||
if s.cfg.CertFlag != "" {
|
||||
creds, err := credentials.NewClientTLSFromFile(s.cfg.CertFlag, "")
|
||||
if err != nil {
|
||||
log.Errorf("Could not get valid slasher credentials: %v", err)
|
||||
return nil
|
||||
}
|
||||
dialOpt = grpc.WithTransportCredentials(creds)
|
||||
} else {
|
||||
dialOpt = grpc.WithInsecure()
|
||||
log.Warn("You are using an insecure slasher gRPC connection! Please provide a certificate and key to use a secure connection.")
|
||||
}
|
||||
|
||||
s.ctx = grpcutil.AppendHeaders(s.ctx, s.grpcHeaders)
|
||||
|
||||
opts := []grpc.DialOption{
|
||||
dialOpt,
|
||||
grpc.WithDefaultCallOptions(
|
||||
grpc_retry.WithMax(s.cfg.GrpcRetriesFlag),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffLinear(s.cfg.GrpcRetryDelay)),
|
||||
),
|
||||
grpc.WithStatsHandler(&ocgrpc.ClientHandler{}),
|
||||
grpc.WithStreamInterceptor(middleware.ChainStreamClient(
|
||||
grpc_opentracing.StreamClientInterceptor(),
|
||||
grpc_prometheus.StreamClientInterceptor,
|
||||
grpc_retry.StreamClientInterceptor(),
|
||||
)),
|
||||
grpc.WithUnaryInterceptor(middleware.ChainUnaryClient(
|
||||
grpc_opentracing.UnaryClientInterceptor(),
|
||||
grpc_prometheus.UnaryClientInterceptor,
|
||||
grpc_retry.UnaryClientInterceptor(),
|
||||
grpcutil.LogRequests,
|
||||
)),
|
||||
}
|
||||
conn, err := grpc.DialContext(s.ctx, s.cfg.Endpoint, opts...)
|
||||
if err != nil {
|
||||
log.Errorf("Could not dial slasher endpoint: %s, %v", s.cfg.Endpoint, err)
|
||||
return nil
|
||||
}
|
||||
log.Debug("Successfully started slasher gRPC connection")
|
||||
s.conn = conn
|
||||
return ethsl.NewSlasherClient(s.conn)
|
||||
|
||||
}
|
||||
|
||||
// Stop the validator service.
|
||||
func (s *Service) Stop() error {
|
||||
s.cancel()
|
||||
log.Info("Stopping slashing protection service")
|
||||
if s.conn != nil {
|
||||
return s.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status checks if the connection to slasher server is ready,
|
||||
// returns error otherwise.
|
||||
func (s *Service) Status() error {
|
||||
if s.conn == nil {
|
||||
return errors.New("no connection to slasher RPC")
|
||||
}
|
||||
if s.conn.GetState() != connectivity.Ready {
|
||||
return fmt.Errorf("can`t connect to slasher server at: %v connection status: %v ", s.cfg.Endpoint, s.conn.GetState())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package slashingprotection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/testing/assert"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestGrpcHeaders(t *testing.T) {
|
||||
s := &Service{
|
||||
cfg: &Config{},
|
||||
ctx: context.Background(),
|
||||
grpcHeaders: []string{"first=value1", "second=value2"},
|
||||
}
|
||||
s.startSlasherClient()
|
||||
md, _ := metadata.FromOutgoingContext(s.ctx)
|
||||
require.Equal(t, 2, md.Len(), "MetadataV0 contains wrong number of values")
|
||||
assert.Equal(t, "value1", md.Get("first")[0])
|
||||
assert.Equal(t, "value2", md.Get("second")[0])
|
||||
}
|
||||
Reference in New Issue
Block a user