Skip to content

Commit

Permalink
app: fix builder registration and add teku integration test (#957)
Browse files Browse the repository at this point in the history
- Fixes issue with builder registration domain calculation. 
- Also adds teku proposer config endpoint
- Also fixes registration deadline issue
- Adds integration test using teku/develop image.

category: feature 
ticket: #849
  • Loading branch information
corverroos committed Aug 11, 2022
1 parent 9202362 commit 62495dd
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 55 deletions.
37 changes: 34 additions & 3 deletions app/simnet_test.go
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, tekuVC)
}
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,17 @@ 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",
}
)

// startTeku starts a teku validator client for the provided node and returns updated args.
Expand Down Expand Up @@ -348,6 +372,13 @@ 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,
"--validators-proposer-config-refresh-enabled=true",
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 +387,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
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
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
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
@@ -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
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
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
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

0 comments on commit 62495dd

Please sign in to comment.