Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

testutil/validatormock: implement #187

Merged
merged 2 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions testutil/validatormock/testdata/TestAttest_0
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[
{
"aggregation_bits": "0x0101",
"data": {
"slot": "32",
"index": "0",
"beacon_block_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
"source": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
},
"target": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"signature": "0x914cff835a769156ba43ad50b931083c2dadd94e8359ce394bc7a3e06424d0214922ddf15f81640530b9c25c0bc0d490000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"aggregation_bits": "0x0101",
"data": {
"slot": "32",
"index": "0",
"beacon_block_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
"source": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
},
"target": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"signature": "0x8dae41352b69f2b3a1c0b05330c1bf65f03730c520273028864b11fcb94d8ce8f26d64f979a0ee3025467f45fd2241ea000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"aggregation_bits": "0x0101",
"data": {
"slot": "32",
"index": "0",
"beacon_block_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
"source": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
},
"target": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"signature": "0x8ee91545183c8c2db86633626f5074fd8ef93c4c9b7a2879ad1768f600c5b5906c3af20d47de42c3b032956fa8db1a76000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
]
19 changes: 19 additions & 0 deletions testutil/validatormock/testdata/TestAttest_1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"aggregation_bits": "0x0101",
"data": {
"slot": "32",
"index": "0",
"beacon_block_root": "0x0000000000000000000000000000000000000000000000000000000000000000",
"source": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
},
"target": {
"epoch": "0",
"root": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"signature": "0x914cff835a769156ba43ad50b931083c2dadd94e8359ce394bc7a3e06424d0214922ddf15f81640530b9c25c0bc0d490000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
]
126 changes: 126 additions & 0 deletions testutil/validatormock/validatormock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Package validatormock provides mock validator client functionality.
package validatormock

import (
"context"
"fmt"

eth2client "github.com/attestantio/go-eth2-client"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/prysmaticlabs/go-bitfield"

"github.com/obolnetwork/charon/app/errors"
)

// Eth2AttProvider defines the eth2 beacon api providers required to perform attestations.
type Eth2AttProvider interface {
eth2client.AttestationDataProvider
eth2client.AttesterDutiesProvider
eth2client.AttestationsSubmitter
eth2client.SlotsPerEpochProvider
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alphabetical order maybe here too

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

eth2client.ValidatorsProvider
eth2client.SpecProvider
eth2client.DomainProvider
}

// SignFunc abstract signing done by the validator client.
type SignFunc func(context.Context, eth2p0.BLSPubKey, eth2p0.SigningData) (eth2p0.BLSSignature, error)

// Attest performs attestation duties for the provided slot and pubkeys (validators).
func Attest(ctx context.Context, eth2Cl Eth2AttProvider, signFunc SignFunc,
slot eth2p0.Slot, pubkeys []eth2p0.BLSPubKey,
) error {
slotsPerEpoch, err := eth2Cl.SlotsPerEpoch(ctx)
if err != nil {
return err
}

epoch := eth2p0.Epoch(uint64(slot) / slotsPerEpoch)

domain, err := getDomain(ctx, eth2Cl, "DOMAIN_BEACON_ATTESTER", epoch)
if err != nil {
return err
}

valMap, err := eth2Cl.ValidatorsByPubKey(ctx, fmt.Sprint(slot), pubkeys)
if err != nil {
return err
}

var indexes []eth2p0.ValidatorIndex
for index, val := range valMap {
if !val.Status.IsActive() {
continue
}
indexes = append(indexes, index)
}

duties, err := eth2Cl.AttesterDuties(ctx, epoch, indexes)
if err != nil {
return err
}

var atts []*eth2p0.Attestation
for _, duty := range duties {
if duty.Slot != slot {
continue
}

data, err := eth2Cl.AttestationData(ctx, duty.Slot, duty.CommitteeIndex)
if err != nil {
return err
}

root, err := data.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "hash attestation")
}

sig, err := signFunc(ctx, duty.PubKey, eth2p0.SigningData{
ObjectRoot: root,
Domain: domain,
})
if err != nil {
return err
}

aggBits := bitfield.NewBitlist(duty.CommitteeLength)
aggBits.SetBitAt(duty.ValidatorCommitteeIndex, true)
Comment on lines +87 to +88
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dB2510 here you can see that validator client includes duty.CommitteeLength and duty.ValidatorCommitteeIndex in the signed attestation. That is what we extract in the validatorapi to identify the DV the ParSignedData belongs to.

Copy link
Contributor

@dB2510 dB2510 Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah got it! now dots are connecting :')


atts = append(atts, &eth2p0.Attestation{
AggregationBits: aggBits,
Data: data,
Signature: sig,
})
}

