ethclient/lightclient: execution light client

This commit is contained in:
Zsolt Felfoldi 2024-04-08 10:35:38 +02:00
parent 6154f87c33
commit aa764f994e
11 changed files with 1357 additions and 49 deletions

View file

@ -17,25 +17,20 @@
package blsync package blsync
import ( import (
"strings" "github.com/ethereum/go-ethereum/beacon/config"
"github.com/ethereum/go-ethereum/beacon/light" "github.com/ethereum/go-ethereum/beacon/light"
"github.com/ethereum/go-ethereum/beacon/light/api" "github.com/ethereum/go-ethereum/beacon/light/api"
"github.com/ethereum/go-ethereum/beacon/light/request" "github.com/ethereum/go-ethereum/beacon/light/request"
"github.com/ethereum/go-ethereum/beacon/light/sync" "github.com/ethereum/go-ethereum/beacon/light/sync"
"github.com/ethereum/go-ethereum/beacon/types" "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/common/mclock"
"github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
) )
type Client struct { type Client struct {
urls []string config config.LightClientConfig
customHeader map[string]string
chainConfig *lightClientConfig
scheduler *request.Scheduler scheduler *request.Scheduler
blockSync *beaconBlockSync blockSync *beaconBlockSync
engineRPC *rpc.Client engineRPC *rpc.Client
@ -44,34 +39,18 @@ type Client struct {
engineClient *engineClient engineClient *engineClient
} }
func NewClient(ctx *cli.Context) *Client { func NewClient(config config.LightClientConfig) *Client {
if !ctx.IsSet(utils.BeaconApiFlag.Name) {
utils.Fatalf("Beacon node light client API URL not specified")
}
var (
chainConfig = makeChainConfig(ctx)
customHeader = make(map[string]string)
)
for _, s := range ctx.StringSlice(utils.BeaconApiHeaderFlag.Name) {
kv := strings.Split(s, ":")
if len(kv) != 2 {
utils.Fatalf("Invalid custom API header entry: %s", s)
}
customHeader[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
}
// create data structures // create data structures
var ( var (
db = memorydb.New() db = memorydb.New()
threshold = ctx.Int(utils.BeaconThresholdFlag.Name) committeeChain = light.NewCommitteeChain(db, config.ChainConfig, config.SignerThreshold, config.EnforceTime)
committeeChain = light.NewCommitteeChain(db, chainConfig.ChainConfig, threshold, !ctx.Bool(utils.BeaconNoFilterFlag.Name)) headTracker = light.NewHeadTracker(committeeChain, config.SignerThreshold)
headTracker = light.NewHeadTracker(committeeChain, threshold)
) )
headSync := sync.NewHeadSync(headTracker, committeeChain) headSync := sync.NewHeadSync(headTracker, committeeChain)
// set up scheduler and sync modules // set up scheduler and sync modules
scheduler := request.NewScheduler() scheduler := request.NewScheduler()
checkpointInit := sync.NewCheckpointInit(committeeChain, chainConfig.Checkpoint) checkpointInit := sync.NewCheckpointInit(committeeChain, config.Checkpoint)
forwardSync := sync.NewForwardUpdateSync(committeeChain) forwardSync := sync.NewForwardUpdateSync(committeeChain)
beaconBlockSync := newBeaconBlockSync(headTracker) beaconBlockSync := newBeaconBlockSync(headTracker)
scheduler.RegisterTarget(headTracker) scheduler.RegisterTarget(headTracker)
@ -83,9 +62,7 @@ func NewClient(ctx *cli.Context) *Client {
return &Client{ return &Client{
scheduler: scheduler, scheduler: scheduler,
urls: ctx.StringSlice(utils.BeaconApiFlag.Name), config: config,
customHeader: customHeader,
chainConfig: &chainConfig,
blockSync: beaconBlockSync, blockSync: beaconBlockSync,
} }
} }
@ -97,11 +74,11 @@ func (c *Client) SetEngineRPC(engine *rpc.Client) {
func (c *Client) Start() error { func (c *Client) Start() error {
headCh := make(chan types.ChainHeadEvent, 16) headCh := make(chan types.ChainHeadEvent, 16)
c.chainHeadSub = c.blockSync.SubscribeChainHead(headCh) 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() c.scheduler.Start()
for _, url := range c.urls { for _, url := range c.config.ApiUrls {
beaconApi := api.NewBeaconLightApi(url, c.customHeader) beaconApi := api.NewBeaconLightApi(url, c.config.CustomHeader)
c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{})) c.scheduler.RegisterServer(request.NewServer(api.NewApiServer(beaconApi), &mclock.System{}))
} }
return nil return nil

View file

@ -31,14 +31,14 @@ import (
) )
type engineClient struct { type engineClient struct {
config *lightClientConfig config *types.ChainConfig
rpc *rpc.Client rpc *rpc.Client
rootCtx context.Context rootCtx context.Context
cancelRoot context.CancelFunc cancelRoot context.CancelFunc
wg sync.WaitGroup 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()) ctx, cancel := context.WithCancel(context.Background())
ec := &engineClient{ ec := &engineClient{
config: config, config: config,

View file

@ -14,9 +14,11 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package blsync package config
import ( import (
"strings"
"github.com/ethereum/go-ethereum/beacon/types" "github.com/ethereum/go-ethereum/beacon/types"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -24,14 +26,23 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// lightClientConfig contains beacon light client configuration // LightChainConfig contains beacon light chain configuration
type lightClientConfig struct { type LightChainConfig struct {
*types.ChainConfig *types.ChainConfig
Checkpoint common.Hash Checkpoint common.Hash
} }
// LightClientConfig contains beacon light client configuration
type LightClientConfig struct {
LightChainConfig
ApiUrls []string
CustomHeader map[string]string
SignerThreshold int
EnforceTime bool
}
var ( var (
MainnetConfig = lightClientConfig{ MainnetConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{ ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"), GenesisValidatorsRoot: common.HexToHash("0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"),
GenesisTime: 1606824023, GenesisTime: 1606824023,
@ -44,7 +55,7 @@ var (
Checkpoint: common.HexToHash("0x388be41594ec7d6a6894f18c73f3469f07e2c19a803de4755d335817ed8e2e5a"), Checkpoint: common.HexToHash("0x388be41594ec7d6a6894f18c73f3469f07e2c19a803de4755d335817ed8e2e5a"),
} }
SepoliaConfig = lightClientConfig{ SepoliaConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{ ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"), GenesisValidatorsRoot: common.HexToHash("0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078"),
GenesisTime: 1655733600, GenesisTime: 1655733600,
@ -57,7 +68,7 @@ var (
Checkpoint: common.HexToHash("0x1005a6d9175e96bfbce4d35b80f468e9bff0b674e1e861d16e09e10005a58e81"), Checkpoint: common.HexToHash("0x1005a6d9175e96bfbce4d35b80f468e9bff0b674e1e861d16e09e10005a58e81"),
} }
GoerliConfig = lightClientConfig{ GoerliConfig = LightChainConfig{
ChainConfig: (&types.ChainConfig{ ChainConfig: (&types.ChainConfig{
GenesisValidatorsRoot: common.HexToHash("0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"), GenesisValidatorsRoot: common.HexToHash("0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb"),
GenesisTime: 1614588812, GenesisTime: 1614588812,
@ -71,8 +82,8 @@ var (
} }
) )
func makeChainConfig(ctx *cli.Context) lightClientConfig { func MakeLightChainConfig(ctx *cli.Context) LightChainConfig {
var config lightClientConfig var config LightChainConfig
customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name) customConfig := ctx.IsSet(utils.BeaconConfigFlag.Name)
utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag, utils.BeaconConfigFlag) utils.CheckExclusive(ctx, utils.MainnetFlag, utils.GoerliFlag, utils.SepoliaFlag, utils.BeaconConfigFlag)
switch { switch {
@ -127,3 +138,25 @@ func makeChainConfig(ctx *cli.Context) lightClientConfig {
} }
return config 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),
}
}

View file

@ -68,6 +68,17 @@ func (eh *ExecutionHeader) PayloadRoot() merkle.Value {
return merkle.Value(eh.obj.HashTreeRoot(tree.GetHashFn())) 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 { func (eh *ExecutionHeader) BlockHash() common.Hash {
switch obj := eh.obj.(type) { switch obj := eh.obj.(type) {
case *capella.ExecutionPayloadHeader: case *capella.ExecutionPayloadHeader:
@ -78,3 +89,25 @@ func (eh *ExecutionHeader) BlockHash() common.Hash {
panic(fmt.Errorf("unsupported ExecutionPayloadHeader type %T", obj)) 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))
}
}

View file

@ -23,6 +23,7 @@ import (
"os" "os"
"github.com/ethereum/go-ethereum/beacon/blsync" "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/cmd/utils"
"github.com/ethereum/go-ethereum/internal/flags" "github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log" "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))) log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor)))
// set up blsync // set up blsync
client := blsync.NewClient(ctx) client := blsync.NewClient(config.MakeLightClientConfig(ctx))
client.SetEngineRPC(makeRPCClient(ctx)) client.SetEngineRPC(makeRPCClient(ctx))
client.Start() client.Start()

169
cmd/bltest/main.go Normal file
View 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
}

View file

@ -335,6 +335,11 @@ var (
Usage: "Path to a JWT secret to use for target engine API endpoint", Usage: "Path to a JWT secret to use for target engine API endpoint",
Category: flags.BeaconCategory, 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 // Transaction pool settings
TxPoolLocalsFlag = &cli.StringFlag{ TxPoolLocalsFlag = &cli.StringFlag{
Name: "txpool.locals", Name: "txpool.locals",

View 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)
}

View 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
}

View 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()
}

View 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
}