From b3289536f1f7330d4bf09014c41bb3fe35c6c225 Mon Sep 17 00:00:00 2001 From: lightclient Date: Fri, 5 Jan 2024 08:23:39 -0700 Subject: [PATCH] all: integrate blsync into geth cmd --- beacon/light/chain.go | 99 ++++++++++--------- beacon/light/store.go | 2 - beacon/types/light_client.go | 2 +- cmd/blsync/main.go | 185 ----------------------------------- cmd/geth/config.go | 3 + cmd/geth/main.go | 2 + cmd/utils/flags.go | 18 ++++ eth/backend.go | 12 +++ eth/catalyst/blsync.go | 86 ++++++++++++++++ eth/ethconfig/config.go | 4 + internal/flags/categories.go | 1 + 11 files changed, 181 insertions(+), 233 deletions(-) delete mode 100644 cmd/blsync/main.go create mode 100644 eth/catalyst/blsync.go diff --git a/beacon/light/chain.go b/beacon/light/chain.go index d108184355..8ca7c318a3 100644 --- a/beacon/light/chain.go +++ b/beacon/light/chain.go @@ -40,6 +40,7 @@ type LightClient struct { store *store chainHeadFeed event.Feed + quitCh chan struct{} } // Bootstrap retrieves a light client bootstrap and authenticates it against the @@ -68,6 +69,7 @@ func Bootstrap(ctx context.Context, server string, root common.Hash) (*LightClie optimistic: &bs.Header.Header, finalized: &bs.Header.Header, }, + quitCh: make(chan struct{}), }, nil } @@ -90,65 +92,72 @@ func (c *LightClient) Finalized() *types.Header { // Start executes the main active loop of the light client which drives the // underlying light client store. -func (c *LightClient) Start() { - log.Info("beacon light client starting") +func (c *LightClient) Start() error { var ( ticker = time.NewTicker(params.SlotLength * time.Second) lastFinality = time.Now() ) - for ; ; <-ticker.C { - if c.store.next == nil { - log.Debug("fetching committee update", "period", c.store.finalizedPeriod()) - updates, err := c.beacon.GetRangeUpdate(c.store.finalizedPeriod(), 1) - if err != nil { - log.Error("failed to fetch next committee", "err", err) - } else { - for _, update := range updates { - if err := c.store.Insert(update); err != nil { - log.Error("failed to insert committee update", "err", err) - break + for { + select { + case <-c.quitCh: + return nil + case <-ticker.C: + if c.store.next == nil { + log.Debug("Fetching committee update", "period", c.store.finalizedPeriod()) + updates, err := c.beacon.GetRangeUpdate(c.store.finalizedPeriod(), 1) + if err != nil { + log.Error("Failed to fetch next committee", "err", err) + } else { + for _, update := range updates { + if err := c.store.Insert(update); err != nil { + log.Error("Failed to insert committee update", "err", err) + break + } } } } - } - var ( - update *types.LightClientUpdate - err error - ) - if time.Since(lastFinality) > time.Minute*5 { - log.Trace("fetching finality update") - lastFinality = time.Now() - update, err = c.beacon.GetFinalityUpdate() - } else { - log.Trace("fetching optimistic update") - update, err = c.beacon.GetOptimisticUpdate() - } - if err != nil { - log.Error("failed to retrieve update", "err", err) - continue - } - log.Trace("got update", "slot", update.AttestedHeader.Slot, "root", update.AttestedHeader.Hash(), "sigslot", update.SignatureSlot, "period", update.AttestedHeader.SyncPeriod(), "hasFinalized", update.FinalizedHeader != nil, "hasNext", update.NextSyncCommittee != nil) - if err := c.store.Insert(update); err != nil { - log.Error("failed to insert update", "err", err) - continue - } - head := update.AttestedHeader - log.Info("beacon head updated", "slot", head.Slot, "root", head.Hash(), "finalized", c.Finalized().Hash(), "signers", update.SyncAggregate.SignerCount()) + var ( + update *types.LightClientUpdate + err error + ) + if time.Since(lastFinality) > time.Minute*5 { + lastFinality = time.Now() + update, err = c.beacon.GetFinalityUpdate() + } else { + update, err = c.beacon.GetOptimisticUpdate() + } + if err != nil { + log.Error("Failed to retrieve update", "err", err) + continue + } + log.Trace("New beacon update", "slot", update.AttestedHeader.Slot, "root", update.AttestedHeader.Hash(), "sigslot", update.SignatureSlot, "period", update.AttestedHeader.SyncPeriod(), "hasFinalized", update.FinalizedHeader != nil, "hasNext", update.NextSyncCommittee != nil) + if err := c.store.Insert(update); err != nil { + log.Error("Failed to insert update", "err", err) + continue + } + head := update.AttestedHeader + log.Info("Beacon head updated", "slot", head.Slot, "root", head.Hash(), "finalized", c.Finalized().Hash(), "signers", update.SyncAggregate.SignerCount()) - // Fetch full execution payload from beacon provider and send to head feed. - data, err := c.getExecutableData(head.Hash()) - if err != nil { - log.Error("failed to insert update", "err", err) - continue + // Fetch full execution payload from beacon provider and send to head feed. + data, err := c.fetchExecutableData(head.Hash()) + if err != nil { + log.Error("Failed to insert update", "err", err) + continue + } + c.chainHeadFeed.Send(ChainHeadEvent{Data: data}) } - c.chainHeadFeed.Send(ChainHeadEvent{Data: data}) } } -// getExecutableData retrieves the full beacon block associated with the beacon +func (c *LightClient) Stop() error { + close(c.quitCh) + return nil +} + +// fetchExecutableData retrieves the full beacon block associated with the beacon // block root and returns the inner execution payload. -func (c *LightClient) getExecutableData(head common.Hash) (*engine.ExecutableData, error) { +func (c *LightClient) fetchExecutableData(head common.Hash) (*engine.ExecutableData, error) { block, err := c.beacon.GetBlock(head) if err != nil { return nil, fmt.Errorf("failed to get execution payload: %w", err) diff --git a/beacon/light/store.go b/beacon/light/store.go index 3775f4d0fe..93d3aac442 100644 --- a/beacon/light/store.go +++ b/beacon/light/store.go @@ -25,7 +25,6 @@ import ( "github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" ) var ( @@ -71,7 +70,6 @@ func (s *store) validate(update *types.LightClientUpdate) error { ) // Verify update does not skip a sync committee. if updatePeriod != storedPeriod && (s.next == nil || updatePeriod != storedPeriod+1) { - log.Error("update not from current or next sync committee", "stored", storedPeriod, "update", updatePeriod) return errWrongPeriod } if !(update.AttestedHeader.Slot > s.finalized.Slot || update.NextSyncCommittee != nil) { diff --git a/beacon/types/light_client.go b/beacon/types/light_client.go index b251927e5b..7d8bcc529c 100644 --- a/beacon/types/light_client.go +++ b/beacon/types/light_client.go @@ -1,4 +1,4 @@ -// Copyright 2024 The go-ethereum Authors +// copyright 2023 the go-ethereum authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify diff --git a/cmd/blsync/main.go b/cmd/blsync/main.go deleted file mode 100644 index 4e4fefe206..0000000000 --- a/cmd/blsync/main.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library 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 Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package main - -import ( - "context" - "fmt" - "io" - "os" - "time" - - "github.com/ethereum/go-ethereum/beacon/engine" - "github.com/ethereum/go-ethereum/beacon/light" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/internal/flags" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" -) - -var app = &cli.App{ - Usage: "go-ethereum beacon light sync tool", - Action: run, -} - -var ( - EngineApiFlag = &cli.StringFlag{ - Name: "engine", - Usage: "url to execution client engine api", - } - JwtSecretFlag = &cli.StringFlag{ - Name: "jwtsecret", - Usage: "path to jwt secret used to communicate with execution client", - } - LightClientServerFlag = &cli.StringFlag{ - Name: "server", - Usage: "server which provides the beacon light client apis", - } - TrustedBlockRootFlag = &cli.StringFlag{ - Name: "trusted-root", - Usage: "root of trusted block within the weak-subjectivity window", - } - VerbosityFlag = &cli.IntFlag{ - Name: "verbosity", - Usage: "Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail", - Value: 3, - } -) - -func init() { - app.Flags = flags.Merge([]cli.Flag{ - EngineApiFlag, - JwtSecretFlag, - LightClientServerFlag, - TrustedBlockRootFlag, - VerbosityFlag, - }) -} - -func main() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func run(ctx *cli.Context) error { - // Setup logger. - var ( - usecolor = (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" - output = io.Writer(os.Stdout) - verbosity = log.FromLegacyLevel(ctx.Int(VerbosityFlag.Name)) - ) - if usecolor { - output = colorable.NewColorable(os.Stdout) - } - log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor))) - - // Start light client. - var ( - root = ctx.String(TrustedBlockRootFlag.Name) - engine = makeRPCClient(ctx) - server = ctx.String(LightClientServerFlag.Name) - ) - chain, err := light.Bootstrap(context.Background(), server, common.HexToHash(root)) - if err != nil { - return fmt.Errorf("failed to bootstrap: %v", err) - } - go chain.Start() - - headCh := make(chan light.ChainHeadEvent) - chain.SubscribeChainHeadEvent(headCh) - - // Send new head events to engine api. - for { - select { - case head := <-headCh: - if err := sendUpdate(engine, head.Data, chain.Finalized().Hash()); err != nil { - log.Error("unable to send update to execution client", "err", err) - } - } - } -} - -// sendUpdate passes the execution payload to execution client and affirms it -// with a forck choice updated. -func sendUpdate(engine *rpc.Client, ep *engine.ExecutableData, finalized common.Hash) error { - if _, err := callNewPayloadV2(engine, ep); err != nil { - return fmt.Errorf("failed to send new payload: %w", err) - } - if _, err := callForkchoiceUpdatedV1(engine, ep.BlockHash, finalized); err != nil { - return fmt.Errorf("failed to send forkchoice updated: %w", err) - } - return nil -} - -func makeRPCClient(ctx *cli.Context) *rpc.Client { - if !ctx.IsSet(EngineApiFlag.Name) { - log.Warn("No engine API target specified, performing a dry run") - return nil - } - if !ctx.IsSet(JwtSecretFlag.Name) { - exit(fmt.Errorf("JWT secret parameter missing")) // TODO use default if datadir is specified - } - engineApiUrl, jwtFileName := ctx.String(EngineApiFlag.Name), ctx.String(JwtSecretFlag.Name) - var jwtSecret [32]byte - if jwt, err := os.ReadFile(jwtFileName); err != nil { - utils.Fatalf("Error loading or generating JWT secret: %v", err) - } else { - copy(jwtSecret[:], common.FromHex(string(jwt))) - } - auth := node.NewJWTAuth(jwtSecret) - cl, err := rpc.DialOptions(context.Background(), engineApiUrl, rpc.WithHTTPAuth(auth)) - if err != nil { - utils.Fatalf("Could not create RPC client: %v", err) - } - return cl -} - -func callNewPayloadV2(client *rpc.Client, ep *engine.ExecutableData) (string, error) { - var resp engine.PayloadStatusV1 - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - err := client.CallContext(ctx, &resp, "engine_newPayloadV2", ep) - cancel() - return resp.Status, err -} - -func callForkchoiceUpdatedV1(client *rpc.Client, headHash, finalizedHash common.Hash) (string, error) { - var resp engine.ForkChoiceResponse - update := engine.ForkchoiceStateV1{ - HeadBlockHash: headHash, - SafeBlockHash: common.Hash{}, - FinalizedBlockHash: common.Hash{}, - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - err := client.CallContext(ctx, &resp, "engine_forkchoiceUpdatedV1", update, nil) - cancel() - return resp.PayloadStatus.Status, err -} - -func exit(err interface{}) { - if err == nil { - os.Exit(0) - } - fmt.Fprintln(os.Stderr, err) - os.Exit(1) -} diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 5f52f1df54..a343c19b36 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -221,6 +221,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon) stack.RegisterLifecycle(simBeacon) + } else if ctx.IsSet(utils.BeaconAPIFlag.Name) && ctx.IsSet(utils.BeaconTrustedBlockRootFlag.Name) { + blsync := catalyst.NewBlsync(eth) + stack.RegisterLifecycle(blsync) } else { err := catalyst.Register(stack, eth) if err != nil { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 4438cef560..a81c7c386e 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -146,6 +146,8 @@ var ( configFileFlag, utils.LogDebugFlag, utils.LogBacktraceAtFlag, + utils.BeaconAPIFlag, + utils.BeaconTrustedBlockRootFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 159c47ca01..08d8b232f7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -908,6 +908,18 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: metrics.DefaultConfig.InfluxDBOrganization, Category: flags.MetricsCategory, } + + // Beacon light client flags. + BeaconAPIFlag = &cli.StringFlag{ + Name: "beacon.api", + Usage: "server which provides the beacon light client apis", + Category: flags.BeaconCategory, + } + BeaconTrustedBlockRootFlag = &cli.StringFlag{ + Name: "beacon.wss", + Usage: "root of trusted block within the weak-subjectivity window", + Category: flags.BeaconCategory, + } ) var ( @@ -1842,6 +1854,12 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if err := kzg4844.UseCKZG(ctx.String(CryptoKZGFlag.Name) == "ckzg"); err != nil { Fatalf("Failed to set KZG library implementation to %s: %v", ctx.String(CryptoKZGFlag.Name), err) } + + if (ctx.IsSet(BeaconAPIFlag.Name) && !ctx.IsSet(BeaconTrustedBlockRootFlag.Name)) || (!ctx.IsSet(BeaconAPIFlag.Name) && ctx.IsSet(BeaconTrustedBlockRootFlag.Name)) { + Fatalf("Must set both beacon api flag and beacon trusted block root flag to use beacon light client") + } + cfg.BeaconAPI = ctx.String(BeaconAPIFlag.Name) + cfg.BeaconTrustedBlockRoot = ctx.String(BeaconTrustedBlockRootFlag.Name) } // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if diff --git a/eth/backend.go b/eth/backend.go index 774ffaf248..8de18f6581 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -18,6 +18,7 @@ package eth import ( + "context" "errors" "fmt" "math/big" @@ -25,6 +26,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/beacon/light" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -71,6 +73,7 @@ type Ethereum struct { txPool *txpool.TxPool blockchain *core.BlockChain + beacon *light.LightClient handler *handler ethDialCandidates enode.Iterator snapDialCandidates enode.Iterator @@ -273,6 +276,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { return nil, err } + if config.BeaconAPI != "" && config.BeaconTrustedBlockRoot != "" { + beacon, err := light.Bootstrap(context.Background(), config.BeaconAPI, common.HexToHash(config.BeaconTrustedBlockRoot)) + if err != nil { + return nil, err + } + eth.beacon = beacon + } + // Start the RPC service eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID) @@ -476,6 +487,7 @@ func (s *Ethereum) Miner() *miner.Miner { return s.miner } func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager } func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } +func (s *Ethereum) Beacon() *light.LightClient { return s.beacon } func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool } func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux } func (s *Ethereum) Engine() consensus.Engine { return s.engine } diff --git a/eth/catalyst/blsync.go b/eth/catalyst/blsync.go new file mode 100644 index 0000000000..5a3e01be5d --- /dev/null +++ b/eth/catalyst/blsync.go @@ -0,0 +1,86 @@ +// Copyright 2024 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package catalyst + +import ( + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/beacon/light" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" +) + +// Blsync tracks the head of the beacon chain through the beacon light client +// and drives the local node via ConsensusAPI. +type Blsync struct { + eth *eth.Ethereum + engine *ConsensusAPI + client *light.LightClient + headCh chan light.ChainHeadEvent + headSub event.Subscription + + quitCh chan struct{} +} + +// NewBlsync creates a new beacon light syncer. +func NewBlsync(eth *eth.Ethereum) *Blsync { + engine := newConsensusAPIWithoutHeartbeat(eth) + return &Blsync{ + eth: eth, + engine: engine, + client: eth.Beacon(), + headCh: make(chan light.ChainHeadEvent, 128), + quitCh: make(chan struct{}), + } +} + +// Start starts underlying beacon light client and the sync logic for driving +// the local node. +func (b *Blsync) Start() error { + log.Info("Blsync started") + b.headSub = b.client.SubscribeChainHeadEvent(b.headCh) + go b.client.Start() + + for { + select { + case <-b.quitCh: + return nil + case head := <-b.headCh: + if _, err := b.engine.NewPayloadV2(*head.Data); err != nil { + log.Error("failed to send new payload", "err", err) + continue + } + update := engine.ForkchoiceStateV1{ + HeadBlockHash: head.Data.BlockHash, + SafeBlockHash: common.Hash{}, + FinalizedBlockHash: common.Hash{}, + } + if _, err := b.engine.ForkchoiceUpdatedV1(update, nil); err != nil { + log.Error("failed to send forkchoice updated", "err", err) + continue + } + } + } +} + +// Stop signals to the light client and syncer to exit. +func (b *Blsync) Stop() error { + b.client.Stop() + close(b.quitCh) + return nil +} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index ad664afb5b..0846fb4de7 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -113,6 +113,10 @@ type Config struct { LightNoPrune bool `toml:",omitempty"` // Whether to disable light chain pruning LightNoSyncServe bool `toml:",omitempty"` // Whether to serve light clients before syncing + // Beacon light client options + BeaconAPI string + BeaconTrustedBlockRoot string + // Database options SkipBcVersionCheck bool `toml:"-"` DatabaseHandles int `toml:"-"` diff --git a/internal/flags/categories.go b/internal/flags/categories.go index 3ff0767921..72aa0f4cb0 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -20,6 +20,7 @@ import "github.com/urfave/cli/v2" const ( EthCategory = "ETHEREUM" + BeaconCategory = "BEACON" LightCategory = "LIGHT CLIENT" DevCategory = "DEVELOPER CHAIN" StateCategory = "STATE HISTORY MANAGEMENT"