diff --git a/app/simnet_test.go b/app/simnet_test.go index 8f7a95496..ee469ee22 100644 --- a/app/simnet_test.go +++ b/app/simnet_test.go @@ -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()) @@ -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. @@ -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{ @@ -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) diff --git a/core/deadline.go b/core/deadline.go index f927e36aa..17b9370f5 100644 --- a/core/deadline.go +++ b/core/deadline.go @@ -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) } diff --git a/core/sigagg/sigagg_test.go b/core/sigagg/sigagg_test.go index 78c0cec97..94c86022b 100644 --- a/core/sigagg/sigagg_test.go +++ b/core/sigagg/sigagg_test.go @@ -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) diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go index 711803707..99b2ff53c 100644 --- a/core/validatorapi/router.go +++ b/core/validatorapi/router.go @@ -64,6 +64,7 @@ type Handler interface { eth2client.ValidatorsProvider eth2client.ValidatorRegistrationsSubmitter eth2client.VoluntaryExitSubmitter + TekuProposerConfigProvider // Above sorted alphabetically. } @@ -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() @@ -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) { diff --git a/core/validatorapi/teku.go b/core/validatorapi/teku.go new file mode 100644 index 000000000..5dba82f97 --- /dev/null +++ b/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 . + +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 +} diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go index 5e7cbeed2..6e15d2486 100644 --- a/core/validatorapi/validatorapi.go +++ b/core/validatorapi/validatorapi.go @@ -85,6 +85,7 @@ 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 { @@ -92,6 +93,10 @@ func NewComponent(eth2Svc eth2client.Service, pubShareByKey map[*bls_sig.PublicK 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 @@ -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 } @@ -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 @@ -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 diff --git a/eth2util/signing/signing.go b/eth2util/signing/signing.go index 82363b1f4..3dc678451 100644 --- a/eth2util/signing/signing.go +++ b/eth2util/signing/signing.go @@ -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 := (ð2p0.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 := (ð2p0.SigningData{ObjectRoot: root, Domain: domain}).HashTreeRoot() @@ -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) } diff --git a/eth2util/signing/signing_test.go b/eth2util/signing/signing_test.go index ad15b9b12..0dc9dd6ab 100644 --- a/eth2util/signing/signing_test.go +++ b/eth2util/signing/signing_test.go @@ -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" @@ -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[:]) diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go index 08f52118d..2a2803004 100644 --- a/testutil/beaconmock/options.go +++ b/testutil/beaconmock/options.go @@ -31,6 +31,7 @@ import ( eth2v1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/bellatrix" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/jonboulle/clockwork" "github.com/prysmaticlabs/go-bitfield" @@ -343,10 +344,10 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo httpServer: httpServer, BeaconBlockProposalFunc: func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) { return &spec.VersionedBeaconBlock{ - Version: spec.DataVersionAltair, - Altair: &altair.BeaconBlock{ + Version: spec.DataVersionBellatrix, + Bellatrix: &bellatrix.BeaconBlock{ Slot: slot, - Body: &altair.BeaconBlockBody{ + Body: &bellatrix.BeaconBlockBody{ RANDAOReveal: randaoReveal, ETH1Data: ð2p0.ETH1Data{ DepositRoot: testutil.RandomRoot(), @@ -363,6 +364,7 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo SyncCommitteeBits: bitfield.NewBitvector512(), SyncCommitteeSignature: testutil.RandomEth2Signature(), }, + ExecutionPayload: testutil.RandomExecutionPayLoad(), }, }, }, nil diff --git a/testutil/beaconmock/server.go b/testutil/beaconmock/server.go index f23ecca51..4de387c2d 100644 --- a/testutil/beaconmock/server.go +++ b/testutil/beaconmock/server.go @@ -110,6 +110,10 @@ func newHTTPServer(addr string, overrides ...staticOverride) (*http.Server, erro _, _ = w.Write([]byte(`{"data": {"header": {"message": {"slot": "1"}}}}`)) }, }, + { + Path: "/eth/v1/validator/prepare_beacon_proposer", + Handler: func(w http.ResponseWriter, r *http.Request) {}, + }, { Path: "/eth/v1/events", Handler: func(w http.ResponseWriter, r *http.Request) { diff --git a/testutil/beaconmock/static.json b/testutil/beaconmock/static.json index b1393d656..3f5400fc2 100644 --- a/testutil/beaconmock/static.json +++ b/testutil/beaconmock/static.json @@ -9,7 +9,7 @@ "/eth/v1/config/deposit_contract": { "data": { "chain_id": "5", - "address": "0xff50ed3d0ec03ac01d4c79aad74928bff48a7b2b" + "address": "0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b" } }, "/eth/v1/config/fork_schedule": { @@ -23,12 +23,17 @@ "previous_version": "0x00001020", "current_version": "0x01001020", "epoch": "36660" + }, + { + "previous_version": "0x01001020", + "current_version": "0x02001020", + "epoch": "112260" } ] }, "/eth/v1/node/version": { "data": { - "version": "teku/v22.3.1/linux-x86_64/-eclipseadoptium-openjdk64bitservervm-java-17" + "version": "teku/v22.8.0/linux-x86_64/-eclipseadoptium-openjdk64bitservervm-java-17" } }, "/eth/v1/config/spec": { @@ -36,16 +41,17 @@ "ALTAIR_FORK_EPOCH": "36660", "ALTAIR_FORK_VERSION": "0x01001020", "BASE_REWARD_FACTOR": "64", - "BELLATRIX_FORK_EPOCH": "18446744073709551615", + "BELLATRIX_FORK_EPOCH": "112260", "BELLATRIX_FORK_VERSION": "0x02001020", "BLS_WITHDRAWAL_PREFIX": "0x00", "BYTES_PER_LOGS_BLOOM": "256", "CHURN_LIMIT_QUOTIENT": "65536", - "CONFIG_NAME": "goerli", + "CONFIG_NAME": "prater", "DEPOSIT_CHAIN_ID": "5", "DEPOSIT_CONTRACT_ADDRESS": "0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b", "DEPOSIT_NETWORK_ID": "5", "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "DOMAIN_APPLICATION_BUILDER": "0x00000001", "DOMAIN_BEACON_ATTESTER": "0x01000000", "DOMAIN_BEACON_PROPOSER": "0x00000000", "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000", @@ -119,13 +125,15 @@ "TARGET_COMMITTEE_SIZE": "128", "TERMINAL_BLOCK_HASH": "0x0000000000000000000000000000000000000000000000000000000000000000", "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615", - "TERMINAL_TOTAL_DIFFICULTY": "115792089237316195423570985008687907853269984665640564039457584007913129638912", + "TERMINAL_TOTAL_DIFFICULTY": "10790000", + "UPDATE_TIMEOUT": "8192", "VALIDATOR_REGISTRY_LIMIT": "1099511627776", "WHISTLEBLOWER_REWARD_QUOTIENT": "512" } }, "/eth/v2/beacon/blocks/0": { "version": "phase0", + "execution_optimistic": false, "data": { "message": { "slot": "0", diff --git a/testutil/validatormock/validatormock.go b/testutil/validatormock/validatormock.go index ba25128a2..5a22901e5 100644 --- a/testutil/validatormock/validatormock.go +++ b/testutil/validatormock/validatormock.go @@ -347,13 +347,13 @@ func ProposeBlindedBlock(ctx context.Context, eth2Cl Eth2Provider, signFunc Sign func Register(ctx context.Context, eth2Cl Eth2Provider, signFunc SignFunc, registration *eth2api.VersionedValidatorRegistration, pubshare eth2p0.BLSPubKey, ) error { - // TODO(corver): refactor to registration.HashTreeRoot() once available. - sigRoot, err := registration.V1.HashTreeRoot() + sigRoot, err := registration.Root() if err != nil { return err } - sigData, err := signing.GetDataRoot(nil, nil, signing.DomainApplicationBuilder, 0, sigRoot) + // Always use epoch 0 for DomainApplicationBuilder + sigData, err := signing.GetDataRoot(ctx, eth2Cl, signing.DomainApplicationBuilder, 0, sigRoot) if err != nil { return err }