diff --git a/cmd/blsync/block_sync.go b/beacon/blsync/block_sync.go similarity index 51% rename from cmd/blsync/block_sync.go rename to beacon/blsync/block_sync.go index b07fe13bc3..b2fa42f696 100755 --- a/cmd/blsync/block_sync.go +++ b/beacon/blsync/block_sync.go @@ -14,15 +14,26 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package main +package blsync import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/light/request" "github.com/ethereum/go-ethereum/beacon/light/sync" "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" + ctypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" "github.com/protolambda/zrnt/eth2/beacon/capella" + "github.com/protolambda/zrnt/eth2/configs" + "github.com/protolambda/ztyp/tree" ) // beaconBlockSync implements request.Module; it fetches the beacon blocks belonging @@ -33,13 +44,8 @@ type beaconBlockSync struct { serverHeads map[request.Server]common.Hash headTracker headTracker - lastHeadInfo types.HeadInfo - headCh chan headData -} - -type headData struct { - block *capella.BeaconBlock - update types.FinalityUpdate + lastHeadInfo types.HeadInfo + chainHeadFeed *event.Feed } type headTracker interface { @@ -49,13 +55,13 @@ type headTracker interface { } // newBeaconBlockSync returns a new beaconBlockSync. -func newBeaconBlockSync(headTracker headTracker) *beaconBlockSync { +func newBeaconBlockSync(headTracker headTracker, chainHeadFeed *event.Feed) *beaconBlockSync { return &beaconBlockSync{ - headTracker: headTracker, - recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10), - locked: make(map[common.Hash]request.ServerAndID), - serverHeads: make(map[request.Server]common.Hash), - headCh: make(chan headData, 1), + headTracker: headTracker, + chainHeadFeed: chainHeadFeed, + recentBlocks: lru.NewCache[common.Hash, *capella.BeaconBlock](10), + locked: make(map[common.Hash]request.ServerAndID), + serverHeads: make(map[request.Server]common.Hash), } } @@ -78,7 +84,7 @@ func (s *beaconBlockSync) Process(requester request.Requester, events []request. delete(s.serverHeads, event.Server) } } - s.updateValidatedHead() + s.updateEventFeed() // request validated head block if unavailable and not yet requested if vh, ok := s.headTracker.ValidatedHead(); ok { s.tryRequestBlock(requester, vh.Header.Hash(), false) @@ -89,31 +95,6 @@ func (s *beaconBlockSync) Process(requester request.Requester, events []request. } } -func (s *beaconBlockSync) updateValidatedHead() { - head, ok := s.headTracker.ValidatedHead() - if !ok { - return - } - finality, ok := s.headTracker.ValidatedFinality() //TODO fetch directly if subscription does not deliver - if !ok || head.Header.Epoch() != finality.Attested.Header.Epoch() { - return - } - validatedHead := head.Header.Hash() - headBlock, ok := s.recentBlocks.Get(validatedHead) - if !ok { - return - } - headInfo := blockHeadInfo(headBlock) - if headInfo == s.lastHeadInfo { - return - } - select { - case s.headCh <- headData{block: headBlock, update: finality}: - s.lastHeadInfo = headInfo - default: - } -} - func (s *beaconBlockSync) tryRequestBlock(requester request.Requester, blockRoot common.Hash, needSameHead bool) { if _, ok := s.recentBlocks.Get(blockRoot); ok { return @@ -130,9 +111,93 @@ func (s *beaconBlockSync) tryRequestBlock(requester request.Requester, blockRoot return } } + func blockHeadInfo(block *capella.BeaconBlock) types.HeadInfo { if block == nil { return types.HeadInfo{} } return types.HeadInfo{Slot: uint64(block.Slot), BlockRoot: beaconBlockHash(block)} } + +// beaconBlockHash calculates the hash of a beacon block. +func beaconBlockHash(beaconBlock *capella.BeaconBlock) common.Hash { + return common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn())) +} + +// getExecBlock extracts the execution block from the beacon block's payload. +func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) { + payload := &beaconBlock.Body.ExecutionPayload + txs := make([]*ctypes.Transaction, len(payload.Transactions)) + for i, opaqueTx := range payload.Transactions { + var tx ctypes.Transaction + if err := tx.UnmarshalBinary(opaqueTx); err != nil { + return nil, fmt.Errorf("failed to parse tx %d: %v", i, err) + } + txs[i] = &tx + } + withdrawals := make([]*ctypes.Withdrawal, len(payload.Withdrawals)) + for i, w := range payload.Withdrawals { + withdrawals[i] = &ctypes.Withdrawal{ + Index: uint64(w.Index), + Validator: uint64(w.ValidatorIndex), + Address: common.Address(w.Address), + Amount: uint64(w.Amount), + } + } + wroot := ctypes.DeriveSha(ctypes.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + execHeader := &ctypes.Header{ + ParentHash: common.Hash(payload.ParentHash), + UncleHash: ctypes.EmptyUncleHash, + Coinbase: common.Address(payload.FeeRecipient), + Root: common.Hash(payload.StateRoot), + TxHash: ctypes.DeriveSha(ctypes.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: common.Hash(payload.ReceiptsRoot), + Bloom: ctypes.Bloom(payload.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(uint64(payload.BlockNumber)), + GasLimit: uint64(payload.GasLimit), + GasUsed: uint64(payload.GasUsed), + Time: uint64(payload.Timestamp), + Extra: []byte(payload.ExtraData), + MixDigest: common.Hash(payload.PrevRandao), // reused in merge + Nonce: ctypes.BlockNonce{}, // zero + BaseFee: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(), + WithdrawalsHash: &wroot, + } + execBlock := ctypes.NewBlockWithHeader(execHeader).WithBody(txs, nil).WithWithdrawals(withdrawals) + if execBlockHash := execBlock.Hash(); execBlockHash != common.Hash(payload.BlockHash) { + return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", common.Hash(payload.BlockHash), execBlockHash) + } + return execBlock, nil +} + +func (s *beaconBlockSync) updateEventFeed() { + head, ok := s.headTracker.ValidatedHead() + if !ok { + return + } + finality, ok := s.headTracker.ValidatedFinality() //TODO fetch directly if subscription does not deliver + if !ok || head.Header.Epoch() != finality.Attested.Header.Epoch() { + return + } + validatedHead := head.Header.Hash() + headBlock, ok := s.recentBlocks.Get(validatedHead) + if !ok { + return + } + headInfo := blockHeadInfo(headBlock) + if headInfo == s.lastHeadInfo { + return + } + s.lastHeadInfo = headInfo + // new head block and finality info available; extract executable data and send event to feed + execBlock, err := getExecBlock(headBlock) + if err != nil { + log.Error("Error extracting execution block from validated beacon block", "error", err) + return + } + s.chainHeadFeed.Send(types.ChainHeadEvent{ + HeadBlock: engine.BlockToExecutableData(execBlock, nil, nil).ExecutionPayload, + Finalized: common.Hash(finality.Finalized.PayloadHeader.BlockHash), + }) +} diff --git a/cmd/blsync/block_sync_test.go b/beacon/blsync/block_sync_test.go similarity index 99% rename from cmd/blsync/block_sync_test.go rename to beacon/blsync/block_sync_test.go index e6cfc3d185..4227be48af 100644 --- a/cmd/blsync/block_sync_test.go +++ b/beacon/blsync/block_sync_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package main +package blsync import ( "testing" diff --git a/beacon/blsync/client.go b/beacon/blsync/client.go new file mode 100644 index 0000000000..1bfbb13160 --- /dev/null +++ b/beacon/blsync/client.go @@ -0,0 +1,103 @@ +// 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 blsync + +import ( + "strings" + + "github.com/ethereum/go-ethereum/beacon/light" + "github.com/ethereum/go-ethereum/beacon/light/api" + "github.com/ethereum/go-ethereum/beacon/light/request" + "github.com/ethereum/go-ethereum/beacon/light/sync" + "github.com/ethereum/go-ethereum/beacon/types" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common/mclock" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/event" + "github.com/urfave/cli/v2" +) + +type Client struct { + scheduler *request.Scheduler + chainHeadFeed *event.Feed + urls []string + customHeader map[string]string +} + +func NewClient(ctx *cli.Context) *Client { + if !ctx.IsSet(utils.BeaconApiFlag.Name) { + utils.Fatalf("Beacon node light client API URL not specified") + } + var ( + chainConfig = makeChainConfig(ctx) + customHeader = make(map[string]string) + ) + for _, s := range ctx.StringSlice(utils.BeaconApiHeaderFlag.Name) { + kv := strings.Split(s, ":") + if len(kv) != 2 { + utils.Fatalf("Invalid custom API header entry: %s", s) + } + customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + // create data structures + var ( + db = memorydb.New() + threshold = ctx.Int(utils.BeaconThresholdFlag.Name) + committeeChain = light.NewCommitteeChain(db, chainConfig.ChainConfig, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name)) + headTracker = light.NewHeadTracker(committeeChain, threshold) + ) + headSync := sync.NewHeadSync(headTracker, committeeChain) + + // set up scheduler and sync modules + chainHeadFeed := new(event.Feed) + scheduler := request.NewScheduler() + checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint) + forwardSync := sync.NewForwardUpdateSync(committeeChain) + beaconBlockSync := newBeaconBlockSync(headTracker, chainHeadFeed) + scheduler.RegisterTarget(headTracker) + scheduler.RegisterTarget(committeeChain) + scheduler.RegisterModule(checkpointInit, "checkpointInit") + scheduler.RegisterModule(forwardSync, "forwardSync") + scheduler.RegisterModule(headSync, "headSync") + scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync") + + return &Client{ + scheduler: scheduler, + urls: ctx.StringSlice(utils.BeaconApiFlag.Name), + customHeader: customHeader, + chainHeadFeed: chainHeadFeed, + } +} + +// SubscribeChainHeadEvent allows callers to subscribe a provided channel to new +// head updates. +func (c *Client) SubscribeChainHeadEvent(ch chan<- types.ChainHeadEvent) event.Subscription { + return c.chainHeadFeed.Subscribe(ch) +} + +func (c *Client) Start() { + c.scheduler.Start() + // register server(s) + for _, url := range c.urls { + beaconApi := api.NewBeaconLightApi(url, c.customHeader) + c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{})) + } +} + +func (c *Client) Stop() { + c.scheduler.Stop() +} diff --git a/cmd/blsync/config.go b/beacon/blsync/config.go similarity index 82% rename from cmd/blsync/config.go rename to beacon/blsync/config.go index 5bb80dc5aa..b51d3e2b05 100644 --- a/cmd/blsync/config.go +++ b/beacon/blsync/config.go @@ -14,18 +14,13 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package main +package blsync import ( - "context" - "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/rpc" "github.com/urfave/cli/v2" ) @@ -116,27 +111,3 @@ func makeChainConfig(ctx *cli.Context) lightClientConfig { } return config } - -func makeRPCClient(ctx *cli.Context) *rpc.Client { - if !ctx.IsSet(utils.BlsyncApiFlag.Name) { - log.Warn("No engine API target specified, performing a dry run") - return nil - } - if !ctx.IsSet(utils.BlsyncJWTSecretFlag.Name) { - utils.Fatalf("JWT secret parameter missing") //TODO use default if datadir is specified - } - - engineApiUrl, jwtFileName := ctx.String(utils.BlsyncApiFlag.Name), ctx.String(utils.BlsyncJWTSecretFlag.Name) - var jwtSecret [32]byte - if jwt, err := node.ObtainJWTSecret(jwtFileName); err == nil { - copy(jwtSecret[:], jwt) - } else { - utils.Fatalf("Error loading or generating JWT secret: %v", err) - } - 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 -} diff --git a/beacon/types/light_sync.go b/beacon/types/light_sync.go index f8a473e253..ed62d237f1 100644 --- a/beacon/types/light_sync.go +++ b/beacon/types/light_sync.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/beacon/merkle" "github.com/ethereum/go-ethereum/beacon/params" "github.com/ethereum/go-ethereum/common" @@ -181,3 +182,11 @@ func (u *FinalityUpdate) Validate() error { } return merkle.VerifyProof(u.Attested.StateRoot, params.StateIndexFinalBlock, u.FinalityBranch, merkle.Value(u.Finalized.Hash())) } + +// ChainHeadEvent returns an authenticated execution payload associated with the +// latest accepted head of the beacon chain, along with the hash of the latest +// finalized execution block. +type ChainHeadEvent struct { + HeadBlock *engine.ExecutableData + Finalized common.Hash +} diff --git a/cmd/blsync/engine_api.go b/cmd/blsync/engine_api.go index 31bba0e703..d10750e295 100644 --- a/cmd/blsync/engine_api.go +++ b/cmd/blsync/engine_api.go @@ -18,105 +18,39 @@ package main import ( "context" - "fmt" - "math/big" "time" "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/common" - ctypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/trie" - "github.com/holiman/uint256" - "github.com/protolambda/zrnt/eth2/beacon/capella" - "github.com/protolambda/zrnt/eth2/configs" - "github.com/protolambda/ztyp/tree" ) -func updateEngineApi(client *rpc.Client, headCh chan headData) { - for headData := range headCh { - execBlock, err := getExecBlock(headData.block) - if err != nil { - log.Error("Error extracting execution block from validated beacon block", "error", err) - continue - } - execRoot := execBlock.Hash() - finalizedRoot := common.Hash(headData.update.Finalized.PayloadHeader.BlockHash) +func updateEngineApi(client *rpc.Client, headCh chan types.ChainHeadEvent) { + for event := range headCh { if client == nil { // dry run, no engine API specified - log.Info("New execution block retrieved", "block number", execBlock.NumberU64(), "block hash", execRoot, "finalized block hash", finalizedRoot) + log.Info("New execution block retrieved", "block number", event.HeadBlock.Number, "block hash", event.HeadBlock.BlockHash, "finalized block hash", event.Finalized) } else { - if status, err := callNewPayloadV2(client, execBlock); err == nil { - log.Info("Successful NewPayload", "block number", execBlock.NumberU64(), "block hash", execRoot, "status", status) + if status, err := callNewPayloadV2(client, event.HeadBlock); err == nil { + log.Info("Successful NewPayload", "block number", event.HeadBlock.Number, "block hash", event.HeadBlock.BlockHash, "status", status) } else { - log.Error("Failed NewPayload", "block number", execBlock.NumberU64(), "block hash", execRoot, "error", err) + log.Error("Failed NewPayload", "block number", event.HeadBlock.Number, "block hash", event.HeadBlock.BlockHash, "error", err) } - if status, err := callForkchoiceUpdatedV1(client, execRoot, finalizedRoot); err == nil { - log.Info("Successful ForkchoiceUpdated", "head", execRoot, "status", status) + if status, err := callForkchoiceUpdatedV1(client, event.HeadBlock.BlockHash, event.Finalized); err == nil { + log.Info("Successful ForkchoiceUpdated", "head", event.HeadBlock.BlockHash, "status", status) } else { - log.Error("Failed ForkchoiceUpdated", "head", execRoot, "error", err) + log.Error("Failed ForkchoiceUpdated", "head", event.HeadBlock.BlockHash, "error", err) } } } } -// getExecBlock extracts the execution block from the beacon block's payload. -func getExecBlock(beaconBlock *capella.BeaconBlock) (*ctypes.Block, error) { - payload := &beaconBlock.Body.ExecutionPayload - txs := make([]*ctypes.Transaction, len(payload.Transactions)) - for i, opaqueTx := range payload.Transactions { - var tx ctypes.Transaction - if err := tx.UnmarshalBinary(opaqueTx); err != nil { - return nil, fmt.Errorf("failed to parse tx %d: %v", i, err) - } - txs[i] = &tx - } - withdrawals := make([]*ctypes.Withdrawal, len(payload.Withdrawals)) - for i, w := range payload.Withdrawals { - withdrawals[i] = &ctypes.Withdrawal{ - Index: uint64(w.Index), - Validator: uint64(w.ValidatorIndex), - Address: common.Address(w.Address), - Amount: uint64(w.Amount), - } - } - wroot := ctypes.DeriveSha(ctypes.Withdrawals(withdrawals), trie.NewStackTrie(nil)) - execHeader := &ctypes.Header{ - ParentHash: common.Hash(payload.ParentHash), - UncleHash: ctypes.EmptyUncleHash, - Coinbase: common.Address(payload.FeeRecipient), - Root: common.Hash(payload.StateRoot), - TxHash: ctypes.DeriveSha(ctypes.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: common.Hash(payload.ReceiptsRoot), - Bloom: ctypes.Bloom(payload.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(uint64(payload.BlockNumber)), - GasLimit: uint64(payload.GasLimit), - GasUsed: uint64(payload.GasUsed), - Time: uint64(payload.Timestamp), - Extra: []byte(payload.ExtraData), - MixDigest: common.Hash(payload.PrevRandao), // reused in merge - Nonce: ctypes.BlockNonce{}, // zero - BaseFee: (*uint256.Int)(&payload.BaseFeePerGas).ToBig(), - WithdrawalsHash: &wroot, - } - execBlock := ctypes.NewBlockWithHeader(execHeader).WithBody(txs, nil).WithWithdrawals(withdrawals) - if execBlockHash := execBlock.Hash(); execBlockHash != common.Hash(payload.BlockHash) { - return nil, fmt.Errorf("Sanity check failed, payload hash does not match (expected %x, got %x)", common.Hash(payload.BlockHash), execBlockHash) - } - return execBlock, nil -} - -// beaconBlockHash calculates the hash of a beacon block. -func beaconBlockHash(beaconBlock *capella.BeaconBlock) common.Hash { - return common.Hash(beaconBlock.HashTreeRoot(configs.Mainnet, tree.GetHashFn())) -} - -func callNewPayloadV2(client *rpc.Client, block *ctypes.Block) (string, error) { +func callNewPayloadV2(client *rpc.Client, execData *engine.ExecutableData) (string, error) { var resp engine.PayloadStatusV1 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - err := client.CallContext(ctx, &resp, "engine_newPayloadV2", *engine.BlockToExecutableData(block, nil, nil).ExecutionPayload) + err := client.CallContext(ctx, &resp, "engine_newPayloadV2", execData) cancel() return resp.Status, err } diff --git a/cmd/blsync/main.go b/cmd/blsync/main.go index 93be22ecb6..422a4943eb 100644 --- a/cmd/blsync/main.go +++ b/cmd/blsync/main.go @@ -17,20 +17,18 @@ package main import ( + "context" "fmt" "io" "os" - "strings" - "github.com/ethereum/go-ethereum/beacon/light" - "github.com/ethereum/go-ethereum/beacon/light/api" - "github.com/ethereum/go-ethereum/beacon/light/request" - "github.com/ethereum/go-ethereum/beacon/light/sync" + "github.com/ethereum/go-ethereum/beacon/blsync" + "github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb/memorydb" "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" @@ -72,7 +70,7 @@ func main() { verbosityFlag, vmoduleFlag, } - app.Action = blsync + app.Action = sync if err := app.Run(os.Args); err != nil { fmt.Fprintln(os.Stderr, err) @@ -80,7 +78,7 @@ func main() { } } -func blsync(ctx *cli.Context) error { +func sync(ctx *cli.Context) error { usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" output := io.Writer(os.Stderr) if usecolor { @@ -89,53 +87,39 @@ func blsync(ctx *cli.Context) error { verbosity := log.FromLegacyLevel(ctx.Int(verbosityFlag.Name)) log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor))) - if !ctx.IsSet(utils.BeaconApiFlag.Name) { - utils.Fatalf("Beacon node light client API URL not specified") - } - var ( - chainConfig = makeChainConfig(ctx) - customHeader = make(map[string]string) - ) - - for _, s := range ctx.StringSlice(utils.BeaconApiHeaderFlag.Name) { - kv := strings.Split(s, ":") - if len(kv) != 2 { - utils.Fatalf("Invalid custom API header entry: %s", s) - } - customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) - } - - // create data structures - var ( - db = memorydb.New() - threshold = ctx.Int(utils.BeaconThresholdFlag.Name) - committeeChain = light.NewCommitteeChain(db, chainConfig.ChainConfig, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name)) - headTracker = light.NewHeadTracker(committeeChain, threshold) - ) - headSync := sync.NewHeadSync(headTracker, committeeChain) - - // set up scheduler and sync modules - scheduler := request.NewScheduler() - checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint) - forwardSync := sync.NewForwardUpdateSync(committeeChain) - beaconBlockSync := newBeaconBlockSync(headTracker) - scheduler.RegisterTarget(headTracker) - scheduler.RegisterTarget(committeeChain) - scheduler.RegisterModule(checkpointInit, "checkpointInit") - scheduler.RegisterModule(forwardSync, "forwardSync") - scheduler.RegisterModule(headSync, "headSync") - scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync") - go updateEngineApi(makeRPCClient(ctx), beaconBlockSync.headCh) - // start - scheduler.Start() - // register server(s) - for _, url := range ctx.StringSlice(utils.BeaconApiFlag.Name) { - beaconApi := api.NewBeaconLightApi(url, customHeader) - scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{})) - } + headCh := make(chan types.ChainHeadEvent, 1) + client := blsync.NewClient(ctx) + sub := client.SubscribeChainHeadEvent(headCh) + go updateEngineApi(makeRPCClient(ctx), headCh) + client.Start() // run until stopped <-ctx.Done() - scheduler.Stop() - close(beaconBlockSync.headCh) + client.Stop() + sub.Unsubscribe() + close(headCh) return nil } + +func makeRPCClient(ctx *cli.Context) *rpc.Client { + if !ctx.IsSet(utils.BlsyncApiFlag.Name) { + log.Warn("No engine API target specified, performing a dry run") + return nil + } + if !ctx.IsSet(utils.BlsyncJWTSecretFlag.Name) { + utils.Fatalf("JWT secret parameter missing") //TODO use default if datadir is specified + } + + engineApiUrl, jwtFileName := ctx.String(utils.BlsyncApiFlag.Name), ctx.String(utils.BlsyncJWTSecretFlag.Name) + var jwtSecret [32]byte + if jwt, err := node.ObtainJWTSecret(jwtFileName); err == nil { + copy(jwtSecret[:], jwt) + } else { + utils.Fatalf("Error loading or generating JWT secret: %v", err) + } + 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 +} diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 5f52f1df54..37d17fb1e7 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" + "github.com/ethereum/go-ethereum/beacon/blsync" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -221,6 +222,8 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon) stack.RegisterLifecycle(simBeacon) + } else if ctx.IsSet(utils.BeaconApiFlag.Name) { + stack.RegisterLifecycle(catalyst.NewBlsync(blsync.NewClient(ctx), eth)) } else { err := catalyst.Register(stack, eth) if err != nil { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 9a88e9f2e8..d79d23e226 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -146,6 +146,14 @@ var ( configFileFlag, utils.LogDebugFlag, utils.LogBacktraceAtFlag, + utils.BeaconApiFlag, + utils.BeaconApiHeaderFlag, + utils.BeaconThresholdFlag, + utils.BeaconNoFilterFlag, + utils.BeaconConfigFlag, + utils.BeaconGenesisRootFlag, + utils.BeaconGenesisTimeFlag, + utils.BeaconCheckpointFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/eth/catalyst/blsync.go b/eth/catalyst/blsync.go new file mode 100644 index 0000000000..8f86afa95a --- /dev/null +++ b/eth/catalyst/blsync.go @@ -0,0 +1,88 @@ +// 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/types" + "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 { + engine *ConsensusAPI + client Client + headCh chan types.ChainHeadEvent + headSub event.Subscription + + quitCh chan struct{} +} + +type Client interface { + SubscribeChainHeadEvent(ch chan<- types.ChainHeadEvent) event.Subscription + Start() + Stop() +} + +// NewBlsync creates a new beacon light syncer. +func NewBlsync(client Client, eth *eth.Ethereum) *Blsync { + return &Blsync{ + engine: newConsensusAPIWithoutHeartbeat(eth), + client: client, + headCh: make(chan types.ChainHeadEvent, 1), + 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("Beacon light sync 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.HeadBlock); err != nil { + log.Error("failed to send new payload", "err", err) + continue + } + update := engine.ForkchoiceStateV1{ + HeadBlockHash: head.HeadBlock.BlockHash, + SafeBlockHash: head.Finalized, //TODO pass finalized or empty hash here? + FinalizedBlockHash: head.Finalized, + } + 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 +}