Skip to content

Commit c658104

Browse files
nkryuchkoviurii-ssvy0sher
authored
protocol: majority fork protection (#2358)
* majority fork protection * fix linter * fix log * fix returned value * fix log text * fix checkConsensusDataSameToOwn logic for aggregator * only check source/target attestation epoch * value checker interface * create a type for SlashingProtectionData * rename ValueCheckF * add a unit test * update comment about spec decision * spec alignment * code review * differ * differ * differ * fix build issue * differ * fix issues after merging * check target root (#2545) * 2358: iurii refactor (#2551) * 2358: iurii refactor * clarify what valCheck is * fix linter * fix spec tests * cleanup * fix marshaling spectest * cleanup ValueChecker marshaling leftovers --------- Co-authored-by: Nikita Kryuchkov <[email protected]> * code review comments * differ * update checks as per SIP --------- Co-authored-by: iurii-ssv <[email protected]> Co-authored-by: rehs0y <[email protected]>
1 parent 5e9a5ec commit c658104

23 files changed

+417
-237
lines changed

operator/validator/controller.go

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,11 +1030,10 @@ func SetupCommitteeRunners(
10301030
ctx context.Context,
10311031
options *validator.Options,
10321032
) validator.CommitteeRunnerFunc {
1033-
buildController := func(role spectypes.RunnerRole, valueCheckF specqbft.ProposedValueCheckF) *qbftcontroller.Controller {
1033+
buildController := func(role spectypes.RunnerRole) *qbftcontroller.Controller {
10341034
config := &qbft.Config{
10351035
BeaconSigner: options.Signer,
10361036
Domain: options.NetworkConfig.DomainType,
1037-
ValueCheckF: valueCheckF,
10381037
ProposerF: func(state *specqbft.State, round specqbft.Round) spectypes.OperatorID {
10391038
leader := qbft.RoundRobinProposer(state, round)
10401039
return leader
@@ -1055,18 +1054,15 @@ func SetupCommitteeRunners(
10551054
attestingValidators []phase0.BLSPubKey,
10561055
dutyGuard runner.CommitteeDutyGuard,
10571056
) (*runner.CommitteeRunner, error) {
1058-
// Create a committee runner.
1059-
epoch := options.NetworkConfig.EstimatedEpochAtSlot(slot)
1060-
valCheck := ssv.BeaconVoteValueCheckF(options.Signer, slot, attestingValidators, epoch)
10611057
crunner, err := runner.NewCommitteeRunner(
10621058
options.NetworkConfig,
10631059
shares,
1064-
buildController(spectypes.RoleCommittee, valCheck),
1060+
attestingValidators,
1061+
buildController(spectypes.RoleCommittee),
10651062
options.Beacon,
10661063
options.Network,
10671064
options.Signer,
10681065
options.OperatorSigner,
1069-
valCheck,
10701066
dutyGuard,
10711067
options.DoppelgangerHandler,
10721068
)
@@ -1095,11 +1091,10 @@ func SetupRunners(
10951091
spectypes.RoleVoluntaryExit,
10961092
}
10971093

1098-
buildController := func(role spectypes.RunnerRole, valueCheckF specqbft.ProposedValueCheckF) *qbftcontroller.Controller {
1094+
buildController := func(role spectypes.RunnerRole) *qbftcontroller.Controller {
10991095
config := &qbft.Config{
11001096
BeaconSigner: options.Signer,
11011097
Domain: options.NetworkConfig.DomainType,
1102-
ValueCheckF: nil, // is set per role type
11031098
ProposerF: func(state *specqbft.State, round specqbft.Round) spectypes.OperatorID {
11041099
leader := qbft.RoundRobinProposer(state, round)
11051100
return leader
@@ -1108,7 +1103,6 @@ func SetupRunners(
11081103
Timer: roundtimer.New(ctx, options.NetworkConfig.Beacon, role, nil),
11091104
CutOffRound: roundtimer.CutOffRound,
11101105
}
1111-
config.ValueCheckF = valueCheckF
11121106

11131107
identifier := spectypes.NewMsgID(options.NetworkConfig.DomainType, share.ValidatorPubKey[:], role)
11141108
qbftCtrl := qbftcontroller.NewController(identifier[:], operator, config, options.OperatorSigner, options.FullNode)
@@ -1123,17 +1117,17 @@ func SetupRunners(
11231117
for _, role := range runnersType {
11241118
switch role {
11251119
case spectypes.RoleProposer:
1126-
proposedValueCheck := ssv.ProposerValueCheckF(options.Signer, options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex, phase0.BLSPubKey(share.SharePubKey))
1127-
qbftCtrl := buildController(spectypes.RoleProposer, proposedValueCheck)
1120+
proposedValueCheck := ssv.NewProposerChecker(options.Signer, options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex, phase0.BLSPubKey(share.SharePubKey))
1121+
qbftCtrl := buildController(spectypes.RoleProposer)
11281122
runners[role], err = runner.NewProposerRunner(logger, options.NetworkConfig, shareMap, qbftCtrl, options.Beacon, options.Network, options.Signer, options.OperatorSigner, options.DoppelgangerHandler, proposedValueCheck, 0, options.Graffiti, options.ProposerDelay)
11291123
case spectypes.RoleAggregator:
1130-
aggregatorValueCheckF := ssv.AggregatorValueCheckF(options.Signer, options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex)
1131-
qbftCtrl := buildController(spectypes.RoleAggregator, aggregatorValueCheckF)
1132-
runners[role], err = runner.NewAggregatorRunner(options.NetworkConfig, shareMap, qbftCtrl, options.Beacon, options.Network, options.Signer, options.OperatorSigner, aggregatorValueCheckF, 0)
1124+
aggregatorValueChecker := ssv.NewAggregatorChecker(options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex)
1125+
qbftCtrl := buildController(spectypes.RoleAggregator)
1126+
runners[role], err = runner.NewAggregatorRunner(options.NetworkConfig, shareMap, qbftCtrl, options.Beacon, options.Network, options.Signer, options.OperatorSigner, aggregatorValueChecker, 0)
11331127
case spectypes.RoleSyncCommitteeContribution:
1134-
syncCommitteeContributionValueCheckF := ssv.SyncCommitteeContributionValueCheckF(options.Signer, options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex)
1135-
qbftCtrl := buildController(spectypes.RoleSyncCommitteeContribution, syncCommitteeContributionValueCheckF)
1136-
runners[role], err = runner.NewSyncCommitteeAggregatorRunner(options.NetworkConfig, shareMap, qbftCtrl, options.Beacon, options.Network, options.Signer, options.OperatorSigner, syncCommitteeContributionValueCheckF, 0)
1128+
syncCommitteeContributionValueChecker := ssv.NewSyncCommitteeContributionChecker(options.NetworkConfig.Beacon, share.ValidatorPubKey, share.ValidatorIndex)
1129+
qbftCtrl := buildController(spectypes.RoleSyncCommitteeContribution)
1130+
runners[role], err = runner.NewSyncCommitteeAggregatorRunner(options.NetworkConfig, shareMap, qbftCtrl, options.Beacon, options.Network, options.Signer, options.OperatorSigner, syncCommitteeContributionValueChecker, 0)
11371131
case spectypes.RoleValidatorRegistration:
11381132
runners[role], err = runner.NewValidatorRegistrationRunner(options.NetworkConfig, shareMap, options.Beacon, options.Network, options.Signer, options.OperatorSigner, validatorRegistrationSubmitter, validatorStore, options.GasLimit)
11391133
case spectypes.RoleVoluntaryExit:

protocol/v2/qbft/config.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import (
44
specqbft "github.com/ssvlabs/ssv-spec/qbft"
55
spectypes "github.com/ssvlabs/ssv-spec/types"
66

7-
"github.com/ssvlabs/ssv/protocol/v2/qbft/roundtimer"
87
"github.com/ssvlabs/ssv/ssvsigner/ekm"
9-
)
108

11-
var CutOffRound specqbft.Round = specqbft.Round(specqbft.CutoffRound)
9+
"github.com/ssvlabs/ssv/protocol/v2/qbft/roundtimer"
10+
)
1211

1312
type signing interface {
1413
// GetShareSigner returns a BeaconSigner instance
@@ -19,8 +18,6 @@ type signing interface {
1918

2019
type IConfig interface {
2120
signing
22-
// GetValueCheckF returns value check function
23-
GetValueCheckF() specqbft.ProposedValueCheckF
2421
// GetProposerF returns func used to calculate proposer
2522
GetProposerF() specqbft.ProposerF
2623
// GetNetwork returns a p2p Network instance
@@ -34,7 +31,6 @@ type IConfig interface {
3431
type Config struct {
3532
BeaconSigner ekm.BeaconSigner
3633
Domain spectypes.DomainType
37-
ValueCheckF specqbft.ProposedValueCheckF
3834
ProposerF specqbft.ProposerF
3935
Network specqbft.Network
4036
Timer roundtimer.Timer
@@ -51,11 +47,6 @@ func (c *Config) GetSignatureDomainType() spectypes.DomainType {
5147
return c.Domain
5248
}
5349

54-
// GetValueCheckF returns value check instance
55-
func (c *Config) GetValueCheckF() specqbft.ProposedValueCheckF {
56-
return c.ValueCheckF
57-
}
58-
5950
// GetProposerF returns func used to calculate proposer
6051
func (c *Config) GetProposerF() specqbft.ProposerF {
6152
return c.ProposerF

protocol/v2/qbft/controller/controller.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/ssvlabs/ssv/protocol/v2/qbft"
2222
"github.com/ssvlabs/ssv/protocol/v2/qbft/instance"
2323
qbftstorage "github.com/ssvlabs/ssv/protocol/v2/qbft/storage"
24+
"github.com/ssvlabs/ssv/protocol/v2/ssv"
2425
ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types"
2526
)
2627

@@ -60,13 +61,19 @@ func NewController(
6061
}
6162

6263
// StartNewInstance will start a new QBFT instance, if can't will return error
63-
func (c *Controller) StartNewInstance(ctx context.Context, logger *zap.Logger, height specqbft.Height, value []byte) error {
64+
func (c *Controller) StartNewInstance(
65+
ctx context.Context,
66+
logger *zap.Logger,
67+
height specqbft.Height,
68+
value []byte,
69+
valueChecker ssv.ValueChecker,
70+
) error {
6471
ctx, span := tracer.Start(ctx,
6572
observability.InstrumentName(observabilityNamespace, "qbft.controller.start"),
6673
trace.WithAttributes(observability.BeaconSlotAttribute(phase0.Slot(height))))
6774
defer span.End()
6875

69-
if err := c.GetConfig().GetValueCheckF()(value); err != nil {
76+
if err := valueChecker.CheckValue(value); err != nil {
7077
return traces.Errorf(span, "value invalid: %w", err)
7178
}
7279

@@ -83,7 +90,7 @@ func (c *Controller) StartNewInstance(ctx context.Context, logger *zap.Logger, h
8390
newInstance := c.addAndStoreNewInstance()
8491

8592
span.AddEvent("start new instance")
86-
newInstance.Start(ctx, logger, value, height)
93+
newInstance.Start(ctx, logger, value, height, valueChecker)
8794
c.forceStopAllInstanceExceptCurrent()
8895

8996
span.SetStatus(codes.Ok, "")

protocol/v2/qbft/instance/instance.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/ssvlabs/ssv/observability"
1919
"github.com/ssvlabs/ssv/observability/log/fields"
2020
"github.com/ssvlabs/ssv/protocol/v2/qbft"
21+
"github.com/ssvlabs/ssv/protocol/v2/ssv"
2122
ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types"
2223
)
2324

@@ -31,8 +32,9 @@ type Instance struct {
3132
processMsgF *spectypes.ThreadSafeF
3233
startOnce sync.Once
3334

34-
forceStop bool
35-
StartValue []byte
35+
forceStop bool
36+
StartValue []byte
37+
ValueChecker ssv.ValueChecker `json:"-"`
3638

3739
metrics *metricsRecorder
3840
}
@@ -73,7 +75,13 @@ func (i *Instance) ForceStop() {
7375
}
7476

7577
// Start is an interface implementation
76-
func (i *Instance) Start(ctx context.Context, logger *zap.Logger, value []byte, height specqbft.Height) {
78+
func (i *Instance) Start(
79+
ctx context.Context,
80+
logger *zap.Logger,
81+
value []byte,
82+
height specqbft.Height,
83+
valueChecker ssv.ValueChecker,
84+
) {
7785
i.startOnce.Do(func() {
7886
_, span := tracer.Start(ctx,
7987
observability.InstrumentName(observabilityNamespace, "qbft.instance.start"),
@@ -83,6 +91,7 @@ func (i *Instance) Start(ctx context.Context, logger *zap.Logger, value []byte,
8391
i.StartValue = value
8492
i.bumpToRound(specqbft.FirstRound)
8593
i.State.Height = height
94+
i.ValueChecker = valueChecker
8695
i.metrics.Start()
8796
i.config.GetTimer().TimeoutForRound(height, specqbft.FirstRound)
8897

protocol/v2/qbft/instance/proposal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (i *Instance) isProposalJustification(
137137
round specqbft.Round,
138138
fullData []byte,
139139
) error {
140-
if err := i.config.GetValueCheckF()(fullData); err != nil {
140+
if err := i.ValueChecker.CheckValue(fullData); err != nil {
141141
return errors.Wrap(err, "proposal fullData invalid")
142142
}
143143

protocol/v2/qbft/spectest/controller_type.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,14 @@ func testBroadcastedDecided(
145145
}
146146
}
147147

148-
func runInstanceWithData(t *testing.T, logger *zap.Logger, height specqbft.Height, contr *controller.Controller, runData *spectests.RunInstanceData) error {
149-
err := contr.StartNewInstance(context.TODO(), logger, height, runData.InputValue)
148+
func runInstanceWithData(
149+
t *testing.T,
150+
logger *zap.Logger,
151+
height specqbft.Height,
152+
contr *controller.Controller,
153+
runData *spectests.RunInstanceData,
154+
) error {
155+
err := contr.StartNewInstance(context.TODO(), logger, height, runData.InputValue, qbfttesting.TestingValueChecker{})
150156
var lastErr error
151157
if err != nil {
152158
lastErr = err

protocol/v2/qbft/spectest/msg_processing_type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func RunMsgProcessing(t *testing.T, test *spectests.MsgProcessingSpecTest) {
3939
test.Pre.State.Height,
4040
signer,
4141
)
42+
pre.ValueChecker = qbfttesting.TestingValueChecker{}
4243
require.NoError(t, pre.Decode(preByts))
4344

4445
preInstance := pre

protocol/v2/qbft/testing/utils.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,31 @@ import (
99
"github.com/ssvlabs/ssv-spec/types/testingutils"
1010
"go.uber.org/zap"
1111

12+
"github.com/ssvlabs/ssv/ssvsigner/ekm"
13+
1214
"github.com/ssvlabs/ssv/protocol/v2/qbft"
1315
"github.com/ssvlabs/ssv/protocol/v2/qbft/controller"
1416
"github.com/ssvlabs/ssv/protocol/v2/qbft/roundtimer"
15-
"github.com/ssvlabs/ssv/ssvsigner/ekm"
1617
)
1718

19+
type TestingValueChecker struct{}
20+
21+
func (TestingValueChecker) CheckValue(data []byte) error {
22+
if bytes.Equal(data, TestingInvalidValueCheck) {
23+
return errors.New("invalid value")
24+
}
25+
26+
// as a base validation we do not accept nil values
27+
if len(data) == 0 {
28+
return errors.New("invalid value")
29+
}
30+
return nil
31+
}
32+
1833
var TestingConfig = func(logger *zap.Logger, keySet *testingutils.TestKeySet) *qbft.Config {
1934
return &qbft.Config{
2035
BeaconSigner: ekm.NewTestingKeyManagerAdapter(testingutils.NewTestingKeyManager()),
2136
Domain: testingutils.TestingSSVDomainType,
22-
ValueCheckF: func(data []byte) error {
23-
if bytes.Equal(data, TestingInvalidValueCheck) {
24-
return errors.New("invalid value")
25-
}
26-
27-
// as a base validation we do not accept nil values
28-
if len(data) == 0 {
29-
return errors.New("invalid value")
30-
}
31-
return nil
32-
},
3337
ProposerF: func(state *specqbft.State, round specqbft.Round) types.OperatorID {
3438
return 1
3539
},

protocol/v2/ssv/runner/aggregator.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/ssvlabs/ssv/observability/traces"
2929
"github.com/ssvlabs/ssv/protocol/v2/blockchain/beacon"
3030
"github.com/ssvlabs/ssv/protocol/v2/qbft/controller"
31+
"github.com/ssvlabs/ssv/protocol/v2/ssv"
3132
ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types"
3233
)
3334

@@ -38,9 +39,11 @@ type AggregatorRunner struct {
3839
network specqbft.Network
3940
signer ekm.BeaconSigner
4041
operatorSigner ssvtypes.OperatorSigner
41-
valCheck specqbft.ProposedValueCheckF
4242
measurements measurementsStore
4343

44+
// ValCheck is used to validate the qbft-value(s) proposed by other Operators.
45+
ValCheck ssv.ValueChecker
46+
4447
// IsAggregator returns true if the signature is from the input validator. The committee
4548
// count is provided as an argument rather than imported implementation from spec. Having
4649
// committee count as an argument allows cheaper computation at run time.
@@ -66,7 +69,7 @@ func NewAggregatorRunner(
6669
network specqbft.Network,
6770
signer ekm.BeaconSigner,
6871
operatorSigner ssvtypes.OperatorSigner,
69-
valCheck specqbft.ProposedValueCheckF,
72+
valCheck ssv.ValueChecker,
7073
highestDecidedSlot phase0.Slot,
7174
) (*AggregatorRunner, error) {
7275
if len(share) != 1 {
@@ -86,7 +89,7 @@ func NewAggregatorRunner(
8689
network: network,
8790
signer: signer,
8891
operatorSigner: operatorSigner,
89-
valCheck: valCheck,
92+
ValCheck: valCheck,
9093
measurements: NewMeasurementsStore(),
9194

9295
IsAggregator: isAggregatorFn(),
@@ -185,7 +188,7 @@ func (r *AggregatorRunner) ProcessPreConsensus(ctx context.Context, logger *zap.
185188
DataSSZ: byts,
186189
}
187190

188-
if err := r.BaseRunner.decide(ctx, logger, r, duty.Slot, input); err != nil {
191+
if err := r.BaseRunner.decide(ctx, logger, r, duty.Slot, input, r.ValCheck); err != nil {
189192
return traces.Errorf(span, "can't start new duty runner instance for duty: %w", err)
190193
}
191194

@@ -204,7 +207,7 @@ func (r *AggregatorRunner) ProcessConsensus(ctx context.Context, logger *zap.Log
204207
defer span.End()
205208

206209
span.AddEvent("checking if instance is decided")
207-
decided, encDecidedValue, err := r.BaseRunner.baseConsensusMsgProcessing(ctx, logger, r.GetValCheckF(), signedMsg, &spectypes.ValidatorConsensusData{})
210+
decided, encDecidedValue, err := r.BaseRunner.baseConsensusMsgProcessing(ctx, logger, r.ValCheck.CheckValue, signedMsg, &spectypes.ValidatorConsensusData{})
208211
if err != nil {
209212
return traces.Errorf(span, "failed processing consensus message: %w", err)
210213
}
@@ -487,10 +490,6 @@ func (r *AggregatorRunner) GetState() *State {
487490
return r.BaseRunner.State
488491
}
489492

490-
func (r *AggregatorRunner) GetValCheckF() specqbft.ProposedValueCheckF {
491-
return r.valCheck
492-
}
493-
494493
func (r *AggregatorRunner) GetSigner() ekm.BeaconSigner {
495494
return r.signer
496495
}

0 commit comments

Comments
 (0)