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