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"