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

app: fix builder registration and add teku integration test #957

Merged
merged 11 commits into from
Aug 11, 2022
43 changes: 40 additions & 3 deletions app/simnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ func TestSimnetNoNetwork_WithExitTekuVC(t *testing.T) {
testSimnet(t, args)
}

func TestSimnetNoNetwork_WithBuilderRegistrationTekuVC(t *testing.T) {
if !*integration {
t.Skip("Skipping Teku integration test")
}

args := newSimnetArgs(t)
args.BuilderRegistration = true
for i := 0; i < args.N; i++ {
args = startTeku(t, args, i, tekuRegister)
}
args.BMockOpts = append(args.BMockOpts, beaconmock.WithNoAttesterDuties())
args.BMockOpts = append(args.BMockOpts, beaconmock.WithNoProposerDuties())
testSimnet(t, args)
}

func TestSimnetNoNetwork_WithAttesterMockVCs(t *testing.T) {
args := newSimnetArgs(t)
args.BMockOpts = append(args.BMockOpts, beaconmock.WithNoProposerDuties())
Expand Down Expand Up @@ -319,8 +334,24 @@ func newRegistrationProvider(t *testing.T, args simnetArgs) func() <-chan *eth2a
type tekuCmd []string

var (
tekuVC tekuCmd = []string{"validator-client", "--network=auto"}
tekuExit tekuCmd = []string{"voluntary-exit", "--confirmation-enabled=false", "--epoch=1"}
tekuVC tekuCmd = []string{
"validator-client",
"--network=auto",
"--log-destination=console",
"--validators-proposer-default-fee-recipient=0x000000000000000000000000000000000000dead",
}
tekuExit tekuCmd = []string{
"voluntary-exit",
"--confirmation-enabled=false",
"--epoch=1",
}
tekuRegister tekuCmd = []string{
"validator-client",
"--network=auto",
"--log-destination=console",
"--validators-proposer-default-fee-recipient=0x000000000000000000000000000000000000dead",
"--validators-proposer-config-refresh-enabled=true",
}
)

// startTeku starts a teku validator client for the provided node and returns updated args.
Expand Down Expand Up @@ -348,6 +379,12 @@ func startTeku(t *testing.T, args simnetArgs, node int, cmd tekuCmd) simnetArgs
fmt.Sprintf("--beacon-node-api-endpoint=http://%s", args.VAPIAddrs[node]),
)

if args.BuilderRegistration {
tekuArgs = append(tekuArgs,
fmt.Sprintf("--validators-proposer-config=http://%s/teku_proposer_config", args.VAPIAddrs[node]),
)
}

