Skip to content

Commit

Permalink
testutil/validatormock: implement (#187)
Browse files Browse the repository at this point in the history
To test signature aggregation, we need to create signatures. This add a validator client mock that performs attestations, including signatures as per spec. This will be used to test the SigAgg component.

category: testing
ticket: #184
  • Loading branch information
corverroos committed Mar 7, 2022
1 parent 0629cfe commit 518a573
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 0 deletions.
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
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)

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[:])

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)
})
}
}

0 comments on commit 518a573

Please sign in to comment.