diff --git a/http/service_test.go b/http/service_test.go index 9a6e3c58c..e75a2b7fa 100644 --- a/http/service_test.go +++ b/http/service_test.go @@ -145,6 +145,7 @@ func TestInterfaces(t *testing.T) { assert.Implements(t, (*client.ValidatorBalancesProvider)(nil), s) assert.Implements(t, (*client.ValidatorsProvider)(nil), s) assert.Implements(t, (*client.VoluntaryExitSubmitter)(nil), s) + assert.Implements(t, (*client.VoluntaryExitPoolProvider)(nil), s) // Non-standard extensions. assert.Implements(t, (*client.DomainProvider)(nil), s) diff --git a/http/voluntaryexitpool.go b/http/voluntaryexitpool.go new file mode 100644 index 000000000..5ce9cbdf9 --- /dev/null +++ b/http/voluntaryexitpool.go @@ -0,0 +1,49 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http + +import ( + "context" + "encoding/json" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +type voluntaryExitPoolJSON struct { + Data []*phase0.SignedVoluntaryExit `json:"data"` +} + +// VoluntaryExitPool obtains the voluntary exit pool. +func (s *Service) VoluntaryExitPool(ctx context.Context) ([]*phase0.SignedVoluntaryExit, error) { + respBodyReader, err := s.get(ctx, "/eth/v1/beacon/pool/voluntary_exits") + if err != nil { + return nil, errors.Wrap(err, "failed to request voluntary exit pool") + } + if respBodyReader == nil { + return nil, errors.New("failed to obtain voluntary exit pool") + } + + var voluntaryExitPoolJSON voluntaryExitPoolJSON + if err := json.NewDecoder(respBodyReader).Decode(&voluntaryExitPoolJSON); err != nil { + return nil, errors.Wrap(err, "failed to parse voluntary exit pool") + } + + // Ensure the data returned to us is as expected given our input. + if voluntaryExitPoolJSON.Data == nil { + return nil, errors.New("voluntary exit pool not returned") + } + + return voluntaryExitPoolJSON.Data, nil +} diff --git a/http/voluntaryexitpool_test.go b/http/voluntaryexitpool_test.go new file mode 100644 index 000000000..82e6964b4 --- /dev/null +++ b/http/voluntaryexitpool_test.go @@ -0,0 +1,51 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package http_test + +import ( + "context" + "os" + "testing" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/http" + "github.com/stretchr/testify/require" +) + +func TestVoluntartExitPool(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tests := []struct { + name string + }{ + { + name: "Good", + }, + } + + service, err := http.New(ctx, + http.WithTimeout(timeout), + http.WithAddress(os.Getenv("HTTP_ADDRESS")), + ) + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + voluntaryExitPool, err := service.(client.VoluntaryExitPoolProvider).VoluntaryExitPool(ctx) + require.NoError(t, err) + require.NotNil(t, voluntaryExitPool) + }) + } +} diff --git a/mock/voluntaryexitpool.go b/mock/voluntaryexitpool.go new file mode 100644 index 000000000..a4ffb48de --- /dev/null +++ b/mock/voluntaryexitpool.go @@ -0,0 +1,43 @@ +// Copyright © 2020 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mock + +import ( + "context" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// VoluntaryExitPool fetches the voluntary exit pool. +func (s *Service) VoluntaryExitPool(_ context.Context) ([]*spec.SignedVoluntaryExit, error) { + res := make([]*spec.SignedVoluntaryExit, 5) + for i := 0; i < 5; i++ { + res[i] = &spec.SignedVoluntaryExit{ + Message: &spec.VoluntaryExit{ + Epoch: 1, + ValidatorIndex: 1, + }, + Signature: spec.BLSSignature([96]byte{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + }), + } + } + + return res, nil +} diff --git a/multi/service_test.go b/multi/service_test.go index 75055e535..85e493563 100644 --- a/multi/service_test.go +++ b/multi/service_test.go @@ -134,6 +134,7 @@ func TestInterfaces(t *testing.T) { assert.Implements(t, (*client.ValidatorBalancesProvider)(nil), s) assert.Implements(t, (*client.ValidatorsProvider)(nil), s) assert.Implements(t, (*client.VoluntaryExitSubmitter)(nil), s) + assert.Implements(t, (*client.VoluntaryExitPoolProvider)(nil), s) // Non-standard extensions. assert.Implements(t, (*client.DomainProvider)(nil), s) diff --git a/multi/voluntaryexitpool.go b/multi/voluntaryexitpool.go new file mode 100644 index 000000000..d2bcb45e8 --- /dev/null +++ b/multi/voluntaryexitpool.go @@ -0,0 +1,39 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multi + +import ( + "context" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// VoluntaryExitPool obtains the voluntary exit pool. +func (s *Service) VoluntaryExitPool(ctx context.Context) ([]*phase0.SignedVoluntaryExit, error) { + res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (interface{}, error) { + voluntaryExitPool, err := client.(consensusclient.VoluntaryExitPoolProvider).VoluntaryExitPool(ctx) + if err != nil { + return nil, err + } + return voluntaryExitPool, nil + }, nil) + if err != nil { + return nil, err + } + if res == nil { + return nil, nil + } + return res.([]*phase0.SignedVoluntaryExit), nil +} diff --git a/multi/voluntaryexitpool_test.go b/multi/voluntaryexitpool_test.go new file mode 100644 index 000000000..e75100559 --- /dev/null +++ b/multi/voluntaryexitpool_test.go @@ -0,0 +1,59 @@ +// Copyright © 2021 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multi_test + +import ( + "context" + "testing" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/mock" + "github.com/attestantio/go-eth2-client/multi" + "github.com/attestantio/go-eth2-client/testclients" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +func TestVoluntaryExitPool(t *testing.T) { + ctx := context.Background() + + client1, err := mock.New(ctx, mock.WithName("mock 1")) + require.NoError(t, err) + erroringClient1, err := testclients.NewErroring(ctx, 0.1, client1) + require.NoError(t, err) + client2, err := mock.New(ctx, mock.WithName("mock 2")) + require.NoError(t, err) + erroringClient2, err := testclients.NewErroring(ctx, 0.1, client2) + require.NoError(t, err) + client3, err := mock.New(ctx, mock.WithName("mock 3")) + require.NoError(t, err) + + multiClient, err := multi.New(ctx, + multi.WithLogLevel(zerolog.Disabled), + multi.WithClients([]consensusclient.Service{ + erroringClient1, + erroringClient2, + client3, + }), + ) + require.NoError(t, err) + + for i := 0; i < 128; i++ { + res, err := multiClient.(consensusclient.VoluntaryExitPoolProvider).VoluntaryExitPool(ctx) + require.NoError(t, err) + require.NotNil(t, res) + } + // At this point we expect mock 3 to be in active (unless probability hates us). + require.Equal(t, "mock 3", multiClient.Address()) +} diff --git a/service.go b/service.go index afb0eb6e8..c6554dd9f 100644 --- a/service.go +++ b/service.go @@ -383,6 +383,12 @@ type VoluntaryExitSubmitter interface { SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase0.SignedVoluntaryExit) error } +// VoluntaryExitPoolProvider is the interface for providing voluntary exit pools. +type VoluntaryExitPoolProvider interface { + // VoluntaryExitPool fetches the voluntary exit pool. + VoluntaryExitPool(ctx context.Context) ([]*phase0.SignedVoluntaryExit, error) +} + // // Local extensions // diff --git a/testclients/erroring.go b/testclients/erroring.go index 915c9d8f5..e3994671a 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -618,6 +618,18 @@ func (s *Erroring) SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase return next.SubmitVoluntaryExit(ctx, voluntaryExit) } +// VoluntaryExitPool fetches the voluntary exit pool. +func (s *Erroring) VoluntaryExitPool(ctx context.Context) ([]*phase0.SignedVoluntaryExit, error) { + if err := s.maybeError(ctx); err != nil { + return nil, err + } + next, isNext := s.next.(consensusclient.VoluntaryExitPoolProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + return next.VoluntaryExitPool(ctx) +} + // Domain provides a domain for a given domain type at a given epoch. func (s *Erroring) Domain(ctx context.Context, domainType phase0.DomainType, epoch phase0.Epoch) (phase0.Domain, error) { if err := s.maybeError(ctx); err != nil { diff --git a/testclients/sleepy.go b/testclients/sleepy.go index 82050a658..20a323be7 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -427,6 +427,16 @@ func (s *Sleepy) SubmitVoluntaryExit(ctx context.Context, voluntaryExit *phase0. return next.SubmitVoluntaryExit(ctx, voluntaryExit) } +// VoluntaryExitPool fetches the voluntary exit pool. +func (s *Sleepy) VoluntaryExitPool(ctx context.Context) ([]*phase0.SignedVoluntaryExit, error) { + s.sleep(ctx) + next, isNext := s.next.(consensusclient.VoluntaryExitPoolProvider) + if !isNext { + return nil, errors.New("next does not support this call") + } + return next.VoluntaryExitPool(ctx) +} + // Domain provides a domain for a given domain type at a given epoch. func (s *Sleepy) Domain(ctx context.Context, domainType phase0.DomainType, epoch phase0.Epoch) (phase0.Domain, error) { s.sleep(ctx)