diff --git a/integration/qbft/tests/regular_test.go b/integration/qbft/tests/regular_test.go index 4b8e776e24..ebc6266e56 100644 --- a/integration/qbft/tests/regular_test.go +++ b/integration/qbft/tests/regular_test.go @@ -11,7 +11,7 @@ import ( ) func TestRegular4CommitteeScenario(t *testing.T) { - t.Skip("tests in this package are stuck") + t.Skip("to be fixed") regular := &Scenario{ Committee: 4, @@ -29,15 +29,14 @@ func TestRegular4CommitteeScenario(t *testing.T) { }, } - regular.Run(t, spectypes.BNRoleAttester) - regular.Run(t, spectypes.BNRoleAggregator) - regular.Run(t, spectypes.BNRoleProposer) - regular.Run(t, spectypes.BNRoleSyncCommittee) - regular.Run(t, spectypes.BNRoleSyncCommitteeContribution) + //regular.Run(t, spectypes.RoleCommittee) // fails because committee runner needs to be created + regular.Run(t, spectypes.RoleAggregator) + regular.Run(t, spectypes.RoleProposer) + regular.Run(t, spectypes.RoleSyncCommitteeContribution) } func TestRegular7CommitteeScenario(t *testing.T) { - t.Skip("tests in this package are stuck") + t.Skip("to be fixed") regular := &Scenario{ Committee: 7, @@ -61,15 +60,14 @@ func TestRegular7CommitteeScenario(t *testing.T) { }, } - regular.Run(t, spectypes.BNRoleAttester) - regular.Run(t, spectypes.BNRoleAggregator) - regular.Run(t, spectypes.BNRoleProposer) - regular.Run(t, spectypes.BNRoleSyncCommittee) - regular.Run(t, spectypes.BNRoleSyncCommitteeContribution) + regular.Run(t, spectypes.RoleCommittee) + regular.Run(t, spectypes.RoleAggregator) + regular.Run(t, spectypes.RoleProposer) + regular.Run(t, spectypes.RoleSyncCommitteeContribution) } func TestRegular10CommitteeScenario(t *testing.T) { - t.Skip("tests in this package are stuck") + t.Skip("to be fixed") regular := &Scenario{ Committee: 10, @@ -99,11 +97,10 @@ func TestRegular10CommitteeScenario(t *testing.T) { }, } - regular.Run(t, spectypes.BNRoleAttester) - regular.Run(t, spectypes.BNRoleAggregator) - regular.Run(t, spectypes.BNRoleProposer) - regular.Run(t, spectypes.BNRoleSyncCommittee) - regular.Run(t, spectypes.BNRoleSyncCommitteeContribution) + regular.Run(t, spectypes.RoleCommittee) + regular.Run(t, spectypes.RoleAggregator) + regular.Run(t, spectypes.RoleProposer) + regular.Run(t, spectypes.RoleSyncCommitteeContribution) } func regularValidator() func(t *testing.T, committee int, actual *protocolstorage.StoredInstance) { @@ -112,6 +109,6 @@ func regularValidator() func(t *testing.T, committee int, actual *protocolstorag require.Equal(t, int(qbft.FirstRound), int(actual.State.Round), "round not matching") require.NotNil(t, actual.DecidedMessage, "no decided message") - require.Greater(t, len(actual.DecidedMessage.Signatures), quorum(committee)-1, "no commit qourum") + require.Greater(t, len(actual.DecidedMessage.Signatures), quorum(committee)-1, "no commit quorum") } } diff --git a/integration/qbft/tests/round_change_test.go b/integration/qbft/tests/round_change_test.go index 4e6505a202..c3a4693333 100644 --- a/integration/qbft/tests/round_change_test.go +++ b/integration/qbft/tests/round_change_test.go @@ -12,7 +12,7 @@ import ( ) func TestRoundChange4CommitteeScenario(t *testing.T) { - t.Skip("tests in this package are stuck") + t.Skip("to be fixed") t.SkipNow() // TODO: test is flakey @@ -32,10 +32,9 @@ func TestRoundChange4CommitteeScenario(t *testing.T) { }, } - roundChange.Run(t, spectypes.BNRoleAttester) + roundChange.Run(t, spectypes.RoleCommittee) //roundChange.Run(t, spectypes.BNRoleAggregator) todo implement aggregator role support //roundChange.Run(t, spectypes.BNRoleProposer) todo implement proposer role support - roundChange.Run(t, spectypes.BNRoleSyncCommittee) //roundChange.Run(t, spectypes.BNRoleSyncCommitteeContribution) todo implement sync committee contribution role support } diff --git a/integration/qbft/tests/scenario_test.go b/integration/qbft/tests/scenario_test.go index 8b8729becd..b670d80d58 100644 --- a/integration/qbft/tests/scenario_test.go +++ b/integration/qbft/tests/scenario_test.go @@ -45,9 +45,7 @@ type Scenario struct { validators map[spectypes.OperatorID]*protocolvalidator.Validator } -func (s *Scenario) Run(t *testing.T, role spectypes.BeaconRole) { - t.Skip("tests in this package are stuck") - +func (s *Scenario) Run(t *testing.T, role spectypes.RunnerRole) { t.Run(role.String(), func(t *testing.T) { //preparing resources ctx, cancel := context.WithCancel(context.Background()) @@ -73,18 +71,31 @@ func (s *Scenario) Run(t *testing.T, role spectypes.BeaconRole) { duty := createDuty(getKeySet(s.Committee).ValidatorPK.Serialize(), dutyProp.Slot, dutyProp.ValidatorIndex, role) var pk spec.BLSPubKey copy(pk[:], getKeySet(s.Committee).ValidatorPK.Serialize()) - ssvMsg, err := validator.CreateDutyExecuteMsg(duty.(*spectypes.ValidatorDuty), pk[:], networkconfig.TestNetwork.DomainType()) - require.NoError(t, err) + + var ssvMsg *spectypes.SSVMessage + switch d := duty.(type) { + case *spectypes.ValidatorDuty: + msg, err := validator.CreateDutyExecuteMsg(d, pk[:], networkconfig.TestNetwork.DomainType()) + require.NoError(t, err) + + ssvMsg = msg + case *spectypes.CommitteeDuty: + msg, err := validator.CreateCommitteeDutyExecuteMsg(d, spectypes.CommitteeID(pk[16:]), networkconfig.TestNetwork.DomainType()) + require.NoError(t, err) + + ssvMsg = msg + } + dec, err := queue.DecodeSSVMessage(ssvMsg) require.NoError(t, err) - s.validators[id].Queues[spectypes.MapDutyToRunnerRole(role)].Q.Push(dec) + s.validators[id].Queues[role].Q.Push(dec) }(id, dutyProp) } //validating state of validator after invoking duties for id, validationFunc := range s.ValidationFunctions { - identifier := spectypes.NewMsgID(networkconfig.TestNetwork.DomainType(), getKeySet(s.Committee).ValidatorPK.Serialize(), spectypes.MapDutyToRunnerRole(role)) + identifier := spectypes.NewMsgID(networkconfig.TestNetwork.DomainType(), getKeySet(s.Committee).ValidatorPK.Serialize(), role) //getting stored state of validator var storedInstance *protocolstorage.StoredInstance for { @@ -164,12 +175,14 @@ func newStores(logger *zap.Logger) *qbftstorage.QBFTStores { storageMap := qbftstorage.NewStores() roles := []convert.RunnerRole{ - convert.RoleCommittee, - convert.RoleProposer, + convert.RoleAttester, convert.RoleAggregator, + convert.RoleProposer, convert.RoleSyncCommitteeContribution, + convert.RoleSyncCommittee, convert.RoleValidatorRegistration, convert.RoleVoluntaryExit, + convert.RoleCommittee, } for _, role := range roles { storageMap.Add(role, qbftstorage.New(db, role.String())) @@ -202,8 +215,9 @@ func createValidator(t *testing.T, pCtx context.Context, id spectypes.OperatorID Liquidated: false, }, }, - Beacon: NewTestingBeaconNodeWrapped(), - Signer: km, + Beacon: NewTestingBeaconNodeWrapped(), + Signer: km, + Operator: spectestingutils.TestingCommitteeMember(keySet), } options.DutyRunners = validator.SetupRunners(ctx, logger, options) diff --git a/integration/qbft/tests/setup_test.go b/integration/qbft/tests/setup_test.go index 59ae3a130a..75238885a1 100644 --- a/integration/qbft/tests/setup_test.go +++ b/integration/qbft/tests/setup_test.go @@ -1,12 +1,20 @@ package tests import ( + "context" "testing" + eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" spectypes "github.com/ssvlabs/ssv-spec/types" + spectestingutils "github.com/ssvlabs/ssv-spec/types/testingutils" "github.com/stretchr/testify/require" + "go.uber.org/zap" + "github.com/ssvlabs/ssv/logging" "github.com/ssvlabs/ssv/network" + p2pv1 "github.com/ssvlabs/ssv/network/p2p" + beaconprotocol "github.com/ssvlabs/ssv/protocol/v2/blockchain/beacon" + ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types" ) const ( @@ -27,39 +35,50 @@ func GetSharedData(t *testing.T) SharedData { //singleton B-) } func TestMain(m *testing.M) { - _, _ = maxSupportedCommittee, maxSupportedQuorum - m.Run() // TODO: fix tests in the package and remove this block - - //ctx := context.Background() - //if err := logging.SetGlobalLogger("debug", "capital", "console", nil); err != nil { - // panic(err) - //} - // - //logger := zap.L().Named("integration-tests") - // - //ln, err := p2pv1.CreateAndStartLocalNet(ctx, logger, p2pv1.LocalNetOptions{ - // Nodes: maxSupportedCommittee, - // MinConnected: maxSupportedQuorum, - // UseDiscv5: false, - //}) - //if err != nil { - // logger.Fatal("error creating and start local net", zap.Error(err)) - // return - //} - // - //nodes := map[spectypes.OperatorID]network.P2PNetwork{} - //for i := 0; i < len(ln.Nodes); i++ { - // nodes[spectypes.OperatorID(i+1)] = ln.Nodes[i] - //} - // - //sharedData = &SharedData{ - // Nodes: nodes, - //} - // - //m.Run() - // - ////teardown - //for i := 0; i < len(ln.Nodes); i++ { - // _ = ln.Nodes[i].Close() - //} + ctx := context.Background() + if err := logging.SetGlobalLogger("debug", "capital", "console", nil); err != nil { + panic(err) + } + + logger := zap.L().Named("integration-tests") + + shares := []*ssvtypes.SSVShare{ + { + Share: *spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex), + Metadata: ssvtypes.Metadata{ + BeaconMetadata: &beaconprotocol.ValidatorMetadata{ + Status: eth2apiv1.ValidatorStateActiveOngoing, + Index: spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex).ValidatorIndex, + }, + Liquidated: false, + }, + }, + } + + ln, err := p2pv1.CreateAndStartLocalNet(ctx, logger, p2pv1.LocalNetOptions{ + Nodes: maxSupportedCommittee, + MinConnected: maxSupportedQuorum, + UseDiscv5: false, + Shares: shares, + }) + if err != nil { + logger.Fatal("error creating and start local net", zap.Error(err)) + return + } + + nodes := map[spectypes.OperatorID]network.P2PNetwork{} + for i := 0; i < len(ln.Nodes); i++ { + nodes[spectypes.OperatorID(i+1)] = ln.Nodes[i] + } + + sharedData = &SharedData{ + Nodes: nodes, + } + + m.Run() + + //teardown + for i := 0; i < len(ln.Nodes); i++ { + _ = ln.Nodes[i].Close() + } } diff --git a/integration/qbft/tests/test_duty.go b/integration/qbft/tests/test_duty.go index c29f5eeb25..8d40901366 100644 --- a/integration/qbft/tests/test_duty.go +++ b/integration/qbft/tests/test_duty.go @@ -20,28 +20,30 @@ type DutyProperties struct { Delay time.Duration } -func createDuty(pk []byte, slot phase0.Slot, idx phase0.ValidatorIndex, role spectypes.BeaconRole) spectypes.Duty { +func createDuty(pk []byte, slot phase0.Slot, idx phase0.ValidatorIndex, role spectypes.RunnerRole) spectypes.Duty { var pkBytes [48]byte copy(pkBytes[:], pk) var testingDuty spectypes.ValidatorDuty + var beaconRole spectypes.BeaconRole switch role { - case spectypes.BNRoleAttester: + case spectypes.RoleCommittee: return spectestingutils.TestingCommitteeAttesterDuty(slot, []int{int(idx)}) - case spectypes.BNRoleAggregator: + case spectypes.RoleAggregator: testingDuty = spectestingutils.TestingAggregatorDuty - case spectypes.BNRoleProposer: + beaconRole = spectypes.BNRoleAggregator + case spectypes.RoleProposer: testingDuty = *spectestingutils.TestingProposerDutyV(spec.DataVersionCapella) - case spectypes.BNRoleSyncCommittee: - return spectestingutils.TestingCommitteeSyncCommitteeDuty(slot, []int{int(idx)}) - case spectypes.BNRoleSyncCommitteeContribution: + beaconRole = spectypes.BNRoleProposer + case spectypes.RoleSyncCommitteeContribution: testingDuty = spectestingutils.TestingSyncCommitteeContributionDuty + beaconRole = spectypes.BNRoleSyncCommitteeContribution default: panic("unknown role") } return &spectypes.ValidatorDuty{ - Type: role, + Type: beaconRole, PubKey: pkBytes, Slot: slot, ValidatorIndex: idx, diff --git a/network/p2p/p2p_pubsub.go b/network/p2p/p2p_pubsub.go index 5ddbd46e56..b0829ff011 100644 --- a/network/p2p/p2p_pubsub.go +++ b/network/p2p/p2p_pubsub.go @@ -7,8 +7,6 @@ import ( "math/rand" "time" - genesisqueue "github.com/ssvlabs/ssv/protocol/genesis/ssv/genesisqueue" - pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/pkg/errors" spectypes "github.com/ssvlabs/ssv-spec/types" @@ -19,6 +17,7 @@ import ( "github.com/ssvlabs/ssv/network/commons" "github.com/ssvlabs/ssv/network/records" genesismessage "github.com/ssvlabs/ssv/protocol/genesis/message" + "github.com/ssvlabs/ssv/protocol/genesis/ssv/genesisqueue" "github.com/ssvlabs/ssv/protocol/v2/message" p2pprotocol "github.com/ssvlabs/ssv/protocol/v2/p2p" "github.com/ssvlabs/ssv/protocol/v2/ssv/queue" diff --git a/network/p2p/p2p_test.go b/network/p2p/p2p_test.go index e3f68d42e9..6afc80f239 100644 --- a/network/p2p/p2p_test.go +++ b/network/p2p/p2p_test.go @@ -1,7 +1,9 @@ package p2pv1 import ( + "bytes" "context" + "crypto/sha256" "encoding/hex" "math/rand" "sync" @@ -9,11 +11,15 @@ import ( "testing" "time" + eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/libp2p/go-libp2p/core/peer" "github.com/pkg/errors" genesisspecqbft "github.com/ssvlabs/ssv-spec-pre-cc/qbft" + "github.com/ssvlabs/ssv-spec-pre-cc/types" specqbft "github.com/ssvlabs/ssv-spec/qbft" spectypes "github.com/ssvlabs/ssv-spec/types" + spectestingutils "github.com/ssvlabs/ssv-spec/types/testingutils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -21,8 +27,10 @@ import ( "github.com/ssvlabs/ssv/network" "github.com/ssvlabs/ssv/network/commons" "github.com/ssvlabs/ssv/networkconfig" + beaconprotocol "github.com/ssvlabs/ssv/protocol/v2/blockchain/beacon" "github.com/ssvlabs/ssv/protocol/v2/message" p2pprotocol "github.com/ssvlabs/ssv/protocol/v2/p2p" + ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types" ) func TestGetMaxPeers(t *testing.T) { @@ -35,22 +43,35 @@ func TestGetMaxPeers(t *testing.T) { } func TestP2pNetwork_SubscribeBroadcast(t *testing.T) { - t.Skip("need to implement validator store") - n := 4 ctx, cancel := context.WithCancel(context.Background()) defer cancel() - pks := []string{"8e80066551a81b318258709edaf7dd1f63cd686a0e4db8b29bbb7acfe65608677af5a527d9448ee47835485e02b50bc0"} + shares := []*ssvtypes.SSVShare{ + { + Share: *spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex), + Metadata: ssvtypes.Metadata{ + BeaconMetadata: &beaconprotocol.ValidatorMetadata{ + Status: eth2apiv1.ValidatorStateActiveOngoing, + Index: spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex).ValidatorIndex, + }, + Liquidated: false, + }, + }, + } + ln, routers, err := createNetworkAndSubscribe(t, ctx, LocalNetOptions{ Nodes: n, MinConnected: n/2 - 1, UseDiscv5: false, - }, pks...) + Shares: shares, + }) require.NoError(t, err) require.NotNil(t, routers) require.NotNil(t, ln) + time.Sleep(3 * time.Second) + defer func() { for _, node := range ln.Nodes { require.NoError(t, node.(*p2pNetwork).Close()) @@ -64,28 +85,31 @@ func TestP2pNetwork_SubscribeBroadcast(t *testing.T) { go func() { defer wg.Done() - msgID1, msg1 := dummyMsgCommittee(t, pks[0], 1) - msgID3, msg3 := dummyMsgCommittee(t, pks[0], 3) - require.NoError(t, node1.Broadcast(msgID1, msg1)) - <-time.After(time.Millisecond * 10) - require.NoError(t, node2.Broadcast(msgID3, msg3)) - <-time.After(time.Millisecond * 2) - require.NoError(t, node2.Broadcast(msgID1, msg1)) + msg1 := generateMsg(spectestingutils.Testing4SharesSet(), 1) + msg3 := generateMsg(spectestingutils.Testing4SharesSet(), 3) + require.NoError(t, node1.Broadcast(msg1.SSVMessage.GetID(), msg1)) + <-time.After(time.Millisecond * 20) + require.NoError(t, node2.Broadcast(msg3.SSVMessage.GetID(), msg3)) + <-time.After(time.Millisecond * 20) + require.NoError(t, node2.Broadcast(msg1.SSVMessage.GetID(), msg1)) }() wg.Add(1) go func() { defer wg.Done() - msgID1, msg1 := dummyMsgCommittee(t, pks[0], 1) - msgID2, msg2 := dummyMsgCommittee(t, pks[1], 2) - msgID3, msg3 := dummyMsgCommittee(t, pks[0], 3) + + msg1 := generateMsg(spectestingutils.Testing4SharesSet(), 1) + msg2 := generateMsg(spectestingutils.Testing4SharesSet(), 2) + msg3 := generateMsg(spectestingutils.Testing4SharesSet(), 3) require.NoError(t, err) - time.Sleep(time.Millisecond * 10) - require.NoError(t, node1.Broadcast(msgID2, msg2)) - time.Sleep(time.Millisecond * 2) - require.NoError(t, node2.Broadcast(msgID1, msg1)) - require.NoError(t, node1.Broadcast(msgID3, msg3)) + + time.Sleep(time.Millisecond * 20) + require.NoError(t, node1.Broadcast(msg2.SSVMessage.GetID(), msg2)) + + time.Sleep(time.Millisecond * 20) + require.NoError(t, node2.Broadcast(msg1.SSVMessage.GetID(), msg1)) + require.NoError(t, node1.Broadcast(msg3.SSVMessage.GetID(), msg3)) }() wg.Wait() @@ -105,26 +129,37 @@ func TestP2pNetwork_SubscribeBroadcast(t *testing.T) { wg.Wait() for _, r := range routers { - require.GreaterOrEqual(t, atomic.LoadUint64(&r.count), uint64(2), "router", r.i) + assert.GreaterOrEqual(t, atomic.LoadUint64(&r.count), uint64(2), "router %d", r.i) } - - <-time.After(time.Millisecond * 10) } func TestP2pNetwork_Stream(t *testing.T) { - t.Skip("test gets stuck") + t.Skip("will be removed in https://github.com/ssvlabs/ssv/pull/1544") n := 12 ctx, cancel := context.WithCancel(context.Background()) logger := logging.TestLogger(t) defer cancel() - pkHex := "8e80066551a81b318258709edaf7dd1f63cd686a0e4db8b29bbb7acfe65608677af5a527d9448ee47835485e02b50bc0" + shares := []*ssvtypes.SSVShare{ + { + Share: *spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex), + Metadata: ssvtypes.Metadata{ + BeaconMetadata: &beaconprotocol.ValidatorMetadata{ + Status: eth2apiv1.ValidatorStateActiveOngoing, + Index: spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), spectestingutils.TestingValidatorIndex).ValidatorIndex, + }, + Liquidated: false, + }, + }, + } + ln, _, err := createNetworkAndSubscribe(t, ctx, LocalNetOptions{ Nodes: n, MinConnected: n/2 - 1, UseDiscv5: false, - }, pkHex) + Shares: shares, + }) defer func() { for _, node := range ln.Nodes { @@ -134,10 +169,7 @@ func TestP2pNetwork_Stream(t *testing.T) { require.NoError(t, err) require.Len(t, ln.Nodes, n) - pk, err := hex.DecodeString(pkHex) - require.NoError(t, err) - - mid := spectypes.NewMsgID(networkconfig.TestNetwork.DomainType(), pk, spectypes.RoleCommittee) + mid := spectypes.NewMsgID(networkconfig.TestNetwork.DomainType(), shares[0].ValidatorPubKey[:], spectypes.RoleCommittee) rounds := []specqbft.Round{ 1, 1, 1, 1, 2, 2, @@ -225,49 +257,86 @@ func TestWaitSubsetOfPeers(t *testing.T) { } } -func dummyMsgCommittee(t *testing.T, pkHex string, height int) (spectypes.MessageID, *spectypes.SignedSSVMessage) { - return dummyMsg(t, pkHex, height, spectypes.RoleCommittee) +func generateMsg(ks *spectestingutils.TestKeySet, round specqbft.Round) *spectypes.SignedSSVMessage { + netCfg := networkconfig.TestNetwork + height := specqbft.Height(netCfg.Beacon.EstimatedCurrentSlot()) + + share := &ssvtypes.SSVShare{ + Share: *spectestingutils.TestingShare(ks, spectestingutils.TestingValidatorIndex), + Metadata: ssvtypes.Metadata{ + BeaconMetadata: &beaconprotocol.ValidatorMetadata{ + Status: eth2apiv1.ValidatorStateActiveOngoing, + Index: spectestingutils.TestingShare(ks, spectestingutils.TestingValidatorIndex).ValidatorIndex, + }, + Liquidated: false, + }, + } + committeeID := share.CommitteeID() + + fullData := spectestingutils.TestingQBFTFullData + + encodedCommitteeID := append(bytes.Repeat([]byte{0}, 16), committeeID[:]...) + committeeIdentifier := spectypes.NewMsgID(netCfg.DomainType(), encodedCommitteeID, spectypes.RoleCommittee) + + qbftMessage := &specqbft.Message{ + MsgType: specqbft.ProposalMsgType, + Height: height, + Round: round, + Identifier: committeeIdentifier[:], + Root: sha256.Sum256(fullData), + + RoundChangeJustification: [][]byte{}, + PrepareJustification: [][]byte{}, + } + + leader := roundLeader(ks, height, round) + signedSSVMessage := spectestingutils.SignQBFTMsg(ks.OperatorKeys[leader], leader, qbftMessage) + signedSSVMessage.FullData = fullData + + return signedSSVMessage +} + +func roundLeader(ks *spectestingutils.TestKeySet, height specqbft.Height, round specqbft.Round) types.OperatorID { + share := spectestingutils.TestingShare(ks, 1) + + firstRoundIndex := 0 + if height != specqbft.FirstHeight { + firstRoundIndex += int(height) % len(share.Committee) + } + + index := (firstRoundIndex + int(round) - int(specqbft.FirstRound)) % len(share.Committee) + return share.Committee[index].Signer } func dummyMsg(t *testing.T, pkHex string, height int, role spectypes.RunnerRole) (spectypes.MessageID, *spectypes.SignedSSVMessage) { pk, err := hex.DecodeString(pkHex) require.NoError(t, err) id := spectypes.NewMsgID(networkconfig.TestNetwork.DomainType(), pk, role) - signedMsg := &genesisspecqbft.SignedMessage{ - Message: genesisspecqbft.Message{ - MsgType: genesisspecqbft.CommitMsgType, - Round: 2, - Identifier: id[:], - Height: genesisspecqbft.Height(height), - Root: [32]byte{0x1, 0x2, 0x3}, - }, - Signature: []byte("sVV0fsvqQlqliKv/ussGIatxpe8LDWhc9uoaM5WpjbiYvvxUr1eCpz0ja7UT1PGNDdmoGi6xbMC1g/ozhAt4uCdpy0Xdfqbv"), - Signers: []spectypes.OperatorID{1, 3, 4}, - } - data, err := signedMsg.Encode() - require.NoError(t, err) - ssvMsg := &spectypes.SSVMessage{ - MsgType: spectypes.SSVConsensusMsgType, - MsgID: id, - Data: data, + + qbftMessage := &specqbft.Message{ + MsgType: specqbft.CommitMsgType, + Height: specqbft.Height(height), + Round: 2, + Identifier: id[:], + Root: [32]byte{0x1, 0x2, 0x3}, } - sig, err := dummySignSSVMessage(ssvMsg) + encodedQBFTMessage, err := qbftMessage.Encode() require.NoError(t, err) signedSSVMsg := &spectypes.SignedSSVMessage{ - Signatures: [][]byte{sig}, - OperatorIDs: []spectypes.OperatorID{1}, - SSVMessage: ssvMsg, + SSVMessage: &spectypes.SSVMessage{ + MsgType: spectypes.SSVConsensusMsgType, + MsgID: id, + Data: encodedQBFTMessage, + }, + Signatures: [][]byte{[]byte("sVV0fsvqQlqliKv/ussGIatxpe8LDWhc9uoaM5WpjbiYvvxUr1eCpz0ja7UT1PGNDdmoGi6xbMC1g/ozhAt4uCdpy0Xdfqbv")}, + OperatorIDs: []spectypes.OperatorID{1, 3, 4}, } return id, signedSSVMsg } -func dummySignSSVMessage(_ *spectypes.SSVMessage) ([]byte, error) { - return []byte{}, nil -} - type dummyRouter struct { count uint64 i int @@ -277,7 +346,7 @@ func (r *dummyRouter) Route(_ context.Context, _ network.DecodedSSVMessage) { atomic.AddUint64(&r.count, 1) } -func createNetworkAndSubscribe(t *testing.T, ctx context.Context, options LocalNetOptions, pks ...string) (*LocalNet, []*dummyRouter, error) { +func createNetworkAndSubscribe(t *testing.T, ctx context.Context, options LocalNetOptions) (*LocalNet, []*dummyRouter, error) { logger, err := zap.NewDevelopment() require.NoError(t, err) ln, err := CreateAndStartLocalNet(ctx, logger.Named("createNetworkAndSubscribe"), options) @@ -301,11 +370,7 @@ func createNetworkAndSubscribe(t *testing.T, ctx context.Context, options LocalN logger.Debug("subscribing to topics") var wg sync.WaitGroup - for _, pk := range pks { - vpk, err := hex.DecodeString(pk) - if err != nil { - return nil, nil, errors.Wrap(err, "could not decode validator public key") - } + for _, share := range options.Shares { for _, node := range ln.Nodes { wg.Add(1) go func(node network.P2PNetwork, vpk spectypes.ValidatorPK) { @@ -313,7 +378,7 @@ func createNetworkAndSubscribe(t *testing.T, ctx context.Context, options LocalN if err := node.Subscribe(vpk); err != nil { logger.Warn("could not subscribe to topic", zap.Error(err)) } - }(node, spectypes.ValidatorPK(vpk)) + }(node, share.ValidatorPubKey) } } wg.Wait() diff --git a/network/p2p/p2p_validation_test.go b/network/p2p/p2p_validation_test.go index 9f5e708da2..c427043eb7 100644 --- a/network/p2p/p2p_validation_test.go +++ b/network/p2p/p2p_validation_test.go @@ -13,17 +13,22 @@ import ( "testing" "time" - "github.com/cornelk/hashmap" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/aquasecurity/table" + eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/cornelk/hashmap" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" "github.com/sourcegraph/conc/pool" - "github.com/stretchr/testify/require" - + specqbft "github.com/ssvlabs/ssv-spec/qbft" spectypes "github.com/ssvlabs/ssv-spec/types" + spectestingutils "github.com/ssvlabs/ssv-spec/types/testingutils" + "github.com/stretchr/testify/require" "github.com/ssvlabs/ssv/message/validation" + beaconprotocol "github.com/ssvlabs/ssv/protocol/v2/blockchain/beacon" + "github.com/ssvlabs/ssv/protocol/v2/ssv/queue" + ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types" ) // TestP2pNetwork_MessageValidation tests p2pNetwork would score peers according @@ -34,8 +39,6 @@ import ( // and finally asserts that each node scores it's peers according to their // played role (accepted > ignored > rejected). func TestP2pNetwork_MessageValidation(t *testing.T) { - t.Skip("test gets stuck") - const ( nodeCount = 4 validatorCount = 20 @@ -46,13 +49,8 @@ func TestP2pNetwork_MessageValidation(t *testing.T) { defer cancel() // Create 20 fake validator public keys. - validators := make([]string, validatorCount) - for i := 0; i < validatorCount; i++ { - var validator [48]byte - cryptorand.Read(validator[:]) - validators[i] = hex.EncodeToString(validator[:]) - } - var mtx sync.Mutex + shares := generateShares(t, validatorCount) + // Create a MessageValidator to accept/reject/ignore messages according to their role type. const ( acceptedRole = spectypes.RoleProposer @@ -60,14 +58,94 @@ func TestP2pNetwork_MessageValidation(t *testing.T) { rejectedRole = spectypes.RoleSyncCommitteeContribution ) messageValidators := make([]*MockMessageValidator, nodeCount) + var mtx sync.Mutex + for i := 0; i < nodeCount; i++ { + i := i + messageValidators[i] = &MockMessageValidator{ + Accepted: make([]int, nodeCount), + Ignored: make([]int, nodeCount), + Rejected: make([]int, nodeCount), + } + messageValidators[i].ValidateFunc = func(ctx context.Context, p peer.ID, pmsg *pubsub.Message) pubsub.ValidationResult { + signedSSVMessage := &spectypes.SignedSSVMessage{} + if err := signedSSVMessage.Decode(pmsg.GetData()); err != nil { + return pubsub.ValidationReject + } + + ssvMessage := signedSSVMessage.SSVMessage + + var body any + + switch ssvMessage.MsgType { + case spectypes.SSVConsensusMsgType: + var qbftMsg specqbft.Message + if err := qbftMsg.Decode(ssvMessage.Data); err != nil { + return pubsub.ValidationReject + } + + body = qbftMsg + + case spectypes.SSVPartialSignatureMsgType: + var psm spectypes.PartialSignatureMessages + if err := psm.Decode(ssvMessage.Data); err != nil { + return pubsub.ValidationReject + } + + body = psm + default: + return pubsub.ValidationReject + } + + pmsg.ValidatorData = &queue.SSVMessage{ + SignedSSVMessage: signedSSVMessage, + SSVMessage: ssvMessage, + Body: body, + } + + peer := vNet.NodeByPeerID(p) + + mtx.Lock() + // Validation according to role. + var validation pubsub.ValidationResult + switch ssvMessage.MsgID.GetRoleType() { + case acceptedRole: + messageValidators[i].Accepted[peer.Index]++ + messageValidators[i].TotalAccepted++ + validation = pubsub.ValidationAccept + case ignoredRole: + messageValidators[i].Ignored[peer.Index]++ + messageValidators[i].TotalIgnored++ + validation = pubsub.ValidationIgnore + case rejectedRole: + messageValidators[i].Rejected[peer.Index]++ + messageValidators[i].TotalRejected++ + validation = pubsub.ValidationReject + default: + panic("unsupported role") + } + mtx.Unlock() + + // Always accept messages from self to make libp2p propagate them, + // while still counting them by their role. + if p == vNet.Nodes[i].Network.Host().ID() { + return pubsub.ValidationAccept + } + + return validation + } + } + // Create a VirtualNet with 4 nodes. - vNet = CreateVirtualNet(t, ctx, 4, validators, func(nodeIndex int) validation.MessageValidator { + vNet = CreateVirtualNet(t, ctx, 4, shares, func(nodeIndex int) validation.MessageValidator { return messageValidators[nodeIndex] }) + defer func() { require.NoError(t, vNet.Close()) }() + time.Sleep(5 * time.Second) + // Prepare a pool of broadcasters. mu := sync.Mutex{} height := atomic.Int64{} @@ -82,12 +160,12 @@ func TestP2pNetwork_MessageValidation(t *testing.T) { roleBroadcasts[role]++ mu.Unlock() - msgID, msg := dummyMsg(t, validators[rand.Intn(len(validators))], int(height.Add(1)), role) + msgID, msg := dummyMsg(t, hex.EncodeToString(shares[rand.Intn(len(shares))].ValidatorPubKey[:]), int(height.Add(1)), role) err := node.Broadcast(msgID, msg) if err != nil { return err } - time.Sleep(10 * time.Millisecond) + time.Sleep(100 * time.Millisecond) } return nil }) @@ -106,10 +184,10 @@ func TestP2pNetwork_MessageValidation(t *testing.T) { // Wait for the broadcasters to finish. err := broadcasters.Wait() require.NoError(t, err) - time.Sleep(500 * time.Millisecond) + time.Sleep(1 * time.Second) // Assert that the messages were distributed as expected. - deadline := time.Now().Add(5 * time.Second) + deadline := time.Now().Add(7 * time.Second) interval := 100 * time.Millisecond for i := 0; i < nodeCount; i++ { // better lock inside loop than wait interval locked @@ -202,10 +280,11 @@ func TestP2pNetwork_MessageValidation(t *testing.T) { break } if i == len(validOrders)-1 { - require.Fail(t, "invalid order", "node %d", node.Index) + require.Fail(t, "invalid order", "node %d, peers %v", node.Index, peers) } } } + defer fmt.Println() } @@ -249,7 +328,7 @@ func CreateVirtualNet( t *testing.T, ctx context.Context, nodes int, - validatorPubKeys []string, + shares []*ssvtypes.SSVShare, messageValidatorProvider func(int) validation.MessageValidator, ) *VirtualNet { var doneSetup atomic.Bool @@ -287,7 +366,8 @@ func CreateVirtualNet( }, PeerScoreInspectorInterval: time.Millisecond * 5, - }, validatorPubKeys...) + Shares: shares, + }) require.NoError(t, err) require.NotNil(t, routers) @@ -323,3 +403,33 @@ func (vn *VirtualNet) Close() error { } return nil } + +func generateShares(t *testing.T, count int) []*ssvtypes.SSVShare { + var shares []*ssvtypes.SSVShare + + for i := 0; i < count; i++ { + validatorIndex := phase0.ValidatorIndex(i) + specShare := *spectestingutils.TestingShare(spectestingutils.Testing4SharesSet(), validatorIndex) + + var pk spectypes.ValidatorPK + _, err := cryptorand.Read(pk[:]) + require.NoError(t, err) + + specShare.ValidatorPubKey = pk + + share := &ssvtypes.SSVShare{ + Share: specShare, + Metadata: ssvtypes.Metadata{ + BeaconMetadata: &beaconprotocol.ValidatorMetadata{ + Status: eth2apiv1.ValidatorStateActiveOngoing, + Index: validatorIndex, + }, + Liquidated: false, + }, + } + + shares = append(shares, share) + } + + return shares +} diff --git a/network/p2p/test_utils.go b/network/p2p/test_utils.go index 11ae76ec0f..8746aed4ba 100644 --- a/network/p2p/test_utils.go +++ b/network/p2p/test_utils.go @@ -6,13 +6,13 @@ import ( "fmt" "time" + "github.com/ethereum/go-ethereum/common" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/peer" spectypes "github.com/ssvlabs/ssv-spec/types" "go.uber.org/zap" "golang.org/x/sync/errgroup" - "github.com/ssvlabs/ssv/message/signatureverifier" "github.com/ssvlabs/ssv/message/validation" "github.com/ssvlabs/ssv/monitoring/metricsreporter" "github.com/ssvlabs/ssv/network" @@ -24,6 +24,7 @@ import ( operatordatastore "github.com/ssvlabs/ssv/operator/datastore" "github.com/ssvlabs/ssv/operator/duties/dutystore" "github.com/ssvlabs/ssv/operator/storage" + ssvtypes "github.com/ssvlabs/ssv/protocol/v2/types" registrystorage "github.com/ssvlabs/ssv/registry/storage" "github.com/ssvlabs/ssv/storage/basedb" "github.com/ssvlabs/ssv/storage/kv" @@ -123,6 +124,12 @@ func CreateAndStartLocalNet(pCtx context.Context, logger *zap.Logger, options Lo } } +type mockSignatureVerifier struct{} + +func (mockSignatureVerifier) VerifySignature(operatorID spectypes.OperatorID, message *spectypes.SSVMessage, signature []byte) error { + return nil +} + // NewTestP2pNetwork creates a new network.P2PNetwork instance func (ln *LocalNet) NewTestP2pNetwork(ctx context.Context, nodeIndex int, keys testing.NodeKeys, logger *zap.Logger, options LocalNetOptions) (network.P2PNetwork, error) { operatorPubkey, err := keys.OperatorKey.Public().Base64() @@ -140,12 +147,31 @@ func (ln *LocalNet) NewTestP2pNetwork(ctx context.Context, nodeIndex int, keys t return nil, err } + for _, share := range options.Shares { + if err := nodeStorage.Shares().Save(nil, share); err != nil { + return nil, err + } + } + + for _, share := range options.Shares { + for _, sm := range share.Committee { + _, err := nodeStorage.SaveOperatorData(nil, ®istrystorage.OperatorData{ + ID: sm.Signer, + PublicKey: operatorPubkey, + OwnerAddress: common.BytesToAddress([]byte("testOwnerAddress")), + }) + if err != nil { + return nil, err + } + } + } + dutyStore := dutystore.New() - signatureVerifier := signatureverifier.NewSignatureVerifier(nodeStorage) + signatureVerifier := &mockSignatureVerifier{} cfg := NewNetConfig(keys, format.OperatorID(operatorPubkey), ln.Bootnode, testing.RandomTCPPort(12001, 12999), ln.udpRand.Next(13001, 13999), options.Nodes) cfg.Ctx = ctx - cfg.Subnets = "00000000000000000000020000000000" //PAY ATTENTION for future test scenarios which use more than one eth-validator we need to make this field dynamically changing + cfg.Subnets = "00000000000000000100000400000400" // calculated for topics 64, 90, 114; PAY ATTENTION for future test scenarios which use more than one eth-validator we need to make this field dynamically changing cfg.NodeStorage = nodeStorage cfg.Metrics = nil cfg.MessageValidator = validation.New( @@ -208,6 +234,7 @@ type LocalNetOptions struct { TotalValidators, ActiveValidators, MyValidators int PeerScoreInspector func(selfPeer peer.ID, peerMap map[peer.ID]*pubsub.PeerScoreSnapshot) PeerScoreInspectorInterval time.Duration + Shares []*ssvtypes.SSVShare } // NewLocalNet creates a new mdns network diff --git a/network/topics/controller_test.go b/network/topics/controller_test.go index e5695fc8f9..cb584bf8ed 100644 --- a/network/topics/controller_test.go +++ b/network/topics/controller_test.go @@ -5,41 +5,37 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "fmt" "math" "sync" "sync/atomic" "testing" "time" - genesisspecqbft "github.com/ssvlabs/ssv-spec-pre-cc/qbft" - - registrystorage "github.com/ssvlabs/ssv/registry/storage" - "github.com/ssvlabs/ssv/storage/basedb" - "github.com/ssvlabs/ssv/storage/kv" - "github.com/libp2p/go-libp2p" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/host" libp2pnetwork "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + genesisspecqbft "github.com/ssvlabs/ssv-spec-pre-cc/qbft" + spectypes "github.com/ssvlabs/ssv-spec/types" "github.com/stretchr/testify/require" "go.uber.org/zap" - genesisvalidation "github.com/ssvlabs/ssv/message/validation/genesis" - - spectypes "github.com/ssvlabs/ssv-spec/types" - "github.com/ssvlabs/ssv/logging" "github.com/ssvlabs/ssv/message/validation" + genesisvalidation "github.com/ssvlabs/ssv/message/validation/genesis" "github.com/ssvlabs/ssv/monitoring/metricsreporter" "github.com/ssvlabs/ssv/network/commons" "github.com/ssvlabs/ssv/network/discovery" "github.com/ssvlabs/ssv/networkconfig" + registrystorage "github.com/ssvlabs/ssv/registry/storage" + "github.com/ssvlabs/ssv/storage/basedb" + "github.com/ssvlabs/ssv/storage/kv" ) func TestTopicManager(t *testing.T) { - t.Skip("the test gets stuck, needs to be fixed") - + // TODO: reduce running time of this test, use channels instead of long timeouts logger := logging.TestLogger(t) // TODO: rework this test to use message validation @@ -174,16 +170,27 @@ func baseTest(t *testing.T, ctx context.Context, logger *zap.Logger, peers []*P, wg.Add(1) go func(p *P, pk string) { defer wg.Done() - require.NoError(t, p.tm.Unsubscribe(logger, validatorTopic(pk), false)) + + topic := validatorTopic(pk) + topicFullName := commons.GetTopicFullName(topic) + + err := p.tm.Unsubscribe(logger, topic, false) + require.NoError(t, err) + go func(p *P) { <-time.After(time.Millisecond) - require.NoError(t, p.tm.Unsubscribe(logger, validatorTopic(pk), false)) + + err := p.tm.Unsubscribe(logger, topic, false) + require.ErrorContains(t, err, fmt.Sprintf("failed to unsubscribe from topic %s: not subscribed", topicFullName)) }(p) + wg.Add(1) go func(p *P) { defer wg.Done() <-time.After(time.Millisecond * 50) - require.NoError(t, p.tm.Unsubscribe(logger, validatorTopic(pk), false)) + + err := p.tm.Unsubscribe(logger, topic, false) + require.ErrorContains(t, err, fmt.Sprintf("failed to unsubscribe from topic %s: not subscribed", topicFullName)) }(p) }(p, pks[i]) } diff --git a/protocol/v2/ssv/runner/committee.go b/protocol/v2/ssv/runner/committee.go index 61117141e0..3d2449cad4 100644 --- a/protocol/v2/ssv/runner/committee.go +++ b/protocol/v2/ssv/runner/committee.go @@ -544,7 +544,7 @@ func (cr CommitteeRunner) expectedPreConsensusRootsAndDomain() ([]ssz.HashRoot, // This function signature returns only one domain type... but we can have mixed domains // instead we rely on expectedPostConsensusRootsAndBeaconObjects that is called later func (cr CommitteeRunner) expectedPostConsensusRootsAndDomain() ([]ssz.HashRoot, phase0.DomainType, error) { - return []ssz.HashRoot{}, spectypes.DomainAttester, nil + return nil, spectypes.DomainError, errors.New("expected post consensus roots function is unused") } func (cr *CommitteeRunner) expectedPostConsensusRootsAndBeaconObjects() ( diff --git a/protocol/v2/types/crypto.go b/protocol/v2/types/crypto.go index 2879ea07d1..f8db363732 100644 --- a/protocol/v2/types/crypto.go +++ b/protocol/v2/types/crypto.go @@ -49,7 +49,7 @@ func ReconstructSignature(ps *specssv.PartialSigContainer, root [32]byte, valida validatorPubKeyCopy := make([]byte, len(validatorPubKey)) copy(validatorPubKeyCopy, validatorPubKey) - if err := VerifyReconstructedSignature(signature, validatorPubKey, root); err != nil { + if err := VerifyReconstructedSignature(signature, validatorPubKeyCopy, root); err != nil { return nil, errors.Wrap(err, "failed to verify reconstruct signature") } return signature.Serialize(), nil diff --git a/scripts/spec-alignment/differ.config.yaml b/scripts/spec-alignment/differ.config.yaml index ecfa96ecde..120bb9c45d 100644 --- a/scripts/spec-alignment/differ.config.yaml +++ b/scripts/spec-alignment/differ.config.yaml @@ -1,28 +1,4 @@ -ApprovedChanges: ["256a3dc0f1eb7abf","22b66e9a63ba145b","12c1c3a1622fb7cc","1c44824d71b5c502","8e30d75eb6b02ec9","f7efb56ca768b59a","3244def428adeb84", - "e4a62b36b122e3dc","80ae9e6205bbf4da","d1e758f8c0d53aab","920a32aae48fadfc","e28e02f2afc1435c","592b02c8c59eabac","6945d51ab1f64602", - "10fc3a8409f13e8b","56f57ba70ab09f42","2ac4feb2b67e46c0","484f78bc04c520f0","b06833aec59b4702","f8f7a100070e00c4","eb2d40a21add9dbd", - "38aed088743c431e","e3dc79486f65bbfd","99d912f6989c77ba","3bc69adb21972b11","96aa231afd3e914","59b2375130aef5df","42448d531a36a19d", - "8980f976020e6f46","2a6230c1d8e2cbdd","2f12dcf68f2f8463","23103ca39c8fb43d","6247a32b8ff6b311","5458d821d32c401","cb0077057b56e137", - "70852fd2321ba1c5","d2cafc5ec262f84d","8850900b5d9bcc65","f47fee244932848d","88c74f6aaf78edf","b1e2e1d7012c2111","556cb12b8c458cf0", - "17a1cf958f2b3b06","be53b04b01612f32","a5d3405074af9f39","374be3ca8e029b32","86f0ead2480383f","576a7230fab694d7","d6e610032cfaa0c", - "db32f358b6e8e2bb","f372e174e1f34c3b","bc47b3d202e8cd0d","86a6abca1a1c16d6","1655d21d5a4cad4","ac4e427097fc5533","6b4d5a114f8066ff", - "9482fb9b6a953c48","5778a05e0976a6eb","24e2c7f54d5dd1d","2a8937e50d20faa9","587c629a67ef07ed","9d06d8e0ee4e1113","e624ec802068e711", - "943be3ce709a99d3","5b3bb2d2262fe8be","c20c4c7ed8d1711d","b10c6fc7dd9eee7","c121cdaab6c1c698","e12b17f3910be26b","e47bf52e962c90af", - "90b8a0c8d2c30e95","e8292a58d2eb08ab","17cf3119ac6879f2","3f31546191c9e6b2","29c96f90edc2458d","f29db2624fd63635","dff6fea2c2d32a5f", - "ae1b53fc580ce346","c117bd5db3eeabd6","d06552d71b9ca4cd","4cb333a88af66575","2a580187c312c79a","bf8cf93c55c1eadb","6d877e24991465e4", - "b1c8e0148a4a755","2c25abb7c776bd54","a1754e08473bd1fa","4dbab14670fa155d","2a3667a499a23b16","930379d323dd95e8","65efe31656e8814f", - "1270cef2e573f846","aeafb38ca9114f12","2a83e3384b45f2d7","91fbb874b3ce2570","74ad51ca63526e1e","defd8406641d53a5","efa21b9890ec787b", - "6bd22f1a688bbca8","6b29645373ffac35","cfd236570c82c478","e2b0e9c6454c1c08","d5ddc708de23543a","11f8f0ea5709e42e","172a32c59d6f082e", - "63bbaffe74361d14","e783fe0d6b6643a","dcc4bcccb6a98797","68f1865003f0849c","f1d890dac1a7c433","bd2d3d706847daeb","b70a277af0882438", - "47fec869ebd67c1","c5328c01d31741fb","dfa274ccd46d9e9e","cc12f634cdeb7e22","559f5bb236c90516","751721f614146c0e","7e0fba46f25e32e7", - "1b365fd720b2cf88","cb88f469b8fdd502","1347c8a043eaee9c","6eb76135ab3ff5b3","f6dd8cd4425c08d2","115695a6f44c2992","93cc839f6191c49c", - "d568b636fb576723","816b7c5775db8b72","599ac6719279a60d","75df6f41946e30f9","1d1ee94344e8dbf1","e00af924549bb3a0","6e24eb2b68393ee3", - "5a735e3f406bdc8","e976fcbbf0d38e85","854236f7176e9409","82c0b3c3aefbdd16","39833136313e7acb","d3f59761171d9ca7","504b3c0c2d388c43", - "6929293120b9dcf2","72a2780c27f4a02a","e254e9f8ed6ba20d","d6a31392f787304","755462038c7fa85b","2bd2412dde186c69","7de9d4a562280273", - "c22195cba94f6474","64377c51b28cd4f9","fc09e2474bd2df7d","9a7d945a280671d7","48011af7487ac774","53335a0af3048355","28d9399b816ac004", - "cedb31c6145220fd","44a6d807fb609a11","ce4d844d442c2729","e3631341aa036184","298c4d8614f2d6b7","e1dbc6c1741e4f36","1528a4a1f864d9f9", - "676a6bc46dfd5e37","209dddbd628efcba","913d38095eeb8df0","a6cbee7114556a80","2d2db553673e9364","d4d69c94a8883a5a","f658f41f6254b8c0", - "a0556c8c6b92532a","432a9c54d7cdfac0","d1e9372188042f52","417eec4813e3284c","482a96d93b63d128","d8edd3161472ba94"] +ApprovedChanges: ["870a3a66aeccd737","4e22a08543b079b","56ceb03cd44ff702","188adfe8914e04c1","2438f9c5b82b69a3","1a716ee3bdb3170","90b166f78390af18","68219b82a1d9d829","c4c4caa5d0938b85","dfe99ce1d27b6cb1","35f5dab1f128d193","9a3973b64d7e8932","f33f07301a770d03","3e9e0dddfad3b302","d4fef6512374c1f5","b49f54cb45787e4b","59b2375130aef5df","f094cd0460432170","8e51881e527dd603","a7d6d58d9fa06379","1d124224ca4d0fe3","39ea06bfd1477d2d","7e2550bab51f22b2","87ebd29bd49fc52f","ef39dd5223e0d080","fe14e7f0503ea188","6146023d4d5708a2","aebb8e4348b6d667","973a2e6704dbf3","fb4cac598a68c592","257c7eb81d6eb245","2a8e94fe037e13fd","5e7eb878de54eec6","960a9c64cd4ec93c","57dfd255520bd849","ec333ff8a708db69","1cc1ff39ad91ee69","5714652b88e2d44f","7a53b3b037c56325","8c02ef1964464c30","19a268910a20da3d","af6e01ed565029f3","318b5169ac4dabb6","372c6b332e8ba699","c0d8a364c0db855a","4287381be4fb1841","b1614afc1da7794f","c214975412f3fd7","8bbf7eba3fa0cf7e","8e4ec8debe331b36","7a671d8fcefc3793","e2b0e9c6454c1c08","6707ecfefa5fec21","d5a7389d730464f1","8dfae3b3223d2de0","a81c092c985de728","968df5082c727ed6","9e53c73ee60b1cc2","9d265e99dd31d4f5","a34619e078d2e42f","17e8cec4f0625d53","e913f373aa88f333","cfc1e05c372d88dc","e5de6901d78b8833","57c1885b43dd8d19","e8a49856a5edd893","22ea21d10a2f861c","954e4fce01631c4e","108b9575f7c1d4bc","1f8d076449068f64","5a7ad98296703f6","159536003eeddac8","8ca8f82e67ddd3dd","16ebe47404323cc1","48bfe5cf1e578b47","dd83182b693a7216","308d21d9830f7047","6dde03147e012b1a","730c3e5e59393b7d","5b44a4b425ecc397","df5debc50ec8babc","92a41554b2910bb8","c36c680554dde59f","447feaa5cdc1a010","fda90c61f44cb149","cdbb4930eced584c","274336ec1127e6c0","2a496f5b3ad542d2","6b395912dde33b0e","cac56ec14994216b","8850900b5d9bcc65","15e7706486c6359e","cc22f28953b787ea","3bad6ae11596a574","8f84422a240d889c","5b265432dfbbaac7","43794bf5953db193","7975821460ebe1e7","173c505e12aabb8f","47ee0d148148a56f","8cc38593ebe049b6","bda3aec7157b095a","248712911696a851","f4d9c910f1dbaef7","1a2146fcad37acb8","b0b146f9bdab64b6","edfd442b4d725fbb","122f053573538a32","d720d714a20833e1"] IgnoredIdentifiers: - logger