all: integrate blsync into geth cmd

This commit is contained in:
lightclient 2024-01-05 08:23:39 -07:00
parent ca0feab9d3
commit b3289536f1
No known key found for this signature in database
GPG key ID: 75C916AFEE20183E
11 changed files with 181 additions and 233 deletions

View file

@ -40,6 +40,7 @@ type LightClient struct {
store *store store *store
chainHeadFeed event.Feed chainHeadFeed event.Feed
quitCh chan struct{}
} }
// Bootstrap retrieves a light client bootstrap and authenticates it against the // 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, optimistic: &bs.Header.Header,
finalized: &bs.Header.Header, finalized: &bs.Header.Header,
}, },
quitCh: make(chan struct{}),
}, nil }, nil
} }
@ -90,22 +92,25 @@ func (c *LightClient) Finalized() *types.Header {
// Start executes the main active loop of the light client which drives the // Start executes the main active loop of the light client which drives the
// underlying light client store. // underlying light client store.
func (c *LightClient) Start() { func (c *LightClient) Start() error {
log.Info("beacon light client starting")
var ( var (
ticker = time.NewTicker(params.SlotLength * time.Second) ticker = time.NewTicker(params.SlotLength * time.Second)
lastFinality = time.Now() lastFinality = time.Now()
) )
for ; ; <-ticker.C { for {
select {
case <-c.quitCh:
return nil
case <-ticker.C:
if c.store.next == nil { if c.store.next == nil {
log.Debug("fetching committee update", "period", c.store.finalizedPeriod()) log.Debug("Fetching committee update", "period", c.store.finalizedPeriod())
updates, err := c.beacon.GetRangeUpdate(c.store.finalizedPeriod(), 1) updates, err := c.beacon.GetRangeUpdate(c.store.finalizedPeriod(), 1)
if err != nil { if err != nil {
log.Error("failed to fetch next committee", "err", err) log.Error("Failed to fetch next committee", "err", err)
} else { } else {
for _, update := range updates { for _, update := range updates {
if err := c.store.Insert(update); err != nil { if err := c.store.Insert(update); err != nil {
log.Error("failed to insert committee update", "err", err) log.Error("Failed to insert committee update", "err", err)
break break
} }
} }
@ -117,38 +122,42 @@ func (c *LightClient) Start() {
err error err error
) )
if time.Since(lastFinality) > time.Minute*5 { if time.Since(lastFinality) > time.Minute*5 {
log.Trace("fetching finality update")
lastFinality = time.Now() lastFinality = time.Now()
update, err = c.beacon.GetFinalityUpdate() update, err = c.beacon.GetFinalityUpdate()
} else { } else {
log.Trace("fetching optimistic update")
update, err = c.beacon.GetOptimisticUpdate() update, err = c.beacon.GetOptimisticUpdate()
} }
if err != nil { if err != nil {
log.Error("failed to retrieve update", "err", err) log.Error("Failed to retrieve update", "err", err)
continue 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) 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 { if err := c.store.Insert(update); err != nil {
log.Error("failed to insert update", "err", err) log.Error("Failed to insert update", "err", err)
continue continue
} }
head := update.AttestedHeader head := update.AttestedHeader
log.Info("beacon head updated", "slot", head.Slot, "root", head.Hash(), "finalized", c.Finalized().Hash(), "signers", update.SyncAggregate.SignerCount()) 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. // Fetch full execution payload from beacon provider and send to head feed.
data, err := c.getExecutableData(head.Hash()) data, err := c.fetchExecutableData(head.Hash())
if err != nil { if err != nil {
log.Error("failed to insert update", "err", err) log.Error("Failed to insert update", "err", err)
continue 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. // 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) block, err := c.beacon.GetBlock(head)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get execution payload: %w", err) return nil, fmt.Errorf("failed to get execution payload: %w", err)

View file

@ -25,7 +25,6 @@ import (
"github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/beacon/params"
"github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
) )
var ( var (
@ -71,7 +70,6 @@ func (s *store) validate(update *types.LightClientUpdate) error {
) )
// Verify update does not skip a sync committee. // Verify update does not skip a sync committee.
if updatePeriod != storedPeriod && (s.next == nil || updatePeriod != storedPeriod+1) { 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 return errWrongPeriod
} }
if !(update.AttestedHeader.Slot > s.finalized.Slot || update.NextSyncCommittee != nil) { if !(update.AttestedHeader.Slot > s.finalized.Slot || update.NextSyncCommittee != nil) {

View file

@ -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. // This file is part of the go-ethereum library.
// //
// The go-ethereum library is free software: you can redistribute it and/or modify // The go-ethereum library is free software: you can redistribute it and/or modify

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -221,6 +221,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
} }
catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon) catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon)
stack.RegisterLifecycle(simBeacon) stack.RegisterLifecycle(simBeacon)
} else if ctx.IsSet(utils.BeaconAPIFlag.Name) && ctx.IsSet(utils.BeaconTrustedBlockRootFlag.Name) {
blsync := catalyst.NewBlsync(eth)
stack.RegisterLifecycle(blsync)
} else { } else {
err := catalyst.Register(stack, eth) err := catalyst.Register(stack, eth)
if err != nil { if err != nil {

View file

@ -146,6 +146,8 @@ var (
configFileFlag, configFileFlag,
utils.LogDebugFlag, utils.LogDebugFlag,
utils.LogBacktraceAtFlag, utils.LogBacktraceAtFlag,
utils.BeaconAPIFlag,
utils.BeaconTrustedBlockRootFlag,
}, utils.NetworkFlags, utils.DatabaseFlags) }, utils.NetworkFlags, utils.DatabaseFlags)
rpcFlags = []cli.Flag{ rpcFlags = []cli.Flag{

View file

@ -908,6 +908,18 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server.
Value: metrics.DefaultConfig.InfluxDBOrganization, Value: metrics.DefaultConfig.InfluxDBOrganization,
Category: flags.MetricsCategory, 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 ( 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 { 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) 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 // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if

View file

@ -18,6 +18,7 @@
package eth package eth
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -25,6 +26,7 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/accounts" "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"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
@ -71,6 +73,7 @@ type Ethereum struct {
txPool *txpool.TxPool txPool *txpool.TxPool
blockchain *core.BlockChain blockchain *core.BlockChain
beacon *light.LightClient
handler *handler handler *handler
ethDialCandidates enode.Iterator ethDialCandidates enode.Iterator
snapDialCandidates enode.Iterator snapDialCandidates enode.Iterator
@ -273,6 +276,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
return nil, err 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 // Start the RPC service
eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID) 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) AccountManager() *accounts.Manager { return s.accountManager }
func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } 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) TxPool() *txpool.TxPool { return s.txPool }
func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux } func (s *Ethereum) EventMux() *event.TypeMux { return s.eventMux }
func (s *Ethereum) Engine() consensus.Engine { return s.engine } func (s *Ethereum) Engine() consensus.Engine { return s.engine }

86
eth/catalyst/blsync.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -113,6 +113,10 @@ type Config struct {
LightNoPrune bool `toml:",omitempty"` // Whether to disable light chain pruning LightNoPrune bool `toml:",omitempty"` // Whether to disable light chain pruning
LightNoSyncServe bool `toml:",omitempty"` // Whether to serve light clients before syncing LightNoSyncServe bool `toml:",omitempty"` // Whether to serve light clients before syncing
// Beacon light client options
BeaconAPI string
BeaconTrustedBlockRoot string
// Database options // Database options
SkipBcVersionCheck bool `toml:"-"` SkipBcVersionCheck bool `toml:"-"`
DatabaseHandles int `toml:"-"` DatabaseHandles int `toml:"-"`

View file

@ -20,6 +20,7 @@ import "github.com/urfave/cli/v2"
const ( const (
EthCategory = "ETHEREUM" EthCategory = "ETHEREUM"
BeaconCategory = "BEACON"
LightCategory = "LIGHT CLIENT" LightCategory = "LIGHT CLIENT"
DevCategory = "DEVELOPER CHAIN" DevCategory = "DEVELOPER CHAIN"
StateCategory = "STATE HISTORY MANAGEMENT" StateCategory = "STATE HISTORY MANAGEMENT"