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

core: voluntary exit #648

Merged
merged 19 commits into from
Jun 3, 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
20 changes: 20 additions & 0 deletions core/bcast/bcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
type eth2Provider interface {
eth2client.AttestationsSubmitter
eth2client.BeaconBlockSubmitter
eth2client.VoluntaryExitSubmitter
}

// New returns a new broadcaster instance.
Expand Down Expand Up @@ -72,6 +73,7 @@ func (b Broadcaster) Broadcast(ctx context.Context, duty core.Duty,
z.U64("slot", uint64(att.Data.Slot)),
z.U64("target_epoch", uint64(att.Data.Target.Epoch)),
z.Hex("agg_bits", att.AggregationBits.Bytes()),
z.Any("pubkey", pubkey),
)
}

Expand All @@ -86,6 +88,24 @@ func (b Broadcaster) Broadcast(ctx context.Context, duty core.Duty,
if err == nil {
log.Info(ctx, "Block proposal successfully submitted to beacon node",
z.U64("slot", uint64(duty.Slot)),
z.Any("pubkey", pubkey),
)
}

return err

case core.DutyExit:
exit, err := core.DecodeExitAggSignedData(aggData)
if err != nil {
return err
}

err = b.eth2Cl.SubmitVoluntaryExit(ctx, exit)
if err == nil {
log.Info(ctx, "Voluntary exit successfully submitted to beacon node",
z.U64("epoch", uint64(exit.Message.Epoch)),
z.U64("validator_index", uint64(exit.Message.ValidatorIndex)),
z.Any("pubkey", pubkey),
)
}

Expand Down
25 changes: 25 additions & 0 deletions core/bcast/bcast_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,28 @@ func TestBroadcastBeaconBlock(t *testing.T) {

<-ctx.Done()
}

func TestBroadcastExit(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
mock, err := beaconmock.New()
require.NoError(t, err)

exit := testutil.RandomExit()
aggData, err := core.EncodeExitAggSignedData(exit)
require.NoError(t, err)

mock.SubmitVoluntaryExitFunc = func(ctx context.Context, exit2 *eth2p0.SignedVoluntaryExit) error {
require.Equal(t, *exit, *exit2)
cancel()

return ctx.Err()
}

bcaster, err := bcast.New(mock)
require.NoError(t, err)

err = bcaster.Broadcast(ctx, core.Duty{Type: core.DutyExit}, "", aggData)
require.ErrorIs(t, err, context.Canceled)

<-ctx.Done()
}
51 changes: 50 additions & 1 deletion core/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ func EncodeAttestationParSignedData(att *eth2p0.Attestation, shareIdx int) (ParS
}, nil
}

// EncodeExitParSignedData returns the signed voluntary exit as an encoded ParSignedData.
func EncodeExitParSignedData(exit *eth2p0.SignedVoluntaryExit, shareIdx int) (ParSignedData, error) {
data, err := json.Marshal(exit)
if err != nil {
return ParSignedData{}, errors.Wrap(err, "marshal signed voluntary exit")
}

return ParSignedData{
Data: data,
Signature: SigFromETH2(exit.Signature),
ShareIdx: shareIdx,
}, nil
}

