Skip to content

Commit

Permalink
testutil/validatormock: integrate DutySyncMessage (#1293)
Browse files Browse the repository at this point in the history
Integrates DutySyncMessage by adding support to validatormock and adds a simnet_test.

category: feature
ticket: #1261
  • Loading branch information
dB2510 committed Oct 17, 2022
1 parent aee9e2f commit 43f0a73
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 58 deletions.
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,
}

if conf.SimnetBMock { // Configure the beacon mock.
const dutyFactor = 100 // Duty factor spreads duties deterministicly in an epoch.
const dutyFactor = 100 // Duty factor spreads duties deterministically in an epoch.
opts := []beaconmock.Option{
beaconmock.WithSlotDuration(time.Second),
beaconmock.WithDeterministicAttesterDuties(dutyFactor),
Expand Down
21 changes: 21 additions & 0 deletions app/eth2wrap/eth2wrap_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/eth2wrap/genwrap/genwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type Client interface {
"AttestationsSubmitter": true,
"AttesterDutiesProvider": true,
"BeaconBlockProposalProvider": true,
"BeaconBlockRootProvider": false,
"BeaconBlockSubmitter": true,
"BeaconCommitteeSubscriptionsSubmitter": true,
"BlindedBeaconBlockProposalProvider": true,
Expand Down
9 changes: 9 additions & 0 deletions app/simnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ func TestSimnetNoNetwork_WithBuilderRegistrationMockVCs(t *testing.T) {
testSimnet(t, args, expect)
}

func TestSimnetNoNetwork_WithSyncCommitteeMockVCs(t *testing.T) {
args := newSimnetArgs(t)
args.BMockOpts = append(args.BMockOpts, beaconmock.WithSyncCommitteeDuties())
args.BMockOpts = append(args.BMockOpts, beaconmock.WithNoAttesterDuties())
args.BMockOpts = append(args.BMockOpts, beaconmock.WithNoProposerDuties())
expect := newSimnetExpect(args.N, core.DutySyncMessage)
testSimnet(t, args, expect)
}

type simnetArgs struct {
N int
VMocks []bool
Expand Down
77 changes: 60 additions & 17 deletions app/vmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,49 @@ func wireValidatorMock(conf Config, pubshares []eth2p0.BLSPubKey, sched core.Sch
return err
}

// Prepare attestations when slots tick.
onStartup := true
sched.SubscribeSlots(func(ctx context.Context, slot core.Slot) error {
vMockWrap(ctx, slot.Slot, func(ctx context.Context, state vMockState) error {
// Prepare attestations when slots tick.
vMockWrap(ctx, slot.Slot, slot.Epoch(), func(ctx context.Context, state vMockState) error {
return state.Attester.Prepare(ctx)
})

// Prepare sync committee message when epoch tick.
if onStartup || slot.FirstInEpoch() {
vMockWrap(ctx, slot.Slot, slot.Epoch(), func(ctx context.Context, state vMockState) error {
// Either call if it is first slot in epoch or on charon startup.
return state.SyncCommMember.PrepareEpoch(ctx)
})
}

onStartup = false

// Prepare sync committee selections when slots tick.
vMockWrap(ctx, slot.Slot, slot.Epoch(), func(ctx context.Context, state vMockState) error {
// Either call if it is first slot in epoch or on charon startup.
return state.SyncCommMember.PrepareSlot(ctx, eth2p0.Slot(slot.Slot))
})

// Submit sync committee message 1/3 into the slot.
vMockWrap(ctx, slot.Slot, slot.Epoch(), func(ctx context.Context, state vMockState) error {
thirdDuration := slot.SlotDuration / 3
thirdTime := slot.Time.Add(thirdDuration)

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Until(thirdTime)):
return state.SyncCommMember.Message(ctx, eth2p0.Slot(slot.Slot))
}
})

return nil
})

// Handle duties when triggered.
sched.SubscribeDuties(func(ctx context.Context, duty core.Duty, _ core.DutyDefinitionSet) error {
vMockWrap(ctx, duty.Slot, func(ctx context.Context, state vMockState) error {
return handleVMockDuty(ctx, duty, state.Eth2Cl, state.SignFunc, pubshares, state.Attester)
vMockWrap(ctx, duty.Slot, 0, func(ctx context.Context, state vMockState) error {
return handleVMockDuty(ctx, duty, state.Eth2Cl, state.SignFunc, pubshares, state.Attester, state.SyncCommMember)
})

return nil
Expand All @@ -66,7 +96,7 @@ func wireValidatorMock(conf Config, pubshares []eth2p0.BLSPubKey, sched core.Sch
go func() {
// TODO(corver): Improve registrations to use lock file and trigger on epoch transitions.
for registration := range conf.TestConfig.BuilderRegistration {
vMockWrap(context.Background(), 0, func(ctx context.Context, state vMockState) error {
vMockWrap(context.Background(), 0, 0, func(ctx context.Context, state vMockState) error {
return validatormock.Register(ctx, state.Eth2Cl, state.SignFunc, registration, pubshares[0])
})
}
Expand All @@ -77,16 +107,17 @@ func wireValidatorMock(conf Config, pubshares []eth2p0.BLSPubKey, sched core.Sch

// vMockState is the current validator mock state.
type vMockState struct {
Eth2Cl eth2wrap.Client
SignFunc validatormock.SignFunc
Attester *validatormock.SlotAttester // Changes every slot
Eth2Cl eth2wrap.Client
SignFunc validatormock.SignFunc
Attester *validatormock.SlotAttester // Changes every slot
SyncCommMember *validatormock.SyncCommMember
}

// vMockCallback is a validator mock callback function that has access to latest state.
// vMockCallback is a validator mock callback function that has access to the latest state.
type vMockCallback func(context.Context, vMockState) error

// newVMockWrapper returns a stateful validator mock wrapper function.
func newVMockWrapper(conf Config, pubshares []eth2p0.BLSPubKey) (func(context.Context, int64, vMockCallback), error) {
func newVMockWrapper(conf Config, pubshares []eth2p0.BLSPubKey) (func(ctx context.Context, slot int64, epoch int64, callback vMockCallback), error) {
// Immutable state and providers.
signFunc, err := newVMockSigner(conf, pubshares)
if err != nil {
Expand All @@ -97,11 +128,12 @@ func newVMockWrapper(conf Config, pubshares []eth2p0.BLSPubKey) (func(context.Co

// Mutable state
var (
mu sync.Mutex
attester = new(validatormock.SlotAttester)
mu sync.Mutex
attester = new(validatormock.SlotAttester)
syncCommMem = new(validatormock.SyncCommMember)
)

return func(ctx context.Context, slot int64, fn vMockCallback) {
return func(ctx context.Context, slot, epoch int64, fn vMockCallback) {
mu.Lock()
defer mu.Unlock()

Expand All @@ -117,11 +149,15 @@ func newVMockWrapper(conf Config, pubshares []eth2p0.BLSPubKey) (func(context.Co
if slot != 0 && attester.Slot() != eth2p0.Slot(slot) {
attester = validatormock.NewSlotAttester(eth2Cl, eth2p0.Slot(slot), signFunc, pubshares)
}
if epoch != 0 && syncCommMem.Epoch() != eth2p0.Epoch(epoch) {
syncCommMem = validatormock.NewSyncCommMember(eth2Cl, eth2p0.Epoch(epoch), signFunc, pubshares)
}

state := vMockState{
Eth2Cl: eth2Cl,
SignFunc: signFunc,
Attester: attester,
Eth2Cl: eth2Cl,
SignFunc: signFunc,
Attester: attester,
SyncCommMember: syncCommMem,
}

// Validator mock calls are async
Expand All @@ -130,7 +166,7 @@ func newVMockWrapper(conf Config, pubshares []eth2p0.BLSPubKey) (func(context.Co
defer cancel()

err := fn(ctx2, state)
if err != nil && ctx.Err() == nil { // Only log if parrent context wasn't closed.
if err != nil && ctx.Err() == nil { // Only log if parent context wasn't closed.
log.Error(ctx, "Validator mock error", err)
return
}
Expand Down Expand Up @@ -212,6 +248,7 @@ func newVMockSigner(conf Config, pubshares []eth2p0.BLSPubKey) (validatormock.Si
// handleVMockDuty calls appropriate validator mock function to attestation and block proposal.
func handleVMockDuty(ctx context.Context, duty core.Duty, eth2Cl eth2wrap.Client,
signer validatormock.SignFunc, pubshares []eth2p0.BLSPubKey, attester *validatormock.SlotAttester,
syncCommMember *validatormock.SyncCommMember,
) error {
switch duty.Type {
case core.DutyAttester:
Expand Down Expand Up @@ -239,6 +276,12 @@ func handleVMockDuty(ctx context.Context, duty core.Duty, eth2Cl eth2wrap.Client
return errors.Wrap(err, "mock builder proposal failed")
}
log.Info(ctx, "Mock blinded block proposal submitted to validatorapi", z.I64("slot", duty.Slot))
case core.DutySyncContribution:
err := syncCommMember.Aggregate(ctx, eth2p0.Slot(duty.Slot))
if err != nil {
return errors.Wrap(err, "mock sync contribution failed")
}
log.Info(ctx, "Mock sync contribution submitted to validatorapi", z.I64("slot", duty.Slot))
default:
return errors.New("invalid duty type")
}
Expand Down
15 changes: 15 additions & 0 deletions core/validatorapi/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ func proposerDuties(p eth2client.ProposerDutiesProvider) handlerFunc {
return nil, err
}

// response.data cannot be nil, it leads to NullPointerException in teku.
if len(data) == 0 {
data = []*eth2v1.ProposerDuty{}
}

return proposerDutiesResponse{
DependentRoot: stubRoot(epoch), // TODO(corver): Fill this properly
Data: data,
Expand All @@ -375,6 +380,11 @@ func attesterDuties(p eth2client.AttesterDutiesProvider) handlerFunc {
return nil, err
}

// response.data cannot be nil, it leads to NullPointerException in teku.
if len(data) == 0 {
data = []*eth2v1.AttesterDuty{}
}

return attesterDutiesResponse{
DependentRoot: stubRoot(epoch), // TODO(corver): Fill this properly
Data: data,
Expand All @@ -400,6 +410,11 @@ func syncCommitteeDuties(p eth2client.SyncCommitteeDutiesProvider) handlerFunc {
return nil, err
}

// response.data cannot be nil, it leads to NullPointerException in teku.
if len(data) == 0 {
data = []*eth2v1.SyncCommitteeDuty{}
}

return syncCommitteeDutiesResponse{Data: data}, nil
}
}
Expand Down
5 changes: 5 additions & 0 deletions testutil/beaconmock/beaconmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ type Mock struct {
BlindedBeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*eth2api.VersionedBlindedBeaconBlock, error)
BeaconCommitteesFunc func(ctx context.Context, stateID string) ([]*eth2v1.BeaconCommittee, error)
BeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error)
BeaconBlockRootFunc func(ctx context.Context, blockID string) (*eth2p0.Root, error)
ProposerDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error)
SubmitAttestationsFunc func(context.Context, []*eth2p0.Attestation) error
SubmitBeaconBlockFunc func(context.Context, *spec.VersionedSignedBeaconBlock) error
Expand Down Expand Up @@ -179,6 +180,10 @@ func (m Mock) BeaconBlockProposal(ctx context.Context, slot eth2p0.Slot, randaoR
return m.BeaconBlockProposalFunc(ctx, slot, randaoReveal, graffiti)
}

func (m Mock) BeaconBlockRoot(ctx context.Context, blockID string) (*eth2p0.Root, error) {
return m.BeaconBlockRootFunc(ctx, blockID)
}

func (m Mock) BeaconCommittees(ctx context.Context, stateID string) ([]*eth2v1.BeaconCommittee, error) {
return m.BeaconCommitteesFunc(ctx, stateID)
}
Expand Down
48 changes: 40 additions & 8 deletions testutil/beaconmock/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,35 @@ func WithNoAttesterDuties() Option {
}
}

// WithSyncCommitteeDuties configures the mock to override SyncCommitteeDutiesFunc to return sync committee
// duties for epochs not divisible by 3.
func WithSyncCommitteeDuties() Option {
return func(mock *Mock) {
mock.SyncCommitteeDutiesFunc = func(ctx context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) {
vals, err := mock.Validators(ctx, "", indices)
if err != nil {
return nil, err
}

var resp []*eth2v1.SyncCommitteeDuty
for i, index := range indices {
val, ok := vals[index]
if !ok {
continue
}

resp = append(resp, &eth2v1.SyncCommitteeDuty{
PubKey: val.Validator.PublicKey,
ValidatorIndex: index,
ValidatorSyncCommitteeIndices: []eth2p0.CommitteeIndex{eth2p0.CommitteeIndex(i)},
})
}

return resp, nil
}
}
}

// WithClock configures the mock with the provided clock.
func WithClock(clock clockwork.Clock) Option {
return func(mock *Mock) {
Expand Down Expand Up @@ -457,22 +486,22 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
Data: attData,
}, nil
},
ValidatorsFunc: func(ctx context.Context, stateID string, indices []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
ValidatorsFunc: func(context.Context, string, []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
return nil, nil
},
ValidatorsByPubKeyFunc: func(ctx context.Context, stateID string, pubkeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
ValidatorsByPubKeyFunc: func(context.Context, string, []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
return nil, nil
},
SubmitAttestationsFunc: func(ctx context.Context, atts []*eth2p0.Attestation) error {
SubmitAttestationsFunc: func(context.Context, []*eth2p0.Attestation) error {
return nil
},
SubmitBeaconBlockFunc: func(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
SubmitBeaconBlockFunc: func(context.Context, *spec.VersionedSignedBeaconBlock) error {
return nil
},
SubmitBlindedBeaconBlockFunc: func(ctx context.Context, block *eth2api.VersionedSignedBlindedBeaconBlock) error {
SubmitBlindedBeaconBlockFunc: func(context.Context, *eth2api.VersionedSignedBlindedBeaconBlock) error {
return nil
},
SubmitVoluntaryExitFunc: func(ctx context.Context, exit *eth2p0.SignedVoluntaryExit) error {
SubmitVoluntaryExitFunc: func(context.Context, *eth2p0.SignedVoluntaryExit) error {
return nil
},
GenesisTimeFunc: func(ctx context.Context) (time.Time, error) {
Expand Down Expand Up @@ -508,12 +537,15 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo
SlotsPerEpochFunc: func(ctx context.Context) (uint64, error) {
return httpMock.SlotsPerEpoch(ctx)
},
SubmitProposalPreparationsFunc: func(_ context.Context, _ []*eth2v1.ProposalPreparation) error {
SubmitProposalPreparationsFunc: func(context.Context, []*eth2v1.ProposalPreparation) error {
return nil
},
SyncCommitteeDutiesFunc: func(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) {
SyncCommitteeDutiesFunc: func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) {
return []*eth2v1.SyncCommitteeDuty{}, nil
},
SubmitSyncCommitteeMessagesFunc: func(context.Context, []*altair.SyncCommitteeMessage) error {
return nil
},
}
}

Expand Down
13 changes: 10 additions & 3 deletions testutil/beaconmock/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var staticJSON []byte
// It serves all proxied endpoints not handled by charon's validatorapi.
// Endpoints include static endpoints defined in static.json and a few stubbed paths.
type HTTPMock interface {
eth2client.BeaconBlockRootProvider
eth2client.DepositContractProvider
eth2client.DomainProvider
eth2client.ForkProvider
Expand All @@ -51,7 +52,7 @@ type HTTPMock interface {
eth2client.SlotDurationProvider
eth2client.SlotsPerEpochProvider
eth2client.SpecProvider
eth2client.SyncCommitteeDutiesProvider
eth2client.SyncCommitteeSubscriptionsSubmitter
}

// staticOverride defines a http server static override for a endpoint response value.
Expand All @@ -75,9 +76,15 @@ func newHTTPServer(addr string, overrides ...staticOverride) (*http.Server, erro
Handler: func(w http.ResponseWriter, r *http.Request) {},
},
{
Path: "/eth/v1/validator/duties/sync/{epoch}",
Path: "/eth/v1/validator/sync_committee_subscriptions",
Handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data":[]}`))
w.WriteHeader(http.StatusOK)
},
},
{
Path: "/eth/v1/beacon/blocks/head/root",
Handler: func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data": {"root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}}`))
},
},
{
Expand Down
Loading

0 comments on commit 43f0a73

Please sign in to comment.