mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-19 18:32:23 +00:00
ethclient/lightclient: execution light client
This commit is contained in:
parent
6154f87c33
commit
aa764f994e
11 changed files with 1357 additions and 49 deletions
|
|
@ -17,25 +17,20 @@
|
|||
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
|
||||
config config.LightClientConfig
|
||||
scheduler *request.Scheduler
|
||||
blockSync *beaconBlockSync
|
||||
engineRPC *rpc.Client
|
||||
|
|
@ -44,34 +39,18 @@ type Client struct {
|
|||
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)
|
||||
|
|
@ -83,9 +62,7 @@ func NewClient(ctx *cli.Context) *Client {
|
|||
|
||||
return &Client{
|
||||
scheduler: scheduler,
|
||||
urls: ctx.StringSlice(utils.BeaconApiFlag.Name),
|
||||
customHeader: customHeader,
|
||||
chainConfig: &chainConfig,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
169
cmd/bltest/main.go
Normal file
169
cmd/bltest/main.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//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 <pattern>=<level> (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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
403
ethclient/lightclient/chain.go
Normal file
403
ethclient/lightclient/chain.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
226
ethclient/lightclient/lightclient.go
Normal file
226
ethclient/lightclient/lightclient.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
130
ethclient/lightclient/request_map.go
Normal file
130
ethclient/lightclient/request_map.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
}
|
||||
331
ethclient/lightclient/state.go
Normal file
331
ethclient/lightclient/state.go
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in a new issue