diff --git a/beacon/blsync/client.go b/beacon/blsync/client.go
index 39a1c6ea76..f8d9d2fb11 100644
--- a/beacon/blsync/client.go
+++ b/beacon/blsync/client.go
@@ -17,61 +17,40 @@
package blsync
import (
- "strings"
-
+ "github.com/ethereum/go-ethereum/beacon/config"
"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/ethereum/go-ethereum/rpc"
- "github.com/urfave/cli/v2"
)
type Client struct {
- urls []string
- customHeader map[string]string
- chainConfig *lightClientConfig
- scheduler *request.Scheduler
- blockSync *beaconBlockSync
- engineRPC *rpc.Client
+ config config.LightClientConfig
+ scheduler *request.Scheduler
+ blockSync *beaconBlockSync
+ engineRPC *rpc.Client
chainHeadSub event.Subscription
engineClient *engineClient
}
-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])
- }
-
+func NewClient(config config.LightClientConfig) *Client {
// 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)
+ committeeChain = light.NewCommitteeChain(db, config.ChainConfig, config.SignerThreshold, config.EnforceTime)
+ headTracker = light.NewHeadTracker(committeeChain, config.SignerThreshold)
)
headSync := sync.NewHeadSync(headTracker, committeeChain)
// set up scheduler and sync modules
scheduler := request.NewScheduler()
- checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint)
+ checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSync(headTracker)
scheduler.RegisterTarget(headTracker)
@@ -82,11 +61,9 @@ func NewClient(ctx *cli.Context) *Client {
scheduler.RegisterModule(beaconBlockSync, "beaconBlockSync")
return &Client{
- scheduler: scheduler,
- urls: ctx.StringSlice(utils.BeaconApiFlag.Name),
- customHeader: customHeader,
- chainConfig: &chainConfig,
- blockSync: beaconBlockSync,
+ scheduler: scheduler,
+ config: config,
+ blockSync: beaconBlockSync,
}
}
@@ -97,11 +74,11 @@ func (c *Client) SetEngineRPC(engine *rpc.Client) {
func (c *Client) Start() error {
headCh := make(chan types.ChainHeadEvent, 16)
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh)
- c.engineClient = startEngineClient(c.chainConfig, c.engineRPC, headCh)
+ c.engineClient = startEngineClient(c.config.ChainConfig, c.engineRPC, headCh)
c.scheduler.Start()
- for _, url := range c.urls {
- beaconApi := api.NewBeaconLightApi(url, c.customHeader)
+ for _, url := range c.config.ApiUrls {
+ beaconApi := api.NewBeaconLightApi(url, c.config.CustomHeader)
c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
}
return nil
diff --git a/beacon/blsync/engineclient.go b/beacon/blsync/engineclient.go
index 97ef6f5cb8..a64401ba5c 100644
--- a/beacon/blsync/engineclient.go
+++ b/beacon/blsync/engineclient.go
@@ -31,14 +31,14 @@ import (
)
type engineClient struct {
- config *lightClientConfig
+ config *types.ChainConfig
rpc *rpc.Client
rootCtx context.Context
cancelRoot context.CancelFunc
wg sync.WaitGroup
}
-func startEngineClient(config *lightClientConfig, rpc *rpc.Client, headCh <-chan types.ChainHeadEvent) *engineClient {
+func startEngineClient(config *types.ChainConfig, rpc *rpc.Client, headCh <-chan types.ChainHeadEvent) *engineClient {
ctx, cancel := context.WithCancel(context.Background())
ec := &engineClient{
config: config,
diff --git a/beacon/blsync/config.go b/beacon/config/config.go
similarity index 79%
rename from beacon/blsync/config.go
rename to beacon/config/config.go
index 93ed81306c..e9e703bbc3 100644
--- a/beacon/blsync/config.go
+++ b/beacon/config/config.go
@@ -14,9 +14,11 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-package blsync
+package config
import (
+ "strings"
+
"github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
@@ -24,14 +26,23 @@ import (
"github.com/urfave/cli/v2"
)
-// lightClientConfig contains beacon light client configuration
-type lightClientConfig struct {
+// LightChainConfig contains beacon light chain configuration
+type LightChainConfig struct {
*types.ChainConfig
Checkpoint common.Hash
}
+// LightClientConfig contains beacon light client configuration
+type LightClientConfig struct {
+ LightChainConfig
+ ApiUrls []string
+ CustomHeader map[string]string
+ SignerThreshold int
+ EnforceTime bool
+}
+
var (
- MainnetConfig = lightClientConfig{
+ MainnetConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"),
GenesisTime: 1606824023,
@@ -44,7 +55,7 @@ var (
Checkpoint: common.HexToHash("0x388be41594ec7d6a6894f18c73f3469f07e2c19a803de4755d335817ed8e2e5a"),
}
- SepoliaConfig = lightClientConfig{
+ SepoliaConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"),
GenesisTime: 1655733600,
@@ -57,7 +68,7 @@ var (
Checkpoint: common.HexToHash("0x1005a6d9175e96bfbce4d35b80f468e9bff0b674e1e861d16e09e10005a58e81"),
}
- GoerliConfig = lightClientConfig{
+ GoerliConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"),
GenesisTime: 1614588812,
@@ -71,8 +82,8 @@ var (
}
)
-func makeChainConfig(ctx *cli.Context) lightClientConfig {
- var config lightClientConfig
+func MakeLightChainConfig(ctx *cli.Context) LightChainConfig {
+ var config LightChainConfig
customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name)
utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag, utils.BeaconConfigFlag)
switch {
@@ -127,3 +138,25 @@ func makeChainConfig(ctx *cli.Context) lightClientConfig {
}
return config
}
+
+func MakeLightClientConfig(ctx *cli.Context) LightClientConfig {
+ if !ctx.IsSet(utils.BeaconApiFlag.Name) {
+ utils.Fatalf("Beacon node light client API URL not specified")
+ }
+ 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])
+ }
+
+ return LightClientConfig{
+ LightChainConfig: MakeLightChainConfig(ctx),
+ ApiUrls: ctx.StringSlice(utils.BeaconApiFlag.Name),
+ CustomHeader: customHeader,
+ SignerThreshold: ctx.Int(utils.BeaconThresholdFlag.Name),
+ EnforceTime: !ctx.Bool(utils.BeaconNoFilterFlag.Name),
+ }
+}
diff --git a/beacon/types/exec_header.go b/beacon/types/exec_header.go
index dce101ba20..6248bb953b 100644
--- a/beacon/types/exec_header.go
+++ b/beacon/types/exec_header.go
@@ -68,6 +68,17 @@ func (eh *ExecutionHeader) PayloadRoot() merkle.Value {
return merkle.Value(eh.obj.HashTreeRoot(tree.GetHashFn()))
}
+func (eh *ExecutionHeader) BlockNumber() uint64 {
+ switch obj := eh.obj.(type) {
+ case *capella.ExecutionPayloadHeader:
+ return uint64(obj.BlockNumber)
+ case *deneb.ExecutionPayloadHeader:
+ return uint64(obj.BlockNumber)
+ default:
+ panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
+ }
+}
+
func (eh *ExecutionHeader) BlockHash() common.Hash {
switch obj := eh.obj.(type) {
case *capella.ExecutionPayloadHeader:
@@ -78,3 +89,25 @@ func (eh *ExecutionHeader) BlockHash() common.Hash {
panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
}
}
+
+func (eh *ExecutionHeader) ParentHash() common.Hash {
+ switch obj := eh.obj.(type) {
+ case *capella.ExecutionPayloadHeader:
+ return common.Hash(obj.ParentHash)
+ case *deneb.ExecutionPayloadHeader:
+ return common.Hash(obj.ParentHash)
+ default:
+ panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
+ }
+}
+
+func (eh *ExecutionHeader) StateRoot() common.Hash {
+ switch obj := eh.obj.(type) {
+ case *capella.ExecutionPayloadHeader:
+ return common.Hash(obj.StateRoot)
+ case *deneb.ExecutionPayloadHeader:
+ return common.Hash(obj.StateRoot)
+ default:
+ panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj))
+ }
+}
diff --git a/cmd/blsync/main.go b/cmd/blsync/main.go
index 2aa3d9a24e..9ee3d3101e 100644
--- a/cmd/blsync/main.go
+++ b/cmd/blsync/main.go
@@ -23,6 +23,7 @@ import (
"os"
"github.com/ethereum/go-ethereum/beacon/blsync"
+ "github.com/ethereum/go-ethereum/beacon/config"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log"
@@ -87,7 +88,7 @@ func sync(ctx *cli.Context) error {
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor)))
// set up blsync
- client := blsync.NewClient(ctx)
+ client := blsync.NewClient(config.MakeLightClientConfig(ctx))
client.SetEngineRPC(makeRPCClient(ctx))
client.Start()
diff --git a/cmd/bltest/main.go b/cmd/bltest/main.go
new file mode 100644
index 0000000000..3a76809eeb
--- /dev/null
+++ b/cmd/bltest/main.go
@@ -0,0 +1,169 @@
+// 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 .
+
+//TODO only for manual testing of ethclient/lightclient; remove before merging to master
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math/big"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/ethereum/go-ethereum/beacon/config"
+ "github.com/ethereum/go-ethereum/cmd/utils"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethclient/lightclient"
+ "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/rpc"
+ "github.com/mattn/go-colorable"
+ "github.com/mattn/go-isatty"
+ "github.com/urfave/cli/v2"
+)
+
+var (
+ verbosityFlag = &cli.IntFlag{
+ Name: "verbosity",
+ Usage: "Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail",
+ Value: 3,
+ Category: flags.LoggingCategory,
+ }
+ vmoduleFlag = &cli.StringFlag{
+ Name: "vmodule",
+ Usage: "Per-module verbosity: comma-separated list of = (e.g. eth/*=5,p2p=4)",
+ Value: "",
+ Hidden: true,
+ Category: flags.LoggingCategory,
+ }
+)
+
+func main() {
+ app := flags.NewApp("beacon light syncer tool")
+ app.Flags = []cli.Flag{
+ utils.BeaconApiFlag,
+ utils.BeaconApiHeaderFlag,
+ utils.BeaconThresholdFlag,
+ utils.BeaconNoFilterFlag,
+ utils.BeaconConfigFlag,
+ utils.BeaconGenesisRootFlag,
+ utils.BeaconGenesisTimeFlag,
+ utils.BeaconCheckpointFlag,
+ utils.BltestApiFlag,
+ //TODO datadir for optional permanent database
+ utils.MainnetFlag,
+ utils.SepoliaFlag,
+ utils.GoerliFlag,
+ verbosityFlag,
+ vmoduleFlag,
+ }
+ app.Action = sync
+
+ if err := app.Run(os.Args); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+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 {
+ output = colorable.NewColorable(os.Stderr)
+ }
+ verbosity := log.FromLegacyLevel(ctx.Int(verbosityFlag.Name))
+ log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor)))
+
+ customHeaders := make(http.Header)
+ for _, s := range ctx.StringSlice(utils.BeaconApiHeaderFlag.Name) { //TODO separate header flag for EL
+ kv := strings.Split(s, ":")
+ if len(kv) != 2 {
+ utils.Fatalf("Invalid custom API header entry: %s", s)
+ }
+ customHeaders.Add(strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]))
+ }
+
+ var opts []rpc.ClientOption
+ if len(customHeaders) > 0 {
+ opts = append(opts, rpc.WithHeaders(customHeaders))
+ }
+ rpcClient, err := rpc.DialOptions(context.Background(), ctx.String(utils.BltestApiFlag.Name), opts...)
+ if err != nil {
+ utils.Fatalf("Could not create RPC client: %v", err)
+ }
+ client := lightclient.NewClient(config.MakeLightClientConfig(ctx), memorydb.New(), rpcClient)
+ client.Start()
+
+ headCh := make(chan *types.Header, 1)
+ client.SubscribeNewHead(context.Background(), headCh)
+
+ // run until stopped
+loop:
+ for {
+ select {
+ case head := <-headCh:
+ log.Info("SubscribeNewHead delivered new head", "number", head.Number, "hash", head.Hash(), "parentHash", head.ParentHash)
+ ctx, _ := context.WithTimeout(context.Background(), time.Second*10)
+ if block, err := client.BlockByHash(ctx, head.ParentHash); err == nil {
+ log.Info("BlockByHash", "hash", head.ParentHash, "block.Hash", block.Hash(), "block.Number", block.Number(), "len(block.Transactions)", len(block.Transactions()))
+ } else {
+ log.Error("BlockByHash", "hash", head.ParentHash, "error", err)
+ }
+ num := big.NewInt(2)
+ num.Sub(head.Number, num)
+ if block, err := client.BlockByNumber(ctx, num); err == nil {
+ log.Info("BlockByNumber", "number", num, "block.Hash", block.Hash(), "block.Number", block.Number(), "len(block.Transactions)", len(block.Transactions()))
+ } else {
+ log.Error("BlockByNumber", "number", num, "error", err)
+ }
+ if tc, err := client.TransactionCount(ctx, head.Hash()); err == nil {
+ log.Info("TransactionCount", "hash", head.Hash(), "count", tc)
+ } else {
+ log.Error("TransactionCount", "hash", head.Hash(), "error", err)
+ }
+ testState := func(addr common.Address) {
+ if balance, err := client.BalanceAt(ctx, addr, big.NewInt(int64(rpc.LatestBlockNumber))); err == nil {
+ log.Info("BalanceAt ", "address", addr, "balance", balance)
+ } else {
+ log.Error("BalanceAt ", "address", addr, "error", err)
+ }
+ if code, err := client.CodeAt(ctx, addr, big.NewInt(int64(rpc.LatestBlockNumber))); err == nil {
+ log.Info("CodeAt ", "address", addr, "len(code)", len(code))
+ } else {
+ log.Error("CodeAt ", "address", addr, "error", err)
+ }
+ if storage, err := client.StorageAt(ctx, addr, common.Hash{}, big.NewInt(int64(rpc.LatestBlockNumber))); err == nil {
+ log.Info("StorageAt ", "address", addr, "key", common.Hash{}, "storage", storage)
+ } else {
+ log.Error("StorageAt ", "address", addr, "key", common.Hash{}, "error", err)
+ }
+ }
+ testState(common.Address{})
+ testState(common.HexToAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")) // WETH contract
+ case <-ctx.Done():
+ break loop
+ }
+ }
+
+ client.Stop()
+ return nil
+}
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index ecf6acc186..d9b9388f54 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -335,6 +335,11 @@ var (
Usage: "Path to a JWT secret to use for target engine API endpoint",
Category: flags.BeaconCategory,
}
+ BltestApiFlag = &cli.StringFlag{ //TODO remove before merging to master
+ Name: "bltest.rpc",
+ Usage: "Target EL rpc API URL",
+ Category: flags.BeaconCategory,
+ }
// Transaction pool settings
TxPoolLocalsFlag = &cli.StringFlag{
Name: "txpool.locals",
diff --git a/ethclient/lightclient/chain.go b/ethclient/lightclient/chain.go
new file mode 100644
index 0000000000..690b5f1141
--- /dev/null
+++ b/ethclient/lightclient/chain.go
@@ -0,0 +1,403 @@
+// 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 lightclient
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "math/big"
+ "sync"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/beacon/light"
+ "github.com/ethereum/go-ethereum/beacon/light/request"
+ btypes "github.com/ethereum/go-ethereum/beacon/types"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+const recentCanonicalLength = 256
+
+type canonicalChain struct {
+ lock sync.Mutex
+ headTracker *light.HeadTracker
+ blocksAndHeaders *blocksAndHeaders
+ newHeadCb func(common.Hash)
+
+ head, finality *btypes.ExecutionHeader
+ recent map[uint64]common.Hash // nil until initialized
+ recentTail uint64 // if recent != nil then recent hashes are available from recentTail to head
+ finalized *lru.Cache[uint64, common.Hash] // finalized but not recent hashes
+ requests *requestMap[uint64, common.Hash] // requested; neither recent nor finalized
+}
+
+func newCanonicalChain(headTracker *light.HeadTracker, blocksAndHeaders *blocksAndHeaders, newHeadCb func(common.Hash)) *canonicalChain {
+ return &canonicalChain{
+ headTracker: headTracker,
+ blocksAndHeaders: blocksAndHeaders,
+ newHeadCb: newHeadCb,
+ finalized: lru.NewCache[uint64, common.Hash](10000),
+ requests: newRequestMap[uint64, common.Hash](nil),
+ }
+}
+
+// Process implements request.Module in order to get notified about new heads.
+func (c *canonicalChain) Process(requester request.Requester, events []request.Event) {
+ if finality, ok := c.headTracker.ValidatedFinality(); ok {
+ finalized := finality.Finalized.PayloadHeader
+ c.setFinality(finalized)
+ c.blocksAndHeaders.addPayloadHeader(finalized)
+ }
+ if optimistic, ok := c.headTracker.ValidatedOptimistic(); ok {
+ head := optimistic.Attested.PayloadHeader
+ c.blocksAndHeaders.addPayloadHeader(head)
+ if c.setHead(head) {
+ c.newHeadCb(head.BlockHash()) // should not block
+ }
+ }
+}
+
+func (c *canonicalChain) getHash(ctx context.Context, number uint64) (common.Hash, error) {
+ c.lock.Lock()
+ if hash, ok := c.recent[number]; ok {
+ c.lock.Unlock()
+ return hash, nil
+ }
+ if hash, ok := c.finalized.Get(number); ok {
+ c.lock.Unlock()
+ return hash, nil
+ }
+ req := c.requests.request(number)
+ c.lock.Unlock()
+ return req.getResult(ctx)
+}
+
+func (c *canonicalChain) setHead(head *btypes.ExecutionHeader) bool {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ headNum, headHash := head.BlockNumber(), head.BlockHash()
+ if c.head != nil && c.head.BlockHash() == headHash {
+ return false
+ }
+ if c.recent == nil || c.head == nil || c.head.BlockNumber()+1 != headNum || headHash != head.ParentHash() {
+ c.recent = make(map[uint64]common.Hash)
+ if headNum > 0 {
+ c.recent[headNum-1] = head.ParentHash()
+ c.recentTail = headNum - 1
+ } else {
+ c.recentTail = 0
+ }
+ }
+ c.head = head
+ c.recent[headNum] = headHash
+ for headNum >= c.recentTail+recentCanonicalLength {
+ if c.finality != nil && c.recentTail <= c.finality.BlockNumber() {
+ c.finalized.Add(c.recentTail, c.recent[c.recentTail])
+ }
+ delete(c.recent, c.recentTail)
+ c.recentTail++
+ }
+ c.requests.tryDeliver(headNum, headHash)
+ return true
+}
+
+func (c *canonicalChain) setFinality(finality *btypes.ExecutionHeader) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ c.finality = finality
+ finalNum := finality.BlockNumber()
+ if finalNum < c.recentTail {
+ c.finalized.Add(finalNum, finality.BlockHash())
+ }
+ c.requests.tryDeliver(finalNum, finality.BlockHash())
+}
+
+func (c *canonicalChain) addRecentTail(tail *types.Header) bool {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ if c.recent == nil || tail.Number.Uint64() != c.recentTail || c.recent[c.recentTail] != tail.Hash() {
+ return false
+ }
+ if c.recentTail > 0 {
+ c.recentTail--
+ c.recent[c.recentTail] = tail.ParentHash
+ c.requests.tryDeliver(c.recentTail, tail.ParentHash)
+ }
+ return true
+}
+
+func (c *canonicalChain) getHead() *btypes.ExecutionHeader {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ return c.head
+}
+
+func (c *canonicalChain) getFinality() *btypes.ExecutionHeader {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ return c.finality
+}
+
+func (c *canonicalChain) resolveBlockNumber(number *big.Int) (uint64, *btypes.ExecutionHeader, error) {
+ if !number.IsInt64() {
+ return 0, nil, errors.New("Invalid block number")
+ }
+ num := number.Int64()
+ if num < 0 {
+ switch rpc.BlockNumber(num) {
+ case rpc.SafeBlockNumber, rpc.FinalizedBlockNumber:
+ if header := c.getFinality(); header != nil {
+ return header.BlockNumber(), header, nil
+ }
+ return 0, nil, errors.New("Finalized block unknown")
+ case rpc.LatestBlockNumber, rpc.PendingBlockNumber:
+ if header := c.getHead(); header != nil {
+ return header.BlockNumber(), header, nil
+ }
+ return 0, nil, errors.New("Head block unknown")
+ default:
+ return 0, nil, errors.New("Invalid block number")
+ }
+ }
+ return uint64(num), nil, nil
+}
+
+func (c *canonicalChain) blockNumberToHash(ctx context.Context, number *big.Int) (common.Hash, error) {
+ num, header, err := c.resolveBlockNumber(number)
+ if err != nil {
+ return common.Hash{}, err
+ }
+ if header != nil {
+ return header.BlockHash(), nil
+ }
+ return c.getHash(ctx, num)
+}
+
+type blocksAndHeaders struct {
+ client *rpc.Client
+ headerCache *lru.Cache[common.Hash, *types.Header]
+ headerRequests *requestMap[common.Hash, *types.Header]
+ payloadHeaderCache *lru.Cache[common.Hash, *btypes.ExecutionHeader]
+ blockCache *lru.Cache[common.Hash, *types.Block]
+ blockRequests *requestMap[common.Hash, *types.Block]
+}
+
+func newBlocksAndHeaders(client *rpc.Client) *blocksAndHeaders {
+ b := &blocksAndHeaders{
+ client: client,
+ headerCache: lru.NewCache[common.Hash, *types.Header](1000),
+ payloadHeaderCache: lru.NewCache[common.Hash, *btypes.ExecutionHeader](1000),
+ blockCache: lru.NewCache[common.Hash, *types.Block](10),
+ }
+ b.headerRequests = newRequestMap[common.Hash, *types.Header](b.requestHeader)
+ b.blockRequests = newRequestMap[common.Hash, *types.Block](b.requestBlock)
+ return b
+}
+
+func (b *blocksAndHeaders) requestHeader(ctx context.Context, hash common.Hash) (*types.Header, error) {
+ var header *types.Header
+ log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false)
+ err := b.client.CallContext(ctx, &header, "eth_getBlockByHash", hash, false)
+ if err == nil && header.Hash() != hash {
+ header, err = nil, errors.New("header hash does not match")
+ }
+ log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", false, "error", err)
+ return header, err
+}
+
+func (b *blocksAndHeaders) requestBlock(ctx context.Context, hash common.Hash) (*types.Block, error) {
+ var (
+ raw json.RawMessage
+ block *types.Block
+ )
+ log.Debug("Starting RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true)
+ err := b.client.CallContext(ctx, &raw, "eth_getBlockByHash", hash, true)
+ log.Debug("Finished RPC request", "type", "eth_getBlockByHash", "hash", hash, "full", true, "error", err)
+ if err == nil {
+ block, err = decodeBlock(raw)
+ if block.Hash() != hash {
+ block, err = nil, errors.New("block hash does not match")
+ }
+ }
+ return block, err
+}
+
+func (b *blocksAndHeaders) getHeader(ctx context.Context, hash common.Hash) (*types.Header, error) {
+ if header, ok := b.headerCache.Get(hash); ok {
+ return header, nil
+ }
+ if block, ok := b.blockCache.Get(hash); ok {
+ return block.Header(), nil
+ }
+ if b.blockRequests.has(hash) && !b.headerRequests.has(hash) {
+ req := b.blockRequests.request(hash)
+ block, err := req.getResult(ctx)
+ if err == nil {
+ header := block.Header()
+ b.headerCache.Add(hash, header)
+ b.blockCache.Add(hash, block)
+ req.release()
+ return header, nil
+ } else {
+ req.release()
+ return nil, err
+ }
+ }
+ req := b.headerRequests.request(hash)
+ header, err := req.getResult(ctx)
+ if err == nil {
+ b.headerCache.Add(hash, header)
+ }
+ req.release()
+ return header, err
+}
+
+func (b *blocksAndHeaders) getPayloadHeader(hash common.Hash) *btypes.ExecutionHeader {
+ pheader, _ := b.payloadHeaderCache.Get(hash)
+ return pheader
+}
+
+func (b *blocksAndHeaders) getBlock(ctx context.Context, hash common.Hash) (*types.Block, error) {
+ if block, ok := b.blockCache.Get(hash); ok {
+ return block, nil
+ }
+ req := b.blockRequests.request(hash)
+ block, err := req.getResult(ctx)
+ if err == nil {
+ header := block.Header()
+ b.headerCache.Add(hash, header)
+ b.headerRequests.tryDeliver(hash, header)
+ b.blockCache.Add(hash, block)
+ }
+ req.release()
+ return block, err
+}
+
+//TODO de-duplicate json block decoding
+type rpcBlock struct {
+ Hash common.Hash `json:"hash"`
+ Transactions []rpcTransaction `json:"transactions"`
+ UncleHashes []common.Hash `json:"uncles"`
+ Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty"`
+}
+
+type rpcTransaction struct {
+ tx *types.Transaction
+ txExtraInfo
+}
+
+type txExtraInfo struct {
+ BlockNumber *string `json:"blockNumber,omitempty"`
+ BlockHash *common.Hash `json:"blockHash,omitempty"`
+ From *common.Address `json:"from,omitempty"`
+}
+
+func (tx *rpcTransaction) UnmarshalJSON(msg []byte) error {
+ if err := json.Unmarshal(msg, &tx.tx); err != nil {
+ return err
+ }
+ return json.Unmarshal(msg, &tx.txExtraInfo)
+}
+
+// senderFromServer is a types.Signer that remembers the sender address returned by the RPC
+// server. It is stored in the transaction's sender address cache to avoid an additional
+// request in TransactionSender.
+type senderFromServer struct {
+ addr common.Address
+ blockhash common.Hash
+}
+
+func setSenderFromServer(tx *types.Transaction, addr common.Address, block common.Hash) {
+ // Use types.Sender for side-effect to store our signer into the cache.
+ types.Sender(&senderFromServer{addr, block}, tx)
+}
+
+var errNotCached = errors.New("sender not cached")
+
+func (s *senderFromServer) Equal(other types.Signer) bool {
+ os, ok := other.(*senderFromServer)
+ return ok && os.blockhash == s.blockhash
+}
+
+func (s *senderFromServer) Sender(tx *types.Transaction) (common.Address, error) {
+ if s.addr == (common.Address{}) {
+ return common.Address{}, errNotCached
+ }
+ return s.addr, nil
+}
+
+func (s *senderFromServer) ChainID() *big.Int {
+ panic("can't sign with senderFromServer")
+}
+func (s *senderFromServer) Hash(tx *types.Transaction) common.Hash {
+ panic("can't sign with senderFromServer")
+}
+func (s *senderFromServer) SignatureValues(tx *types.Transaction, sig []byte) (R, S, V *big.Int, err error) {
+ panic("can't sign with senderFromServer")
+}
+
+func decodeBlock(raw json.RawMessage) (*types.Block, error) {
+ // Decode header and transactions.
+ var head *types.Header
+ if err := json.Unmarshal(raw, &head); err != nil {
+ return nil, err
+ }
+ // When the block is not found, the API returns JSON null.
+ if head == nil {
+ return nil, ethereum.NotFound
+ }
+
+ var body rpcBlock
+ if err := json.Unmarshal(raw, &body); err != nil {
+ return nil, err
+ }
+ // Quick-verify transaction and uncle lists. This mostly helps with debugging the server.
+ if head.UncleHash == types.EmptyUncleHash && len(body.UncleHashes) > 0 {
+ return nil, errors.New("server returned non-empty uncle list but block header indicates no uncles")
+ }
+ if head.UncleHash != types.EmptyUncleHash && len(body.UncleHashes) == 0 {
+ return nil, errors.New("server returned empty uncle list but block header indicates uncles")
+ }
+ if head.TxHash == types.EmptyTxsHash && len(body.Transactions) > 0 {
+ return nil, errors.New("server returned non-empty transaction list but block header indicates no transactions")
+ }
+ if head.TxHash != types.EmptyTxsHash && len(body.Transactions) == 0 {
+ return nil, errors.New("server returned empty transaction list but block header indicates transactions")
+ }
+ // Fill the sender cache of transactions in the block.
+ txs := make([]*types.Transaction, len(body.Transactions))
+ for i, tx := range body.Transactions {
+ if tx.From != nil {
+ setSenderFromServer(tx.tx, *tx.From, body.Hash)
+ }
+ txs[i] = tx.tx
+ }
+ return types.NewBlockWithHeader(head).WithBody(types.Body{Transactions: txs, Withdrawals: body.Withdrawals}), nil
+}
+
+func (b *blocksAndHeaders) addPayloadHeader(header *btypes.ExecutionHeader) {
+ b.payloadHeaderCache.Add(header.BlockHash(), header)
+}
diff --git a/ethclient/lightclient/lightclient.go b/ethclient/lightclient/lightclient.go
new file mode 100644
index 0000000000..c482c2cfd2
--- /dev/null
+++ b/ethclient/lightclient/lightclient.go
@@ -0,0 +1,226 @@
+// 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 lightclient
+
+import (
+ "context"
+ "errors"
+ "math/big"
+ ssync "sync"
+
+ "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/beacon/config"
+ "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/common"
+ "github.com/ethereum/go-ethereum/common/mclock"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+type Client struct {
+ config config.LightClientConfig
+ scheduler *request.Scheduler
+ canonicalChain *canonicalChain
+ blocksAndHeaders *blocksAndHeaders
+ state *lightState
+ headSubLock ssync.Mutex
+ headSubs map[*headSub]struct{}
+ cancelHeadFetch func()
+ headFetchCounter int
+}
+
+func NewClient(config config.LightClientConfig, db ethdb.KeyValueStore, rpcClient *rpc.Client) *Client {
+ // create data structures
+ var (
+ committeeChain = light.NewCommitteeChain(db, config.ChainConfig, config.SignerThreshold, config.EnforceTime)
+ headTracker = light.NewHeadTracker(committeeChain, config.SignerThreshold)
+ )
+ // set up scheduler and sync modules
+ //chainHeadFeed := new(event.Feed)
+ scheduler := request.NewScheduler()
+ blocksAndHeaders := newBlocksAndHeaders(rpcClient)
+ client := &Client{
+ config: config,
+ scheduler: scheduler,
+ blocksAndHeaders: blocksAndHeaders,
+ headSubs: make(map[*headSub]struct{}),
+ }
+ canonicalChain := newCanonicalChain(headTracker, blocksAndHeaders, client.newHead)
+ client.canonicalChain = canonicalChain
+ client.state = newLightState(rpcClient, canonicalChain, blocksAndHeaders)
+
+ checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint)
+ forwardSync := sync.NewForwardUpdateSync(committeeChain)
+ headSync := sync.NewHeadSync(headTracker, committeeChain)
+ scheduler.RegisterTarget(headTracker)
+ scheduler.RegisterTarget(committeeChain)
+ scheduler.RegisterModule(checkpointInit, "checkpointInit")
+ scheduler.RegisterModule(forwardSync, "forwardSync")
+ scheduler.RegisterModule(headSync, "headSync")
+ scheduler.RegisterModule(client.canonicalChain, "canonicalChain")
+ return client
+}
+
+func (c *Client) Start() {
+ c.scheduler.Start()
+ for _, url := range c.config.ApiUrls {
+ beaconApi := api.NewBeaconLightApi(url, c.config.CustomHeader)
+ c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
+ }
+}
+
+func (c *Client) Stop() {
+ c.scheduler.Stop()
+}
+
+func (c *Client) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) {
+ return c.blocksAndHeaders.getBlock(ctx, hash)
+}
+
+func (c *Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) {
+ hash, err := c.canonicalChain.blockNumberToHash(ctx, number)
+ if err != nil {
+ return nil, err
+ }
+ return c.BlockByHash(ctx, hash)
+}
+
+func (c *Client) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
+ return c.blocksAndHeaders.getHeader(ctx, hash)
+}
+
+func (c *Client) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
+ hash, err := c.canonicalChain.blockNumberToHash(ctx, number)
+ if err != nil {
+ return nil, err
+ }
+ return c.HeaderByHash(ctx, hash)
+}
+
+func (c *Client) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) {
+ block, err := c.BlockByHash(ctx, blockHash)
+ if err != nil {
+ return 0, err
+ }
+ return uint(len(block.Transactions())), nil
+}
+
+func (c *Client) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) {
+ block, err := c.BlockByHash(ctx, blockHash)
+ if err != nil {
+ return nil, err
+ }
+ txs := block.Transactions()
+ if index >= uint(len(txs)) {
+ return nil, errors.New("Invalid transaction index")
+ }
+ return txs[index], nil
+}
+
+func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) {
+ sub := &headSub{
+ client: c,
+ headCh: ch,
+ errCh: make(chan error, 1),
+ }
+ c.headSubLock.Lock()
+ c.headSubs[sub] = struct{}{}
+ c.headSubLock.Unlock()
+ return sub, nil
+}
+
+func (c *Client) newHead(hash common.Hash) {
+ go func() {
+ log.Trace("New execution payload header received", "hash", hash)
+ ctx, cancel := context.WithCancel(context.Background())
+ c.headSubLock.Lock()
+ if c.cancelHeadFetch != nil {
+ c.cancelHeadFetch()
+ }
+ c.cancelHeadFetch = cancel
+ c.headFetchCounter++
+ hfc := c.headFetchCounter
+ c.headSubLock.Unlock()
+
+ head, err := c.blocksAndHeaders.getHeader(ctx, hash)
+ c.headSubLock.Lock()
+ if c.headFetchCounter == hfc {
+ c.cancelHeadFetch = nil
+ }
+ if err == nil {
+ for sub := range c.headSubs {
+ sub.headCh <- head
+ }
+ }
+ c.headSubLock.Unlock()
+ }()
+}
+
+func (c *Client) unsubscribeNewHead(sub *headSub) {
+ c.headSubLock.Lock()
+ delete(c.headSubs, sub)
+ c.headSubLock.Unlock()
+}
+
+type headSub struct {
+ client *Client
+ headCh chan<- *types.Header
+ errCh chan error
+}
+
+func (h *headSub) Unsubscribe() {
+ h.client.unsubscribeNewHead(h)
+ close(h.errCh)
+}
+
+func (h *headSub) Err() <-chan error {
+ return h.errCh
+}
+
+func (c *Client) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) {
+ proof, _, err := c.state.getProof(ctx, blockNumber, account, nil, false)
+ if err != nil {
+ return nil, err
+ }
+ return proof.Balance, nil
+}
+
+func (c *Client) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) {
+ proof, _, err := c.state.getProof(ctx, blockNumber, account, []string{key.Hex()}, false) //TODO hashed key?
+ if err != nil {
+ return nil, err
+ }
+ return stValueBytes(proof.StorageProof[0].Value)
+}
+
+func (c *Client) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) {
+ _, code, err := c.state.getProof(ctx, blockNumber, account, nil, true)
+ return code, err
+}
+
+func (c *Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) {
+ proof, _, err := c.state.getProof(ctx, blockNumber, account, nil, false)
+ if err != nil {
+ return 0, err
+ }
+ return proof.Nonce, nil
+}
diff --git a/ethclient/lightclient/request_map.go b/ethclient/lightclient/request_map.go
new file mode 100644
index 0000000000..fa8ea58d9d
--- /dev/null
+++ b/ethclient/lightclient/request_map.go
@@ -0,0 +1,130 @@
+// 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 lightclient
+
+import (
+ "context"
+ "sync"
+)
+
+type requestMap[K comparable, V any] struct {
+ lock sync.Mutex
+ requestFn func(context.Context, K) (V, error)
+ requests map[K]*mappedRequest[K, V]
+}
+
+func newRequestMap[K comparable, V any](requestFn func(context.Context, K) (V, error)) *requestMap[K, V] {
+ return &requestMap[K, V]{
+ requestFn: requestFn,
+ requests: make(map[K]*mappedRequest[K, V]),
+ }
+}
+
+func (rm *requestMap[K, V]) request(key K) *mappedRequest[K, V] {
+ rm.lock.Lock()
+ defer rm.lock.Unlock()
+
+ if r, ok := rm.requests[key]; ok {
+ r.lock.Lock()
+ r.refCount++
+ r.lock.Unlock()
+ return r
+ }
+ ctx, cancelFn := context.WithCancel(context.Background())
+ r := &mappedRequest[K, V]{
+ rm: rm,
+ key: key,
+ refCount: 1,
+ deliveredCh: make(chan struct{}),
+ cancelFn: cancelFn,
+ }
+ if rm.requestFn != nil {
+ go func() {
+ result, err := rm.requestFn(ctx, key)
+ r.deliver(result, err)
+ }()
+ }
+ return r
+}
+
+func (rm *requestMap[K, V]) has(key K) bool {
+ rm.lock.Lock()
+ defer rm.lock.Unlock()
+
+ _, ok := rm.requests[key]
+ return ok
+}
+
+// should only be called with validated results of successful requests
+func (rm *requestMap[K, V]) tryDeliver(key K, result V) {
+ rm.lock.Lock()
+ defer rm.lock.Unlock()
+
+ if r, ok := rm.requests[key]; ok {
+ r.deliver(result, nil)
+ }
+}
+
+type mappedRequest[K comparable, V any] struct {
+ lock sync.Mutex
+ rm *requestMap[K, V]
+ key K
+ refCount int
+ delivered bool
+ deliveredCh chan struct{}
+ cancelFn func() // called when delivered || refCount == 0 becomes true
+ result V
+ err error
+}
+
+func (r *mappedRequest[K, V]) deliver(result V, err error) {
+ r.lock.Lock()
+ if !r.delivered {
+ r.result, r.err = result, err
+ r.delivered = true
+ close(r.deliveredCh)
+ if r.refCount != 0 {
+ r.cancelFn()
+ }
+ }
+ r.lock.Unlock()
+}
+
+func (r *mappedRequest[K, V]) getResult(ctx context.Context) (V, error) {
+ select {
+ case <-r.deliveredCh:
+ // not changed after deliveredCh is closed
+ return r.result, r.err
+ case <-ctx.Done():
+ var null V
+ return null, ctx.Err()
+ }
+}
+
+func (r *mappedRequest[K, V]) release() {
+ r.rm.lock.Lock()
+ r.lock.Lock()
+ r.refCount--
+ if r.refCount == 0 {
+ delete(r.rm.requests, r.key)
+ if !r.delivered {
+ r.cancelFn()
+ }
+ }
+ r.lock.Unlock()
+ r.rm.lock.Unlock()
+}
diff --git a/ethclient/lightclient/state.go b/ethclient/lightclient/state.go
new file mode 100644
index 0000000000..db9e3322fa
--- /dev/null
+++ b/ethclient/lightclient/state.go
@@ -0,0 +1,331 @@
+// 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 lightclient
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "math/big"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/common/lru"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethclient/gethclient"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/holiman/uint256"
+)
+
+type proofRequest struct {
+ blockNumber uint64
+ address common.Address
+ storageKeys string
+}
+
+type codeRequest struct {
+ blockNumber uint64
+ address common.Address
+}
+
+type lightState struct {
+ client *rpc.Client
+ canonicalChain *canonicalChain
+ blocksAndHeaders *blocksAndHeaders
+ proofCache *lru.Cache[proofRequest, *gethclient.AccountResult]
+ proofRequests *requestMap[proofRequest, *gethclient.AccountResult]
+ codeCache *lru.Cache[codeRequest, []byte]
+ codeRequests *requestMap[codeRequest, []byte]
+}
+
+func newLightState(client *rpc.Client, canonicalChain *canonicalChain, blocksAndHeaders *blocksAndHeaders) *lightState {
+ s := &lightState{
+ client: client,
+ canonicalChain: canonicalChain,
+ blocksAndHeaders: blocksAndHeaders,
+ proofCache: lru.NewCache[proofRequest, *gethclient.AccountResult](100),
+ codeCache: lru.NewCache[codeRequest, []byte](10),
+ }
+ s.proofRequests = newRequestMap[proofRequest, *gethclient.AccountResult](s.requestProof)
+ s.codeRequests = newRequestMap[codeRequest, []byte](s.requestCode)
+ return s
+}
+
+func (s *lightState) fetchProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) {
+ if proof, ok := s.proofCache.Get(req); ok {
+ return proof, nil
+ }
+ request := s.proofRequests.request(req)
+ proof, err := request.getResult(ctx)
+ if err == nil {
+ s.proofCache.Add(req, proof) //TODO cached before validation; remove and retry if invalid
+ }
+ request.release()
+ return proof, err
+}
+
+func (s *lightState) requestProof(ctx context.Context, req proofRequest) (*gethclient.AccountResult, error) {
+ type storageResult struct {
+ Key string `json:"key"`
+ Value *hexutil.Big `json:"value"`
+ Proof []string `json:"proof"`
+ }
+
+ type accountResult struct {
+ Address common.Address `json:"address"`
+ AccountProof []string `json:"accountProof"`
+ Balance *hexutil.Big `json:"balance"`
+ CodeHash common.Hash `json:"codeHash"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ StorageHash common.Hash `json:"storageHash"`
+ StorageProof []storageResult `json:"storageProof"`
+ }
+
+ var storageKeys []string
+ if len(req.storageKeys) > 0 {
+ storageKeys = strings.Split(req.storageKeys, ",")
+ }
+ log.Debug("Starting RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys))
+ var res accountResult
+ err := s.client.CallContext(ctx, &res, "eth_getProof", req.address, storageKeys, hexutil.EncodeUint64(req.blockNumber))
+ log.Debug("Finished RPC request", "type", "eth_getProof", "blockNumber", req.blockNumber, "address", req.address, "storageKeys", len(storageKeys), "error", err)
+ var proof *gethclient.AccountResult
+ if err == nil { //TODO de-duplicate
+ // Turn hexutils back to normal datatypes
+ storageResults := make([]gethclient.StorageResult, 0, len(res.StorageProof))
+ for _, st := range res.StorageProof {
+ storageResults = append(storageResults, gethclient.StorageResult{
+ Key: st.Key,
+ Value: st.Value.ToInt(),
+ Proof: st.Proof,
+ })
+ }
+ proof = &gethclient.AccountResult{
+ Address: res.Address,
+ AccountProof: res.AccountProof,
+ Balance: res.Balance.ToInt(),
+ Nonce: uint64(res.Nonce),
+ CodeHash: res.CodeHash,
+ StorageHash: res.StorageHash,
+ StorageProof: storageResults,
+ }
+ }
+ return proof, err
+}
+
+func (s *lightState) fetchCode(ctx context.Context, req codeRequest) ([]byte, error) {
+ if code, ok := s.codeCache.Get(req); ok {
+ return code, nil
+ }
+ request := s.codeRequests.request(req)
+ code, err := request.getResult(ctx)
+ if err == nil {
+ s.codeCache.Add(req, code) //TODO cached before validation; remove and retry if invalid
+ }
+ request.release()
+ return code, err
+}
+
+func (s *lightState) requestCode(ctx context.Context, req codeRequest) ([]byte, error) {
+ var code hexutil.Bytes
+ log.Debug("Starting RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address)
+ err := s.client.CallContext(ctx, &code, "eth_getCode", req.address, hexutil.EncodeUint64(req.blockNumber))
+ log.Debug("Finished RPC request", "type", "eth_getCode", "blockNumber", req.blockNumber, "address", req.address, "error", err)
+ return code, err
+}
+
+// proofReader implements ethdb.KeyValueReader.
+type proofReader map[string][]byte
+
+func (p proofReader) Has(key []byte) (bool, error) {
+ _, ok := p[string(key)]
+ return ok, nil
+}
+
+func (p proofReader) Get(key []byte) ([]byte, error) {
+ if value, ok := p[string(key)]; ok {
+ return value, nil
+ }
+ return nil, errors.New("not found")
+}
+
+func makeProofReader(proof []string) (proofReader, error) {
+ pr := make(proofReader)
+ for _, s := range proof {
+ node, err := hexutil.Decode(s)
+ if err != nil {
+ return nil, err
+ }
+ pr[string(crypto.Keccak256(node))] = node
+ }
+ return pr, nil
+}
+
+func stValueBytes(value *big.Int) ([]byte, error) {
+ if value == nil {
+ return nil, errors.New("storage value is nil")
+ }
+ switch value.Sign() {
+ case -1:
+ return nil, errors.New("negative storage value")
+ case 1:
+ if value.BitLen() > 256 {
+ return nil, errors.New("storage value bigger than uint256")
+ }
+ stv := make([]byte, 32)
+ value.FillBytes(stv)
+ return stv, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (s *lightState) getProof(ctx context.Context, blockNumber *big.Int, account common.Address, storageKeys []string, getCode bool) (*gethclient.AccountResult, []byte, error) {
+ num, pheader, err := s.canonicalChain.resolveBlockNumber(blockNumber)
+ if err != nil {
+ return nil, nil, err
+ }
+ var (
+ stateRoot common.Hash
+ stateRootErr error
+ stateRootCh = make(chan struct{})
+ )
+ if pheader != nil {
+ stateRoot = pheader.StateRoot()
+ close(stateRootCh)
+ } else {
+ go func() {
+ defer close(stateRootCh)
+
+ blockHash, err := s.canonicalChain.getHash(ctx, num)
+ if err != nil {
+ stateRootErr = err
+ return
+ }
+ if pheader := s.blocksAndHeaders.getPayloadHeader(blockHash); pheader != nil {
+ stateRoot = pheader.StateRoot()
+ return
+ }
+ header, err := s.blocksAndHeaders.getHeader(ctx, blockHash)
+ if err != nil {
+ stateRootErr = err
+ return
+ }
+ stateRoot = header.Root
+ }()
+ }
+ var (
+ code []byte
+ codeErr error
+ codeCh = make(chan struct{})
+ )
+ if getCode {
+ go func() {
+ code, codeErr = s.fetchCode(ctx, codeRequest{blockNumber: num, address: account})
+ close(codeCh)
+ }()
+ }
+ proof, proofErr := s.fetchProof(ctx, proofRequest{blockNumber: num, address: account, storageKeys: strings.Join(storageKeys, ",")})
+ if proofErr != nil {
+ return nil, nil, proofErr
+ }
+ <-stateRootCh
+ if stateRootErr != nil {
+ return nil, nil, stateRootErr
+ }
+ proofReader, err := makeProofReader(proof.AccountProof)
+ if err != nil {
+ return nil, nil, err
+ }
+ value, err := trie.VerifyProof(stateRoot, crypto.Keccak256(account.Bytes()), proofReader)
+ if err != nil {
+ return nil, nil, err
+ }
+ if proof.Balance == nil {
+ return nil, nil, errors.New("account balance is nil")
+ }
+ balance, overflow := uint256.FromBig(proof.Balance)
+ if overflow {
+ return nil, nil, errors.New("account balance overflow")
+ }
+ stateAccount := types.StateAccount{
+ Nonce: proof.Nonce,
+ Balance: balance,
+ Root: proof.StorageHash,
+ CodeHash: proof.CodeHash.Bytes(),
+ }
+ enc, _ := rlp.EncodeToBytes(&stateAccount)
+ if !bytes.Equal(enc, value) {
+ return nil, nil, errors.New("account RLP mismatch")
+ }
+ if len(storageKeys) != len(proof.StorageProof) {
+ return nil, nil, errors.New("invalid number of storage proofs")
+ }
+ for i, st := range proof.StorageProof {
+ if proof.StorageHash == types.EmptyRootHash {
+ // no storage trie, expect empty proofs and values
+ if len(st.Proof) != 0 {
+ return nil, nil, errors.New("non-empty storage proof from empty storage")
+ }
+ value, err := stValueBytes(st.Value)
+ if err != nil {
+ return nil, nil, err
+ }
+ if value != nil {
+ return nil, nil, errors.New("non-empty storage value from empty storage")
+ }
+ continue
+ }
+ proofReader, err := makeProofReader(st.Proof)
+ if err != nil {
+ return nil, nil, err
+ }
+ key, err := hexutil.Decode(storageKeys[i])
+ if err != nil {
+ return nil, nil, err
+ }
+ key = common.BytesToHash(key).Bytes() // TODO 32 byte padding needed???
+ value, err := trie.VerifyProof(proof.StorageHash, crypto.Keccak256(key), proofReader)
+ if err != nil {
+ return nil, nil, err
+ }
+ stv, err := stValueBytes(st.Value)
+ if err != nil {
+ return nil, nil, err
+ }
+ enc, _ := rlp.EncodeToBytes(stv)
+ if !bytes.Equal(enc, value) { //TODO check for empty value
+ //log.Info("storage value mismatch", "value", enc, "proven", value)
+ return nil, nil, errors.New("storage value mismatch")
+ }
+ }
+ if getCode {
+ <-codeCh
+ if codeErr != nil {
+ return nil, nil, codeErr
+ }
+ if crypto.Keccak256Hash(code) != proof.CodeHash {
+ return nil, nil, errors.New("code hash mismatch")
+ }
+ }
+ return proof, code, nil
+}