return eth2Cl.SubmitAttestations(ctx, atts)
}

// eth2DomainProvider is the subset of eth2 beacon api provider required to get a signing domain.
type eth2DomainProvider interface {
eth2client.SpecProvider
eth2client.DomainProvider
}

// getDomain returns the beacon domain for the provided type.
// Types are defined in eth2 spec, see "specs/[phase0|altair]/beacon-chain.md#domain-types"
// at https://github.com/ethereum/consensus-specs.
func getDomain(ctx context.Context, eth2Cl eth2DomainProvider, typ string, epoch eth2p0.Epoch) (eth2p0.Domain, error) {
spec, err := eth2Cl.Spec(ctx)
if err != nil {
return eth2p0.Domain{}, err
}

domainType, ok := spec[typ]
if !ok {
return eth2p0.Domain{}, errors.New("domain type not found")
}

domainTyped, ok := domainType.(eth2p0.DomainType)
if !ok {
return eth2p0.Domain{}, errors.New("invalid domain type")
}

return eth2Cl.Domain(ctx, domainTyped, epoch)
}
80 changes: 80 additions & 0 deletions testutil/validatormock/validatormock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package validatormock_test

import (
"context"
"fmt"
"testing"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/testutil"
"github.com/obolnetwork/charon/testutil/beaconmock"
"github.com/obolnetwork/charon/testutil/validatormock"
)

//go:generate go test -run=TestAttest -update -clean

func TestAttest(t *testing.T) {
tests := []struct {
DutyFactor int
Expect int
}{
{
DutyFactor: 0, // All validators in first slot of epoch
Expect: 3,
},
{
DutyFactor: 1, // Validators spread over 1st, 2nd, 3rd slots of epoch
Expect: 1,
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.DutyFactor), func(t *testing.T) {
ctx := context.Background()

// Configure beacon mock
static, err := beaconmock.NewStaticProvider(ctx)
require.NoError(t, err)

valSet := beaconmock.ValidatorSetA
beaconMock := beaconmock.New(
beaconmock.WithStaticProvider(static),
beaconmock.WithValidatorSet(valSet),
beaconmock.WithDeterministicDuties(test.DutyFactor),
)
require.NoError(t, err)

// Callback to collect attestations
var atts []*eth2p0.Attestation
beaconMock.SubmitAttestationsFunc = func(_ context.Context, attestations []*eth2p0.Attestation) error {
atts = attestations
return nil
}

// Signature stub function
signFunc := func(ctx context.Context, key eth2p0.BLSPubKey, _ eth2p0.SigningData) (eth2p0.BLSSignature, error) {
var sig eth2p0.BLSSignature
copy(sig[:], key[:])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do actual signature using kryptology?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This make it easy to see which pubkey signed in the testdata.


return sig, nil
}

// Get first slot in epoch 1
slotsPerEpoch, err := static.SlotsPerEpoch(ctx)
require.NoError(t, err)

// Call attest function
err = validatormock.Attest(ctx,
beaconMock, signFunc,
eth2p0.Slot(slotsPerEpoch),
valSet.ETH2PubKeys(),
)
require.NoError(t, err)

// Assert length and expected attestations
require.Len(t, atts, test.Expect)
testutil.RequireGoldenJSON(t, atts)
})
}
}