// DecodeAttestationParSignedData returns the attestation from the encoded ParSignedData.
func DecodeAttestationParSignedData(data ParSignedData) (*eth2p0.Attestation, error) {
att := new(eth2p0.Attestation)
Expand All @@ -113,6 +127,17 @@ func DecodeAttestationParSignedData(data ParSignedData) (*eth2p0.Attestation, er
return att, nil
}

// DecodeExitParSignedData returns the signed voluntary exit from the encoded ParSignedData.
func DecodeExitParSignedData(data ParSignedData) (*eth2p0.SignedVoluntaryExit, error) {
exit := new(eth2p0.SignedVoluntaryExit)
err := json.Unmarshal(data.Data, exit)
if err != nil {
return nil, errors.Wrap(err, "unmarshal signed voluntary exit")
}

return exit, nil
}

// EncodeAttestationAggSignedData returns the attestation as an encoded AggSignedData.
func EncodeAttestationAggSignedData(att *eth2p0.Attestation) (AggSignedData, error) {
data, err := json.Marshal(att)
Expand Down Expand Up @@ -231,7 +256,7 @@ func DecodeBlockParSignedData(data ParSignedData) (*spec.VersionedSignedBeaconBl
return block, nil
}

// EncodeBlockAggSignedData returns the partially signed block data as an encoded AggSignedData.
// EncodeBlockAggSignedData returns the aggregated signed block data as an encoded AggSignedData.
func EncodeBlockAggSignedData(block *spec.VersionedSignedBeaconBlock) (AggSignedData, error) {
data, err := json.Marshal(block)
if err != nil {
Expand Down Expand Up @@ -275,3 +300,27 @@ func DecodeBlockAggSignedData(data AggSignedData) (*spec.VersionedSignedBeaconBl

return block, nil
}

// EncodeExitAggSignedData returns the aggregated signed voluntary exit from the encoded AggSignedData.
func EncodeExitAggSignedData(exit *eth2p0.SignedVoluntaryExit) (AggSignedData, error) {
data, err := json.Marshal(exit)
if err != nil {
return AggSignedData{}, errors.Wrap(err, "marshal voluntary exit")
}

return AggSignedData{
Data: data,
Signature: SigFromETH2(exit.Signature),
}, nil
}

// DecodeExitAggSignedData returns the aggregated signed voluntary exit from the encoded AggSignedData.
func DecodeExitAggSignedData(data AggSignedData) (*eth2p0.SignedVoluntaryExit, error) {
var resp *eth2p0.SignedVoluntaryExit
err := json.Unmarshal(data.Data, &resp)
if err != nil {
return nil, errors.Wrap(err, "unmarshal voluntary exit")
}

return resp, nil
}
30 changes: 30 additions & 0 deletions core/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ func TestEncodeAttesterAggSignedData(t *testing.T) {
require.Equal(t, data1, data2)
}

func TestEncodExitAggSignedData(t *testing.T) {
exit1 := testutil.RandomExit()

data1, err := core.EncodeExitAggSignedData(exit1)
require.NoError(t, err)

exit2, err := core.DecodeExitAggSignedData(data1)
require.NoError(t, err)

data2, err := core.EncodeExitAggSignedData(exit2)
require.NoError(t, err)

require.Equal(t, exit1, exit2)
require.Equal(t, data1, data2)
}

func TestEncodeRandaoParSignedData(t *testing.T) {
randao1 := testutil.RandomEth2Signature()

Expand All @@ -104,6 +120,20 @@ func TestEncodeRandaoParSignedData(t *testing.T) {
require.Equal(t, data1, data2)
}

func TestEncodeExitParSignedData(t *testing.T) {
exit1 := testutil.RandomExit()

data1, err := core.EncodeExitParSignedData(exit1, 1)
require.NoError(t, err)
exit2, err := core.DecodeExitParSignedData(data1)
require.NoError(t, err)
data2, err := core.EncodeExitParSignedData(exit2, 1)
require.NoError(t, err)

require.Equal(t, exit1, exit2)
require.Equal(t, data1, data2)
}

func TestEncodeRandaoAggSignedData(t *testing.T) {
randao1 := testutil.RandomEth2Signature()

Expand Down
16 changes: 16 additions & 0 deletions core/sigagg/sigagg.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ func getAggSignedData(typ core.DutyType, data core.ParSignedData, aggSig *bls_si
}

return core.EncodeBlockAggSignedData(block)
case core.DutyExit:
exit, err := core.DecodeExitParSignedData(data)
if err != nil {
return core.AggSignedData{}, err
}

exit.Signature = eth2Sig

return core.EncodeExitAggSignedData(exit)
default:
return core.AggSignedData{}, errors.New("unsupported duty type")
}
Expand Down Expand Up @@ -178,6 +187,13 @@ func getSignedRoot(typ core.DutyType, data core.ParSignedData) (eth2p0.Root, err
}

return block.Root()
case core.DutyExit:
ve, err := core.DecodeExitParSignedData(data)
if err != nil {
return eth2p0.Root{}, err
}

return ve.Message.HashTreeRoot()
default:
return eth2p0.Root{}, errors.New("unsupported duty type")
}
Expand Down
68 changes: 68 additions & 0 deletions core/sigagg/sigagg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package sigagg_test
import (
"context"
"crypto/rand"
"encoding/json"
"testing"

"github.com/attestantio/go-eth2-client/spec"
Expand Down Expand Up @@ -151,6 +152,73 @@ func TestSigAgg_DutyRandao(t *testing.T) {
require.NoError(t, err)
}

func TestSigAgg_DutyExit(t *testing.T) {
ctx := context.Background()

const (
threshold = 3
peers = 4
)

// Generate private shares
tss, secrets, err := tbls.GenerateTSS(threshold, peers, rand.Reader)
require.NoError(t, err)

exit := testutil.RandomExit()
msg, err := exit.Message.MarshalSSZ()
require.NoError(t, err)

// Create partial signatures (in two formats)
var (
parsigs []core.ParSignedData
psigs []*bls_sig.PartialSignature
)
for _, secret := range secrets {
// Ignoring domain for this test
psig, err := tbls.PartialSign(secret, msg)
require.NoError(t, err)

sig := tblsconv.SigToETH2(tblsconv.SigFromPartial(psig))
data, err := json.Marshal(&eth2p0.SignedVoluntaryExit{
Message: exit.Message,
Signature: sig,
})
require.NoError(t, err)

parsig := core.ParSignedData{
Data: data,
Signature: core.SigFromETH2(sig),
ShareIdx: int(psig.Identifier),
}

psigs = append(psigs, psig)
parsigs = append(parsigs, parsig)
}

aggSig, err := tbls.Aggregate(psigs)
require.NoError(t, err)
expect := tblsconv.SigToCore(aggSig)

agg := sigagg.New(threshold)

// Assert output
agg.Subscribe(func(_ context.Context, _ core.Duty, _ core.PubKey, aggData core.AggSignedData) error {
require.Equal(t, expect, aggData.Signature)
sig, err := tblsconv.SigFromCore(aggData.Signature)
require.NoError(t, err)

ok, err := tbls.Verify(tss.PublicKey(), msg, sig)
require.NoError(t, err)
require.True(t, ok)

return nil
})

// Run aggregation
err = agg.Aggregate(ctx, core.Duty{Type: core.DutyExit}, "", parsigs)
require.NoError(t, err)
}

func TestSigAgg_DutyProposer(t *testing.T) {
ctx := context.Background()

Expand Down
4 changes: 3 additions & 1 deletion core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ const (
DutyProposer DutyType = 1
DutyAttester DutyType = 2
DutyRandao DutyType = 3
DutyExit DutyType = 4
// Only ever append new types here...

dutySentinel DutyType = 4 // Must always be last
dutySentinel DutyType = 5 // Must always be last
)

func (d DutyType) Valid() bool {
Expand All @@ -51,6 +52,7 @@ func (d DutyType) String() string {
DutyAttester: "attester",
DutyProposer: "proposer",
DutyRandao: "randao",
DutyExit: "exit",
}[d]
}

Expand Down
10 changes: 8 additions & 2 deletions core/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ func TestBackwardsCompatability(t *testing.T) {
require.EqualValues(t, 1, core.DutyProposer)
require.EqualValues(t, 2, core.DutyAttester)
require.EqualValues(t, 3, core.DutyRandao)
require.EqualValues(t, 4, core.DutyExit)
// Add more types here.

const sentinel = core.DutyType(4)
const sentinel = core.DutyType(5)
for i := core.DutyUnknown; i <= sentinel; i++ {
if i == core.DutyUnknown || i == sentinel {
if i == core.DutyUnknown {
require.False(t, i.Valid())
require.Equal(t, "unknown", i.String())
} else if i == sentinel {
require.False(t, i.Valid())
require.Equal(t, "", i.String())
} else {
require.True(t, i.Valid())
require.NotEmpty(t, i.String())
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions core/validatorapi/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Handler interface {
eth2client.BeaconBlockSubmitter
eth2client.ProposerDutiesProvider
eth2client.ValidatorsProvider
eth2client.VoluntaryExitSubmitter
// Above sorted alphabetically.
}

Expand Down Expand Up @@ -112,6 +113,11 @@ func NewRouter(h Handler, beaconNodeAddr string) (*mux.Router, error) {
Path: "/eth/v1/beacon/blocks",
Handler: submitBlock(h),
},
{
Name: "submit_voluntary_exit",
Path: "/eth/v1/beacon/pool/voluntary_exits",
Handler: submitExit(h),
},
// TODO(corver): Add more endpoints
}

Expand Down Expand Up @@ -439,6 +445,18 @@ func submitBlock(p eth2client.BeaconBlockSubmitter) handlerFunc {
}
}

// submitExit returns a handler function for the exit submitter endpoint.
func submitExit(p eth2client.VoluntaryExitSubmitter) handlerFunc {
return func(ctx context.Context, _ map[string]string, _ url.Values, body []byte) (interface{}, error) {
exit := new(eth2p0.SignedVoluntaryExit)
if err := exit.UnmarshalJSON(body); err != nil {
return nil, errors.Wrap(err, "unmarshal signed voluntary exit")
}

return nil, p.SubmitVoluntaryExit(ctx, exit)
}
}

// proxyHandler returns a reverse proxy handler.
func proxyHandler(target string) (http.HandlerFunc, error) {
targetURL, err := url.Parse(target)
Expand Down
Loading