// Configure docker
name := fmt.Sprint(time.Now().UnixNano())
dockerArgs := []string{
Expand All @@ -356,7 +393,7 @@ func startTeku(t *testing.T, args simnetArgs, node int, cmd tekuCmd) simnetArgs
fmt.Sprintf("--name=%s", name),
fmt.Sprintf("--volume=%s:/keys", tempDir),
"--user=root", // Root required to read volume files in GitHub actions.
"consensys/teku:latest",
"consensys/teku:develop",
}
dockerArgs = append(dockerArgs, tekuArgs...)
t.Logf("docker args: %v", dockerArgs)
Expand Down
4 changes: 2 additions & 2 deletions core/deadline.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ func NewDutyDeadlineFunc(ctx context.Context, eth2Svc eth2client.Service) (func(
}

return func(duty Duty) time.Time {
if duty.Type == DutyExit {
// Do not timeout exit duties.
if duty.Type == DutyExit || duty.Type == DutyBuilderRegistration {
// Do not timeout exit or registration duties.
return time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC)
}

Expand Down
2 changes: 1 addition & 1 deletion core/sigagg/sigagg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ func TestSigAgg_DutyBuilderRegistration(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Ignoring Domain for this test
msg, err := test.registration.V1.Message.HashTreeRoot()
msg, err := test.registration.Root()
require.NoError(t, err)

// Create partial signatures (in two formats)
Expand Down
13 changes: 12 additions & 1 deletion core/validatorapi/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Handler interface {
eth2client.ValidatorsProvider
eth2client.ValidatorRegistrationsSubmitter
eth2client.VoluntaryExitSubmitter
TekuProposerConfigProvider
// Above sorted alphabetically.
}

Expand Down Expand Up @@ -137,7 +138,11 @@ func NewRouter(h Handler, eth2Cl eth2client.Service) (*mux.Router, error) {
Path: "/eth/v1/beacon/pool/voluntary_exits",
Handler: submitExit(h),
},
// TODO(corver): Add more endpoints
{
Name: "teku_proposer_config",
Path: "/teku_proposer_config",
Handler: tekuProposerConfig(h),
},
}

r := mux.NewRouter()
Expand Down Expand Up @@ -537,6 +542,12 @@ func submitExit(p eth2client.VoluntaryExitSubmitter) handlerFunc {
}
}

func tekuProposerConfig(p TekuProposerConfigProvider) handlerFunc {
return func(ctx context.Context, _ map[string]string, _ url.Values, _ []byte) (interface{}, error) {
return p.TekuProposerConfig(ctx)
}
}

// proxyHandler returns a reverse proxy handler.
func proxyHandler(eth2Cl eth2client.Service) (http.HandlerFunc, error) {
return func(w http.ResponseWriter, r *http.Request) {
Expand Down
74 changes: 74 additions & 0 deletions core/validatorapi/teku.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package validatorapi

import (
"context"
"fmt"
)

type TekuProposerConfigResponse struct {
Proposers map[string]TekuProposerConfig `json:"proposer_config"`
Default TekuProposerConfig `json:"default_config"`
}

type TekuProposerConfig struct {
FeeRecipient string `json:"fee_recipient"`
Builder TekuBuilder `json:"builder"`
}

type TekuBuilder struct {
Enabled bool `json:"enabled"`
Overrides map[string]string `json:"registration_overrides"`
}

const dead = "0x000000000000000000000000000000000000dead"

type TekuProposerConfigProvider interface {
TekuProposerConfig(ctx context.Context) (TekuProposerConfigResponse, error)
}

func (c Component) TekuProposerConfig(ctx context.Context) (TekuProposerConfigResponse, error) {
resp := TekuProposerConfigResponse{
Proposers: make(map[string]TekuProposerConfig),
Default: TekuProposerConfig{ // Default doesn't make sense, disable for now.
FeeRecipient: dead,
Builder: TekuBuilder{
Enabled: false,
},
},
}

genesis, err := c.eth2Cl.GenesisTime(ctx)
if err != nil {
return TekuProposerConfigResponse{}, nil
}

for pubkey, pubshare := range c.sharesByKey {
resp.Proposers[string(pubshare)] = TekuProposerConfig{
FeeRecipient: dead,
Builder: TekuBuilder{
Enabled: true,
Overrides: map[string]string{
"timestamp": fmt.Sprint(genesis.Unix()),
"public_key": string(pubkey),
},
},
}
}

return resp, nil
}
9 changes: 9 additions & 0 deletions core/validatorapi/validatorapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,18 @@ func NewComponent(eth2Svc eth2client.Service, pubShareByKey map[*bls_sig.PublicK
sharesByKey = make(map[eth2p0.BLSPubKey]eth2p0.BLSPubKey)
keysByShare = make(map[eth2p0.BLSPubKey]eth2p0.BLSPubKey)
sharesByCoreKey = make(map[core.PubKey]*bls_sig.PublicKey)
coreSharesByKey = make(map[core.PubKey]core.PubKey)
)

for pubkey, pubshare := range pubShareByKey {
coreKey, err := tblsconv.KeyToCore(pubkey)
if err != nil {
return nil, err
}
coreShare, err := tblsconv.KeyToCore(pubshare)
if err != nil {
return nil, err
}
key, err := tblsconv.KeyToETH2(pubkey)
if err != nil {
return nil, err
Expand All @@ -101,6 +106,7 @@ func NewComponent(eth2Svc eth2client.Service, pubShareByKey map[*bls_sig.PublicK
return nil, err
}
sharesByCoreKey[coreKey] = pubshare
coreSharesByKey[coreKey] = coreShare
sharesByKey[key] = share
keysByShare[share] = key
}
Expand Down Expand Up @@ -133,6 +139,7 @@ func NewComponent(eth2Svc eth2client.Service, pubShareByKey map[*bls_sig.PublicK
getVerifyShareFunc: getVerifyShareFunc,
getPubShareFunc: getPubShareFunc,
getPubKeyFunc: getPubKeyFunc,
sharesByKey: coreSharesByKey,
eth2Cl: eth2Cl,
shareIdx: shareIdx,
}, nil
Expand All @@ -150,6 +157,8 @@ type Component struct {
getPubShareFunc func(eth2p0.BLSPubKey) (eth2p0.BLSPubKey, bool)
// getPubKeyFunc return the root public key for a public shares.
getPubKeyFunc func(eth2p0.BLSPubKey) (eth2p0.BLSPubKey, error)
// sharesByKey contains this nodes public shares (value) by root public (key)
sharesByKey map[core.PubKey]core.PubKey

// Registered input functions

Expand Down
40 changes: 5 additions & 35 deletions eth2util/signing/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,42 +75,12 @@ func GetDomain(ctx context.Context, eth2Cl Eth2Provider, name DomainName, epoch
return eth2Cl.Domain(ctx, domainTyped, epoch)
}

// GetRegistrationDomain returns a non-standard domain for validator builder registration.
// See https://github.com/ethereum/builder-specs/blob/main/specs/builder.md#signing.
func GetRegistrationDomain() (eth2p0.Domain, error) {
root, err := (&eth2p0.ForkData{}).HashTreeRoot() // Zero fork data
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "hash fork data")
}

// See https://github.com/ethereum/builder-specs/blob/main/specs/builder.md#domain-types.
registrationDomainType := eth2p0.DomainType{0, 0, 0, 1}

var domain eth2p0.Domain
copy(domain[0:], registrationDomainType[:])
copy(domain[4:], root[:])

return domain, nil
}

// GetDataRoot wraps the signing root with the domain and returns signing data hash tree root.
// The result should be identical to what was signed by the VC.
func GetDataRoot(ctx context.Context, eth2Cl Eth2Provider, name DomainName, epoch eth2p0.Epoch, root eth2p0.Root) ([32]byte, error) {
var (
domain eth2p0.Domain
err error
)
if name == DomainApplicationBuilder {
// Builder registration uses non-standard domain.
domain, err = GetRegistrationDomain()
if err != nil {
return [32]byte{}, err
}
} else {
domain, err = GetDomain(ctx, eth2Cl, name, epoch)
if err != nil {
return [32]byte{}, err
}
domain, err := GetDomain(ctx, eth2Cl, name, epoch)
if err != nil {
return [32]byte{}, err
}

msg, err := (&eth2p0.SigningData{ObjectRoot: root, Domain: domain}).HashTreeRoot()
Expand Down Expand Up @@ -220,12 +190,12 @@ func VerifyVoluntaryExit(ctx context.Context, eth2Cl Eth2Provider, pubkey *bls_s
}

func VerifyValidatorRegistration(ctx context.Context, eth2Cl Eth2Provider, pubkey *bls_sig.PublicKey, reg *eth2api.VersionedSignedValidatorRegistration) error {
// TODO: switch to signed.Root() when implemented on go-eth2-client (PR has been raised)
sigRoot, err := reg.V1.Message.HashTreeRoot()
sigRoot, err := reg.Root()
if err != nil {
return err
}

// Always use epoch 0 for DomainApplicationBuilder.
return verify(ctx, eth2Cl, DomainApplicationBuilder, 0, sigRoot, reg.V1.Signature, pubkey)
}

Expand Down
51 changes: 49 additions & 2 deletions eth2util/signing/signing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ package signing_test

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"testing"

eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/coinbase/kryptology/pkg/signatures/bls/bls_sig"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -145,16 +149,59 @@ func TestVerifyBlindedBeaconBlock(t *testing.T) {
require.NoError(t, signing.VerifyBlindedBlock(context.Background(), bmock, pubkey, &versionedBlock))
}

func TestVerifyRegistrationReference(t *testing.T) {
bmock, err := beaconmock.New()
require.NoError(t, err)

// Test data obtained from teku.

secretShareBytes, err := hex.DecodeString("345768c0245f1dc702df9e50e811002f61ebb2680b3d5931527ef59f96cbaf9b")
require.NoError(t, err)

secretShare, err := tblsconv.SecretFromBytes(secretShareBytes)
require.NoError(t, err)

registrationJSON := `
{
"message": {
"fee_recipient": "0x000000000000000000000000000000000000dead",
"gas_limit": "30000000",
"timestamp": "1646092800",
"pubkey": "0x86966350b672bd502bfbdb37a6ea8a7392e8fb7f5ebb5c5e2055f4ee168ebfab0fef63084f28c9f62c3ba71f825e527e"
},
"signature": "0xb101da0fc08addcc5d010ee569f6bbbdca049a5cb27efad231565bff2e3af504ec2bb87b11ed22843e9c1094f1dfe51a0b2a5ad1808df18530a2f59f004032dbf6281ecf0fc3df86d032da5b9d32a3d282c05923de491381f8f28c2863a00180"
}`

registration := new(eth2v1.SignedValidatorRegistration)
err = json.Unmarshal([]byte(registrationJSON), registration)
require.NoError(t, err)

sigRoot, err := registration.Message.HashTreeRoot()
require.NoError(t, err)

sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainApplicationBuilder, 0, sigRoot)
require.NoError(t, err)

sig, err := tbls.Sign(secretShare, sigData[:])
require.NoError(t, err)

sigEth2 := tblsconv.SigToETH2(sig)
require.Equal(t,
fmt.Sprintf("%x", registration.Signature),
fmt.Sprintf("%x", sigEth2),
)
}

func TestVerifyBuilderRegistration(t *testing.T) {
bmock, err := beaconmock.New()
require.NoError(t, err)

registration := testutil.RandomCoreVersionedSignedValidatorRegistration(t).VersionedSignedValidatorRegistration

sigRoot, err := registration.V1.Message.HashTreeRoot()
sigRoot, err := registration.Root()
require.NoError(t, err)

sigData, err := signing.GetDataRoot(nil, nil, signing.DomainApplicationBuilder, 0, sigRoot)
sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainApplicationBuilder, 0, sigRoot)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
Expand Down
Loading