diff --git a/cmd/faucet/README.md b/cmd/faucet/README.md deleted file mode 100644 index 7e857fa0ee..0000000000 --- a/cmd/faucet/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Faucet - -The `faucet` is a simplistic web application with the goal of distributing small amounts of Ether in private and test networks. - -Users need to post their Ethereum addresses to fund in a Twitter status update or public Facebook post and share the link to the faucet. The faucet will in turn deduplicate user requests and send the Ether. After a funding round, the faucet prevents the same user from requesting again for a pre-configured amount of time, proportional to the amount of Ether requested. - -## Operation - -The `faucet` is a single binary app (everything included) with all configurations set via command line flags and a few files. - -First things first, the `faucet` needs to connect to an Ethereum network, for which it needs the necessary genesis and network infos. Each of the following flags must be set: - -- `-genesis` is a path to a file containing the network `genesis.json`. or using: - - `-goerli` with the faucet with Görli network config - - `-sepolia` with the faucet with Sepolia network config -- `-network` is the devp2p network id used during connection -- `-bootnodes` is a list of `enode://` ids to join the network through - -The `faucet` will use the `les` protocol to join the configured Ethereum network and will store its data in `$HOME/.faucet` (currently not configurable). - -## Funding - -To be able to distribute funds, the `faucet` needs access to an already funded Ethereum account. This can be configured via: - -- `-account.json` is a path to the Ethereum account's JSON key file -- `-account.pass` is a path to a text file with the decryption passphrase - -The faucet is able to distribute various amounts of Ether in exchange for various timeouts. These can be configured via: - -- `-faucet.amount` is the number of Ethers to send by default -- `-faucet.minutes` is the time to wait before allowing a rerequest -- `-faucet.tiers` is the funding tiers to support (x3 time, x2.5 funds) - -## Sybil protection - -To prevent the same user from exhausting funds in a loop, the `faucet` ties requests to social networks and captcha resolvers. - -Captcha protection uses Google's invisible ReCaptcha, thus the `faucet` needs to run on a live domain. The domain needs to be registered in Google's systems to retrieve the captcha API token and secrets. After doing so, captcha protection may be enabled via: - -- `-captcha.token` is the API token for ReCaptcha -- `-captcha.secret` is the API secret for ReCaptcha - -Sybil protection via Twitter requires an API key as of 15th December, 2020. To obtain it, a Twitter user must be upgraded to developer status and a new Twitter App deployed with it. The app's `Bearer` token is required by the faucet to retrieve tweet data: - -- `-twitter.token` is the Bearer token for `v2` API access -- `-twitter.token.v1` is the Bearer token for `v1` API access - -Sybil protection via Facebook uses the website to directly download post data thus does not currently require an API configuration. - -## Miscellaneous - -Beside the above - mostly essential - CLI flags, there are a number that can be used to fine-tune the `faucet`'s operation. Please see `faucet --help` for a full list. diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go deleted file mode 100644 index 8f4127216e..0000000000 --- a/cmd/faucet/faucet.go +++ /dev/null @@ -1,891 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -// faucet is an Ether faucet backed by a light client. -package main - -import ( - "bytes" - "context" - _ "embed" - "encoding/json" - "errors" - "flag" - "fmt" - "html/template" - "io" - "math" - "math/big" - "net/http" - "net/url" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/ethstats" - "github.com/ethereum/go-ethereum/internal/version" - "github.com/ethereum/go-ethereum/les" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nat" - "github.com/ethereum/go-ethereum/params" - "github.com/gorilla/websocket" -) - -var ( - genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") - apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") - ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection") - bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with") - netFlag = flag.Uint64("network", 0, "Network ID to use for the Ethereum protocol") - statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string") - - netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet") - payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request") - minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds") - tiersFlag = flag.Int("faucet.tiers", 3, "Number of funding tiers to enable (x3 time, x2.5 funds)") - - accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with") - accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds") - - captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side") - captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side") - - noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication") - logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") - - twitterTokenFlag = flag.String("twitter.token", "", "Bearer token to authenticate with the v2 Twitter API") - twitterTokenV1Flag = flag.String("twitter.token.v1", "", "Bearer token to authenticate with the v1.1 Twitter API") - - goerliFlag = flag.Bool("goerli", false, "Initializes the faucet with Görli network config") - sepoliaFlag = flag.Bool("sepolia", false, "Initializes the faucet with Sepolia network config") -) - -var ( - ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) -) - -//go:embed faucet.html -var websiteTmpl string - -func main() { - // Parse the flags and set up the logger to print everything requested - flag.Parse() - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) - - // Construct the payout tiers - amounts := make([]string, *tiersFlag) - periods := make([]string, *tiersFlag) - for i := 0; i < *tiersFlag; i++ { - // Calculate the amount for the next tier and format it - amount := float64(*payoutFlag) * math.Pow(2.5, float64(i)) - amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64)) - if amount == 1 { - amounts[i] = strings.TrimSuffix(amounts[i], "s") - } - // Calculate the period for the next tier and format it - period := *minutesFlag * int(math.Pow(3, float64(i))) - periods[i] = fmt.Sprintf("%d mins", period) - if period%60 == 0 { - period /= 60 - periods[i] = fmt.Sprintf("%d hours", period) - - if period%24 == 0 { - period /= 24 - periods[i] = fmt.Sprintf("%d days", period) - } - } - if period == 1 { - periods[i] = strings.TrimSuffix(periods[i], "s") - } - } - website := new(bytes.Buffer) - err := template.Must(template.New("").Parse(websiteTmpl)).Execute(website, map[string]interface{}{ - "Network": *netnameFlag, - "Amounts": amounts, - "Periods": periods, - "Recaptcha": *captchaToken, - "NoAuth": *noauthFlag, - }) - if err != nil { - log.Crit("Failed to render the faucet template", "err", err) - } - // Load and parse the genesis block requested by the user - genesis, err := getGenesis(*genesisFlag, *goerliFlag, *sepoliaFlag) - if err != nil { - log.Crit("Failed to parse genesis config", "err", err) - } - // Convert the bootnodes to internal enode representations - var enodes []*enode.Node - for _, boot := range strings.Split(*bootFlag, ",") { - if url, err := enode.Parse(enode.ValidSchemes, boot); err == nil { - enodes = append(enodes, url) - } else { - log.Error("Failed to parse bootnode URL", "url", boot, "err", err) - } - } - // Load up the account key and decrypt its password - blob, err := os.ReadFile(*accPassFlag) - if err != nil { - log.Crit("Failed to read account password contents", "file", *accPassFlag, "err", err) - } - pass := strings.TrimSuffix(string(blob), "\n") - - ks := keystore.NewKeyStore(filepath.Join(os.Getenv("HOME"), ".faucet", "keys"), keystore.StandardScryptN, keystore.StandardScryptP) - if blob, err = os.ReadFile(*accJSONFlag); err != nil { - log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err) - } - acc, err := ks.Import(blob, pass, pass) - if err != nil && err != keystore.ErrAccountAlreadyExists { - log.Crit("Failed to import faucet signer account", "err", err) - } - if err := ks.Unlock(acc, pass); err != nil { - log.Crit("Failed to unlock faucet signer account", "err", err) - } - // Assemble and start the faucet light service - faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes()) - if err != nil { - log.Crit("Failed to start faucet", "err", err) - } - defer faucet.close() - - if err := faucet.listenAndServe(*apiPortFlag); err != nil { - log.Crit("Failed to launch faucet API", "err", err) - } -} - -// request represents an accepted funding request. -type request struct { - Avatar string `json:"avatar"` // Avatar URL to make the UI nicer - Account common.Address `json:"account"` // Ethereum address being funded - Time time.Time `json:"time"` // Timestamp when the request was accepted - Tx *types.Transaction `json:"tx"` // Transaction funding the account -} - -// faucet represents a crypto faucet backed by an Ethereum light client. -type faucet struct { - config *params.ChainConfig // Chain configurations for signing - stack *node.Node // Ethereum protocol stack - client *ethclient.Client // Client connection to the Ethereum chain - index []byte // Index page to serve up on the web - - keystore *keystore.KeyStore // Keystore containing the single signer - account accounts.Account // Account funding user faucet requests - head *types.Header // Current head header of the faucet - balance *big.Int // Current balance of the faucet - nonce uint64 // Current pending nonce of the faucet - price *big.Int // Current gas price to issue funds with - - conns []*wsConn // Currently live websocket connections - timeouts map[string]time.Time // History of users and their funding timeouts - reqs []*request // Currently pending funding requests - update chan struct{} // Channel to signal request updates - - lock sync.RWMutex // Lock protecting the faucet's internals -} - -// wsConn wraps a websocket connection with a write mutex as the underlying -// websocket library does not synchronize access to the stream. -type wsConn struct { - conn *websocket.Conn - wlock sync.Mutex -} - -func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) { - // Assemble the raw devp2p protocol stack - git, _ := version.VCS() - stack, err := node.New(&node.Config{ - Name: "geth", - Version: params.VersionWithCommit(git.Commit, git.Date), - DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"), - P2P: p2p.Config{ - NAT: nat.Any(), - NoDiscovery: true, - DiscoveryV5: true, - ListenAddr: fmt.Sprintf(":%d", port), - MaxPeers: 25, - BootstrapNodesV5: enodes, - }, - }) - if err != nil { - return nil, err - } - - // Assemble the Ethereum light client protocol - cfg := ethconfig.Defaults - cfg.SyncMode = downloader.LightSync - cfg.NetworkId = network - cfg.Genesis = genesis - utils.SetDNSDiscoveryDefaults(&cfg, genesis.ToBlock().Hash()) - - lesBackend, err := les.New(stack, &cfg) - if err != nil { - return nil, fmt.Errorf("failed to register the Ethereum service: %w", err) - } - - // Assemble the ethstats monitoring and reporting service' - if stats != "" { - if err := ethstats.New(stack, lesBackend.ApiBackend, lesBackend.Engine(), stats); err != nil { - return nil, err - } - } - // Boot up the client and ensure it connects to bootnodes - if err := stack.Start(); err != nil { - return nil, err - } - for _, boot := range enodes { - old, err := enode.Parse(enode.ValidSchemes, boot.String()) - if err == nil { - stack.Server().AddPeer(old) - } - } - // Attach to the client and retrieve and interesting metadatas - api := stack.Attach() - client := ethclient.NewClient(api) - - return &faucet{ - config: genesis.Config, - stack: stack, - client: client, - index: index, - keystore: ks, - account: ks.Accounts()[0], - timeouts: make(map[string]time.Time), - update: make(chan struct{}, 1), - }, nil -} - -// close terminates the Ethereum connection and tears down the faucet. -func (f *faucet) close() error { - return f.stack.Close() -} - -// listenAndServe registers the HTTP handlers for the faucet and boots it up -// for service user funding requests. -func (f *faucet) listenAndServe(port int) error { - go f.loop() - - http.HandleFunc("/", f.webHandler) - http.HandleFunc("/api", f.apiHandler) - return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) -} - -// webHandler handles all non-api requests, simply flattening and returning the -// faucet website. -func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) { - w.Write(f.index) -} - -// apiHandler handles requests for Ether grants and transaction statuses. -func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{} - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - - // Start tracking the connection and drop at the end - defer conn.Close() - - f.lock.Lock() - wsconn := &wsConn{conn: conn} - f.conns = append(f.conns, wsconn) - f.lock.Unlock() - - defer func() { - f.lock.Lock() - for i, c := range f.conns { - if c.conn == conn { - f.conns = append(f.conns[:i], f.conns[i+1:]...) - break - } - } - f.lock.Unlock() - }() - // Gather the initial stats from the network to report - var ( - head *types.Header - balance *big.Int - nonce uint64 - ) - for head == nil || balance == nil { - // Retrieve the current stats cached by the faucet - f.lock.RLock() - if f.head != nil { - head = types.CopyHeader(f.head) - } - if f.balance != nil { - balance = new(big.Int).Set(f.balance) - } - nonce = f.nonce - f.lock.RUnlock() - - if head == nil || balance == nil { - // Report the faucet offline until initial stats are ready - //lint:ignore ST1005 This error is to be displayed in the browser - if err = sendError(wsconn, errors.New("Faucet offline")); err != nil { - log.Warn("Failed to send faucet error to client", "err", err) - return - } - time.Sleep(3 * time.Second) - } - } - // Send over the initial stats and the latest header - f.lock.RLock() - reqs := f.reqs - f.lock.RUnlock() - if err = send(wsconn, map[string]interface{}{ - "funds": new(big.Int).Div(balance, ether), - "funded": nonce, - "peers": f.stack.Server().PeerCount(), - "requests": reqs, - }, 3*time.Second); err != nil { - log.Warn("Failed to send initial stats to client", "err", err) - return - } - if err = send(wsconn, head, 3*time.Second); err != nil { - log.Warn("Failed to send initial header to client", "err", err) - return - } - // Keep reading requests from the websocket until the connection breaks - for { - // Fetch the next funding request and validate against github - var msg struct { - URL string `json:"url"` - Tier uint `json:"tier"` - Captcha string `json:"captcha"` - } - if err = conn.ReadJSON(&msg); err != nil { - return - } - if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://twitter.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") { - if err = sendError(wsconn, errors.New("URL doesn't link to supported services")); err != nil { - log.Warn("Failed to send URL error to client", "err", err) - return - } - continue - } - if msg.Tier >= uint(*tiersFlag) { - //lint:ignore ST1005 This error is to be displayed in the browser - if err = sendError(wsconn, errors.New("Invalid funding tier requested")); err != nil { - log.Warn("Failed to send tier error to client", "err", err) - return - } - continue - } - log.Info("Faucet funds requested", "url", msg.URL, "tier", msg.Tier) - - // If captcha verifications are enabled, make sure we're not dealing with a robot - if *captchaToken != "" { - form := url.Values{} - form.Add("secret", *captchaSecret) - form.Add("response", msg.Captcha) - - res, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", form) - if err != nil { - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send captcha post error to client", "err", err) - return - } - continue - } - var result struct { - Success bool `json:"success"` - Errors json.RawMessage `json:"error-codes"` - } - err = json.NewDecoder(res.Body).Decode(&result) - res.Body.Close() - if err != nil { - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send captcha decode error to client", "err", err) - return - } - continue - } - if !result.Success { - log.Warn("Captcha verification failed", "err", string(result.Errors)) - //lint:ignore ST1005 it's funny and the robot won't mind - if err = sendError(wsconn, errors.New("Beep-bop, you're a robot!")); err != nil { - log.Warn("Failed to send captcha failure to client", "err", err) - return - } - continue - } - } - // Retrieve the Ethereum address to fund, the requesting user and a profile picture - var ( - id string - username string - avatar string - address common.Address - ) - switch { - case strings.HasPrefix(msg.URL, "https://twitter.com/"): - id, username, avatar, address, err = authTwitter(msg.URL, *twitterTokenV1Flag, *twitterTokenFlag) - case strings.HasPrefix(msg.URL, "https://www.facebook.com/"): - username, avatar, address, err = authFacebook(msg.URL) - id = username - case *noauthFlag: - username, avatar, address, err = authNoAuth(msg.URL) - id = username - default: - //lint:ignore ST1005 This error is to be displayed in the browser - err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues") - } - if err != nil { - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send prefix error to client", "err", err) - return - } - continue - } - log.Info("Faucet request valid", "url", msg.URL, "tier", msg.Tier, "user", username, "address", address) - - // Ensure the user didn't request funds too recently - f.lock.Lock() - var ( - fund bool - timeout time.Time - ) - if timeout = f.timeouts[id]; time.Now().After(timeout) { - // User wasn't funded recently, create the funding transaction - amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether) - amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) - amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) - - tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) - signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) - if err != nil { - f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction creation error to client", "err", err) - return - } - continue - } - // Submit the transaction and mark as funded if successful - if err := f.client.SendTransaction(context.Background(), signed); err != nil { - f.lock.Unlock() - if err = sendError(wsconn, err); err != nil { - log.Warn("Failed to send transaction transmission error to client", "err", err) - return - } - continue - } - f.reqs = append(f.reqs, &request{ - Avatar: avatar, - Account: address, - Time: time.Now(), - Tx: signed, - }) - timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute - grace := timeout / 288 // 24h timeout => 5m grace - - f.timeouts[id] = time.Now().Add(timeout - grace) - fund = true - } - f.lock.Unlock() - - // Send an error if too frequent funding, othewise a success - if !fund { - if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple - log.Warn("Failed to send funding error to client", "err", err) - return - } - continue - } - if err = sendSuccess(wsconn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil { - log.Warn("Failed to send funding success to client", "err", err) - return - } - select { - case f.update <- struct{}{}: - default: - } - } -} - -// refresh attempts to retrieve the latest header from the chain and extract the -// associated faucet balance and nonce for connectivity caching. -func (f *faucet) refresh(head *types.Header) error { - // Ensure a state update does not run for too long - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // If no header was specified, use the current chain head - var err error - if head == nil { - if head, err = f.client.HeaderByNumber(ctx, nil); err != nil { - return err - } - } - // Retrieve the balance, nonce and gas price from the current head - var ( - balance *big.Int - nonce uint64 - price *big.Int - ) - if balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number); err != nil { - return err - } - if nonce, err = f.client.NonceAt(ctx, f.account.Address, head.Number); err != nil { - return err - } - if price, err = f.client.SuggestGasPrice(ctx); err != nil { - return err - } - // Everything succeeded, update the cached stats and eject old requests - f.lock.Lock() - f.head, f.balance = head, balance - f.price, f.nonce = price, nonce - for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { - f.reqs = f.reqs[1:] - } - f.lock.Unlock() - - return nil -} - -// loop keeps waiting for interesting events and pushes them out to connected -// websockets. -func (f *faucet) loop() { - // Wait for chain events and push them to clients - heads := make(chan *types.Header, 16) - sub, err := f.client.SubscribeNewHead(context.Background(), heads) - if err != nil { - log.Crit("Failed to subscribe to head events", "err", err) - } - defer sub.Unsubscribe() - - // Start a goroutine to update the state from head notifications in the background - update := make(chan *types.Header) - - go func() { - for head := range update { - // New chain head arrived, query the current stats and stream to clients - timestamp := time.Unix(int64(head.Time), 0) - if time.Since(timestamp) > time.Hour { - log.Warn("Skipping faucet refresh, head too old", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp)) - continue - } - if err := f.refresh(head); err != nil { - log.Warn("Failed to update faucet state", "block", head.Number, "hash", head.Hash(), "err", err) - continue - } - // Faucet state retrieved, update locally and send to clients - f.lock.RLock() - log.Info("Updated faucet state", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp), "balance", f.balance, "nonce", f.nonce, "price", f.price) - - balance := new(big.Int).Div(f.balance, ether) - peers := f.stack.Server().PeerCount() - - for _, conn := range f.conns { - if err := send(conn, map[string]interface{}{ - "funds": balance, - "funded": f.nonce, - "peers": peers, - "requests": f.reqs, - }, time.Second); err != nil { - log.Warn("Failed to send stats to client", "err", err) - conn.conn.Close() - continue - } - if err := send(conn, head, time.Second); err != nil { - log.Warn("Failed to send header to client", "err", err) - conn.conn.Close() - } - } - f.lock.RUnlock() - } - }() - // Wait for various events and assing to the appropriate background threads - for { - select { - case head := <-heads: - // New head arrived, send if for state update if there's none running - select { - case update <- head: - default: - } - - case <-f.update: - // Pending requests updated, stream to clients - f.lock.RLock() - for _, conn := range f.conns { - if err := send(conn, map[string]interface{}{"requests": f.reqs}, time.Second); err != nil { - log.Warn("Failed to send requests to client", "err", err) - conn.conn.Close() - } - } - f.lock.RUnlock() - } - } -} - -// sends transmits a data packet to the remote end of the websocket, but also -// setting a write deadline to prevent waiting forever on the node. -func send(conn *wsConn, value interface{}, timeout time.Duration) error { - if timeout == 0 { - timeout = 60 * time.Second - } - conn.wlock.Lock() - defer conn.wlock.Unlock() - conn.conn.SetWriteDeadline(time.Now().Add(timeout)) - return conn.conn.WriteJSON(value) -} - -// sendError transmits an error to the remote end of the websocket, also setting -// the write deadline to 1 second to prevent waiting forever. -func sendError(conn *wsConn, err error) error { - return send(conn, map[string]string{"error": err.Error()}, time.Second) -} - -// sendSuccess transmits a success message to the remote end of the websocket, also -// setting the write deadline to 1 second to prevent waiting forever. -func sendSuccess(conn *wsConn, msg string) error { - return send(conn, map[string]string{"success": msg}, time.Second) -} - -// authTwitter tries to authenticate a faucet request using Twitter posts, returning -// the uniqueness identifier (user id/username), username, avatar URL and Ethereum address to fund on success. -func authTwitter(url string, tokenV1, tokenV2 string) (string, string, string, common.Address, error) { - // Ensure the user specified a meaningful URL, no fancy nonsense - parts := strings.Split(url, "/") - if len(parts) < 4 || parts[len(parts)-2] != "status" { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") - } - // Strip any query parameters from the tweet id and ensure it's numeric - tweetID := strings.Split(parts[len(parts)-1], "?")[0] - if !regexp.MustCompile("^[0-9]+$").MatchString(tweetID) { - return "", "", "", common.Address{}, errors.New("Invalid Tweet URL") - } - // Twitter's API isn't really friendly with direct links. - // It is restricted to 300 queries / 15 minute with an app api key. - // Anything more will require read only authorization from the users and that we want to avoid. - - // If Twitter bearer token is provided, use the API, selecting the version - // the user would prefer (currently there's a limit of 1 v2 app / developer - // but unlimited v1.1 apps). - switch { - case tokenV1 != "": - return authTwitterWithTokenV1(tweetID, tokenV1) - case tokenV2 != "": - return authTwitterWithTokenV2(tweetID, tokenV2) - } - // Twitter API token isn't provided so we just load the public posts - // and scrape it for the Ethereum address and profile URL. We need to load - // the mobile page though since the main page loads tweet contents via JS. - url = strings.Replace(url, "https://twitter.com/", "https://mobile.twitter.com/", 1) - - res, err := http.Get(url) - if err != nil { - return "", "", "", common.Address{}, err - } - defer res.Body.Close() - - // Resolve the username from the final redirect, no intermediate junk - parts = strings.Split(res.Request.URL.String(), "/") - if len(parts) < 4 || parts[len(parts)-2] != "status" { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") - } - username := parts[len(parts)-3] - - body, err := io.ReadAll(res.Body) - if err != nil { - return "", "", "", common.Address{}, err - } - address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) - if address == (common.Address{}) { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") - } - var avatar string - if parts = regexp.MustCompile(`src="([^"]+twimg\.com/profile_images[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { - avatar = parts[1] - } - return username + "@twitter", username, avatar, address, nil -} - -// authTwitterWithTokenV1 tries to authenticate a faucet request using Twitter's v1 -// API, returning the user id, username, avatar URL and Ethereum address to fund on -// success. -func authTwitterWithTokenV1(tweetID string, token string) (string, string, string, common.Address, error) { - // Query the tweet details from Twitter - url := fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s", tweetID) - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return "", "", "", common.Address{}, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", "", "", common.Address{}, err - } - defer res.Body.Close() - - var result struct { - Text string `json:"text"` - User struct { - ID string `json:"id_str"` - Username string `json:"screen_name"` - Avatar string `json:"profile_image_url"` - } `json:"user"` - } - err = json.NewDecoder(res.Body).Decode(&result) - if err != nil { - return "", "", "", common.Address{}, err - } - address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Text)) - if address == (common.Address{}) { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") - } - return result.User.ID + "@twitter", result.User.Username, result.User.Avatar, address, nil -} - -// authTwitterWithTokenV2 tries to authenticate a faucet request using Twitter's v2 -// API, returning the user id, username, avatar URL and Ethereum address to fund on -// success. -func authTwitterWithTokenV2(tweetID string, token string) (string, string, string, common.Address, error) { - // Query the tweet details from Twitter - url := fmt.Sprintf("https://api.twitter.com/2/tweets/%s?expansions=author_id&user.fields=profile_image_url", tweetID) - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - return "", "", "", common.Address{}, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", "", "", common.Address{}, err - } - defer res.Body.Close() - - var result struct { - Data struct { - AuthorID string `json:"author_id"` - Text string `json:"text"` - } `json:"data"` - Includes struct { - Users []struct { - ID string `json:"id"` - Username string `json:"username"` - Avatar string `json:"profile_image_url"` - } `json:"users"` - } `json:"includes"` - } - - err = json.NewDecoder(res.Body).Decode(&result) - if err != nil { - return "", "", "", common.Address{}, err - } - - address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Data.Text)) - if address == (common.Address{}) { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") - } - return result.Data.AuthorID + "@twitter", result.Includes.Users[0].Username, result.Includes.Users[0].Avatar, address, nil -} - -// authFacebook tries to authenticate a faucet request using Facebook posts, -// returning the username, avatar URL and Ethereum address to fund on success. -func authFacebook(url string) (string, string, common.Address, error) { - // Ensure the user specified a meaningful URL, no fancy nonsense - parts := strings.Split(strings.Split(url, "?")[0], "/") - if parts[len(parts)-1] == "" { - parts = parts[0 : len(parts)-1] - } - if len(parts) < 4 || parts[len(parts)-2] != "posts" { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", common.Address{}, errors.New("Invalid Facebook post URL") - } - username := parts[len(parts)-3] - - // Facebook's Graph API isn't really friendly with direct links. Still, we don't - // want to do ask read permissions from users, so just load the public posts and - // scrape it for the Ethereum address and profile URL. - // - // Facebook recently changed their desktop webpage to use AJAX for loading post - // content, so switch over to the mobile site for now. Will probably end up having - // to use the API eventually. - crawl := strings.Replace(url, "www.facebook.com", "m.facebook.com", 1) - - res, err := http.Get(crawl) - if err != nil { - return "", "", common.Address{}, err - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return "", "", common.Address{}, err - } - address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) - if address == (common.Address{}) { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", common.Address{}, errors.New("No Ethereum address found to fund. Please check the post URL and verify that it can be viewed publicly.") - } - var avatar string - if parts = regexp.MustCompile(`src="([^"]+fbcdn\.net[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { - avatar = parts[1] - } - return username + "@facebook", avatar, address, nil -} - -// authNoAuth tries to interpret a faucet request as a plain Ethereum address, -// without actually performing any remote authentication. This mode is prone to -// Byzantine attack, so only ever use for truly private networks. -func authNoAuth(url string) (string, string, common.Address, error) { - address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url)) - if address == (common.Address{}) { - //lint:ignore ST1005 This error is to be displayed in the browser - return "", "", common.Address{}, errors.New("No Ethereum address found to fund") - } - return address.Hex() + "@noauth", "", address, nil -} - -// getGenesis returns a genesis based on input args -func getGenesis(genesisFlag string, goerliFlag bool, sepoliaFlag bool) (*core.Genesis, error) { - switch { - case genesisFlag != "": - var genesis core.Genesis - err := common.LoadJSON(genesisFlag, &genesis) - return &genesis, err - case goerliFlag: - return core.DefaultGoerliGenesisBlock(), nil - case sepoliaFlag: - return core.DefaultSepoliaGenesisBlock(), nil - default: - return nil, errors.New("no genesis flag provided") - } -} diff --git a/cmd/faucet/faucet.html b/cmd/faucet/faucet.html deleted file mode 100644 index dad5ad84f2..0000000000 --- a/cmd/faucet/faucet.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - {{.Network}}: Authenticated Faucet - - - - - - - - - - - - - -
-
-
-
-

{{.Network}} Authenticated Faucet

-
-
-
-
-
- - - - - -
{{if .Recaptcha}} -
{{end}} -
-
-
-
-
-
-
-
- -
-
-
-
-
-

How does this work?

-

This Ether faucet is running on the {{.Network}} network. To prevent malicious actors from exhausting all available funds or accumulating enough Ether to mount long running spam attacks, requests are tied to common 3rd party social network accounts. Anyone having a Twitter or Facebook account may request funds within the permitted limits.

-
-
-
To request funds via Twitter, make a tweet with your Ethereum address pasted into the contents (surrounding text doesn't matter).
Copy-paste the tweets URL into the above input box and fire away!
- -
-
To request funds via Facebook, publish a new public post with your Ethereum address embedded into the content (surrounding text doesn't matter).
Copy-paste the posts URL into the above input box and fire away!
- - {{if .NoAuth}} -
-
To request funds without authentication, simply copy-paste your Ethereum address into the above input box (surrounding text doesn't matter) and fire away.
This mode is susceptible to Byzantine attacks. Only use for debugging or private networks!
- {{end}} -
-

You can track the current pending requests below the input field to see how much you have to wait until your turn comes.

- {{if .Recaptcha}}The faucet is running invisible reCaptcha protection against bots.{{end}} -
-
-
-
- - {{if .Recaptcha}} - {{end}} - - diff --git a/cmd/faucet/faucet_test.go b/cmd/faucet/faucet_test.go deleted file mode 100644 index 39b62c4939..0000000000 --- a/cmd/faucet/faucet_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -func TestFacebook(t *testing.T) { - t.Parallel() - // TODO: Remove facebook auth or implement facebook api, which seems to require an API key - t.Skipf("The facebook access is flaky, needs to be reimplemented or removed") - for _, tt := range []struct { - url string - want common.Address - }{ - { - "https://www.facebook.com/fooz.gazonk/posts/2837228539847129", - common.HexToAddress("0xDeadDeaDDeaDbEefbEeFbEEfBeeFBeefBeeFbEEF"), - }, - } { - _, _, gotAddress, err := authFacebook(tt.url) - if err != nil { - t.Fatal(err) - } - if gotAddress != tt.want { - t.Fatalf("address wrong, have %v want %v", gotAddress, tt.want) - } - } -} diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 027dac7bd6..5f52f1df54 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -35,7 +35,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/eth/catalyst" - "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/flags" @@ -222,7 +221,7 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) { } catalyst.RegisterSimulatedBeaconAPIs(stack, simBeacon) stack.RegisterLifecycle(simBeacon) - } else if cfg.Eth.SyncMode != downloader.LightSync { + } else { err := catalyst.Register(stack, eth) if err != nil { utils.Fatalf("failed to register catalyst service: %v", err) diff --git a/cmd/geth/les_test.go b/cmd/geth/les_test.go deleted file mode 100644 index 98c8a12dc6..0000000000 --- a/cmd/geth/les_test.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2020 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum 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 General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" -) - -type gethrpc struct { - name string - rpc *rpc.Client - geth *testgeth - nodeInfo *p2p.NodeInfo -} - -func (g *gethrpc) killAndWait() { - g.geth.Kill() - g.geth.WaitExit() -} - -func (g *gethrpc) callRPC(result interface{}, method string, args ...interface{}) { - if err := g.rpc.Call(&result, method, args...); err != nil { - g.geth.Fatalf("callRPC %v: %v", method, err) - } -} - -func (g *gethrpc) addPeer(peer *gethrpc) { - g.geth.Logf("%v.addPeer(%v)", g.name, peer.name) - enode := peer.getNodeInfo().Enode - peerCh := make(chan *p2p.PeerEvent) - sub, err := g.rpc.Subscribe(context.Background(), "admin", peerCh, "peerEvents") - if err != nil { - g.geth.Fatalf("subscribe %v: %v", g.name, err) - } - defer sub.Unsubscribe() - g.callRPC(nil, "admin_addPeer", enode) - dur := 14 * time.Second - timeout := time.After(dur) - select { - case ev := <-peerCh: - g.geth.Logf("%v received event: type=%v, peer=%v", g.name, ev.Type, ev.Peer) - case err := <-sub.Err(): - g.geth.Fatalf("%v sub error: %v", g.name, err) - case <-timeout: - g.geth.Error("timeout adding peer after", dur) - } -} - -// Use this function instead of `g.nodeInfo` directly -func (g *gethrpc) getNodeInfo() *p2p.NodeInfo { - if g.nodeInfo != nil { - return g.nodeInfo - } - g.nodeInfo = &p2p.NodeInfo{} - g.callRPC(&g.nodeInfo, "admin_nodeInfo") - return g.nodeInfo -} - -// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into -// account the set data folders as well as the designated platform we're currently -// running on. -func ipcEndpoint(ipcPath, datadir string) string { - // On windows we can only use plain top-level pipes - if runtime.GOOS == "windows" { - if strings.HasPrefix(ipcPath, `\\.\pipe\`) { - return ipcPath - } - return `\\.\pipe\` + ipcPath - } - // Resolve names into the data directory full paths otherwise - if filepath.Base(ipcPath) == ipcPath { - if datadir == "" { - return filepath.Join(os.TempDir(), ipcPath) - } - return filepath.Join(datadir, ipcPath) - } - return ipcPath -} - -// nextIPC ensures that each ipc pipe gets a unique name. -// On linux, it works well to use ipc pipes all over the filesystem (in datadirs), -// but windows require pipes to sit in "\\.\pipe\". Therefore, to run several -// nodes simultaneously, we need to distinguish between them, which we do by -// the pipe filename instead of folder. -var nextIPC atomic.Uint32 - -func startGethWithIpc(t *testing.T, name string, args ...string) *gethrpc { - ipcName := fmt.Sprintf("geth-%d.ipc", nextIPC.Add(1)) - args = append([]string{"--networkid=42", "--port=0", "--authrpc.port", "0", "--ipcpath", ipcName}, args...) - t.Logf("Starting %v with rpc: %v", name, args) - - g := &gethrpc{ - name: name, - geth: runGeth(t, args...), - } - ipcpath := ipcEndpoint(ipcName, g.geth.Datadir) - // We can't know exactly how long geth will take to start, so we try 10 - // times over a 5 second period. - var err error - for i := 0; i < 10; i++ { - time.Sleep(500 * time.Millisecond) - if g.rpc, err = rpc.Dial(ipcpath); err == nil { - return g - } - } - t.Fatalf("%v rpc connect to %v: %v", name, ipcpath, err) - return nil -} - -func initGeth(t *testing.T) string { - args := []string{"--networkid=42", "init", "./testdata/clique.json"} - t.Logf("Initializing geth: %v ", args) - g := runGeth(t, args...) - datadir := g.Datadir - g.WaitExit() - return datadir -} - -func startLightServer(t *testing.T) *gethrpc { - datadir := initGeth(t) - t.Logf("Importing keys to geth") - runGeth(t, "account", "import", "--datadir", datadir, "--password", "./testdata/password.txt", "--lightkdf", "./testdata/key.prv").WaitExit() - account := "0x02f0d131f1f97aef08aec6e3291b957d9efe7105" - server := startGethWithIpc(t, "lightserver", "--allow-insecure-unlock", "--datadir", datadir, "--password", "./testdata/password.txt", "--unlock", account, "--miner.etherbase=0x02f0d131f1f97aef08aec6e3291b957d9efe7105", "--mine", "--light.serve=100", "--light.maxpeers=1", "--discv4=false", "--nat=extip:127.0.0.1", "--verbosity=4") - return server -} - -func startClient(t *testing.T, name string) *gethrpc { - datadir := initGeth(t) - return startGethWithIpc(t, name, "--datadir", datadir, "--discv4=false", "--syncmode=light", "--nat=extip:127.0.0.1", "--verbosity=4") -} - -func TestPriorityClient(t *testing.T) { - t.Parallel() - lightServer := startLightServer(t) - defer lightServer.killAndWait() - - // Start client and add lightServer as peer - freeCli := startClient(t, "freeCli") - defer freeCli.killAndWait() - freeCli.addPeer(lightServer) - - var peers []*p2p.PeerInfo - freeCli.callRPC(&peers, "admin_peers") - if len(peers) != 1 { - t.Errorf("Expected: # of client peers == 1, actual: %v", len(peers)) - return - } - - // Set up priority client, get its nodeID, increase its balance on the lightServer - prioCli := startClient(t, "prioCli") - defer prioCli.killAndWait() - // 3_000_000_000 once we move to Go 1.13 - tokens := uint64(3000000000) - lightServer.callRPC(nil, "les_addBalance", prioCli.getNodeInfo().ID, tokens) - prioCli.addPeer(lightServer) - - // Check if priority client is actually syncing and the regular client got kicked out - prioCli.callRPC(&peers, "admin_peers") - if len(peers) != 1 { - t.Errorf("Expected: # of prio peers == 1, actual: %v", len(peers)) - } - - nodes := map[string]*gethrpc{ - lightServer.getNodeInfo().ID: lightServer, - freeCli.getNodeInfo().ID: freeCli, - prioCli.getNodeInfo().ID: prioCli, - } - time.Sleep(1 * time.Second) - lightServer.callRPC(&peers, "admin_peers") - peersWithNames := make(map[string]string) - for _, p := range peers { - peersWithNames[nodes[p.ID].name] = p.ID - } - if _, freeClientFound := peersWithNames[freeCli.name]; freeClientFound { - t.Error("client is still a peer of lightServer", peersWithNames) - } - if _, prioClientFound := peersWithNames[prioCli.name]; !prioClientFound { - t.Error("prio client is not among lightServer peers", peersWithNames) - } -} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d1b14b81cd..e5a17e45cf 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -62,7 +62,7 @@ var ( utils.MinFreeDiskSpaceFlag, utils.KeyStoreDirFlag, utils.ExternalSignerFlag, - utils.NoUSBFlag, + utils.NoUSBFlag, // deprecated utils.USBFlag, utils.SmartCardDaemonPathFlag, utils.OverrideCancun, @@ -87,24 +87,24 @@ var ( utils.ExitWhenSyncedFlag, utils.GCModeFlag, utils.SnapshotFlag, - utils.TxLookupLimitFlag, + utils.TxLookupLimitFlag, // deprecated utils.TransactionHistoryFlag, utils.StateHistoryFlag, - utils.LightServeFlag, - utils.LightIngressFlag, - utils.LightEgressFlag, - utils.LightMaxPeersFlag, - utils.LightNoPruneFlag, + utils.LightServeFlag, // deprecated + utils.LightIngressFlag, // deprecated + utils.LightEgressFlag, // deprecated + utils.LightMaxPeersFlag, // deprecated + utils.LightNoPruneFlag, // deprecated utils.LightKDFFlag, - utils.LightNoSyncServeFlag, + utils.LightNoSyncServeFlag, // deprecated utils.EthRequiredBlocksFlag, - utils.LegacyWhitelistFlag, + utils.LegacyWhitelistFlag, // deprecated utils.BloomFilterSizeFlag, utils.CacheFlag, utils.CacheDatabaseFlag, utils.CacheTrieFlag, - utils.CacheTrieJournalFlag, - utils.CacheTrieRejournalFlag, + utils.CacheTrieJournalFlag, // deprecated + utils.CacheTrieRejournalFlag, // deprecated utils.CacheGCFlag, utils.CacheSnapshotFlag, utils.CacheNoPrefetchFlag, @@ -127,7 +127,7 @@ var ( utils.NoDiscoverFlag, utils.DiscoveryV4Flag, utils.DiscoveryV5Flag, - utils.LegacyDiscoveryV5Flag, + utils.LegacyDiscoveryV5Flag, // deprecated utils.NetrestrictFlag, utils.NodeKeyFileFlag, utils.NodeKeyHexFlag, @@ -306,7 +306,7 @@ func prepare(ctx *cli.Context) { log.Info("Starting Geth on Ethereum mainnet...") } // If we're a full node on mainnet without --cache specified, bump default cache allowance - if ctx.String(utils.SyncModeFlag.Name) != "light" && !ctx.IsSet(utils.CacheFlag.Name) && !ctx.IsSet(utils.NetworkIdFlag.Name) { + if !ctx.IsSet(utils.CacheFlag.Name) && !ctx.IsSet(utils.NetworkIdFlag.Name) { // Make sure we're not on any supported preconfigured testnet either if !ctx.IsSet(utils.HoleskyFlag.Name) && !ctx.IsSet(utils.SepoliaFlag.Name) && @@ -317,11 +317,6 @@ func prepare(ctx *cli.Context) { ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096)) } } - // If we're running a light client on any network, drop the cache to some meaningfully low amount - if ctx.String(utils.SyncModeFlag.Name) == "light" && !ctx.IsSet(utils.CacheFlag.Name) { - log.Info("Dropping default light client cache", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 128) - ctx.Set(utils.CacheFlag.Name, strconv.Itoa(128)) - } // Start metrics export if enabled utils.SetupMetrics(ctx) diff --git a/cmd/geth/run_test.go b/cmd/geth/run_test.go index 2e03dc5eaa..1d32880325 100644 --- a/cmd/geth/run_test.go +++ b/cmd/geth/run_test.go @@ -55,6 +55,15 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +func initGeth(t *testing.T) string { + args := []string{"--networkid=42", "init", "./testdata/clique.json"} + t.Logf("Initializing geth: %v ", args) + g := runGeth(t, args...) + datadir := g.Datadir + g.WaitExit() + return datadir +} + // spawns geth with the given command line args. If the args don't set --datadir, the // child g gets a temporary data directory. func runGeth(t *testing.T, args ...string) *testgeth { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 72a56e9c28..b49c7c36d5 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -57,7 +57,6 @@ import ( "github.com/ethereum/go-ethereum/graphql" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/flags" - "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/exp" @@ -255,7 +254,7 @@ var ( } SyncModeFlag = &flags.TextMarshalerFlag{ Name: "syncmode", - Usage: `Blockchain sync mode ("snap", "full" or "light")`, + Usage: `Blockchain sync mode ("snap" or "full")`, Value: &defaultSyncMode, Category: flags.StateCategory, } @@ -282,41 +281,6 @@ var ( Value: ethconfig.Defaults.TransactionHistory, Category: flags.StateCategory, } - // Light server and client settings - LightServeFlag = &cli.IntFlag{ - Name: "light.serve", - Usage: "Maximum percentage of time allowed for serving LES requests (multi-threaded processing allows values over 100)", - Value: ethconfig.Defaults.LightServ, - Category: flags.LightCategory, - } - LightIngressFlag = &cli.IntFlag{ - Name: "light.ingress", - Usage: "Incoming bandwidth limit for serving light clients (kilobytes/sec, 0 = unlimited)", - Value: ethconfig.Defaults.LightIngress, - Category: flags.LightCategory, - } - LightEgressFlag = &cli.IntFlag{ - Name: "light.egress", - Usage: "Outgoing bandwidth limit for serving light clients (kilobytes/sec, 0 = unlimited)", - Value: ethconfig.Defaults.LightEgress, - Category: flags.LightCategory, - } - LightMaxPeersFlag = &cli.IntFlag{ - Name: "light.maxpeers", - Usage: "Maximum number of light clients to serve, or light servers to attach to", - Value: ethconfig.Defaults.LightPeers, - Category: flags.LightCategory, - } - LightNoPruneFlag = &cli.BoolFlag{ - Name: "light.nopruning", - Usage: "Disable ancient light chain data pruning", - Category: flags.LightCategory, - } - LightNoSyncServeFlag = &cli.BoolFlag{ - Name: "light.nosyncserve", - Usage: "Enables serving light clients before syncing", - Category: flags.LightCategory, - } // Transaction pool settings TxPoolLocalsFlag = &cli.StringFlag{ Name: "txpool.locals", @@ -1224,25 +1188,25 @@ func setIPC(ctx *cli.Context, cfg *node.Config) { } } -// setLes configures the les server and ultra light client settings from the command line flags. +// setLes shows the deprecation warnings for LES flags. func setLes(ctx *cli.Context, cfg *ethconfig.Config) { if ctx.IsSet(LightServeFlag.Name) { - cfg.LightServ = ctx.Int(LightServeFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightServeFlag.Name) } if ctx.IsSet(LightIngressFlag.Name) { - cfg.LightIngress = ctx.Int(LightIngressFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightIngressFlag.Name) } if ctx.IsSet(LightEgressFlag.Name) { - cfg.LightEgress = ctx.Int(LightEgressFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightEgressFlag.Name) } if ctx.IsSet(LightMaxPeersFlag.Name) { - cfg.LightPeers = ctx.Int(LightMaxPeersFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightMaxPeersFlag.Name) } if ctx.IsSet(LightNoPruneFlag.Name) { - cfg.LightNoPrune = ctx.Bool(LightNoPruneFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightNoPruneFlag.Name) } if ctx.IsSet(LightNoSyncServeFlag.Name) { - cfg.LightNoSyncServe = ctx.Bool(LightNoSyncServeFlag.Name) + log.Warn("The light server has been deprecated, please remove this flag", "flag", LightNoSyncServeFlag.Name) } } @@ -1340,58 +1304,24 @@ func SetP2PConfig(ctx *cli.Context, cfg *p2p.Config) { setBootstrapNodes(ctx, cfg) setBootstrapNodesV5(ctx, cfg) - lightClient := ctx.String(SyncModeFlag.Name) == "light" - lightServer := (ctx.Int(LightServeFlag.Name) != 0) - - lightPeers := ctx.Int(LightMaxPeersFlag.Name) - if lightClient && !ctx.IsSet(LightMaxPeersFlag.Name) { - // dynamic default - for clients we use 1/10th of the default for servers - lightPeers /= 10 - } - if ctx.IsSet(MaxPeersFlag.Name) { cfg.MaxPeers = ctx.Int(MaxPeersFlag.Name) - if lightServer && !ctx.IsSet(LightMaxPeersFlag.Name) { - cfg.MaxPeers += lightPeers - } - } else { - if lightServer { - cfg.MaxPeers += lightPeers - } - if lightClient && ctx.IsSet(LightMaxPeersFlag.Name) && cfg.MaxPeers < lightPeers { - cfg.MaxPeers = lightPeers - } } - if !(lightClient || lightServer) { - lightPeers = 0 - } - ethPeers := cfg.MaxPeers - lightPeers - if lightClient { - ethPeers = 0 - } - log.Info("Maximum peer count", "ETH", ethPeers, "LES", lightPeers, "total", cfg.MaxPeers) + ethPeers := cfg.MaxPeers + log.Info("Maximum peer count", "ETH", ethPeers, "total", cfg.MaxPeers) if ctx.IsSet(MaxPendingPeersFlag.Name) { cfg.MaxPendingPeers = ctx.Int(MaxPendingPeersFlag.Name) } - if ctx.IsSet(NoDiscoverFlag.Name) || lightClient { + if ctx.IsSet(NoDiscoverFlag.Name) { cfg.NoDiscovery = true } - // Disallow --nodiscover when used in conjunction with light mode. - if (lightClient || lightServer) && ctx.Bool(NoDiscoverFlag.Name) { - Fatalf("Cannot use --" + NoDiscoverFlag.Name + " in light client or light server mode") - } CheckExclusive(ctx, DiscoveryV4Flag, NoDiscoverFlag) CheckExclusive(ctx, DiscoveryV5Flag, NoDiscoverFlag) cfg.DiscoveryV4 = ctx.Bool(DiscoveryV4Flag.Name) cfg.DiscoveryV5 = ctx.Bool(DiscoveryV5Flag.Name) - // If we're running a light client or server, force enable the v5 peer discovery. - if lightClient || lightServer { - cfg.DiscoveryV5 = true - } - if netrestrict := ctx.String(NetrestrictFlag.Name); netrestrict != "" { list, err := netutil.ParseNetlist(netrestrict) if err != nil { @@ -1496,12 +1426,7 @@ func SetDataDir(ctx *cli.Context, cfg *node.Config) { } } -func setGPO(ctx *cli.Context, cfg *gasprice.Config, light bool) { - // If we are running the light client, apply another group - // settings for gas oracle. - if light { - *cfg = ethconfig.LightClientGPO - } +func setGPO(ctx *cli.Context, cfg *gasprice.Config) { if ctx.IsSet(GpoBlocksFlag.Name) { cfg.Blocks = ctx.Int(GpoBlocksFlag.Name) } @@ -1650,12 +1575,11 @@ func CheckExclusive(ctx *cli.Context, args ...interface{}) { func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // Avoid conflicting network flags CheckExclusive(ctx, MainnetFlag, DeveloperFlag, GoerliFlag, SepoliaFlag, HoleskyFlag) - CheckExclusive(ctx, LightServeFlag, SyncModeFlag, "light") CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer // Set configurations from CLI flags setEtherbase(ctx, cfg) - setGPO(ctx, &cfg.GPO, ctx.String(SyncModeFlag.Name) == "light") + setGPO(ctx, &cfg.GPO) setTxPool(ctx, &cfg.TxPool) setMiner(ctx, &cfg.Miner) setRequiredBlocks(ctx, cfg) @@ -1734,9 +1658,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.TransactionHistory = 0 log.Warn("Disabled transaction unindexing for archive node") } - if ctx.IsSet(LightServeFlag.Name) && cfg.TransactionHistory != 0 { - log.Warn("LES server cannot serve old transaction status and cannot connect below les/4 protocol version if transaction lookup index is limited") - } if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheTrieFlag.Name) { cfg.TrieCleanCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheTrieFlag.Name) / 100 } @@ -1913,9 +1834,6 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) { return // already set through flags/config } protocol := "all" - if cfg.SyncMode == downloader.LightSync { - protocol = "les" - } if url := params.KnownDNSNetwork(genesis, protocol); url != "" { cfg.EthDiscoveryURLs = []string{url} cfg.SnapDiscoveryURLs = cfg.EthDiscoveryURLs @@ -1923,27 +1841,12 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) { } // RegisterEthService adds an Ethereum client to the stack. -// The second return value is the full node instance, which may be nil if the -// node is running as a light client. +// The second return value is the full node instance. func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) { - if cfg.SyncMode == downloader.LightSync { - backend, err := les.New(stack, cfg) - if err != nil { - Fatalf("Failed to register the Ethereum service: %v", err) - } - stack.RegisterAPIs(tracers.APIs(backend.ApiBackend)) - return backend.ApiBackend, nil - } backend, err := eth.New(stack, cfg) if err != nil { Fatalf("Failed to register the Ethereum service: %v", err) } - if cfg.LightServ > 0 { - _, err := les.NewLesServer(stack, backend, cfg) - if err != nil { - Fatalf("Failed to create the LES server: %v", err) - } - } stack.RegisterAPIs(tracers.APIs(backend.APIBackend)) return backend.APIBackend, backend } @@ -1965,13 +1868,12 @@ func RegisterGraphQLService(stack *node.Node, backend ethapi.Backend, filterSyst // RegisterFilterAPI adds the eth log filtering RPC API to the node. func RegisterFilterAPI(stack *node.Node, backend ethapi.Backend, ethcfg *ethconfig.Config) *filters.FilterSystem { - isLightClient := ethcfg.SyncMode == downloader.LightSync filterSystem := filters.NewFilterSystem(backend, filters.Config{ LogCacheSize: ethcfg.FilterLogCacheSize, }) stack.RegisterAPIs([]rpc.API{{ Namespace: "eth", - Service: filters.NewFilterAPI(filterSystem, isLightClient), + Service: filters.NewFilterAPI(filterSystem, false), }}) return filterSystem } diff --git a/cmd/utils/flags_legacy.go b/cmd/utils/flags_legacy.go index 6669ff176f..00237fecaf 100644 --- a/cmd/utils/flags_legacy.go +++ b/cmd/utils/flags_legacy.go @@ -39,6 +39,12 @@ var DeprecatedFlags = []cli.Flag{ CacheTrieRejournalFlag, LegacyDiscoveryV5Flag, TxLookupLimitFlag, + LightServeFlag, + LightIngressFlag, + LightEgressFlag, + LightMaxPeersFlag, + LightNoPruneFlag, + LightNoSyncServeFlag, } var ( @@ -77,6 +83,41 @@ var ( Value: ethconfig.Defaults.TransactionHistory, Category: flags.DeprecatedCategory, } + // Light server and client settings, Deprecated November 2023 + LightServeFlag = &cli.IntFlag{ + Name: "light.serve", + Usage: "Maximum percentage of time allowed for serving LES requests (deprecated)", + Value: ethconfig.Defaults.LightServ, + Category: flags.LightCategory, + } + LightIngressFlag = &cli.IntFlag{ + Name: "light.ingress", + Usage: "Incoming bandwidth limit for serving light clients (deprecated)", + Value: ethconfig.Defaults.LightIngress, + Category: flags.LightCategory, + } + LightEgressFlag = &cli.IntFlag{ + Name: "light.egress", + Usage: "Outgoing bandwidth limit for serving light clients (deprecated)", + Value: ethconfig.Defaults.LightEgress, + Category: flags.LightCategory, + } + LightMaxPeersFlag = &cli.IntFlag{ + Name: "light.maxpeers", + Usage: "Maximum number of light clients to serve, or light servers to attach to (deprecated)", + Value: ethconfig.Defaults.LightPeers, + Category: flags.LightCategory, + } + LightNoPruneFlag = &cli.BoolFlag{ + Name: "light.nopruning", + Usage: "Disable ancient light chain data pruning (deprecated)", + Category: flags.LightCategory, + } + LightNoSyncServeFlag = &cli.BoolFlag{ + Name: "light.nosyncserve", + Usage: "Enables serving light clients before syncing (deprecated)", + Category: flags.LightCategory, + } ) // showDeprecated displays deprecated flags that will be soon removed from the codebase. diff --git a/eth/backend.go b/eth/backend.go index 09559f0ac1..774ffaf248 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -108,7 +108,7 @@ type Ethereum struct { func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // Ensure configuration values are compatible and sane if config.SyncMode == downloader.LightSync { - return nil, errors.New("can't run eth.Ethereum in light sync mode, use les.LightEthereum") + return nil, errors.New("can't run eth.Ethereum in light sync mode, light mode has been deprecated") } if !config.SyncMode.IsValid() { return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 5e8f58efdb..ad664afb5b 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -46,16 +46,6 @@ var FullNodeGPO = gasprice.Config{ IgnorePrice: gasprice.DefaultIgnorePrice, } -// LightClientGPO contains default gasprice oracle settings for light client. -var LightClientGPO = gasprice.Config{ - Blocks: 2, - Percentile: 60, - MaxHeaderHistory: 300, - MaxBlockHistory: 5, - MaxPrice: gasprice.DefaultMaxPrice, - IgnorePrice: gasprice.DefaultIgnorePrice, -} - // Defaults contains default settings for use on the Ethereum main net. var Defaults = Config{ SyncMode: downloader.SnapSync, diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go index 84a6722806..75d0faac54 100644 --- a/ethstats/ethstats.go +++ b/ethstats/ethstats.go @@ -38,7 +38,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" ethproto "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/node" @@ -486,7 +485,7 @@ func (s *Service) login(conn *connWrapper) error { if info := infos.Protocols["eth"]; info != nil { network = fmt.Sprintf("%d", info.(*ethproto.NodeInfo).Network) } else { - network = fmt.Sprintf("%d", infos.Protocols["les"].(*les.NodeInfo).Network) + return errors.New("no eth protocol available") } auth := &authMsg{ ID: s.node, diff --git a/les/api.go b/les/api.go deleted file mode 100644 index e8490f7b0f..0000000000 --- a/les/api.go +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2019 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 les - -import ( - "errors" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - vfs "github.com/ethereum/go-ethereum/les/vflux/server" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -var errUnknownBenchmarkType = errors.New("unknown benchmark type") - -// LightServerAPI provides an API to access the LES light server. -type LightServerAPI struct { - server *LesServer - defaultPosFactors, defaultNegFactors vfs.PriceFactors -} - -// NewLightServerAPI creates a new LES light server API. -func NewLightServerAPI(server *LesServer) *LightServerAPI { - return &LightServerAPI{ - server: server, - defaultPosFactors: defaultPosFactors, - defaultNegFactors: defaultNegFactors, - } -} - -// parseNode parses either an enode address a raw hex node id -func parseNode(node string) (enode.ID, error) { - if id, err := enode.ParseID(node); err == nil { - return id, nil - } - if node, err := enode.Parse(enode.ValidSchemes, node); err == nil { - return node.ID(), nil - } else { - return enode.ID{}, err - } -} - -// ServerInfo returns global server parameters -func (api *LightServerAPI) ServerInfo() map[string]interface{} { - res := make(map[string]interface{}) - res["minimumCapacity"] = api.server.minCapacity - res["maximumCapacity"] = api.server.maxCapacity - _, res["totalCapacity"] = api.server.clientPool.Limits() - _, res["totalConnectedCapacity"] = api.server.clientPool.Active() - res["priorityConnectedCapacity"] = 0 //TODO connect when token sale module is added - return res -} - -// ClientInfo returns information about clients listed in the ids list or matching the given tags -func (api *LightServerAPI) ClientInfo(nodes []string) map[enode.ID]map[string]interface{} { - var ids []enode.ID - for _, node := range nodes { - if id, err := parseNode(node); err == nil { - ids = append(ids, id) - } - } - - res := make(map[enode.ID]map[string]interface{}) - if len(ids) == 0 { - ids = api.server.peers.ids() - } - for _, id := range ids { - if peer := api.server.peers.peer(id); peer != nil { - res[id] = api.clientInfo(peer, peer.balance) - } else { - api.server.clientPool.BalanceOperation(id, "", func(balance vfs.AtomicBalanceOperator) { - res[id] = api.clientInfo(nil, balance) - }) - } - } - return res -} - -// PriorityClientInfo returns information about clients with a positive balance -// in the given ID range (stop excluded). If stop is null then the iterator stops -// only at the end of the ID space. MaxCount limits the number of results returned. -// If maxCount limit is applied but there are more potential results then the ID -// of the next potential result is included in the map with an empty structure -// assigned to it. -func (api *LightServerAPI) PriorityClientInfo(start, stop enode.ID, maxCount int) map[enode.ID]map[string]interface{} { - res := make(map[enode.ID]map[string]interface{}) - ids := api.server.clientPool.GetPosBalanceIDs(start, stop, maxCount+1) - if len(ids) > maxCount { - res[ids[maxCount]] = make(map[string]interface{}) - ids = ids[:maxCount] - } - for _, id := range ids { - if peer := api.server.peers.peer(id); peer != nil { - res[id] = api.clientInfo(peer, peer.balance) - } else { - api.server.clientPool.BalanceOperation(id, "", func(balance vfs.AtomicBalanceOperator) { - res[id] = api.clientInfo(nil, balance) - }) - } - } - return res -} - -// clientInfo creates a client info data structure -func (api *LightServerAPI) clientInfo(peer *clientPeer, balance vfs.ReadOnlyBalance) map[string]interface{} { - info := make(map[string]interface{}) - pb, nb := balance.GetBalance() - info["isConnected"] = peer != nil - info["pricing/balance"] = pb - info["priority"] = pb != 0 - // cb := api.server.clientPool.ndb.getCurrencyBalance(id) - // info["pricing/currency"] = cb.amount - if peer != nil { - info["connectionTime"] = float64(mclock.Now()-peer.connectedAt) / float64(time.Second) - info["capacity"] = peer.getCapacity() - info["pricing/negBalance"] = nb - } - return info -} - -// setParams either sets the given parameters for a single connected client (if specified) -// or the default parameters applicable to clients connected in the future -func (api *LightServerAPI) setParams(params map[string]interface{}, client *clientPeer, posFactors, negFactors *vfs.PriceFactors) (updateFactors bool, err error) { - defParams := client == nil - for name, value := range params { - errValue := func() error { - return fmt.Errorf("invalid value for parameter '%s'", name) - } - setFactor := func(v *float64) { - if val, ok := value.(float64); ok && val >= 0 { - *v = val / float64(time.Second) - updateFactors = true - } else { - err = errValue() - } - } - - switch { - case name == "pricing/timeFactor": - setFactor(&posFactors.TimeFactor) - case name == "pricing/capacityFactor": - setFactor(&posFactors.CapacityFactor) - case name == "pricing/requestCostFactor": - setFactor(&posFactors.RequestFactor) - case name == "pricing/negative/timeFactor": - setFactor(&negFactors.TimeFactor) - case name == "pricing/negative/capacityFactor": - setFactor(&negFactors.CapacityFactor) - case name == "pricing/negative/requestCostFactor": - setFactor(&negFactors.RequestFactor) - case !defParams && name == "capacity": - if capacity, ok := value.(float64); ok && uint64(capacity) >= api.server.minCapacity { - _, err = api.server.clientPool.SetCapacity(client.Node(), uint64(capacity), 0, false) - // time factor recalculation is performed automatically by the balance tracker - } else { - err = errValue() - } - default: - if defParams { - err = fmt.Errorf("invalid default parameter '%s'", name) - } else { - err = fmt.Errorf("invalid client parameter '%s'", name) - } - } - if err != nil { - return - } - } - return -} - -// SetClientParams sets client parameters for all clients listed in the ids list -// or all connected clients if the list is empty -func (api *LightServerAPI) SetClientParams(nodes []string, params map[string]interface{}) error { - var err error - for _, node := range nodes { - var id enode.ID - if id, err = parseNode(node); err != nil { - return err - } - if peer := api.server.peers.peer(id); peer != nil { - posFactors, negFactors := peer.balance.GetPriceFactors() - update, e := api.setParams(params, peer, &posFactors, &negFactors) - if update { - peer.balance.SetPriceFactors(posFactors, negFactors) - } - if e != nil { - err = e - } - } else { - err = fmt.Errorf("client %064x is not connected", id) - } - } - return err -} - -// SetDefaultParams sets the default parameters applicable to clients connected in the future -func (api *LightServerAPI) SetDefaultParams(params map[string]interface{}) error { - update, err := api.setParams(params, nil, &api.defaultPosFactors, &api.defaultNegFactors) - if update { - api.server.clientPool.SetDefaultFactors(api.defaultPosFactors, api.defaultNegFactors) - } - return err -} - -// SetConnectedBias set the connection bias, which is applied to already connected clients -// So that already connected client won't be kicked out very soon and we can ensure all -// connected clients can have enough time to request or sync some data. -// When the input parameter `bias` < 0 (illegal), return error. -func (api *LightServerAPI) SetConnectedBias(bias time.Duration) error { - if bias < time.Duration(0) { - return fmt.Errorf("bias illegal: %v less than 0", bias) - } - api.server.clientPool.SetConnectedBias(bias) - return nil -} - -// AddBalance adds the given amount to the balance of a client if possible and returns -// the balance before and after the operation -func (api *LightServerAPI) AddBalance(node string, amount int64) (balance [2]uint64, err error) { - var id enode.ID - if id, err = parseNode(node); err != nil { - return - } - api.server.clientPool.BalanceOperation(id, "", func(nb vfs.AtomicBalanceOperator) { - balance[0], balance[1], err = nb.AddBalance(amount) - }) - return -} - -// Benchmark runs a request performance benchmark with a given set of measurement setups -// in multiple passes specified by passCount. The measurement time for each setup in each -// pass is specified in milliseconds by length. -// -// Note: measurement time is adjusted for each pass depending on the previous ones. -// Therefore a controlled total measurement time is achievable in multiple passes. -func (api *LightServerAPI) Benchmark(setups []map[string]interface{}, passCount, length int) ([]map[string]interface{}, error) { - benchmarks := make([]requestBenchmark, len(setups)) - for i, setup := range setups { - if t, ok := setup["type"].(string); ok { - getInt := func(field string, def int) int { - if value, ok := setup[field].(float64); ok { - return int(value) - } - return def - } - getBool := func(field string, def bool) bool { - if value, ok := setup[field].(bool); ok { - return value - } - return def - } - switch t { - case "header": - benchmarks[i] = &benchmarkBlockHeaders{ - amount: getInt("amount", 1), - skip: getInt("skip", 1), - byHash: getBool("byHash", false), - reverse: getBool("reverse", false), - } - case "body": - benchmarks[i] = &benchmarkBodiesOrReceipts{receipts: false} - case "receipts": - benchmarks[i] = &benchmarkBodiesOrReceipts{receipts: true} - case "proof": - benchmarks[i] = &benchmarkProofsOrCode{code: false} - case "code": - benchmarks[i] = &benchmarkProofsOrCode{code: true} - case "cht": - benchmarks[i] = &benchmarkHelperTrie{ - bloom: false, - reqCount: getInt("amount", 1), - } - case "bloom": - benchmarks[i] = &benchmarkHelperTrie{ - bloom: true, - reqCount: getInt("amount", 1), - } - case "txSend": - benchmarks[i] = &benchmarkTxSend{} - case "txStatus": - benchmarks[i] = &benchmarkTxStatus{} - default: - return nil, errUnknownBenchmarkType - } - } else { - return nil, errUnknownBenchmarkType - } - } - rs := api.server.handler.runBenchmark(benchmarks, passCount, time.Millisecond*time.Duration(length)) - result := make([]map[string]interface{}, len(setups)) - for i, r := range rs { - res := make(map[string]interface{}) - if r.err == nil { - res["totalCount"] = r.totalCount - res["avgTime"] = r.avgTime - res["maxInSize"] = r.maxInSize - res["maxOutSize"] = r.maxOutSize - } else { - res["error"] = r.err.Error() - } - result[i] = res - } - return result, nil -} - -// DebugAPI provides an API to debug LES light server functionality. -type DebugAPI struct { - server *LesServer -} - -// NewDebugAPI creates a new LES light server debug API. -func NewDebugAPI(server *LesServer) *DebugAPI { - return &DebugAPI{ - server: server, - } -} - -// FreezeClient forces a temporary client freeze which normally happens when the server is overloaded -func (api *DebugAPI) FreezeClient(node string) error { - var ( - id enode.ID - err error - ) - if id, err = parseNode(node); err != nil { - return err - } - if peer := api.server.peers.peer(id); peer != nil { - peer.freeze() - return nil - } else { - return fmt.Errorf("client %064x is not connected", id[:]) - } -} diff --git a/les/api_backend.go b/les/api_backend.go deleted file mode 100644 index 3e9dbadce8..0000000000 --- a/les/api_backend.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2016 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 les - -import ( - "context" - "errors" - "math/big" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/bloombits" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" -) - -type LesApiBackend struct { - extRPCEnabled bool - allowUnprotectedTxs bool - eth *LightEthereum - gpo *gasprice.Oracle -} - -func (b *LesApiBackend) ChainConfig() *params.ChainConfig { - return b.eth.chainConfig -} - -func (b *LesApiBackend) CurrentBlock() *types.Header { - return b.eth.BlockChain().CurrentHeader() -} - -func (b *LesApiBackend) SetHead(number uint64) { - b.eth.blockchain.SetHead(number) -} - -func (b *LesApiBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { - // Return the latest current as the pending one since there - // is no pending notion in the light client. TODO(rjl493456442) - // unify the behavior of `HeaderByNumber` and `PendingBlockAndReceipts`. - if number == rpc.PendingBlockNumber { - return b.eth.blockchain.CurrentHeader(), nil - } - if number == rpc.LatestBlockNumber { - return b.eth.blockchain.CurrentHeader(), nil - } - return b.eth.blockchain.GetHeaderByNumberOdr(ctx, uint64(number)) -} - -func (b *LesApiBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.HeaderByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - header, err := b.HeaderByHash(ctx, hash) - if err != nil { - return nil, err - } - if header == nil { - return nil, errors.New("header for hash not found") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { - return nil, errors.New("hash is not currently canonical") - } - return header, nil - } - return nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *LesApiBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { - return b.eth.blockchain.GetHeaderByHash(hash), nil -} - -func (b *LesApiBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { - header, err := b.HeaderByNumber(ctx, number) - if header == nil || err != nil { - return nil, err - } - return b.BlockByHash(ctx, header.Hash()) -} - -func (b *LesApiBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - return b.eth.blockchain.GetBlockByHash(ctx, hash) -} - -func (b *LesApiBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.BlockByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - block, err := b.BlockByHash(ctx, hash) - if err != nil { - return nil, err - } - if block == nil { - return nil, errors.New("header found, but block body is missing") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(block.NumberU64()) != hash { - return nil, errors.New("hash is not currently canonical") - } - return block, nil - } - return nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *LesApiBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) { - return light.GetBody(ctx, b.eth.odr, hash, uint64(number)) -} - -func (b *LesApiBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) { - return nil, nil -} - -func (b *LesApiBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { - header, err := b.HeaderByNumber(ctx, number) - if err != nil { - return nil, nil, err - } - if header == nil { - return nil, nil, errors.New("header not found") - } - return light.NewState(ctx, header, b.eth.odr), header, nil -} - -func (b *LesApiBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { - if blockNr, ok := blockNrOrHash.Number(); ok { - return b.StateAndHeaderByNumber(ctx, blockNr) - } - if hash, ok := blockNrOrHash.Hash(); ok { - header := b.eth.blockchain.GetHeaderByHash(hash) - if header == nil { - return nil, nil, errors.New("header for hash not found") - } - if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { - return nil, nil, errors.New("hash is not currently canonical") - } - return light.NewState(ctx, header, b.eth.odr), header, nil - } - return nil, nil, errors.New("invalid arguments; neither block nor hash specified") -} - -func (b *LesApiBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { - if number := rawdb.ReadHeaderNumber(b.eth.chainDb, hash); number != nil { - return light.GetBlockReceipts(ctx, b.eth.odr, hash, *number) - } - return nil, nil -} - -func (b *LesApiBackend) GetLogs(ctx context.Context, hash common.Hash, number uint64) ([][]*types.Log, error) { - return light.GetBlockLogs(ctx, b.eth.odr, hash, number) -} - -func (b *LesApiBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { - if number := rawdb.ReadHeaderNumber(b.eth.chainDb, hash); number != nil { - return b.eth.blockchain.GetTdOdr(ctx, hash, *number) - } - return nil -} - -func (b *LesApiBackend) GetEVM(ctx context.Context, msg *core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockCtx *vm.BlockContext) (*vm.EVM, func() error) { - if vmConfig == nil { - vmConfig = new(vm.Config) - } - txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(header, b.eth.blockchain, nil) - if blockCtx != nil { - context = *blockCtx - } - return vm.NewEVM(context, txContext, state, b.eth.chainConfig, *vmConfig), state.Error -} - -func (b *LesApiBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { - return b.eth.txPool.Add(ctx, signedTx) -} - -func (b *LesApiBackend) RemoveTx(txHash common.Hash) { - b.eth.txPool.RemoveTx(txHash) -} - -func (b *LesApiBackend) GetPoolTransactions() (types.Transactions, error) { - return b.eth.txPool.GetTransactions() -} - -func (b *LesApiBackend) GetPoolTransaction(txHash common.Hash) *types.Transaction { - return b.eth.txPool.GetTransaction(txHash) -} - -func (b *LesApiBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { - return light.GetTransaction(ctx, b.eth.odr, txHash) -} - -func (b *LesApiBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { - return b.eth.txPool.GetNonce(ctx, addr) -} - -func (b *LesApiBackend) Stats() (pending int, queued int) { - return b.eth.txPool.Stats(), 0 -} - -func (b *LesApiBackend) TxPoolContent() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { - return b.eth.txPool.Content() -} - -func (b *LesApiBackend) TxPoolContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) { - return b.eth.txPool.ContentFrom(addr) -} - -func (b *LesApiBackend) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription { - return b.eth.txPool.SubscribeNewTxsEvent(ch) -} - -func (b *LesApiBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subscription { - return b.eth.blockchain.SubscribeChainEvent(ch) -} - -func (b *LesApiBackend) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription { - return b.eth.blockchain.SubscribeChainHeadEvent(ch) -} - -func (b *LesApiBackend) SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription { - return b.eth.blockchain.SubscribeChainSideEvent(ch) -} - -func (b *LesApiBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription { - return b.eth.blockchain.SubscribeLogsEvent(ch) -} - -func (b *LesApiBackend) SubscribePendingLogsEvent(ch chan<- []*types.Log) event.Subscription { - return event.NewSubscription(func(quit <-chan struct{}) error { - <-quit - return nil - }) -} - -func (b *LesApiBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { - return b.eth.blockchain.SubscribeRemovedLogsEvent(ch) -} - -func (b *LesApiBackend) SyncProgress() ethereum.SyncProgress { - return ethereum.SyncProgress{} -} - -func (b *LesApiBackend) ProtocolVersion() int { - return b.eth.LesVersion() + 10000 -} - -func (b *LesApiBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { - return b.gpo.SuggestTipCap(ctx) -} - -func (b *LesApiBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (firstBlock *big.Int, reward [][]*big.Int, baseFee []*big.Int, gasUsedRatio []float64, err error) { - return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) -} - -func (b *LesApiBackend) ChainDb() ethdb.Database { - return b.eth.chainDb -} - -func (b *LesApiBackend) AccountManager() *accounts.Manager { - return b.eth.accountManager -} - -func (b *LesApiBackend) ExtRPCEnabled() bool { - return b.extRPCEnabled -} - -func (b *LesApiBackend) UnprotectedAllowed() bool { - return b.allowUnprotectedTxs -} - -func (b *LesApiBackend) RPCGasCap() uint64 { - return b.eth.config.RPCGasCap -} - -func (b *LesApiBackend) RPCEVMTimeout() time.Duration { - return b.eth.config.RPCEVMTimeout -} - -func (b *LesApiBackend) RPCTxFeeCap() float64 { - return b.eth.config.RPCTxFeeCap -} - -func (b *LesApiBackend) BloomStatus() (uint64, uint64) { - if b.eth.bloomIndexer == nil { - return 0, 0 - } - sections, _, _ := b.eth.bloomIndexer.Sections() - return params.BloomBitsBlocksClient, sections -} - -func (b *LesApiBackend) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) { - for i := 0; i < bloomFilterThreads; i++ { - go session.Multiplex(bloomRetrievalBatch, bloomRetrievalWait, b.eth.bloomRequests) - } -} - -func (b *LesApiBackend) Engine() consensus.Engine { - return b.eth.engine -} - -func (b *LesApiBackend) CurrentHeader() *types.Header { - return b.eth.blockchain.CurrentHeader() -} - -func (b *LesApiBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtBlock(ctx, block, reexec) -} - -func (b *LesApiBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { - return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) -} diff --git a/les/api_test.go b/les/api_test.go deleted file mode 100644 index 484c95504c..0000000000 --- a/les/api_test.go +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright 2019 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 les - -import ( - "context" - crand "crypto/rand" - "errors" - "flag" - "math/rand" - "os" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/les/flowcontrol" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/simulations" - "github.com/ethereum/go-ethereum/p2p/simulations/adapters" - "github.com/ethereum/go-ethereum/rpc" - "github.com/mattn/go-colorable" -) - -// Additional command line flags for the test binary. -var ( - loglevel = flag.Int("loglevel", 0, "verbosity of logs") - simAdapter = flag.String("adapter", "exec", "type of simulation: sim|socket|exec|docker") -) - -func TestMain(m *testing.M) { - flag.Parse() - log.PrintOrigins(true) - log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true)))) - // register the Delivery service which will run as a devp2p - // protocol when using the exec adapter - adapters.RegisterLifecycles(services) - os.Exit(m.Run()) -} - -// This test is not meant to be a part of the automatic testing process because it -// runs for a long time and also requires a large database in order to do a meaningful -// request performance test. When testServerDataDir is empty, the test is skipped. - -const ( - testServerDataDir = "" // should always be empty on the master branch - testServerCapacity = 200 - testMaxClients = 10 - testTolerance = 0.1 - minRelCap = 0.2 -) - -func TestCapacityAPI3(t *testing.T) { - testCapacityAPI(t, 3) -} - -func TestCapacityAPI6(t *testing.T) { - testCapacityAPI(t, 6) -} - -func TestCapacityAPI10(t *testing.T) { - testCapacityAPI(t, 10) -} - -// testCapacityAPI runs an end-to-end simulation test connecting one server with -// a given number of clients. It sets different priority capacities to all clients -// except a randomly selected one which runs in free client mode. All clients send -// similar requests at the maximum allowed rate and the test verifies whether the -// ratio of processed requests is close enough to the ratio of assigned capacities. -// Running multiple rounds with different settings ensures that changing capacity -// while connected and going back and forth between free and priority mode with -// the supplied API calls is also thoroughly tested. -func testCapacityAPI(t *testing.T, clientCount int) { - // Skip test if no data dir specified - if testServerDataDir == "" { - return - } - for !testSim(t, 1, clientCount, []string{testServerDataDir}, nil, func(ctx context.Context, net *simulations.Network, servers []*simulations.Node, clients []*simulations.Node) bool { - if len(servers) != 1 { - t.Fatalf("Invalid number of servers: %d", len(servers)) - } - server := servers[0] - - serverRpcClient, err := server.Client() - if err != nil { - t.Fatalf("Failed to obtain rpc client: %v", err) - } - headNum, headHash := getHead(ctx, t, serverRpcClient) - minCap, totalCap := getCapacityInfo(ctx, t, serverRpcClient) - testCap := totalCap * 3 / 4 - t.Logf("Server testCap: %d minCap: %d head number: %d head hash: %064x\n", testCap, minCap, headNum, headHash) - reqMinCap := uint64(float64(testCap) * minRelCap / (minRelCap + float64(len(clients)-1))) - if minCap > reqMinCap { - t.Fatalf("Minimum client capacity (%d) bigger than required minimum for this test (%d)", minCap, reqMinCap) - } - freeIdx := rand.Intn(len(clients)) - - clientRpcClients := make([]*rpc.Client, len(clients)) - for i, client := range clients { - var err error - clientRpcClients[i], err = client.Client() - if err != nil { - t.Fatalf("Failed to obtain rpc client: %v", err) - } - t.Log("connecting client", i) - if i != freeIdx { - setCapacity(ctx, t, serverRpcClient, client.ID(), testCap/uint64(len(clients))) - } - net.Connect(client.ID(), server.ID()) - - for { - select { - case <-ctx.Done(): - t.Fatalf("Timeout") - default: - } - num, hash := getHead(ctx, t, clientRpcClients[i]) - if num == headNum && hash == headHash { - t.Log("client", i, "synced") - break - } - time.Sleep(time.Millisecond * 200) - } - } - - var wg sync.WaitGroup - stop := make(chan struct{}) - - reqCount := make([]atomic.Uint64, len(clientRpcClients)) - - // Send light request like crazy. - for i, c := range clientRpcClients { - wg.Add(1) - i, c := i, c - go func() { - defer wg.Done() - - queue := make(chan struct{}, 100) - reqCount[i].Store(0) - for { - select { - case queue <- struct{}{}: - select { - case <-stop: - return - case <-ctx.Done(): - return - default: - wg.Add(1) - go func() { - ok := testRequest(ctx, t, c) - wg.Done() - <-queue - if ok { - if reqCount[i].Add(1)%10000 == 0 { - freezeClient(ctx, t, serverRpcClient, clients[i].ID()) - } - } - }() - } - case <-stop: - return - case <-ctx.Done(): - return - } - } - }() - } - - processedSince := func(start []uint64) []uint64 { - res := make([]uint64, len(reqCount)) - for i := range reqCount { - res[i] = reqCount[i].Load() - if start != nil { - res[i] -= start[i] - } - } - return res - } - - weights := make([]float64, len(clients)) - for c := 0; c < 5; c++ { - setCapacity(ctx, t, serverRpcClient, clients[freeIdx].ID(), minCap) - freeIdx = rand.Intn(len(clients)) - var sum float64 - for i := range clients { - if i == freeIdx { - weights[i] = 0 - } else { - weights[i] = rand.Float64()*(1-minRelCap) + minRelCap - } - sum += weights[i] - } - for i, client := range clients { - weights[i] *= float64(testCap-minCap-100) / sum - capacity := uint64(weights[i]) - if i != freeIdx && capacity < getCapacity(ctx, t, serverRpcClient, client.ID()) { - setCapacity(ctx, t, serverRpcClient, client.ID(), capacity) - } - } - setCapacity(ctx, t, serverRpcClient, clients[freeIdx].ID(), 0) - for i, client := range clients { - capacity := uint64(weights[i]) - if i != freeIdx && capacity > getCapacity(ctx, t, serverRpcClient, client.ID()) { - setCapacity(ctx, t, serverRpcClient, client.ID(), capacity) - } - } - weights[freeIdx] = float64(minCap) - for i := range clients { - weights[i] /= float64(testCap) - } - - time.Sleep(flowcontrol.DecParamDelay) - t.Log("Starting measurement") - t.Logf("Relative weights:") - for i := range clients { - t.Logf(" %f", weights[i]) - } - t.Log() - start := processedSince(nil) - for { - select { - case <-ctx.Done(): - t.Fatalf("Timeout") - default: - } - - _, totalCap = getCapacityInfo(ctx, t, serverRpcClient) - if totalCap < testCap { - t.Log("Total capacity underrun") - close(stop) - wg.Wait() - return false - } - - processed := processedSince(start) - var avg uint64 - t.Logf("Processed") - for i, p := range processed { - t.Logf(" %d", p) - processed[i] = uint64(float64(p) / weights[i]) - avg += processed[i] - } - avg /= uint64(len(processed)) - - if avg >= 10000 { - var maxDev float64 - for _, p := range processed { - dev := float64(int64(p-avg)) / float64(avg) - t.Logf(" %7.4f", dev) - if dev < 0 { - dev = -dev - } - if dev > maxDev { - maxDev = dev - } - } - t.Logf(" max deviation: %f totalCap: %d\n", maxDev, totalCap) - if maxDev <= testTolerance { - t.Log("success") - break - } - } else { - t.Log() - } - time.Sleep(time.Millisecond * 200) - } - } - - close(stop) - wg.Wait() - - for i := range reqCount { - t.Log("client", i, "processed", reqCount[i].Load()) - } - return true - }) { - t.Log("restarting test") - } -} - -func getHead(ctx context.Context, t *testing.T, client *rpc.Client) (uint64, common.Hash) { - res := make(map[string]interface{}) - if err := client.CallContext(ctx, &res, "eth_getBlockByNumber", "latest", false); err != nil { - t.Fatalf("Failed to obtain head block: %v", err) - } - numStr, ok := res["number"].(string) - if !ok { - t.Fatalf("RPC block number field invalid") - } - num, err := hexutil.DecodeUint64(numStr) - if err != nil { - t.Fatalf("Failed to decode RPC block number: %v", err) - } - hashStr, ok := res["hash"].(string) - if !ok { - t.Fatalf("RPC block number field invalid") - } - hash := common.HexToHash(hashStr) - return num, hash -} - -func testRequest(ctx context.Context, t *testing.T, client *rpc.Client) bool { - var res string - var addr common.Address - crand.Read(addr[:]) - c, cancel := context.WithTimeout(ctx, time.Second*12) - defer cancel() - err := client.CallContext(c, &res, "eth_getBalance", addr, "latest") - if err != nil { - t.Log("request error:", err) - } - return err == nil -} - -func freezeClient(ctx context.Context, t *testing.T, server *rpc.Client, clientID enode.ID) { - if err := server.CallContext(ctx, nil, "debug_freezeClient", clientID); err != nil { - t.Fatalf("Failed to freeze client: %v", err) - } -} - -func setCapacity(ctx context.Context, t *testing.T, server *rpc.Client, clientID enode.ID, cap uint64) { - params := make(map[string]interface{}) - params["capacity"] = cap - if err := server.CallContext(ctx, nil, "les_setClientParams", []enode.ID{clientID}, []string{}, params); err != nil { - t.Fatalf("Failed to set client capacity: %v", err) - } -} - -func getCapacity(ctx context.Context, t *testing.T, server *rpc.Client, clientID enode.ID) uint64 { - var res map[enode.ID]map[string]interface{} - if err := server.CallContext(ctx, &res, "les_clientInfo", []enode.ID{clientID}, []string{}); err != nil { - t.Fatalf("Failed to get client info: %v", err) - } - info, ok := res[clientID] - if !ok { - t.Fatalf("Missing client info") - } - v, ok := info["capacity"] - if !ok { - t.Fatalf("Missing field in client info: capacity") - } - vv, ok := v.(float64) - if !ok { - t.Fatalf("Failed to decode capacity field") - } - return uint64(vv) -} - -func getCapacityInfo(ctx context.Context, t *testing.T, server *rpc.Client) (minCap, totalCap uint64) { - var res map[string]interface{} - if err := server.CallContext(ctx, &res, "les_serverInfo"); err != nil { - t.Fatalf("Failed to query server info: %v", err) - } - decode := func(s string) uint64 { - v, ok := res[s] - if !ok { - t.Fatalf("Missing field in server info: %s", s) - } - vv, ok := v.(float64) - if !ok { - t.Fatalf("Failed to decode server info field: %s", s) - } - return uint64(vv) - } - minCap = decode("minimumCapacity") - totalCap = decode("totalCapacity") - return -} - -var services = adapters.LifecycleConstructors{ - "lesclient": newLesClientService, - "lesserver": newLesServerService, -} - -func NewNetwork() (*simulations.Network, func(), error) { - adapter, adapterTeardown, err := NewAdapter(*simAdapter, services) - if err != nil { - return nil, adapterTeardown, err - } - defaultService := "streamer" - net := simulations.NewNetwork(adapter, &simulations.NetworkConfig{ - ID: "0", - DefaultService: defaultService, - }) - teardown := func() { - adapterTeardown() - net.Shutdown() - } - return net, teardown, nil -} - -func NewAdapter(adapterType string, services adapters.LifecycleConstructors) (adapter adapters.NodeAdapter, teardown func(), err error) { - teardown = func() {} - switch adapterType { - case "sim": - adapter = adapters.NewSimAdapter(services) - // case "socket": - // adapter = adapters.NewSocketAdapter(services) - case "exec": - baseDir, err0 := os.MkdirTemp("", "les-test") - if err0 != nil { - return nil, teardown, err0 - } - teardown = func() { os.RemoveAll(baseDir) } - adapter = adapters.NewExecAdapter(baseDir) - /*case "docker": - adapter, err = adapters.NewDockerAdapter() - if err != nil { - return nil, teardown, err - }*/ - default: - return nil, teardown, errors.New("adapter needs to be one of sim, socket, exec, docker") - } - return adapter, teardown, nil -} - -func testSim(t *testing.T, serverCount, clientCount int, serverDir, clientDir []string, test func(ctx context.Context, net *simulations.Network, servers []*simulations.Node, clients []*simulations.Node) bool) bool { - net, teardown, err := NewNetwork() - defer teardown() - if err != nil { - t.Fatalf("Failed to create network: %v", err) - } - timeout := 1800 * time.Second - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - servers := make([]*simulations.Node, serverCount) - clients := make([]*simulations.Node, clientCount) - - for i := range clients { - clientconf := adapters.RandomNodeConfig() - clientconf.Lifecycles = []string{"lesclient"} - if len(clientDir) == clientCount { - clientconf.DataDir = clientDir[i] - } - client, err := net.NewNodeWithConfig(clientconf) - if err != nil { - t.Fatalf("Failed to create client: %v", err) - } - clients[i] = client - } - - for i := range servers { - serverconf := adapters.RandomNodeConfig() - serverconf.Lifecycles = []string{"lesserver"} - if len(serverDir) == serverCount { - serverconf.DataDir = serverDir[i] - } - server, err := net.NewNodeWithConfig(serverconf) - if err != nil { - t.Fatalf("Failed to create server: %v", err) - } - servers[i] = server - } - - for _, client := range clients { - if err := net.Start(client.ID()); err != nil { - t.Fatalf("Failed to start client node: %v", err) - } - } - for _, server := range servers { - if err := net.Start(server.ID()); err != nil { - t.Fatalf("Failed to start server node: %v", err) - } - } - - return test(ctx, net, servers, clients) -} - -func newLesClientService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { - config := ethconfig.Defaults - config.SyncMode = downloader.LightSync - return New(stack, &config) -} - -func newLesServerService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { - config := ethconfig.Defaults - config.SyncMode = downloader.FullSync - config.LightServ = testServerCapacity - config.LightPeers = testMaxClients - ethereum, err := eth.New(stack, &config) - if err != nil { - return nil, err - } - _, err = NewLesServer(stack, ethereum, &config) - if err != nil { - return nil, err - } - return ethereum, nil -} diff --git a/les/benchmark.go b/les/benchmark.go deleted file mode 100644 index d1efa2f5d3..0000000000 --- a/les/benchmark.go +++ /dev/null @@ -1,351 +0,0 @@ -// Copyright 2019 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 les - -import ( - crand "crypto/rand" - "encoding/binary" - "errors" - "math/big" - "math/rand" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/les/flowcontrol" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" -) - -// requestBenchmark is an interface for different randomized request generators -type requestBenchmark interface { - // init initializes the generator for generating the given number of randomized requests - init(h *serverHandler, count int) error - // request initiates sending a single request to the given peer - request(peer *serverPeer, index int) error -} - -// benchmarkBlockHeaders implements requestBenchmark -type benchmarkBlockHeaders struct { - amount, skip int - reverse, byHash bool - offset, randMax int64 - hashes []common.Hash -} - -func (b *benchmarkBlockHeaders) init(h *serverHandler, count int) error { - d := int64(b.amount-1) * int64(b.skip+1) - b.offset = 0 - b.randMax = h.blockchain.CurrentHeader().Number.Int64() + 1 - d - if b.randMax < 0 { - return errors.New("chain is too short") - } - if b.reverse { - b.offset = d - } - if b.byHash { - b.hashes = make([]common.Hash, count) - for i := range b.hashes { - b.hashes[i] = rawdb.ReadCanonicalHash(h.chainDb, uint64(b.offset+rand.Int63n(b.randMax))) - } - } - return nil -} - -func (b *benchmarkBlockHeaders) request(peer *serverPeer, index int) error { - if b.byHash { - return peer.requestHeadersByHash(0, b.hashes[index], b.amount, b.skip, b.reverse) - } - return peer.requestHeadersByNumber(0, uint64(b.offset+rand.Int63n(b.randMax)), b.amount, b.skip, b.reverse) -} - -// benchmarkBodiesOrReceipts implements requestBenchmark -type benchmarkBodiesOrReceipts struct { - receipts bool - hashes []common.Hash -} - -func (b *benchmarkBodiesOrReceipts) init(h *serverHandler, count int) error { - randMax := h.blockchain.CurrentHeader().Number.Int64() + 1 - b.hashes = make([]common.Hash, count) - for i := range b.hashes { - b.hashes[i] = rawdb.ReadCanonicalHash(h.chainDb, uint64(rand.Int63n(randMax))) - } - return nil -} - -func (b *benchmarkBodiesOrReceipts) request(peer *serverPeer, index int) error { - if b.receipts { - return peer.requestReceipts(0, []common.Hash{b.hashes[index]}) - } - return peer.requestBodies(0, []common.Hash{b.hashes[index]}) -} - -// benchmarkProofsOrCode implements requestBenchmark -type benchmarkProofsOrCode struct { - code bool - headHash common.Hash -} - -func (b *benchmarkProofsOrCode) init(h *serverHandler, count int) error { - b.headHash = h.blockchain.CurrentHeader().Hash() - return nil -} - -func (b *benchmarkProofsOrCode) request(peer *serverPeer, index int) error { - key := make([]byte, 32) - crand.Read(key) - if b.code { - return peer.requestCode(0, []CodeReq{{BHash: b.headHash, AccountAddress: key}}) - } - return peer.requestProofs(0, []ProofReq{{BHash: b.headHash, Key: key}}) -} - -// benchmarkHelperTrie implements requestBenchmark -type benchmarkHelperTrie struct { - bloom bool - reqCount int - sectionCount, headNum uint64 -} - -func (b *benchmarkHelperTrie) init(h *serverHandler, count int) error { - if b.bloom { - b.sectionCount, b.headNum, _ = h.server.bloomTrieIndexer.Sections() - } else { - b.sectionCount, _, _ = h.server.chtIndexer.Sections() - b.headNum = b.sectionCount*params.CHTFrequency - 1 - } - if b.sectionCount == 0 { - return errors.New("no processed sections available") - } - return nil -} - -func (b *benchmarkHelperTrie) request(peer *serverPeer, index int) error { - reqs := make([]HelperTrieReq, b.reqCount) - - if b.bloom { - bitIdx := uint16(rand.Intn(2048)) - for i := range reqs { - key := make([]byte, 10) - binary.BigEndian.PutUint16(key[:2], bitIdx) - binary.BigEndian.PutUint64(key[2:], uint64(rand.Int63n(int64(b.sectionCount)))) - reqs[i] = HelperTrieReq{Type: htBloomBits, TrieIdx: b.sectionCount - 1, Key: key} - } - } else { - for i := range reqs { - key := make([]byte, 8) - binary.BigEndian.PutUint64(key[:], uint64(rand.Int63n(int64(b.headNum)))) - reqs[i] = HelperTrieReq{Type: htCanonical, TrieIdx: b.sectionCount - 1, Key: key, AuxReq: htAuxHeader} - } - } - - return peer.requestHelperTrieProofs(0, reqs) -} - -// benchmarkTxSend implements requestBenchmark -type benchmarkTxSend struct { - txs types.Transactions -} - -func (b *benchmarkTxSend) init(h *serverHandler, count int) error { - key, _ := crypto.GenerateKey() - addr := crypto.PubkeyToAddress(key.PublicKey) - signer := types.LatestSigner(h.server.chainConfig) - b.txs = make(types.Transactions, count) - - for i := range b.txs { - data := make([]byte, txSizeCostLimit) - crand.Read(data) - tx, err := types.SignTx(types.NewTransaction(0, addr, new(big.Int), 0, new(big.Int), data), signer, key) - if err != nil { - panic(err) - } - b.txs[i] = tx - } - return nil -} - -func (b *benchmarkTxSend) request(peer *serverPeer, index int) error { - enc, _ := rlp.EncodeToBytes(types.Transactions{b.txs[index]}) - return peer.sendTxs(0, 1, enc) -} - -// benchmarkTxStatus implements requestBenchmark -type benchmarkTxStatus struct{} - -func (b *benchmarkTxStatus) init(h *serverHandler, count int) error { - return nil -} - -func (b *benchmarkTxStatus) request(peer *serverPeer, index int) error { - var hash common.Hash - crand.Read(hash[:]) - return peer.requestTxStatus(0, []common.Hash{hash}) -} - -// benchmarkSetup stores measurement data for a single benchmark type -type benchmarkSetup struct { - req requestBenchmark - totalCount int - totalTime, avgTime time.Duration - maxInSize, maxOutSize uint32 - err error -} - -// runBenchmark runs a benchmark cycle for all benchmark types in the specified -// number of passes -func (h *serverHandler) runBenchmark(benchmarks []requestBenchmark, passCount int, targetTime time.Duration) []*benchmarkSetup { - setup := make([]*benchmarkSetup, len(benchmarks)) - for i, b := range benchmarks { - setup[i] = &benchmarkSetup{req: b} - } - for i := 0; i < passCount; i++ { - log.Info("Running benchmark", "pass", i+1, "total", passCount) - todo := make([]*benchmarkSetup, len(benchmarks)) - copy(todo, setup) - for len(todo) > 0 { - // select a random element - index := rand.Intn(len(todo)) - next := todo[index] - todo[index] = todo[len(todo)-1] - todo = todo[:len(todo)-1] - - if next.err == nil { - // calculate request count - count := 50 - if next.totalTime > 0 { - count = int(uint64(next.totalCount) * uint64(targetTime) / uint64(next.totalTime)) - } - if err := h.measure(next, count); err != nil { - next.err = err - } - } - } - } - log.Info("Benchmark completed") - - for _, s := range setup { - if s.err == nil { - s.avgTime = s.totalTime / time.Duration(s.totalCount) - } - } - return setup -} - -// meteredPipe implements p2p.MsgReadWriter and remembers the largest single -// message size sent through the pipe -type meteredPipe struct { - rw p2p.MsgReadWriter - maxSize uint32 -} - -func (m *meteredPipe) ReadMsg() (p2p.Msg, error) { - return m.rw.ReadMsg() -} - -func (m *meteredPipe) WriteMsg(msg p2p.Msg) error { - if msg.Size > m.maxSize { - m.maxSize = msg.Size - } - return m.rw.WriteMsg(msg) -} - -// measure runs a benchmark for a single type in a single pass, with the given -// number of requests -func (h *serverHandler) measure(setup *benchmarkSetup, count int) error { - clientPipe, serverPipe := p2p.MsgPipe() - clientMeteredPipe := &meteredPipe{rw: clientPipe} - serverMeteredPipe := &meteredPipe{rw: serverPipe} - var id enode.ID - crand.Read(id[:]) - - peer1 := newServerPeer(lpv2, NetworkId, false, p2p.NewPeer(id, "client", nil), clientMeteredPipe) - peer2 := newClientPeer(lpv2, NetworkId, p2p.NewPeer(id, "server", nil), serverMeteredPipe) - peer2.announceType = announceTypeNone - peer2.fcCosts = make(requestCostTable) - c := &requestCosts{} - for code := range requests { - peer2.fcCosts[code] = c - } - peer2.fcParams = flowcontrol.ServerParams{BufLimit: 1, MinRecharge: 1} - peer2.fcClient = flowcontrol.NewClientNode(h.server.fcManager, peer2.fcParams) - defer peer2.fcClient.Disconnect() - - if err := setup.req.init(h, count); err != nil { - return err - } - - errCh := make(chan error, 10) - start := mclock.Now() - - go func() { - for i := 0; i < count; i++ { - if err := setup.req.request(peer1, i); err != nil { - errCh <- err - return - } - } - }() - go func() { - for i := 0; i < count; i++ { - if err := h.handleMsg(peer2, &sync.WaitGroup{}); err != nil { - errCh <- err - return - } - } - }() - go func() { - for i := 0; i < count; i++ { - msg, err := clientPipe.ReadMsg() - if err != nil { - errCh <- err - return - } - var i interface{} - msg.Decode(&i) - } - // at this point we can be sure that the other two - // goroutines finished successfully too - close(errCh) - }() - select { - case err := <-errCh: - if err != nil { - return err - } - case <-h.closeCh: - clientPipe.Close() - serverPipe.Close() - return errors.New("benchmark cancelled") - } - - setup.totalTime += time.Duration(mclock.Now() - start) - setup.totalCount += count - setup.maxInSize = clientMeteredPipe.maxSize - setup.maxOutSize = serverMeteredPipe.maxSize - clientPipe.Close() - serverPipe.Close() - return nil -} diff --git a/les/bloombits.go b/les/bloombits.go deleted file mode 100644 index a98524ce2e..0000000000 --- a/les/bloombits.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2017 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 les - -import ( - "time" - - "github.com/ethereum/go-ethereum/common/bitutil" - "github.com/ethereum/go-ethereum/light" -) - -const ( - // bloomServiceThreads is the number of goroutines used globally by an Ethereum - // instance to service bloombits lookups for all running filters. - bloomServiceThreads = 16 - - // bloomFilterThreads is the number of goroutines used locally per filter to - // multiplex requests onto the global servicing goroutines. - bloomFilterThreads = 3 - - // bloomRetrievalBatch is the maximum number of bloom bit retrievals to service - // in a single batch. - bloomRetrievalBatch = 16 - - // bloomRetrievalWait is the maximum time to wait for enough bloom bit requests - // to accumulate request an entire batch (avoiding hysteresis). - bloomRetrievalWait = time.Microsecond * 100 -) - -// startBloomHandlers starts a batch of goroutines to accept bloom bit database -// retrievals from possibly a range of filters and serving the data to satisfy. -func (eth *LightEthereum) startBloomHandlers(sectionSize uint64) { - for i := 0; i < bloomServiceThreads; i++ { - go func() { - defer eth.wg.Done() - for { - select { - case <-eth.closeCh: - return - - case request := <-eth.bloomRequests: - task := <-request - task.Bitsets = make([][]byte, len(task.Sections)) - compVectors, err := light.GetBloomBits(task.Context, eth.odr, task.Bit, task.Sections) - if err == nil { - for i := range task.Sections { - if blob, err := bitutil.DecompressBytes(compVectors[i], int(sectionSize/8)); err == nil { - task.Bitsets[i] = blob - } else { - task.Error = err - } - } - } else { - task.Error = err - } - request <- task - } - } - }() - } -} diff --git a/les/client.go b/les/client.go deleted file mode 100644 index be5e9fd564..0000000000 --- a/les/client.go +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright 2019 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 les implements the Light Ethereum Subprotocol. -package les - -import ( - "errors" - "strings" - "time" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/bloombits" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/eth/gasprice" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/internal/shutdowncheck" - "github.com/ethereum/go-ethereum/les/vflux" - vfc "github.com/ethereum/go-ethereum/les/vflux/client" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/trie" -) - -type LightEthereum struct { - lesCommons - - peers *serverPeerSet - reqDist *requestDistributor - retriever *retrieveManager - odr *LesOdr - relay *lesTxRelay - handler *clientHandler - txPool *light.TxPool - blockchain *light.LightChain - serverPool *vfc.ServerPool - serverPoolIterator enode.Iterator - merger *consensus.Merger - - bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests - bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports - - ApiBackend *LesApiBackend - eventMux *event.TypeMux - engine consensus.Engine - accountManager *accounts.Manager - netRPCService *ethapi.NetAPI - - p2pServer *p2p.Server - p2pConfig *p2p.Config - udpEnabled bool - - shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully -} - -// New creates an instance of the light client. -func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) { - chainDb, err := stack.OpenDatabase("lightchaindata", config.DatabaseCache, config.DatabaseHandles, "eth/db/chaindata/", false) - if err != nil { - return nil, err - } - lesDb, err := stack.OpenDatabase("les.client", 0, 0, "eth/db/lesclient/", false) - if err != nil { - return nil, err - } - var overrides core.ChainOverrides - if config.OverrideCancun != nil { - overrides.OverrideCancun = config.OverrideCancun - } - if config.OverrideVerkle != nil { - overrides.OverrideVerkle = config.OverrideVerkle - } - triedb := trie.NewDatabase(chainDb, trie.HashDefaults) - chainConfig, genesisHash, genesisErr := core.SetupGenesisBlockWithOverride(chainDb, triedb, config.Genesis, &overrides) - if _, isCompat := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !isCompat { - return nil, genesisErr - } - engine, err := ethconfig.CreateConsensusEngine(chainConfig, chainDb) - if err != nil { - return nil, err - } - log.Info("") - log.Info(strings.Repeat("-", 153)) - for _, line := range strings.Split(chainConfig.Description(), "\n") { - log.Info(line) - } - log.Info(strings.Repeat("-", 153)) - log.Info("") - - peers := newServerPeerSet() - merger := consensus.NewMerger(chainDb) - leth := &LightEthereum{ - lesCommons: lesCommons{ - genesis: genesisHash, - config: config, - chainConfig: chainConfig, - iConfig: light.DefaultClientIndexerConfig, - chainDb: chainDb, - lesDb: lesDb, - closeCh: make(chan struct{}), - }, - peers: peers, - eventMux: stack.EventMux(), - reqDist: newRequestDistributor(peers, &mclock.System{}), - accountManager: stack.AccountManager(), - merger: merger, - engine: engine, - bloomRequests: make(chan chan *bloombits.Retrieval), - bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocksClient, params.HelperTrieConfirmations), - p2pServer: stack.Server(), - p2pConfig: &stack.Config().P2P, - udpEnabled: stack.Config().P2P.DiscoveryV5, - shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb), - } - - var prenegQuery vfc.QueryFunc - if leth.udpEnabled { - prenegQuery = leth.prenegQuery - } - leth.serverPool, leth.serverPoolIterator = vfc.NewServerPool(lesDb, []byte("serverpool:"), time.Second, prenegQuery, &mclock.System{}, nil, requestList) - leth.serverPool.AddMetrics(suggestedTimeoutGauge, totalValueGauge, serverSelectableGauge, serverConnectedGauge, sessionValueMeter, serverDialedMeter) - - leth.retriever = newRetrieveManager(peers, leth.reqDist, leth.serverPool.GetTimeout) - leth.relay = newLesTxRelay(peers, leth.retriever) - - leth.odr = NewLesOdr(chainDb, light.DefaultClientIndexerConfig, leth.peers, leth.retriever) - leth.chtIndexer = light.NewChtIndexer(chainDb, leth.odr, params.CHTFrequency, params.HelperTrieConfirmations, config.LightNoPrune) - leth.bloomTrieIndexer = light.NewBloomTrieIndexer(chainDb, leth.odr, params.BloomBitsBlocksClient, params.BloomTrieFrequency, config.LightNoPrune) - leth.odr.SetIndexers(leth.chtIndexer, leth.bloomTrieIndexer, leth.bloomIndexer) - - // Note: NewLightChain adds the trusted checkpoint so it needs an ODR with - // indexers already set but not started yet - if leth.blockchain, err = light.NewLightChain(leth.odr, leth.chainConfig, leth.engine); err != nil { - return nil, err - } - leth.chainReader = leth.blockchain - leth.txPool = light.NewTxPool(leth.chainConfig, leth.blockchain, leth.relay) - - // Note: AddChildIndexer starts the update process for the child - leth.bloomIndexer.AddChildIndexer(leth.bloomTrieIndexer) - leth.chtIndexer.Start(leth.blockchain) - leth.bloomIndexer.Start(leth.blockchain) - - // Rewind the chain in case of an incompatible config upgrade. - if compat, ok := genesisErr.(*params.ConfigCompatError); ok { - log.Warn("Rewinding chain to upgrade configuration", "err", compat) - if compat.RewindToTime > 0 { - leth.blockchain.SetHeadWithTimestamp(compat.RewindToTime) - } else { - leth.blockchain.SetHead(compat.RewindToBlock) - } - rawdb.WriteChainConfig(chainDb, genesisHash, chainConfig) - } - - leth.ApiBackend = &LesApiBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, leth, nil} - gpoParams := config.GPO - if gpoParams.Default == nil { - gpoParams.Default = config.Miner.GasPrice - } - leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) - - leth.handler = newClientHandler(leth) - leth.netRPCService = ethapi.NewNetAPI(leth.p2pServer, leth.config.NetworkId) - - // Register the backend on the node - stack.RegisterAPIs(leth.APIs()) - stack.RegisterProtocols(leth.Protocols()) - stack.RegisterLifecycle(leth) - - // Successful startup; push a marker and check previous unclean shutdowns. - leth.shutdownTracker.MarkStartup() - - return leth, nil -} - -// VfluxRequest sends a batch of requests to the given node through discv5 UDP TalkRequest and returns the responses -func (s *LightEthereum) VfluxRequest(n *enode.Node, reqs vflux.Requests) vflux.Replies { - if !s.udpEnabled { - return nil - } - reqsEnc, _ := rlp.EncodeToBytes(&reqs) - repliesEnc, _ := s.p2pServer.DiscV5.TalkRequest(s.serverPool.DialNode(n), "vfx", reqsEnc) - var replies vflux.Replies - if len(repliesEnc) == 0 || rlp.DecodeBytes(repliesEnc, &replies) != nil { - return nil - } - return replies -} - -// vfxVersion returns the version number of the "les" service subdomain of the vflux UDP -// service, as advertised in the ENR record -func (s *LightEthereum) vfxVersion(n *enode.Node) uint { - if n.Seq() == 0 { - var err error - if !s.udpEnabled { - return 0 - } - if n, err = s.p2pServer.DiscV5.RequestENR(n); n != nil && err == nil && n.Seq() != 0 { - s.serverPool.Persist(n) - } else { - return 0 - } - } - - var les []rlp.RawValue - if err := n.Load(enr.WithEntry("les", &les)); err != nil || len(les) < 1 { - return 0 - } - var version uint - rlp.DecodeBytes(les[0], &version) // Ignore additional fields (for forward compatibility). - return version -} - -// prenegQuery sends a capacity query to the given server node to determine whether -// a connection slot is immediately available -func (s *LightEthereum) prenegQuery(n *enode.Node) int { - if s.vfxVersion(n) < 1 { - // UDP query not supported, always try TCP connection - return 1 - } - - var requests vflux.Requests - requests.Add("les", vflux.CapacityQueryName, vflux.CapacityQueryReq{ - Bias: 180, - AddTokens: []vflux.IntOrInf{{}}, - }) - replies := s.VfluxRequest(n, requests) - var cqr vflux.CapacityQueryReply - if replies.Get(0, &cqr) != nil || len(cqr) != 1 { // Note: Get returns an error if replies is nil - return -1 - } - if cqr[0] > 0 { - return 1 - } - return 0 -} - -type LightDummyAPI struct{} - -// Etherbase is the address that mining rewards will be sent to -func (s *LightDummyAPI) Etherbase() (common.Address, error) { - return common.Address{}, errors.New("mining is not supported in light mode") -} - -// Coinbase is the address that mining rewards will be sent to (alias for Etherbase) -func (s *LightDummyAPI) Coinbase() (common.Address, error) { - return common.Address{}, errors.New("mining is not supported in light mode") -} - -// Hashrate returns the POW hashrate -func (s *LightDummyAPI) Hashrate() hexutil.Uint { - return 0 -} - -// Mining returns an indication if this node is currently mining. -func (s *LightDummyAPI) Mining() bool { - return false -} - -// APIs returns the collection of RPC services the ethereum package offers. -// NOTE, some of these services probably need to be moved to somewhere else. -func (s *LightEthereum) APIs() []rpc.API { - apis := ethapi.GetAPIs(s.ApiBackend) - apis = append(apis, s.engine.APIs(s.BlockChain().HeaderChain())...) - return append(apis, []rpc.API{ - { - Namespace: "eth", - Service: &LightDummyAPI{}, - }, { - Namespace: "net", - Service: s.netRPCService, - }, { - Namespace: "vflux", - Service: s.serverPool.API(), - }, - }...) -} - -func (s *LightEthereum) ResetWithGenesisBlock(gb *types.Block) { - s.blockchain.ResetWithGenesisBlock(gb) -} - -func (s *LightEthereum) BlockChain() *light.LightChain { return s.blockchain } -func (s *LightEthereum) TxPool() *light.TxPool { return s.txPool } -func (s *LightEthereum) Engine() consensus.Engine { return s.engine } -func (s *LightEthereum) LesVersion() int { return int(ClientProtocolVersions[0]) } -func (s *LightEthereum) EventMux() *event.TypeMux { return s.eventMux } -func (s *LightEthereum) Merger() *consensus.Merger { return s.merger } - -// Protocols returns all the currently configured network protocols to start. -func (s *LightEthereum) Protocols() []p2p.Protocol { - return s.makeProtocols(ClientProtocolVersions, s.handler.runPeer, func(id enode.ID) interface{} { - if p := s.peers.peer(id.String()); p != nil { - return p.Info() - } - return nil - }, s.serverPoolIterator) -} - -// Start implements node.Lifecycle, starting all internal goroutines needed by the -// light ethereum protocol implementation. -func (s *LightEthereum) Start() error { - log.Warn("Light client mode is an experimental feature") - - // Regularly update shutdown marker - s.shutdownTracker.Start() - - if s.udpEnabled && s.p2pServer.DiscV5 == nil { - s.udpEnabled = false - log.Error("Discovery v5 is not initialized") - } - discovery, err := s.setupDiscovery() - if err != nil { - return err - } - s.serverPool.AddSource(discovery) - s.serverPool.Start() - // Start bloom request workers. - s.wg.Add(bloomServiceThreads) - s.startBloomHandlers(params.BloomBitsBlocksClient) - - return nil -} - -// Stop implements node.Lifecycle, terminating all internal goroutines used by the -// Ethereum protocol. -func (s *LightEthereum) Stop() error { - close(s.closeCh) - s.serverPool.Stop() - s.peers.close() - s.reqDist.close() - s.odr.Stop() - s.relay.Stop() - s.bloomIndexer.Close() - s.chtIndexer.Close() - s.blockchain.Stop() - s.handler.stop() - s.txPool.Stop() - s.engine.Close() - s.eventMux.Stop() - // Clean shutdown marker as the last thing before closing db - s.shutdownTracker.Stop() - - s.chainDb.Close() - s.lesDb.Close() - s.wg.Wait() - log.Info("Light ethereum stopped") - return nil -} diff --git a/les/client_handler.go b/les/client_handler.go deleted file mode 100644 index 50f6dce879..0000000000 --- a/les/client_handler.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2019 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 les - -import ( - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -// clientHandler is responsible for receiving and processing all incoming server -// responses. -type clientHandler struct { - forkFilter forkid.Filter - backend *LightEthereum - - closeCh chan struct{} - wg sync.WaitGroup // WaitGroup used to track all connected peers. -} - -func newClientHandler(backend *LightEthereum) *clientHandler { - handler := &clientHandler{ - forkFilter: forkid.NewFilter(backend.blockchain), - backend: backend, - closeCh: make(chan struct{}), - } - return handler -} - -func (h *clientHandler) stop() { - close(h.closeCh) - h.wg.Wait() -} - -// runPeer is the p2p protocol run function for the given version. -func (h *clientHandler) runPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := newServerPeer(int(version), h.backend.config.NetworkId, false, p, newMeteredMsgWriter(rw, int(version))) - defer peer.close() - h.wg.Add(1) - defer h.wg.Done() - err := h.handle(peer, false) - return err -} - -func (h *clientHandler) handle(p *serverPeer, noInitAnnounce bool) error { - if h.backend.peers.len() >= h.backend.config.LightPeers && !p.Peer.Info().Network.Trusted { - return p2p.DiscTooManyPeers - } - p.Log().Debug("Light Ethereum peer connected", "name", p.Name()) - - // Execute the LES handshake - forkid := forkid.NewID(h.backend.blockchain.Config(), h.backend.BlockChain().Genesis(), h.backend.blockchain.CurrentHeader().Number.Uint64(), h.backend.blockchain.CurrentHeader().Time) - if err := p.Handshake(h.backend.blockchain.Genesis().Hash(), forkid, h.forkFilter); err != nil { - p.Log().Debug("Light Ethereum handshake failed", "err", err) - return err - } - // Register peer with the server pool - if h.backend.serverPool != nil { - if nvt, err := h.backend.serverPool.RegisterNode(p.Node()); err == nil { - p.setValueTracker(nvt) - p.updateVtParams() - defer func() { - p.setValueTracker(nil) - h.backend.serverPool.UnregisterNode(p.Node()) - }() - } else { - return err - } - } - // Register the peer locally - if err := h.backend.peers.register(p); err != nil { - p.Log().Error("Light Ethereum peer registration failed", "err", err) - return err - } - - serverConnectionGauge.Update(int64(h.backend.peers.len())) - - connectedAt := mclock.Now() - defer func() { - h.backend.peers.unregister(p.id) - connectionTimer.Update(time.Duration(mclock.Now() - connectedAt)) - serverConnectionGauge.Update(int64(h.backend.peers.len())) - }() - - // Mark the peer starts to be served. - p.serving.Store(true) - defer p.serving.Store(false) - - // Spawn a main loop to handle all incoming messages. - for { - if err := h.handleMsg(p); err != nil { - p.Log().Debug("Light Ethereum message handling failed", "err", err) - p.fcServer.DumpLogs() - return err - } - } -} - -// handleMsg is invoked whenever an inbound message is received from a remote -// peer. The remote connection is torn down upon returning any error. -func (h *clientHandler) handleMsg(p *serverPeer) error { - // Read the next message from the remote peer, and ensure it's fully consumed - msg, err := p.rw.ReadMsg() - if err != nil { - return err - } - p.Log().Trace("Light Ethereum message arrived", "code", msg.Code, "bytes", msg.Size) - - if msg.Size > ProtocolMaxMsgSize { - return errResp(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize) - } - defer msg.Discard() - - var deliverMsg *Msg - - // Handle the message depending on its contents - switch { - case msg.Code == AnnounceMsg: - p.Log().Trace("Received announce message") - var req announceData - if err := msg.Decode(&req); err != nil { - return errResp(ErrDecode, "%v: %v", msg, err) - } - if err := req.sanityCheck(); err != nil { - return err - } - update, size := req.Update.decode() - if p.rejectUpdate(size) { - return errResp(ErrRequestRejected, "") - } - p.updateFlowControl(update) - p.updateVtParams() - - if req.Hash != (common.Hash{}) { - if p.announceType == announceTypeNone { - return errResp(ErrUnexpectedResponse, "") - } - if p.announceType == announceTypeSigned { - if err := req.checkSignature(p.ID(), update); err != nil { - p.Log().Trace("Invalid announcement signature", "err", err) - return err - } - p.Log().Trace("Valid announcement signature") - } - p.Log().Trace("Announce message content", "number", req.Number, "hash", req.Hash, "td", req.Td, "reorg", req.ReorgDepth) - - // Update peer head information first and then notify the announcement - p.updateHead(req.Hash, req.Number, req.Td) - } - case msg.Code == BlockHeadersMsg: - p.Log().Trace("Received block header response message") - var resp struct { - ReqID, BV uint64 - Headers []*types.Header - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - - deliverMsg = &Msg{ - MsgType: MsgBlockHeaders, - ReqID: resp.ReqID, - Obj: resp.Headers, - } - case msg.Code == BlockBodiesMsg: - p.Log().Trace("Received block bodies response") - var resp struct { - ReqID, BV uint64 - Data []*types.Body - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgBlockBodies, - ReqID: resp.ReqID, - Obj: resp.Data, - } - case msg.Code == CodeMsg: - p.Log().Trace("Received code response") - var resp struct { - ReqID, BV uint64 - Data [][]byte - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgCode, - ReqID: resp.ReqID, - Obj: resp.Data, - } - case msg.Code == ReceiptsMsg: - p.Log().Trace("Received receipts response") - var resp struct { - ReqID, BV uint64 - Receipts []types.Receipts - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgReceipts, - ReqID: resp.ReqID, - Obj: resp.Receipts, - } - case msg.Code == ProofsV2Msg: - p.Log().Trace("Received les/2 proofs response") - var resp struct { - ReqID, BV uint64 - Data trienode.ProofList - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgProofsV2, - ReqID: resp.ReqID, - Obj: resp.Data, - } - case msg.Code == HelperTrieProofsMsg: - p.Log().Trace("Received helper trie proof response") - var resp struct { - ReqID, BV uint64 - Data HelperTrieResps - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgHelperTrieProofs, - ReqID: resp.ReqID, - Obj: resp.Data, - } - case msg.Code == TxStatusMsg: - p.Log().Trace("Received tx status response") - var resp struct { - ReqID, BV uint64 - Status []light.TxStatus - } - if err := msg.Decode(&resp); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ReceivedReply(resp.ReqID, resp.BV) - p.answeredRequest(resp.ReqID) - deliverMsg = &Msg{ - MsgType: MsgTxStatus, - ReqID: resp.ReqID, - Obj: resp.Status, - } - case msg.Code == StopMsg && p.version >= lpv3: - p.freeze() - h.backend.retriever.frozen(p) - p.Log().Debug("Service stopped") - case msg.Code == ResumeMsg && p.version >= lpv3: - var bv uint64 - if err := msg.Decode(&bv); err != nil { - return errResp(ErrDecode, "msg %v: %v", msg, err) - } - p.fcServer.ResumeFreeze(bv) - p.unfreeze() - p.Log().Debug("Service resumed") - default: - p.Log().Trace("Received invalid message", "code", msg.Code) - return errResp(ErrInvalidMsgCode, "%v", msg.Code) - } - // Deliver the received response to retriever. - if deliverMsg != nil { - if err := h.backend.retriever.deliver(p, deliverMsg); err != nil { - if val := p.errCount.Add(1, mclock.Now()); val > maxResponseErrors { - return err - } - } - } - return nil -} diff --git a/les/commons.go b/les/commons.go deleted file mode 100644 index cb3fc430b7..0000000000 --- a/les/commons.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2018 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 les - -import ( - "fmt" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" -) - -func errResp(code errCode, format string, v ...interface{}) error { - return fmt.Errorf("%v - %v", code, fmt.Sprintf(format, v...)) -} - -type chainReader interface { - CurrentHeader() *types.Header -} - -// lesCommons contains fields needed by both server and client. -type lesCommons struct { - genesis common.Hash - config *ethconfig.Config - chainConfig *params.ChainConfig - iConfig *light.IndexerConfig - chainDb, lesDb ethdb.Database - chainReader chainReader - chtIndexer, bloomTrieIndexer *core.ChainIndexer - - closeCh chan struct{} - wg sync.WaitGroup -} - -// NodeInfo represents a short summary of the Ethereum sub-protocol metadata -// known about the host peer. -type NodeInfo struct { - Network uint64 `json:"network"` // Ethereum network ID (1=Mainnet, Goerli=5) - Difficulty *big.Int `json:"difficulty"` // Total difficulty of the host's blockchain - Genesis common.Hash `json:"genesis"` // SHA3 hash of the host's genesis block - Config *params.ChainConfig `json:"config"` // Chain configuration for the fork rules - Head common.Hash `json:"head"` // SHA3 hash of the host's best owned block -} - -// makeProtocols creates protocol descriptors for the given LES versions. -func (c *lesCommons) makeProtocols(versions []uint, runPeer func(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) error, peerInfo func(id enode.ID) interface{}, dialCandidates enode.Iterator) []p2p.Protocol { - protos := make([]p2p.Protocol, len(versions)) - for i, version := range versions { - version := version - protos[i] = p2p.Protocol{ - Name: "les", - Version: version, - Length: ProtocolLengths[version], - NodeInfo: c.nodeInfo, - Run: func(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - return runPeer(version, peer, rw) - }, - PeerInfo: peerInfo, - DialCandidates: dialCandidates, - } - } - return protos -} - -// nodeInfo retrieves some protocol metadata about the running host node. -func (c *lesCommons) nodeInfo() interface{} { - head := c.chainReader.CurrentHeader() - hash := head.Hash() - return &NodeInfo{ - Network: c.config.NetworkId, - Difficulty: rawdb.ReadTd(c.chainDb, hash, head.Number.Uint64()), - Genesis: c.genesis, - Config: c.chainConfig, - Head: hash, - } -} diff --git a/les/costtracker.go b/les/costtracker.go deleted file mode 100644 index 695d54e141..0000000000 --- a/les/costtracker.go +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright 2019 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 les - -import ( - "encoding/binary" - "math" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/flowcontrol" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" -) - -const makeCostStats = false // make request cost statistics during operation - -var ( - // average request cost estimates based on serving time - reqAvgTimeCost = requestCostTable{ - GetBlockHeadersMsg: {150000, 30000}, - GetBlockBodiesMsg: {0, 700000}, - GetReceiptsMsg: {0, 1000000}, - GetCodeMsg: {0, 450000}, - GetProofsV2Msg: {0, 600000}, - GetHelperTrieProofsMsg: {0, 1000000}, - SendTxV2Msg: {0, 450000}, - GetTxStatusMsg: {0, 250000}, - } - // maximum incoming message size estimates - reqMaxInSize = requestCostTable{ - GetBlockHeadersMsg: {40, 0}, - GetBlockBodiesMsg: {0, 40}, - GetReceiptsMsg: {0, 40}, - GetCodeMsg: {0, 80}, - GetProofsV2Msg: {0, 80}, - GetHelperTrieProofsMsg: {0, 20}, - SendTxV2Msg: {0, 16500}, - GetTxStatusMsg: {0, 50}, - } - // maximum outgoing message size estimates - reqMaxOutSize = requestCostTable{ - GetBlockHeadersMsg: {0, 556}, - GetBlockBodiesMsg: {0, 100000}, - GetReceiptsMsg: {0, 200000}, - GetCodeMsg: {0, 50000}, - GetProofsV2Msg: {0, 4000}, - GetHelperTrieProofsMsg: {0, 4000}, - SendTxV2Msg: {0, 100}, - GetTxStatusMsg: {0, 100}, - } - // request amounts that have to fit into the minimum buffer size minBufferMultiplier times - minBufferReqAmount = map[uint64]uint64{ - GetBlockHeadersMsg: 192, - GetBlockBodiesMsg: 1, - GetReceiptsMsg: 1, - GetCodeMsg: 1, - GetProofsV2Msg: 1, - GetHelperTrieProofsMsg: 16, - SendTxV2Msg: 8, - GetTxStatusMsg: 64, - } - minBufferMultiplier = 3 -) - -const ( - maxCostFactor = 2 // ratio of maximum and average cost estimates - bufLimitRatio = 6000 // fixed bufLimit/MRR ratio - gfUsageThreshold = 0.5 - gfUsageTC = time.Second - gfRaiseTC = time.Second * 200 - gfDropTC = time.Second * 50 - gfDbKey = "_globalCostFactorV6" -) - -// costTracker is responsible for calculating costs and cost estimates on the -// server side. It continuously updates the global cost factor which is defined -// as the number of cost units per nanosecond of serving time in a single thread. -// It is based on statistics collected during serving requests in high-load periods -// and practically acts as a one-dimension request price scaling factor over the -// pre-defined cost estimate table. -// -// The reason for dynamically maintaining the global factor on the server side is: -// the estimated time cost of the request is fixed(hardcoded) but the configuration -// of the machine running the server is really different. Therefore, the request serving -// time in different machine will vary greatly. And also, the request serving time -// in same machine may vary greatly with different request pressure. -// -// In order to more effectively limit resources, we apply the global factor to serving -// time to make the result as close as possible to the estimated time cost no matter -// the server is slow or fast. And also we scale the totalRecharge with global factor -// so that fast server can serve more requests than estimation and slow server can -// reduce request pressure. -// -// Instead of scaling the cost values, the real value of cost units is changed by -// applying the factor to the serving times. This is more convenient because the -// changes in the cost factor can be applied immediately without always notifying -// the clients about the changed cost tables. -type costTracker struct { - db ethdb.Database - stopCh chan chan struct{} - - inSizeFactor float64 - outSizeFactor float64 - factor float64 - utilTarget float64 - minBufLimit uint64 - - gfLock sync.RWMutex - reqInfoCh chan reqInfo - totalRechargeCh chan uint64 - - stats map[uint64][]atomic.Uint64 // Used for testing purpose. - - // TestHooks - testing bool // Disable real cost evaluation for testing purpose. - testCostList RequestCostList // Customized cost table for testing purpose. -} - -// newCostTracker creates a cost tracker and loads the cost factor statistics from the database. -// It also returns the minimum capacity that can be assigned to any peer. -func newCostTracker(db ethdb.Database, config *ethconfig.Config) (*costTracker, uint64) { - utilTarget := float64(config.LightServ) * flowcontrol.FixedPointMultiplier / 100 - ct := &costTracker{ - db: db, - stopCh: make(chan chan struct{}), - reqInfoCh: make(chan reqInfo, 100), - utilTarget: utilTarget, - } - if config.LightIngress > 0 { - ct.inSizeFactor = utilTarget / float64(config.LightIngress) - } - if config.LightEgress > 0 { - ct.outSizeFactor = utilTarget / float64(config.LightEgress) - } - if makeCostStats { - ct.stats = make(map[uint64][]atomic.Uint64) - for code := range reqAvgTimeCost { - ct.stats[code] = make([]atomic.Uint64, 10) - } - } - ct.gfLoop() - costList := ct.makeCostList(ct.globalFactor() * 1.25) - for _, c := range costList { - amount := minBufferReqAmount[c.MsgCode] - cost := c.BaseCost + amount*c.ReqCost - if cost > ct.minBufLimit { - ct.minBufLimit = cost - } - } - ct.minBufLimit *= uint64(minBufferMultiplier) - return ct, (ct.minBufLimit-1)/bufLimitRatio + 1 -} - -// stop stops the cost tracker and saves the cost factor statistics to the database -func (ct *costTracker) stop() { - stopCh := make(chan struct{}) - ct.stopCh <- stopCh - <-stopCh - if makeCostStats { - ct.printStats() - } -} - -// makeCostList returns upper cost estimates based on the hardcoded cost estimate -// tables and the optionally specified incoming/outgoing bandwidth limits -func (ct *costTracker) makeCostList(globalFactor float64) RequestCostList { - maxCost := func(avgTimeCost, inSize, outSize uint64) uint64 { - cost := avgTimeCost * maxCostFactor - inSizeCost := uint64(float64(inSize) * ct.inSizeFactor * globalFactor) - if inSizeCost > cost { - cost = inSizeCost - } - outSizeCost := uint64(float64(outSize) * ct.outSizeFactor * globalFactor) - if outSizeCost > cost { - cost = outSizeCost - } - return cost - } - var list RequestCostList - for code, data := range reqAvgTimeCost { - baseCost := maxCost(data.baseCost, reqMaxInSize[code].baseCost, reqMaxOutSize[code].baseCost) - reqCost := maxCost(data.reqCost, reqMaxInSize[code].reqCost, reqMaxOutSize[code].reqCost) - if ct.minBufLimit != 0 { - // if minBufLimit is set then always enforce maximum request cost <= minBufLimit - maxCost := baseCost + reqCost*minBufferReqAmount[code] - if maxCost > ct.minBufLimit { - mul := 0.999 * float64(ct.minBufLimit) / float64(maxCost) - baseCost = uint64(float64(baseCost) * mul) - reqCost = uint64(float64(reqCost) * mul) - } - } - - list = append(list, requestCostListItem{ - MsgCode: code, - BaseCost: baseCost, - ReqCost: reqCost, - }) - } - return list -} - -// reqInfo contains the estimated time cost and the actual request serving time -// which acts as a feed source to update factor maintained by costTracker. -type reqInfo struct { - // avgTimeCost is the estimated time cost corresponding to maxCostTable. - avgTimeCost float64 - - // servingTime is the CPU time corresponding to the actual processing of - // the request. - servingTime float64 - - // msgCode indicates the type of request. - msgCode uint64 -} - -// gfLoop starts an event loop which updates the global cost factor which is -// calculated as a weighted average of the average estimate / serving time ratio. -// The applied weight equals the serving time if gfUsage is over a threshold, -// zero otherwise. gfUsage is the recent average serving time per time unit in -// an exponential moving window. This ensures that statistics are collected only -// under high-load circumstances where the measured serving times are relevant. -// The total recharge parameter of the flow control system which controls the -// total allowed serving time per second but nominated in cost units, should -// also be scaled with the cost factor and is also updated by this loop. -func (ct *costTracker) gfLoop() { - var ( - factor, totalRecharge float64 - gfLog, recentTime, recentAvg float64 - - lastUpdate, expUpdate = mclock.Now(), mclock.Now() - ) - - // Load historical cost factor statistics from the database. - data, _ := ct.db.Get([]byte(gfDbKey)) - if len(data) == 8 { - gfLog = math.Float64frombits(binary.BigEndian.Uint64(data[:])) - } - ct.factor = math.Exp(gfLog) - factor, totalRecharge = ct.factor, ct.utilTarget*ct.factor - - // In order to perform factor data statistics under the high request pressure, - // we only adjust factor when recent factor usage beyond the threshold. - threshold := gfUsageThreshold * float64(gfUsageTC) * ct.utilTarget / flowcontrol.FixedPointMultiplier - - go func() { - saveCostFactor := func() { - var data [8]byte - binary.BigEndian.PutUint64(data[:], math.Float64bits(gfLog)) - ct.db.Put([]byte(gfDbKey), data[:]) - log.Debug("global cost factor saved", "value", factor) - } - saveTicker := time.NewTicker(time.Minute * 10) - defer saveTicker.Stop() - - for { - select { - case r := <-ct.reqInfoCh: - relCost := int64(factor * r.servingTime * 100 / r.avgTimeCost) // Convert the value to a percentage form - - // Record more metrics if we are debugging - if metrics.EnabledExpensive { - switch r.msgCode { - case GetBlockHeadersMsg: - relativeCostHeaderHistogram.Update(relCost) - case GetBlockBodiesMsg: - relativeCostBodyHistogram.Update(relCost) - case GetReceiptsMsg: - relativeCostReceiptHistogram.Update(relCost) - case GetCodeMsg: - relativeCostCodeHistogram.Update(relCost) - case GetProofsV2Msg: - relativeCostProofHistogram.Update(relCost) - case GetHelperTrieProofsMsg: - relativeCostHelperProofHistogram.Update(relCost) - case SendTxV2Msg: - relativeCostSendTxHistogram.Update(relCost) - case GetTxStatusMsg: - relativeCostTxStatusHistogram.Update(relCost) - } - } - // SendTxV2 and GetTxStatus requests are two special cases. - // All other requests will only put pressure on the database, and - // the corresponding delay is relatively stable. While these two - // requests involve txpool query, which is usually unstable. - // - // TODO(rjl493456442) fixes this. - if r.msgCode == SendTxV2Msg || r.msgCode == GetTxStatusMsg { - continue - } - requestServedMeter.Mark(int64(r.servingTime)) - requestServedTimer.Update(time.Duration(r.servingTime)) - requestEstimatedMeter.Mark(int64(r.avgTimeCost / factor)) - requestEstimatedTimer.Update(time.Duration(r.avgTimeCost / factor)) - relativeCostHistogram.Update(relCost) - - now := mclock.Now() - dt := float64(now - expUpdate) - expUpdate = now - exp := math.Exp(-dt / float64(gfUsageTC)) - - // calculate factor correction until now, based on previous values - var gfCorr float64 - max := recentTime - if recentAvg > max { - max = recentAvg - } - // we apply continuous correction when MAX(recentTime, recentAvg) > threshold - if max > threshold { - // calculate correction time between last expUpdate and now - if max*exp >= threshold { - gfCorr = dt - } else { - gfCorr = math.Log(max/threshold) * float64(gfUsageTC) - } - // calculate log(factor) correction with the right direction and time constant - if recentTime > recentAvg { - // drop factor if actual serving times are larger than average estimates - gfCorr /= -float64(gfDropTC) - } else { - // raise factor if actual serving times are smaller than average estimates - gfCorr /= float64(gfRaiseTC) - } - } - // update recent cost values with current request - recentTime = recentTime*exp + r.servingTime - recentAvg = recentAvg*exp + r.avgTimeCost/factor - - if gfCorr != 0 { - // Apply the correction to factor - gfLog += gfCorr - factor = math.Exp(gfLog) - // Notify outside modules the new factor and totalRecharge. - if time.Duration(now-lastUpdate) > time.Second { - totalRecharge, lastUpdate = ct.utilTarget*factor, now - ct.gfLock.Lock() - ct.factor = factor - ch := ct.totalRechargeCh - ct.gfLock.Unlock() - if ch != nil { - select { - case ct.totalRechargeCh <- uint64(totalRecharge): - default: - } - } - globalFactorGauge.Update(int64(1000 * factor)) - log.Debug("global cost factor updated", "factor", factor) - } - } - recentServedGauge.Update(int64(recentTime)) - recentEstimatedGauge.Update(int64(recentAvg)) - - case <-saveTicker.C: - saveCostFactor() - - case stopCh := <-ct.stopCh: - saveCostFactor() - close(stopCh) - return - } - } - }() -} - -// globalFactor returns the current value of the global cost factor -func (ct *costTracker) globalFactor() float64 { - ct.gfLock.RLock() - defer ct.gfLock.RUnlock() - - return ct.factor -} - -// totalRecharge returns the current total recharge parameter which is used by -// flowcontrol.ClientManager and is scaled by the global cost factor -func (ct *costTracker) totalRecharge() uint64 { - ct.gfLock.RLock() - defer ct.gfLock.RUnlock() - - return uint64(ct.factor * ct.utilTarget) -} - -// subscribeTotalRecharge returns all future updates to the total recharge value -// through a channel and also returns the current value -func (ct *costTracker) subscribeTotalRecharge(ch chan uint64) uint64 { - ct.gfLock.Lock() - defer ct.gfLock.Unlock() - - ct.totalRechargeCh = ch - return uint64(ct.factor * ct.utilTarget) -} - -// updateStats updates the global cost factor and (if enabled) the real cost vs. -// average estimate statistics -func (ct *costTracker) updateStats(code, amount, servingTime, realCost uint64) { - avg := reqAvgTimeCost[code] - avgTimeCost := avg.baseCost + amount*avg.reqCost - select { - case ct.reqInfoCh <- reqInfo{float64(avgTimeCost), float64(servingTime), code}: - default: - } - if makeCostStats { - realCost <<= 4 - l := 0 - for l < 9 && realCost > avgTimeCost { - l++ - realCost >>= 1 - } - ct.stats[code][l].Add(1) - } -} - -// realCost calculates the final cost of a request based on actual serving time, -// incoming and outgoing message size -// -// Note: message size is only taken into account if bandwidth limitation is applied -// and the cost based on either message size is greater than the cost based on -// serving time. A maximum of the three costs is applied instead of their sum -// because the three limited resources (serving thread time and i/o bandwidth) can -// also be maxed out simultaneously. -func (ct *costTracker) realCost(servingTime uint64, inSize, outSize uint32) uint64 { - cost := float64(servingTime) - inSizeCost := float64(inSize) * ct.inSizeFactor - if inSizeCost > cost { - cost = inSizeCost - } - outSizeCost := float64(outSize) * ct.outSizeFactor - if outSizeCost > cost { - cost = outSizeCost - } - return uint64(cost * ct.globalFactor()) -} - -// printStats prints the distribution of real request cost relative to the average estimates -func (ct *costTracker) printStats() { - if ct.stats == nil { - return - } - for code, arr := range ct.stats { - log.Info("Request cost statistics", "code", code, "1/16", arr[0].Load(), "1/8", arr[1].Load(), "1/4", arr[2].Load(), "1/2", arr[3].Load(), "1", arr[4].Load(), "2", arr[5].Load(), "4", arr[6].Load(), "8", arr[7].Load(), "16", arr[8].Load(), ">16", arr[9].Load()) - } -} - -type ( - // requestCostTable assigns a cost estimate function to each request type - // which is a linear function of the requested amount - // (cost = baseCost + reqCost * amount) - requestCostTable map[uint64]*requestCosts - requestCosts struct { - baseCost, reqCost uint64 - } - - // RequestCostList is a list representation of request costs which is used for - // database storage and communication through the network - RequestCostList []requestCostListItem - requestCostListItem struct { - MsgCode, BaseCost, ReqCost uint64 - } -) - -// getMaxCost calculates the estimated cost for a given request type and amount -func (table requestCostTable) getMaxCost(code, amount uint64) uint64 { - costs := table[code] - return costs.baseCost + amount*costs.reqCost -} - -// decode converts a cost list to a cost table -func (list RequestCostList) decode(protocolLength uint64) requestCostTable { - table := make(requestCostTable) - for _, e := range list { - if e.MsgCode < protocolLength { - table[e.MsgCode] = &requestCosts{ - baseCost: e.BaseCost, - reqCost: e.ReqCost, - } - } - } - return table -} - -// testCostList returns a dummy request cost list used by tests -func testCostList(testCost uint64) RequestCostList { - cl := make(RequestCostList, len(reqAvgTimeCost)) - var max uint64 - for code := range reqAvgTimeCost { - if code > max { - max = code - } - } - i := 0 - for code := uint64(0); code <= max; code++ { - if _, ok := reqAvgTimeCost[code]; ok { - cl[i].MsgCode = code - cl[i].BaseCost = testCost - cl[i].ReqCost = 0 - i++ - } - } - return cl -} diff --git a/les/distributor.go b/les/distributor.go deleted file mode 100644 index a0319c67f7..0000000000 --- a/les/distributor.go +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2017 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 les - -import ( - "container/list" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/les/utils" -) - -// requestDistributor implements a mechanism that distributes requests to -// suitable peers, obeying flow control rules and prioritizing them in creation -// order (even when a resend is necessary). -type requestDistributor struct { - clock mclock.Clock - reqQueue *list.List - lastReqOrder uint64 - peers map[distPeer]struct{} - peerLock sync.RWMutex - loopChn chan struct{} - loopNextSent bool - lock sync.Mutex - - closeCh chan struct{} - wg sync.WaitGroup -} - -// distPeer is an LES server peer interface for the request distributor. -// waitBefore returns either the necessary waiting time before sending a request -// with the given upper estimated cost or the estimated remaining relative buffer -// value after sending such a request (in which case the request can be sent -// immediately). At least one of these values is always zero. -type distPeer interface { - waitBefore(uint64) (time.Duration, float64) - canQueue() bool - queueSend(f func()) bool -} - -// distReq is the request abstraction used by the distributor. It is based on -// three callback functions: -// - getCost returns the upper estimate of the cost of sending the request to a given peer -// - canSend tells if the server peer is suitable to serve the request -// - request prepares sending the request to the given peer and returns a function that -// does the actual sending. Request order should be preserved but the callback itself should not -// block until it is sent because other peers might still be able to receive requests while -// one of them is blocking. Instead, the returned function is put in the peer's send queue. -type distReq struct { - getCost func(distPeer) uint64 - canSend func(distPeer) bool - request func(distPeer) func() - - reqOrder uint64 - sentChn chan distPeer - element *list.Element - waitForPeers mclock.AbsTime - enterQueue mclock.AbsTime -} - -// newRequestDistributor creates a new request distributor -func newRequestDistributor(peers *serverPeerSet, clock mclock.Clock) *requestDistributor { - d := &requestDistributor{ - clock: clock, - reqQueue: list.New(), - loopChn: make(chan struct{}, 2), - closeCh: make(chan struct{}), - peers: make(map[distPeer]struct{}), - } - if peers != nil { - peers.subscribe(d) - } - d.wg.Add(1) - go d.loop() - return d -} - -// registerPeer implements peerSetNotify -func (d *requestDistributor) registerPeer(p *serverPeer) { - d.peerLock.Lock() - d.peers[p] = struct{}{} - d.peerLock.Unlock() -} - -// unregisterPeer implements peerSetNotify -func (d *requestDistributor) unregisterPeer(p *serverPeer) { - d.peerLock.Lock() - delete(d.peers, p) - d.peerLock.Unlock() -} - -// registerTestPeer adds a new test peer -func (d *requestDistributor) registerTestPeer(p distPeer) { - d.peerLock.Lock() - d.peers[p] = struct{}{} - d.peerLock.Unlock() -} - -var ( - // distMaxWait is the maximum waiting time after which further necessary waiting - // times are recalculated based on new feedback from the servers - distMaxWait = time.Millisecond * 50 - - // waitForPeers is the time window in which a request does not fail even if it - // has no suitable peers to send to at the moment - waitForPeers = time.Second * 3 -) - -// main event loop -func (d *requestDistributor) loop() { - defer d.wg.Done() - for { - select { - case <-d.closeCh: - d.lock.Lock() - elem := d.reqQueue.Front() - for elem != nil { - req := elem.Value.(*distReq) - close(req.sentChn) - req.sentChn = nil - elem = elem.Next() - } - d.lock.Unlock() - return - case <-d.loopChn: - d.lock.Lock() - d.loopNextSent = false - loop: - for { - peer, req, wait := d.nextRequest() - if req != nil && wait == 0 { - chn := req.sentChn // save sentChn because remove sets it to nil - d.remove(req) - send := req.request(peer) - if send != nil { - peer.queueSend(send) - requestSendDelay.Update(time.Duration(d.clock.Now() - req.enterQueue)) - } - chn <- peer - close(chn) - } else { - if wait == 0 { - // no request to send and nothing to wait for; the next - // queued request will wake up the loop - break loop - } - d.loopNextSent = true // a "next" signal has been sent, do not send another one until this one has been received - if wait > distMaxWait { - // waiting times may be reduced by incoming request replies, if it is too long, recalculate it periodically - wait = distMaxWait - } - go func() { - d.clock.Sleep(wait) - d.loopChn <- struct{}{} - }() - break loop - } - } - d.lock.Unlock() - } - } -} - -// selectPeerItem represents a peer to be selected for a request by weightedRandomSelect -type selectPeerItem struct { - peer distPeer - req *distReq - weight uint64 -} - -func selectPeerWeight(i interface{}) uint64 { - return i.(selectPeerItem).weight -} - -// nextRequest returns the next possible request from any peer, along with the -// associated peer and necessary waiting time -func (d *requestDistributor) nextRequest() (distPeer, *distReq, time.Duration) { - checkedPeers := make(map[distPeer]struct{}) - elem := d.reqQueue.Front() - var ( - bestWait time.Duration - sel *utils.WeightedRandomSelect - ) - - d.peerLock.RLock() - defer d.peerLock.RUnlock() - - peerCount := len(d.peers) - for (len(checkedPeers) < peerCount || elem == d.reqQueue.Front()) && elem != nil { - req := elem.Value.(*distReq) - canSend := false - now := d.clock.Now() - if req.waitForPeers > now { - canSend = true - wait := time.Duration(req.waitForPeers - now) - if bestWait == 0 || wait < bestWait { - bestWait = wait - } - } - for peer := range d.peers { - if _, ok := checkedPeers[peer]; !ok && peer.canQueue() && req.canSend(peer) { - canSend = true - cost := req.getCost(peer) - wait, bufRemain := peer.waitBefore(cost) - if wait == 0 { - if sel == nil { - sel = utils.NewWeightedRandomSelect(selectPeerWeight) - } - sel.Update(selectPeerItem{peer: peer, req: req, weight: uint64(bufRemain*1000000) + 1}) - } else { - if bestWait == 0 || wait < bestWait { - bestWait = wait - } - } - checkedPeers[peer] = struct{}{} - } - } - next := elem.Next() - if !canSend && elem == d.reqQueue.Front() { - close(req.sentChn) - d.remove(req) - } - elem = next - } - - if sel != nil { - c := sel.Choose().(selectPeerItem) - return c.peer, c.req, 0 - } - return nil, nil, bestWait -} - -// queue adds a request to the distribution queue, returns a channel where the -// receiving peer is sent once the request has been sent (request callback returned). -// If the request is cancelled or timed out without suitable peers, the channel is -// closed without sending any peer references to it. -func (d *requestDistributor) queue(r *distReq) chan distPeer { - d.lock.Lock() - defer d.lock.Unlock() - - if r.reqOrder == 0 { - d.lastReqOrder++ - r.reqOrder = d.lastReqOrder - r.waitForPeers = d.clock.Now().Add(waitForPeers) - } - // Assign the timestamp when the request is queued no matter it's - // a new one or re-queued one. - r.enterQueue = d.clock.Now() - - back := d.reqQueue.Back() - if back == nil || r.reqOrder > back.Value.(*distReq).reqOrder { - r.element = d.reqQueue.PushBack(r) - } else { - before := d.reqQueue.Front() - for before.Value.(*distReq).reqOrder < r.reqOrder { - before = before.Next() - } - r.element = d.reqQueue.InsertBefore(r, before) - } - - if !d.loopNextSent { - d.loopNextSent = true - d.loopChn <- struct{}{} - } - - r.sentChn = make(chan distPeer, 1) - return r.sentChn -} - -// cancel removes a request from the queue if it has not been sent yet (returns -// false if it has been sent already). It is guaranteed that the callback functions -// will not be called after cancel returns. -func (d *requestDistributor) cancel(r *distReq) bool { - d.lock.Lock() - defer d.lock.Unlock() - - if r.sentChn == nil { - return false - } - - close(r.sentChn) - d.remove(r) - return true -} - -// remove removes a request from the queue -func (d *requestDistributor) remove(r *distReq) { - r.sentChn = nil - if r.element != nil { - d.reqQueue.Remove(r.element) - r.element = nil - } -} - -func (d *requestDistributor) close() { - close(d.closeCh) - d.wg.Wait() -} diff --git a/les/distributor_test.go b/les/distributor_test.go deleted file mode 100644 index 9a93dba145..0000000000 --- a/les/distributor_test.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2017 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 les - -import ( - "math/rand" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -type testDistReq struct { - cost, procTime, order uint64 - canSendTo map[*testDistPeer]struct{} -} - -func (r *testDistReq) getCost(dp distPeer) uint64 { - return r.cost -} - -func (r *testDistReq) canSend(dp distPeer) bool { - _, ok := r.canSendTo[dp.(*testDistPeer)] - return ok -} - -func (r *testDistReq) request(dp distPeer) func() { - return func() { dp.(*testDistPeer).send(r) } -} - -type testDistPeer struct { - sent []*testDistReq - sumCost uint64 - lock sync.RWMutex -} - -func (p *testDistPeer) send(r *testDistReq) { - p.lock.Lock() - defer p.lock.Unlock() - - p.sent = append(p.sent, r) - p.sumCost += r.cost -} - -func (p *testDistPeer) worker(t *testing.T, checkOrder bool, stop chan struct{}) { - var last uint64 - for { - wait := time.Millisecond - p.lock.Lock() - if len(p.sent) > 0 { - rq := p.sent[0] - wait = time.Duration(rq.procTime) - p.sumCost -= rq.cost - if checkOrder { - if rq.order <= last { - t.Errorf("Requests processed in wrong order") - } - last = rq.order - } - p.sent = p.sent[1:] - } - p.lock.Unlock() - select { - case <-stop: - return - case <-time.After(wait): - } - } -} - -const ( - testDistBufLimit = 10000000 - testDistMaxCost = 1000000 - testDistPeerCount = 2 - testDistReqCount = 10 - testDistMaxResendCount = 3 -) - -func (p *testDistPeer) waitBefore(cost uint64) (time.Duration, float64) { - p.lock.RLock() - sumCost := p.sumCost + cost - p.lock.RUnlock() - if sumCost < testDistBufLimit { - return 0, float64(testDistBufLimit-sumCost) / float64(testDistBufLimit) - } - return time.Duration(sumCost - testDistBufLimit), 0 -} - -func (p *testDistPeer) canQueue() bool { - return true -} - -func (p *testDistPeer) queueSend(f func()) bool { - f() - return true -} - -func TestRequestDistributor(t *testing.T) { - testRequestDistributor(t, false) -} - -func TestRequestDistributorResend(t *testing.T) { - testRequestDistributor(t, true) -} - -func testRequestDistributor(t *testing.T, resend bool) { - stop := make(chan struct{}) - defer close(stop) - - dist := newRequestDistributor(nil, &mclock.System{}) - var peers [testDistPeerCount]*testDistPeer - for i := range peers { - peers[i] = &testDistPeer{} - go peers[i].worker(t, !resend, stop) - dist.registerTestPeer(peers[i]) - } - // Disable the mechanism that we will wait a few time for request - // even there is no suitable peer to send right now. - waitForPeers = 0 - - var wg sync.WaitGroup - - for i := 1; i <= testDistReqCount; i++ { - cost := uint64(rand.Int63n(testDistMaxCost)) - procTime := uint64(rand.Int63n(int64(cost + 1))) - rq := &testDistReq{ - cost: cost, - procTime: procTime, - order: uint64(i), - canSendTo: make(map[*testDistPeer]struct{}), - } - for _, peer := range peers { - if rand.Intn(2) != 0 { - rq.canSendTo[peer] = struct{}{} - } - } - - wg.Add(1) - req := &distReq{ - getCost: rq.getCost, - canSend: rq.canSend, - request: rq.request, - } - chn := dist.queue(req) - go func() { - cnt := 1 - if resend && len(rq.canSendTo) != 0 { - cnt = rand.Intn(testDistMaxResendCount) + 1 - } - for i := 0; i < cnt; i++ { - if i != 0 { - chn = dist.queue(req) - } - p := <-chn - if p == nil { - if len(rq.canSendTo) != 0 { - t.Errorf("Request that could have been sent was dropped") - } - } else { - peer := p.(*testDistPeer) - if _, ok := rq.canSendTo[peer]; !ok { - t.Errorf("Request sent to wrong peer") - } - } - } - wg.Done() - }() - if rand.Intn(1000) == 0 { - time.Sleep(time.Duration(rand.Intn(5000000))) - } - } - - wg.Wait() -} diff --git a/les/enr_entry.go b/les/enr_entry.go deleted file mode 100644 index 307313fb10..0000000000 --- a/les/enr_entry.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019 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 les - -import ( - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/p2p/dnsdisc" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -// lesEntry is the "les" ENR entry. This is set for LES servers only. -type lesEntry struct { - // Ignore additional fields (for forward compatibility). - VfxVersion uint - Rest []rlp.RawValue `rlp:"tail"` -} - -func (lesEntry) ENRKey() string { return "les" } - -// ethEntry is the "eth" ENR entry. This is redeclared here to avoid depending on package eth. -type ethEntry struct { - ForkID forkid.ID - Tail []rlp.RawValue `rlp:"tail"` -} - -func (ethEntry) ENRKey() string { return "eth" } - -// setupDiscovery creates the node discovery source for the eth protocol. -func (eth *LightEthereum) setupDiscovery() (enode.Iterator, error) { - it := enode.NewFairMix(0) - - // Enable DNS discovery. - if len(eth.config.EthDiscoveryURLs) != 0 { - client := dnsdisc.NewClient(dnsdisc.Config{}) - dns, err := client.NewIterator(eth.config.EthDiscoveryURLs...) - if err != nil { - return nil, err - } - it.AddSource(dns) - } - - // Enable DHT. - if eth.udpEnabled { - it.AddSource(eth.p2pServer.DiscV5.RandomNodes()) - } - - forkFilter := forkid.NewFilter(eth.blockchain) - iterator := enode.Filter(it, func(n *enode.Node) bool { return nodeIsServer(forkFilter, n) }) - return iterator, nil -} - -// nodeIsServer checks whether n is an LES server node. -func nodeIsServer(forkFilter forkid.Filter, n *enode.Node) bool { - var les lesEntry - var eth ethEntry - return n.Load(&les) == nil && n.Load(ð) == nil && forkFilter(eth.ForkID) == nil -} diff --git a/les/flowcontrol/control.go b/les/flowcontrol/control.go deleted file mode 100644 index 76a241fa5a..0000000000 --- a/les/flowcontrol/control.go +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright 2016 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 flowcontrol implements a client side flow control mechanism -package flowcontrol - -import ( - "fmt" - "math" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/log" -) - -const ( - // fcTimeConst is the time constant applied for MinRecharge during linear - // buffer recharge period - fcTimeConst = time.Millisecond - // DecParamDelay is applied at server side when decreasing capacity in order to - // avoid a buffer underrun error due to requests sent by the client before - // receiving the capacity update announcement - DecParamDelay = time.Second * 2 - // keepLogs is the duration of keeping logs; logging is not used if zero - keepLogs = 0 -) - -// ServerParams are the flow control parameters specified by a server for a client -// -// Note: a server can assign different amounts of capacity to each client by giving -// different parameters to them. -type ServerParams struct { - BufLimit, MinRecharge uint64 -} - -// scheduledUpdate represents a delayed flow control parameter update -type scheduledUpdate struct { - time mclock.AbsTime - params ServerParams -} - -// ClientNode is the flow control system's representation of a client -// (used in server mode only) -type ClientNode struct { - params ServerParams - bufValue int64 - lastTime mclock.AbsTime - updateSchedule []scheduledUpdate - sumCost uint64 // sum of req costs received from this client - accepted map[uint64]uint64 // value = sumCost after accepting the given req - connected bool - lock sync.Mutex - cm *ClientManager - log *logger - cmNodeFields -} - -// NewClientNode returns a new ClientNode -func NewClientNode(cm *ClientManager, params ServerParams) *ClientNode { - node := &ClientNode{ - cm: cm, - params: params, - bufValue: int64(params.BufLimit), - lastTime: cm.clock.Now(), - accepted: make(map[uint64]uint64), - connected: true, - } - if keepLogs > 0 { - node.log = newLogger(keepLogs) - } - cm.connect(node) - return node -} - -// Disconnect should be called when a client is disconnected -func (node *ClientNode) Disconnect() { - node.lock.Lock() - defer node.lock.Unlock() - - node.connected = false - node.cm.disconnect(node) -} - -// BufferStatus returns the current buffer value and limit -func (node *ClientNode) BufferStatus() (uint64, uint64) { - node.lock.Lock() - defer node.lock.Unlock() - - if !node.connected { - return 0, 0 - } - now := node.cm.clock.Now() - node.update(now) - node.cm.updateBuffer(node, 0, now) - bv := node.bufValue - if bv < 0 { - bv = 0 - } - return uint64(bv), node.params.BufLimit -} - -// OneTimeCost subtracts the given amount from the node's buffer. -// -// Note: this call can take the buffer into the negative region internally. -// In this case zero buffer value is returned by exported calls and no requests -// are accepted. -func (node *ClientNode) OneTimeCost(cost uint64) { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.cm.clock.Now() - node.update(now) - node.bufValue -= int64(cost) - node.cm.updateBuffer(node, -int64(cost), now) -} - -// Freeze notifies the client manager about a client freeze event in which case -// the total capacity allowance is slightly reduced. -func (node *ClientNode) Freeze() { - node.lock.Lock() - frozenCap := node.params.MinRecharge - node.lock.Unlock() - node.cm.reduceTotalCapacity(frozenCap) -} - -// update recalculates the buffer value at a specified time while also performing -// scheduled flow control parameter updates if necessary -func (node *ClientNode) update(now mclock.AbsTime) { - for len(node.updateSchedule) > 0 && node.updateSchedule[0].time <= now { - node.recalcBV(node.updateSchedule[0].time) - node.updateParams(node.updateSchedule[0].params, now) - node.updateSchedule = node.updateSchedule[1:] - } - node.recalcBV(now) -} - -// recalcBV recalculates the buffer value at a specified time -func (node *ClientNode) recalcBV(now mclock.AbsTime) { - dt := uint64(now - node.lastTime) - if now < node.lastTime { - dt = 0 - } - node.bufValue += int64(node.params.MinRecharge * dt / uint64(fcTimeConst)) - if node.bufValue > int64(node.params.BufLimit) { - node.bufValue = int64(node.params.BufLimit) - } - if node.log != nil { - node.log.add(now, fmt.Sprintf("updated bv=%d MRR=%d BufLimit=%d", node.bufValue, node.params.MinRecharge, node.params.BufLimit)) - } - node.lastTime = now -} - -// UpdateParams updates the flow control parameters of a client node -func (node *ClientNode) UpdateParams(params ServerParams) { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.cm.clock.Now() - node.update(now) - if params.MinRecharge >= node.params.MinRecharge { - node.updateSchedule = nil - node.updateParams(params, now) - } else { - for i, s := range node.updateSchedule { - if params.MinRecharge >= s.params.MinRecharge { - s.params = params - node.updateSchedule = node.updateSchedule[:i+1] - return - } - } - node.updateSchedule = append(node.updateSchedule, scheduledUpdate{time: now.Add(DecParamDelay), params: params}) - } -} - -// updateParams updates the flow control parameters of the node -func (node *ClientNode) updateParams(params ServerParams, now mclock.AbsTime) { - diff := int64(params.BufLimit - node.params.BufLimit) - if diff > 0 { - node.bufValue += diff - } else if node.bufValue > int64(params.BufLimit) { - node.bufValue = int64(params.BufLimit) - } - node.cm.updateParams(node, params, now) -} - -// AcceptRequest returns whether a new request can be accepted and the missing -// buffer amount if it was rejected due to a buffer underrun. If accepted, maxCost -// is deducted from the flow control buffer. -func (node *ClientNode) AcceptRequest(reqID, index, maxCost uint64) (accepted bool, bufShort uint64, priority int64) { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.cm.clock.Now() - node.update(now) - if int64(maxCost) > node.bufValue { - if node.log != nil { - node.log.add(now, fmt.Sprintf("rejected reqID=%d bv=%d maxCost=%d", reqID, node.bufValue, maxCost)) - node.log.dump(now) - } - return false, maxCost - uint64(node.bufValue), 0 - } - node.bufValue -= int64(maxCost) - node.sumCost += maxCost - if node.log != nil { - node.log.add(now, fmt.Sprintf("accepted reqID=%d bv=%d maxCost=%d sumCost=%d", reqID, node.bufValue, maxCost, node.sumCost)) - } - node.accepted[index] = node.sumCost - return true, 0, node.cm.accepted(node, maxCost, now) -} - -// RequestProcessed should be called when the request has been processed -func (node *ClientNode) RequestProcessed(reqID, index, maxCost, realCost uint64) uint64 { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.cm.clock.Now() - node.update(now) - node.cm.processed(node, maxCost, realCost, now) - bv := node.bufValue + int64(node.sumCost-node.accepted[index]) - if node.log != nil { - node.log.add(now, fmt.Sprintf("processed reqID=%d bv=%d maxCost=%d realCost=%d sumCost=%d oldSumCost=%d reportedBV=%d", reqID, node.bufValue, maxCost, realCost, node.sumCost, node.accepted[index], bv)) - } - delete(node.accepted, index) - if bv < 0 { - return 0 - } - return uint64(bv) -} - -// ServerNode is the flow control system's representation of a server -// (used in client mode only) -type ServerNode struct { - clock mclock.Clock - bufEstimate uint64 - bufRecharge bool - lastTime mclock.AbsTime - params ServerParams - sumCost uint64 // sum of req costs sent to this server - pending map[uint64]uint64 // value = sumCost after sending the given req - log *logger - lock sync.RWMutex -} - -// NewServerNode returns a new ServerNode -func NewServerNode(params ServerParams, clock mclock.Clock) *ServerNode { - node := &ServerNode{ - clock: clock, - bufEstimate: params.BufLimit, - bufRecharge: false, - lastTime: clock.Now(), - params: params, - pending: make(map[uint64]uint64), - } - if keepLogs > 0 { - node.log = newLogger(keepLogs) - } - return node -} - -// UpdateParams updates the flow control parameters of the node -func (node *ServerNode) UpdateParams(params ServerParams) { - node.lock.Lock() - defer node.lock.Unlock() - - node.recalcBLE(mclock.Now()) - if params.BufLimit > node.params.BufLimit { - node.bufEstimate += params.BufLimit - node.params.BufLimit - } else { - if node.bufEstimate > params.BufLimit { - node.bufEstimate = params.BufLimit - } - } - node.params = params -} - -// recalcBLE recalculates the lowest estimate for the client's buffer value at -// the given server at the specified time -func (node *ServerNode) recalcBLE(now mclock.AbsTime) { - if now < node.lastTime { - return - } - if node.bufRecharge { - dt := uint64(now - node.lastTime) - node.bufEstimate += node.params.MinRecharge * dt / uint64(fcTimeConst) - if node.bufEstimate >= node.params.BufLimit { - node.bufEstimate = node.params.BufLimit - node.bufRecharge = false - } - } - node.lastTime = now - if node.log != nil { - node.log.add(now, fmt.Sprintf("updated bufEst=%d MRR=%d BufLimit=%d", node.bufEstimate, node.params.MinRecharge, node.params.BufLimit)) - } -} - -// safetyMargin is added to the flow control waiting time when estimated buffer value is low -const safetyMargin = time.Millisecond - -// CanSend returns the minimum waiting time required before sending a request -// with the given maximum estimated cost. Second return value is the relative -// estimated buffer level after sending the request (divided by BufLimit). -func (node *ServerNode) CanSend(maxCost uint64) (time.Duration, float64) { - node.lock.RLock() - defer node.lock.RUnlock() - - if node.params.BufLimit == 0 { - return time.Duration(math.MaxInt64), 0 - } - now := node.clock.Now() - node.recalcBLE(now) - maxCost += uint64(safetyMargin) * node.params.MinRecharge / uint64(fcTimeConst) - if maxCost > node.params.BufLimit { - maxCost = node.params.BufLimit - } - if node.bufEstimate >= maxCost { - relBuf := float64(node.bufEstimate-maxCost) / float64(node.params.BufLimit) - if node.log != nil { - node.log.add(now, fmt.Sprintf("canSend bufEst=%d maxCost=%d true relBuf=%f", node.bufEstimate, maxCost, relBuf)) - } - return 0, relBuf - } - timeLeft := time.Duration((maxCost - node.bufEstimate) * uint64(fcTimeConst) / node.params.MinRecharge) - if node.log != nil { - node.log.add(now, fmt.Sprintf("canSend bufEst=%d maxCost=%d false timeLeft=%v", node.bufEstimate, maxCost, timeLeft)) - } - return timeLeft, 0 -} - -// QueuedRequest should be called when the request has been assigned to the given -// server node, before putting it in the send queue. It is mandatory that requests -// are sent in the same order as the QueuedRequest calls are made. -func (node *ServerNode) QueuedRequest(reqID, maxCost uint64) { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.clock.Now() - node.recalcBLE(now) - // Note: we do not know when requests actually arrive to the server so bufRecharge - // is not turned on here if buffer was full; in this case it is going to be turned - // on by the first reply's bufValue feedback - if node.bufEstimate >= maxCost { - node.bufEstimate -= maxCost - } else { - log.Error("Queued request with insufficient buffer estimate") - node.bufEstimate = 0 - } - node.sumCost += maxCost - node.pending[reqID] = node.sumCost - if node.log != nil { - node.log.add(now, fmt.Sprintf("queued reqID=%d bufEst=%d maxCost=%d sumCost=%d", reqID, node.bufEstimate, maxCost, node.sumCost)) - } -} - -// ReceivedReply adjusts estimated buffer value according to the value included in -// the latest request reply. -func (node *ServerNode) ReceivedReply(reqID, bv uint64) { - node.lock.Lock() - defer node.lock.Unlock() - - now := node.clock.Now() - node.recalcBLE(now) - if bv > node.params.BufLimit { - bv = node.params.BufLimit - } - sc, ok := node.pending[reqID] - if !ok { - return - } - delete(node.pending, reqID) - cc := node.sumCost - sc - newEstimate := uint64(0) - if bv > cc { - newEstimate = bv - cc - } - if newEstimate > node.bufEstimate { - // Note: we never reduce the buffer estimate based on the reported value because - // this can only happen because of the delayed delivery of the latest reply. - // The lowest estimate based on the previous reply can still be considered valid. - node.bufEstimate = newEstimate - } - - node.bufRecharge = node.bufEstimate < node.params.BufLimit - node.lastTime = now - if node.log != nil { - node.log.add(now, fmt.Sprintf("received reqID=%d bufEst=%d reportedBv=%d sumCost=%d oldSumCost=%d", reqID, node.bufEstimate, bv, node.sumCost, sc)) - } -} - -// ResumeFreeze cleans all pending requests and sets the buffer estimate to the -// reported value after resuming from a frozen state -func (node *ServerNode) ResumeFreeze(bv uint64) { - node.lock.Lock() - defer node.lock.Unlock() - - for reqID := range node.pending { - delete(node.pending, reqID) - } - now := node.clock.Now() - node.recalcBLE(now) - if bv > node.params.BufLimit { - bv = node.params.BufLimit - } - node.bufEstimate = bv - node.bufRecharge = node.bufEstimate < node.params.BufLimit - node.lastTime = now - if node.log != nil { - node.log.add(now, fmt.Sprintf("unfreeze bv=%d sumCost=%d", bv, node.sumCost)) - } -} - -// DumpLogs dumps the event log if logging is used -func (node *ServerNode) DumpLogs() { - node.lock.Lock() - defer node.lock.Unlock() - - if node.log != nil { - node.log.dump(node.clock.Now()) - } -} diff --git a/les/flowcontrol/logger.go b/les/flowcontrol/logger.go deleted file mode 100644 index 428d7fbf22..0000000000 --- a/les/flowcontrol/logger.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2019 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 flowcontrol - -import ( - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -// logger collects events in string format and discards events older than the -// "keep" parameter -type logger struct { - events map[uint64]logEvent - writePtr, delPtr uint64 - keep time.Duration -} - -// logEvent describes a single event -type logEvent struct { - time mclock.AbsTime - event string -} - -// newLogger creates a new logger -func newLogger(keep time.Duration) *logger { - return &logger{ - events: make(map[uint64]logEvent), - keep: keep, - } -} - -// add adds a new event and discards old events if possible -func (l *logger) add(now mclock.AbsTime, event string) { - keepAfter := now - mclock.AbsTime(l.keep) - for l.delPtr < l.writePtr && l.events[l.delPtr].time <= keepAfter { - delete(l.events, l.delPtr) - l.delPtr++ - } - l.events[l.writePtr] = logEvent{now, event} - l.writePtr++ -} - -// dump prints all stored events -func (l *logger) dump(now mclock.AbsTime) { - for i := l.delPtr; i < l.writePtr; i++ { - e := l.events[i] - fmt.Println(time.Duration(e.time-now), e.event) - } -} diff --git a/les/flowcontrol/manager.go b/les/flowcontrol/manager.go deleted file mode 100644 index b7cc9bd903..0000000000 --- a/les/flowcontrol/manager.go +++ /dev/null @@ -1,476 +0,0 @@ -// Copyright 2016 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 flowcontrol - -import ( - "fmt" - "math" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/common/prque" -) - -// cmNodeFields are ClientNode fields used by the client manager -// Note: these fields are locked by the client manager's mutex -type cmNodeFields struct { - corrBufValue int64 // buffer value adjusted with the extra recharge amount - rcLastIntValue int64 // past recharge integrator value when corrBufValue was last updated - rcFullIntValue int64 // future recharge integrator value when corrBufValue will reach maximum - queueIndex int // position in the recharge queue (-1 if not queued) -} - -// FixedPointMultiplier is applied to the recharge integrator and the recharge curve. -// -// Note: fixed point arithmetic is required for the integrator because it is a -// constantly increasing value that can wrap around int64 limits (which behavior is -// also supported by the priority queue). A floating point value would gradually lose -// precision in this application. -// The recharge curve and all recharge values are encoded as fixed point because -// sumRecharge is frequently updated by adding or subtracting individual recharge -// values and perfect precision is required. -const FixedPointMultiplier = 1000000 - -var ( - capacityDropFactor = 0.1 - capacityRaiseTC = 1 / (3 * float64(time.Hour)) // time constant for raising the capacity factor - capacityRaiseThresholdRatio = 1.125 // total/connected capacity ratio threshold for raising the capacity factor -) - -// ClientManager controls the capacity assigned to the clients of a server. -// Since ServerParams guarantee a safe lower estimate for processable requests -// even in case of all clients being active, ClientManager calculates a -// corrugated buffer value and usually allows a higher remaining buffer value -// to be returned with each reply. -type ClientManager struct { - clock mclock.Clock - lock sync.Mutex - stop chan chan struct{} - - curve PieceWiseLinear - sumRecharge, totalRecharge, totalConnected uint64 - logTotalCap, totalCapacity float64 - logTotalCapRaiseLimit float64 - minLogTotalCap, maxLogTotalCap float64 - capacityRaiseThreshold uint64 - capLastUpdate mclock.AbsTime - totalCapacityCh chan uint64 - - // recharge integrator is increasing in each moment with a rate of - // (totalRecharge / sumRecharge)*FixedPointMultiplier or 0 if sumRecharge==0 - rcLastUpdate mclock.AbsTime // last time the recharge integrator was updated - rcLastIntValue int64 // last updated value of the recharge integrator - priorityOffset int64 // offset for prque priority values ensures that all priorities stay in the int64 range - // recharge queue is a priority queue with currently recharging client nodes - // as elements. The priority value is rcFullIntValue which allows to quickly - // determine which client will first finish recharge. - rcQueue *prque.Prque[int64, *ClientNode] -} - -// NewClientManager returns a new client manager. -// Client manager enhances flow control performance by allowing client buffers -// to recharge quicker than the minimum guaranteed recharge rate if possible. -// The sum of all minimum recharge rates (sumRecharge) is updated each time -// a clients starts or finishes buffer recharging. Then an adjusted total -// recharge rate is calculated using a piecewise linear recharge curve: -// -// totalRecharge = curve(sumRecharge) -// (totalRecharge >= sumRecharge is enforced) -// -// Then the "bonus" buffer recharge is distributed between currently recharging -// clients proportionally to their minimum recharge rates. -// -// Note: total recharge is proportional to the average number of parallel running -// serving threads. A recharge value of 1000000 corresponds to one thread in average. -// The maximum number of allowed serving threads should always be considerably -// higher than the targeted average number. -// -// Note 2: although it is possible to specify a curve allowing the total target -// recharge starting from zero sumRecharge, it makes sense to add a linear ramp -// starting from zero in order to not let a single low-priority client use up -// the entire server capacity and thus ensure quick availability for others at -// any moment. -func NewClientManager(curve PieceWiseLinear, clock mclock.Clock) *ClientManager { - cm := &ClientManager{ - clock: clock, - rcQueue: prque.New[int64, *ClientNode](func(a *ClientNode, i int) { a.queueIndex = i }), - capLastUpdate: clock.Now(), - stop: make(chan chan struct{}), - } - if curve != nil { - cm.SetRechargeCurve(curve) - } - go func() { - // regularly recalculate and update total capacity - for { - select { - case <-time.After(time.Minute): - cm.lock.Lock() - cm.updateTotalCapacity(cm.clock.Now(), true) - cm.lock.Unlock() - case stop := <-cm.stop: - close(stop) - return - } - } - }() - return cm -} - -// Stop stops the client manager -func (cm *ClientManager) Stop() { - stop := make(chan struct{}) - cm.stop <- stop - <-stop -} - -// SetRechargeCurve updates the recharge curve -func (cm *ClientManager) SetRechargeCurve(curve PieceWiseLinear) { - cm.lock.Lock() - defer cm.lock.Unlock() - - now := cm.clock.Now() - cm.updateRecharge(now) - cm.curve = curve - if len(curve) > 0 { - cm.totalRecharge = curve[len(curve)-1].Y - } else { - cm.totalRecharge = 0 - } -} - -// SetCapacityLimits sets a threshold value used for raising capFactor. -// Either if the difference between total allowed and connected capacity is less -// than this threshold or if their ratio is less than capacityRaiseThresholdRatio -// then capFactor is allowed to slowly raise. -func (cm *ClientManager) SetCapacityLimits(min, max, raiseThreshold uint64) { - if min < 1 { - min = 1 - } - cm.minLogTotalCap = math.Log(float64(min)) - if max < 1 { - max = 1 - } - cm.maxLogTotalCap = math.Log(float64(max)) - cm.logTotalCap = cm.maxLogTotalCap - cm.capacityRaiseThreshold = raiseThreshold - cm.refreshCapacity() -} - -// connect should be called when a client is connected, before passing it to any -// other ClientManager function -func (cm *ClientManager) connect(node *ClientNode) { - cm.lock.Lock() - defer cm.lock.Unlock() - - now := cm.clock.Now() - cm.updateRecharge(now) - node.corrBufValue = int64(node.params.BufLimit) - node.rcLastIntValue = cm.rcLastIntValue - node.queueIndex = -1 - cm.updateTotalCapacity(now, true) - cm.totalConnected += node.params.MinRecharge - cm.updateRaiseLimit() -} - -// disconnect should be called when a client is disconnected -func (cm *ClientManager) disconnect(node *ClientNode) { - cm.lock.Lock() - defer cm.lock.Unlock() - - now := cm.clock.Now() - cm.updateRecharge(cm.clock.Now()) - cm.updateTotalCapacity(now, true) - cm.totalConnected -= node.params.MinRecharge - cm.updateRaiseLimit() -} - -// accepted is called when a request with given maximum cost is accepted. -// It returns a priority indicator for the request which is used to determine placement -// in the serving queue. Older requests have higher priority by default. If the client -// is almost out of buffer, request priority is reduced. -func (cm *ClientManager) accepted(node *ClientNode, maxCost uint64, now mclock.AbsTime) (priority int64) { - cm.lock.Lock() - defer cm.lock.Unlock() - - cm.updateNodeRc(node, -int64(maxCost), &node.params, now) - rcTime := (node.params.BufLimit - uint64(node.corrBufValue)) * FixedPointMultiplier / node.params.MinRecharge - return -int64(now) - int64(rcTime) -} - -// processed updates the client buffer according to actual request cost after -// serving has been finished. -// -// Note: processed should always be called for all accepted requests -func (cm *ClientManager) processed(node *ClientNode, maxCost, realCost uint64, now mclock.AbsTime) { - if realCost > maxCost { - realCost = maxCost - } - cm.updateBuffer(node, int64(maxCost-realCost), now) -} - -// updateBuffer recalculates the corrected buffer value, adds the given value to it -// and updates the node's actual buffer value if possible -func (cm *ClientManager) updateBuffer(node *ClientNode, add int64, now mclock.AbsTime) { - cm.lock.Lock() - defer cm.lock.Unlock() - - cm.updateNodeRc(node, add, &node.params, now) - if node.corrBufValue > node.bufValue { - if node.log != nil { - node.log.add(now, fmt.Sprintf("corrected bv=%d oldBv=%d", node.corrBufValue, node.bufValue)) - } - node.bufValue = node.corrBufValue - } -} - -// updateParams updates the flow control parameters of a client node -func (cm *ClientManager) updateParams(node *ClientNode, params ServerParams, now mclock.AbsTime) { - cm.lock.Lock() - defer cm.lock.Unlock() - - cm.updateRecharge(now) - cm.updateTotalCapacity(now, true) - cm.totalConnected += params.MinRecharge - node.params.MinRecharge - cm.updateRaiseLimit() - cm.updateNodeRc(node, 0, ¶ms, now) -} - -// updateRaiseLimit recalculates the limiting value until which logTotalCap -// can be raised when no client freeze events occur -func (cm *ClientManager) updateRaiseLimit() { - if cm.capacityRaiseThreshold == 0 { - cm.logTotalCapRaiseLimit = 0 - return - } - limit := float64(cm.totalConnected + cm.capacityRaiseThreshold) - limit2 := float64(cm.totalConnected) * capacityRaiseThresholdRatio - if limit2 > limit { - limit = limit2 - } - if limit < 1 { - limit = 1 - } - cm.logTotalCapRaiseLimit = math.Log(limit) -} - -// updateRecharge updates the recharge integrator and checks the recharge queue -// for nodes with recently filled buffers -func (cm *ClientManager) updateRecharge(now mclock.AbsTime) { - lastUpdate := cm.rcLastUpdate - cm.rcLastUpdate = now - // updating is done in multiple steps if node buffers are filled and sumRecharge - // is decreased before the given target time - for cm.sumRecharge > 0 { - sumRecharge := cm.sumRecharge - if sumRecharge > cm.totalRecharge { - sumRecharge = cm.totalRecharge - } - bonusRatio := float64(1) - v := cm.curve.ValueAt(sumRecharge) - s := float64(sumRecharge) - if v > s && s > 0 { - bonusRatio = v / s - } - dt := now - lastUpdate - // fetch the client that finishes first - rcqNode := cm.rcQueue.PopItem() // if sumRecharge > 0 then the queue cannot be empty - // check whether it has already finished - dtNext := mclock.AbsTime(float64(rcqNode.rcFullIntValue-cm.rcLastIntValue) / bonusRatio) - if dt < dtNext { - // not finished yet, put it back, update integrator according - // to current bonusRatio and return - cm.addToQueue(rcqNode) - cm.rcLastIntValue += int64(bonusRatio * float64(dt)) - return - } - lastUpdate += dtNext - // finished recharging, update corrBufValue and sumRecharge if necessary and do next step - if rcqNode.corrBufValue < int64(rcqNode.params.BufLimit) { - rcqNode.corrBufValue = int64(rcqNode.params.BufLimit) - cm.sumRecharge -= rcqNode.params.MinRecharge - } - cm.rcLastIntValue = rcqNode.rcFullIntValue - } -} - -func (cm *ClientManager) addToQueue(node *ClientNode) { - if cm.priorityOffset-node.rcFullIntValue < -0x4000000000000000 { - cm.priorityOffset += 0x4000000000000000 - // recreate priority queue with new offset to avoid overflow; should happen very rarely - newRcQueue := prque.New[int64, *ClientNode](func(a *ClientNode, i int) { a.queueIndex = i }) - for cm.rcQueue.Size() > 0 { - n := cm.rcQueue.PopItem() - newRcQueue.Push(n, cm.priorityOffset-n.rcFullIntValue) - } - cm.rcQueue = newRcQueue - } - cm.rcQueue.Push(node, cm.priorityOffset-node.rcFullIntValue) -} - -// updateNodeRc updates a node's corrBufValue and adds an external correction value. -// It also adds or removes the rcQueue entry and updates ServerParams and sumRecharge if necessary. -func (cm *ClientManager) updateNodeRc(node *ClientNode, bvc int64, params *ServerParams, now mclock.AbsTime) { - cm.updateRecharge(now) - wasFull := true - if node.corrBufValue != int64(node.params.BufLimit) { - wasFull = false - node.corrBufValue += (cm.rcLastIntValue - node.rcLastIntValue) * int64(node.params.MinRecharge) / FixedPointMultiplier - if node.corrBufValue > int64(node.params.BufLimit) { - node.corrBufValue = int64(node.params.BufLimit) - } - node.rcLastIntValue = cm.rcLastIntValue - } - node.corrBufValue += bvc - diff := int64(params.BufLimit - node.params.BufLimit) - if diff > 0 { - node.corrBufValue += diff - } - isFull := false - if node.corrBufValue >= int64(params.BufLimit) { - node.corrBufValue = int64(params.BufLimit) - isFull = true - } - if !wasFull { - cm.sumRecharge -= node.params.MinRecharge - } - if params != &node.params { - node.params = *params - } - if !isFull { - cm.sumRecharge += node.params.MinRecharge - if node.queueIndex != -1 { - cm.rcQueue.Remove(node.queueIndex) - } - node.rcLastIntValue = cm.rcLastIntValue - node.rcFullIntValue = cm.rcLastIntValue + (int64(node.params.BufLimit)-node.corrBufValue)*FixedPointMultiplier/int64(node.params.MinRecharge) - cm.addToQueue(node) - } -} - -// reduceTotalCapacity reduces the total capacity allowance in case of a client freeze event -func (cm *ClientManager) reduceTotalCapacity(frozenCap uint64) { - cm.lock.Lock() - defer cm.lock.Unlock() - - ratio := float64(1) - if frozenCap < cm.totalConnected { - ratio = float64(frozenCap) / float64(cm.totalConnected) - } - now := cm.clock.Now() - cm.updateTotalCapacity(now, false) - cm.logTotalCap -= capacityDropFactor * ratio - if cm.logTotalCap < cm.minLogTotalCap { - cm.logTotalCap = cm.minLogTotalCap - } - cm.updateTotalCapacity(now, true) -} - -// updateTotalCapacity updates the total capacity factor. The capacity factor allows -// the total capacity of the system to go over the allowed total recharge value -// if clients go to frozen state sufficiently rarely. -// The capacity factor is dropped instantly by a small amount if a clients is frozen. -// It is raised slowly (with a large time constant) if the total connected capacity -// is close to the total allowed amount and no clients are frozen. -func (cm *ClientManager) updateTotalCapacity(now mclock.AbsTime, refresh bool) { - dt := now - cm.capLastUpdate - cm.capLastUpdate = now - - if cm.logTotalCap < cm.logTotalCapRaiseLimit { - cm.logTotalCap += capacityRaiseTC * float64(dt) - if cm.logTotalCap > cm.logTotalCapRaiseLimit { - cm.logTotalCap = cm.logTotalCapRaiseLimit - } - } - if cm.logTotalCap > cm.maxLogTotalCap { - cm.logTotalCap = cm.maxLogTotalCap - } - if refresh { - cm.refreshCapacity() - } -} - -// refreshCapacity recalculates the total capacity value and sends an update to the subscription -// channel if the relative change of the value since the last update is more than 0.1 percent -func (cm *ClientManager) refreshCapacity() { - totalCapacity := math.Exp(cm.logTotalCap) - if totalCapacity >= cm.totalCapacity*0.999 && totalCapacity <= cm.totalCapacity*1.001 { - return - } - cm.totalCapacity = totalCapacity - if cm.totalCapacityCh != nil { - select { - case cm.totalCapacityCh <- uint64(cm.totalCapacity): - default: - } - } -} - -// SubscribeTotalCapacity returns all future updates to the total capacity value -// through a channel and also returns the current value -func (cm *ClientManager) SubscribeTotalCapacity(ch chan uint64) uint64 { - cm.lock.Lock() - defer cm.lock.Unlock() - - cm.totalCapacityCh = ch - return uint64(cm.totalCapacity) -} - -// PieceWiseLinear is used to describe recharge curves -type PieceWiseLinear []struct{ X, Y uint64 } - -// ValueAt returns the curve's value at a given point -func (pwl PieceWiseLinear) ValueAt(x uint64) float64 { - l := 0 - h := len(pwl) - if h == 0 { - return 0 - } - for h != l { - m := (l + h) / 2 - if x > pwl[m].X { - l = m + 1 - } else { - h = m - } - } - if l == 0 { - return float64(pwl[0].Y) - } - l-- - if h == len(pwl) { - return float64(pwl[l].Y) - } - dx := pwl[h].X - pwl[l].X - if dx < 1 { - return float64(pwl[l].Y) - } - return float64(pwl[l].Y) + float64(pwl[h].Y-pwl[l].Y)*float64(x-pwl[l].X)/float64(dx) -} - -// Valid returns true if the X coordinates of the curve points are non-strictly monotonic -func (pwl PieceWiseLinear) Valid() bool { - var lastX uint64 - for _, i := range pwl { - if i.X < lastX { - return false - } - lastX = i.X - } - return true -} diff --git a/les/flowcontrol/manager_test.go b/les/flowcontrol/manager_test.go deleted file mode 100644 index 3afc31272f..0000000000 --- a/les/flowcontrol/manager_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2019 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 flowcontrol - -import ( - "math" - "math/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -type testNode struct { - node *ClientNode - bufLimit, capacity uint64 - waitUntil mclock.AbsTime - index, totalCost uint64 -} - -const ( - testMaxCost = 1000000 - testLength = 100000 -) - -// testConstantTotalCapacity simulates multiple request sender nodes and verifies -// whether the total amount of served requests matches the expected value based on -// the total capacity and the duration of the test. -// Some nodes are sending requests occasionally so that their buffer should regularly -// reach the maximum while other nodes (the "max capacity nodes") are sending at the -// maximum permitted rate. The max capacity nodes are changed multiple times during -// a single test. -func TestConstantTotalCapacity(t *testing.T) { - testConstantTotalCapacity(t, 10, 1, 0, false) - testConstantTotalCapacity(t, 10, 1, 1, false) - testConstantTotalCapacity(t, 30, 1, 0, false) - testConstantTotalCapacity(t, 30, 2, 3, false) - testConstantTotalCapacity(t, 100, 1, 0, false) - testConstantTotalCapacity(t, 100, 3, 5, false) - testConstantTotalCapacity(t, 100, 5, 10, false) - testConstantTotalCapacity(t, 100, 3, 5, true) -} - -func testConstantTotalCapacity(t *testing.T, nodeCount, maxCapacityNodes, randomSend int, priorityOverflow bool) { - clock := &mclock.Simulated{} - nodes := make([]*testNode, nodeCount) - var totalCapacity uint64 - for i := range nodes { - nodes[i] = &testNode{capacity: uint64(50000 + rand.Intn(100000))} - totalCapacity += nodes[i].capacity - } - m := NewClientManager(PieceWiseLinear{{0, totalCapacity}}, clock) - if priorityOverflow { - // provoke a situation where rcLastUpdate overflow needs to be handled - m.rcLastIntValue = math.MaxInt64 - 10000000000 - } - for _, n := range nodes { - n.bufLimit = n.capacity * 6000 - n.node = NewClientNode(m, ServerParams{BufLimit: n.bufLimit, MinRecharge: n.capacity}) - } - maxNodes := make([]int, maxCapacityNodes) - for i := range maxNodes { - // we don't care if some indexes are selected multiple times - // in that case we have fewer max nodes - maxNodes[i] = rand.Intn(nodeCount) - } - - var sendCount int - for i := 0; i < testLength; i++ { - now := clock.Now() - for _, idx := range maxNodes { - for nodes[idx].send(t, now) { - } - } - if rand.Intn(testLength) < maxCapacityNodes*3 { - maxNodes[rand.Intn(maxCapacityNodes)] = rand.Intn(nodeCount) - } - - sendCount += randomSend - failCount := randomSend * 10 - for sendCount > 0 && failCount > 0 { - if nodes[rand.Intn(nodeCount)].send(t, now) { - sendCount-- - } else { - failCount-- - } - } - clock.Run(time.Millisecond) - } - - var totalCost uint64 - for _, n := range nodes { - totalCost += n.totalCost - } - ratio := float64(totalCost) / float64(totalCapacity) / testLength - if ratio < 0.98 || ratio > 1.02 { - t.Errorf("totalCost/totalCapacity/testLength ratio incorrect (expected: 1, got: %f)", ratio) - } -} - -func (n *testNode) send(t *testing.T, now mclock.AbsTime) bool { - if now < n.waitUntil { - return false - } - n.index++ - if ok, _, _ := n.node.AcceptRequest(0, n.index, testMaxCost); !ok { - t.Fatalf("Rejected request after expected waiting time has passed") - } - rcost := uint64(rand.Int63n(testMaxCost)) - bv := n.node.RequestProcessed(0, n.index, testMaxCost, rcost) - if bv < testMaxCost { - n.waitUntil = now + mclock.AbsTime((testMaxCost-bv)*1001000/n.capacity) - } - n.totalCost += rcost - return true -} diff --git a/les/handler_test.go b/les/handler_test.go deleted file mode 100644 index c803a5ddb3..0000000000 --- a/les/handler_test.go +++ /dev/null @@ -1,754 +0,0 @@ -// Copyright 2016 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 les - -import ( - "encoding/binary" - "math/big" - "math/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -func expectResponse(r p2p.MsgReader, msgcode, reqID, bv uint64, data interface{}) error { - type resp struct { - ReqID, BV uint64 - Data interface{} - } - return p2p.ExpectMsg(r, msgcode, resp{reqID, bv, data}) -} - -// Tests that block headers can be retrieved from a remote chain based on user queries. -func TestGetBlockHeadersLes2(t *testing.T) { testGetBlockHeaders(t, 2) } -func TestGetBlockHeadersLes3(t *testing.T) { testGetBlockHeaders(t, 3) } -func TestGetBlockHeadersLes4(t *testing.T) { testGetBlockHeaders(t, 4) } - -func testGetBlockHeaders(t *testing.T, protocol int) { - netconfig := testnetConfig{ - blocks: downloader.MaxHeaderFetch + 15, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - bc := server.handler.blockchain - - // Create a "random" unknown hash for testing - var unknown common.Hash - for i := range unknown { - unknown[i] = byte(i) - } - // Create a batch of tests for various scenarios - limit := uint64(MaxHeaderFetch) - tests := []struct { - query *GetBlockHeadersData // The query to execute for header retrieval - expect []common.Hash // The hashes of the block whose headers are expected - }{ - // A single random block should be retrievable by hash and number too - { - &GetBlockHeadersData{Origin: hashOrNumber{Hash: bc.GetBlockByNumber(limit / 2).Hash()}, Amount: 1}, - []common.Hash{bc.GetBlockByNumber(limit / 2).Hash()}, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: limit / 2}, Amount: 1}, - []common.Hash{bc.GetBlockByNumber(limit / 2).Hash()}, - }, - // Multiple headers should be retrievable in both directions - { - &GetBlockHeadersData{Origin: hashOrNumber{Number: limit / 2}, Amount: 3}, - []common.Hash{ - bc.GetBlockByNumber(limit / 2).Hash(), - bc.GetBlockByNumber(limit/2 + 1).Hash(), - bc.GetBlockByNumber(limit/2 + 2).Hash(), - }, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: limit / 2}, Amount: 3, Reverse: true}, - []common.Hash{ - bc.GetBlockByNumber(limit / 2).Hash(), - bc.GetBlockByNumber(limit/2 - 1).Hash(), - bc.GetBlockByNumber(limit/2 - 2).Hash(), - }, - }, - // Multiple headers with skip lists should be retrievable - { - &GetBlockHeadersData{Origin: hashOrNumber{Number: limit / 2}, Skip: 3, Amount: 3}, - []common.Hash{ - bc.GetBlockByNumber(limit / 2).Hash(), - bc.GetBlockByNumber(limit/2 + 4).Hash(), - bc.GetBlockByNumber(limit/2 + 8).Hash(), - }, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: limit / 2}, Skip: 3, Amount: 3, Reverse: true}, - []common.Hash{ - bc.GetBlockByNumber(limit / 2).Hash(), - bc.GetBlockByNumber(limit/2 - 4).Hash(), - bc.GetBlockByNumber(limit/2 - 8).Hash(), - }, - }, - // The chain endpoints should be retrievable - { - &GetBlockHeadersData{Origin: hashOrNumber{Number: 0}, Amount: 1}, - []common.Hash{bc.GetBlockByNumber(0).Hash()}, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: bc.CurrentBlock().Number.Uint64()}, Amount: 1}, - []common.Hash{bc.CurrentBlock().Hash()}, - }, - // Ensure protocol limits are honored - //{ - // &GetBlockHeadersData{Origin: hashOrNumber{Number: bc.CurrentBlock().Number.Uint64()() - 1}, Amount: limit + 10, Reverse: true}, - // []common.Hash{}, - //}, - // Check that requesting more than available is handled gracefully - { - &GetBlockHeadersData{Origin: hashOrNumber{Number: bc.CurrentBlock().Number.Uint64() - 4}, Skip: 3, Amount: 3}, - []common.Hash{ - bc.GetBlockByNumber(bc.CurrentBlock().Number.Uint64() - 4).Hash(), - bc.GetBlockByNumber(bc.CurrentBlock().Number.Uint64()).Hash(), - }, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: 4}, Skip: 3, Amount: 3, Reverse: true}, - []common.Hash{ - bc.GetBlockByNumber(4).Hash(), - bc.GetBlockByNumber(0).Hash(), - }, - }, - // Check that requesting more than available is handled gracefully, even if mid skip - { - &GetBlockHeadersData{Origin: hashOrNumber{Number: bc.CurrentBlock().Number.Uint64() - 4}, Skip: 2, Amount: 3}, - []common.Hash{ - bc.GetBlockByNumber(bc.CurrentBlock().Number.Uint64() - 4).Hash(), - bc.GetBlockByNumber(bc.CurrentBlock().Number.Uint64() - 1).Hash(), - }, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: 4}, Skip: 2, Amount: 3, Reverse: true}, - []common.Hash{ - bc.GetBlockByNumber(4).Hash(), - bc.GetBlockByNumber(1).Hash(), - }, - }, - // Check that non existing headers aren't returned - { - &GetBlockHeadersData{Origin: hashOrNumber{Hash: unknown}, Amount: 1}, - []common.Hash{}, - }, { - &GetBlockHeadersData{Origin: hashOrNumber{Number: bc.CurrentBlock().Number.Uint64() + 1}, Amount: 1}, - []common.Hash{}, - }, - } - // Run each of the tests and verify the results against the chain - var reqID uint64 - for i, tt := range tests { - // Collect the headers to expect in the response - var headers []*types.Header - for _, hash := range tt.expect { - headers = append(headers, bc.GetHeaderByHash(hash)) - } - // Send the hash request and verify the response - reqID++ - - sendRequest(rawPeer.app, GetBlockHeadersMsg, reqID, tt.query) - if err := expectResponse(rawPeer.app, BlockHeadersMsg, reqID, testBufLimit, headers); err != nil { - t.Errorf("test %d: headers mismatch: %v", i, err) - } - } -} - -// Tests that block contents can be retrieved from a remote chain based on their hashes. -func TestGetBlockBodiesLes2(t *testing.T) { testGetBlockBodies(t, 2) } -func TestGetBlockBodiesLes3(t *testing.T) { testGetBlockBodies(t, 3) } -func TestGetBlockBodiesLes4(t *testing.T) { testGetBlockBodies(t, 4) } - -func testGetBlockBodies(t *testing.T, protocol int) { - netconfig := testnetConfig{ - blocks: downloader.MaxHeaderFetch + 15, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - // Create a batch of tests for various scenarios - limit := MaxBodyFetch - tests := []struct { - random int // Number of blocks to fetch randomly from the chain - explicit []common.Hash // Explicitly requested blocks - available []bool // Availability of explicitly requested blocks - expected int // Total number of existing blocks to expect - }{ - {1, nil, nil, 1}, // A single random block should be retrievable - {10, nil, nil, 10}, // Multiple random blocks should be retrievable - {limit, nil, nil, limit}, // The maximum possible blocks should be retrievable - //{limit + 1, nil, nil, limit}, // No more than the possible block count should be returned - {0, []common.Hash{bc.Genesis().Hash()}, []bool{true}, 1}, // The genesis block should be retrievable - {0, []common.Hash{bc.CurrentBlock().Hash()}, []bool{true}, 1}, // The chains head block should be retrievable - {0, []common.Hash{{}}, []bool{false}, 0}, // A non existent block should not be returned - - // Existing and non-existing blocks interleaved should not cause problems - {0, []common.Hash{ - {}, - bc.GetBlockByNumber(1).Hash(), - {}, - bc.GetBlockByNumber(10).Hash(), - {}, - bc.GetBlockByNumber(100).Hash(), - {}, - }, []bool{false, true, false, true, false, true, false}, 3}, - } - // Run each of the tests and verify the results against the chain - var reqID uint64 - for i, tt := range tests { - // Collect the hashes to request, and the response to expect - var hashes []common.Hash - seen := make(map[int64]bool) - var bodies []*types.Body - - for j := 0; j < tt.random; j++ { - for { - num := rand.Int63n(int64(bc.CurrentBlock().Number.Uint64())) - if !seen[num] { - seen[num] = true - - block := bc.GetBlockByNumber(uint64(num)) - hashes = append(hashes, block.Hash()) - if len(bodies) < tt.expected { - bodies = append(bodies, &types.Body{Transactions: block.Transactions(), Uncles: block.Uncles()}) - } - break - } - } - } - for j, hash := range tt.explicit { - hashes = append(hashes, hash) - if tt.available[j] && len(bodies) < tt.expected { - block := bc.GetBlockByHash(hash) - bodies = append(bodies, &types.Body{Transactions: block.Transactions(), Uncles: block.Uncles()}) - } - } - reqID++ - - // Send the hash request and verify the response - sendRequest(rawPeer.app, GetBlockBodiesMsg, reqID, hashes) - if err := expectResponse(rawPeer.app, BlockBodiesMsg, reqID, testBufLimit, bodies); err != nil { - t.Errorf("test %d: bodies mismatch: %v", i, err) - } - } -} - -// Tests that the contract codes can be retrieved based on account addresses. -func TestGetCodeLes2(t *testing.T) { testGetCode(t, 2) } -func TestGetCodeLes3(t *testing.T) { testGetCode(t, 3) } -func TestGetCodeLes4(t *testing.T) { testGetCode(t, 4) } - -func testGetCode(t *testing.T, protocol int) { - // Assemble the test environment - netconfig := testnetConfig{ - blocks: 4, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - var codereqs []*CodeReq - var codes [][]byte - for i := uint64(0); i <= bc.CurrentBlock().Number.Uint64(); i++ { - header := bc.GetHeaderByNumber(i) - req := &CodeReq{ - BHash: header.Hash(), - AccountAddress: testContractAddr[:], - } - codereqs = append(codereqs, req) - if i >= testContractDeployed { - codes = append(codes, testContractCodeDeployed) - } - } - - sendRequest(rawPeer.app, GetCodeMsg, 42, codereqs) - if err := expectResponse(rawPeer.app, CodeMsg, 42, testBufLimit, codes); err != nil { - t.Errorf("codes mismatch: %v", err) - } -} - -// Tests that the stale contract codes can't be retrieved based on account addresses. -func TestGetStaleCodeLes2(t *testing.T) { testGetStaleCode(t, 2) } -func TestGetStaleCodeLes3(t *testing.T) { testGetStaleCode(t, 3) } -func TestGetStaleCodeLes4(t *testing.T) { testGetStaleCode(t, 4) } - -func testGetStaleCode(t *testing.T, protocol int) { - netconfig := testnetConfig{ - blocks: core.TriesInMemory + 4, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - check := func(number uint64, expected [][]byte) { - req := &CodeReq{ - BHash: bc.GetHeaderByNumber(number).Hash(), - AccountAddress: testContractAddr[:], - } - sendRequest(rawPeer.app, GetCodeMsg, 42, []*CodeReq{req}) - if err := expectResponse(rawPeer.app, CodeMsg, 42, testBufLimit, expected); err != nil { - t.Errorf("codes mismatch: %v", err) - } - } - check(0, [][]byte{}) // Non-exist contract - check(testContractDeployed, [][]byte{}) // Stale contract - check(bc.CurrentHeader().Number.Uint64(), [][]byte{testContractCodeDeployed}) // Fresh contract -} - -// Tests that the transaction receipts can be retrieved based on hashes. -func TestGetReceiptLes2(t *testing.T) { testGetReceipt(t, 2) } -func TestGetReceiptLes3(t *testing.T) { testGetReceipt(t, 3) } -func TestGetReceiptLes4(t *testing.T) { testGetReceipt(t, 4) } - -func testGetReceipt(t *testing.T, protocol int) { - // Assemble the test environment - netconfig := testnetConfig{ - blocks: 4, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - // Collect the hashes to request, and the response to expect - var receipts []types.Receipts - var hashes []common.Hash - for i := uint64(0); i <= bc.CurrentBlock().Number.Uint64(); i++ { - block := bc.GetBlockByNumber(i) - - hashes = append(hashes, block.Hash()) - receipts = append(receipts, rawdb.ReadReceipts(server.db, block.Hash(), block.NumberU64(), block.Time(), bc.Config())) - } - // Send the hash request and verify the response - sendRequest(rawPeer.app, GetReceiptsMsg, 42, hashes) - if err := expectResponse(rawPeer.app, ReceiptsMsg, 42, testBufLimit, receipts); err != nil { - t.Errorf("receipts mismatch: %v", err) - } -} - -// Tests that trie merkle proofs can be retrieved -func TestGetProofsLes2(t *testing.T) { testGetProofs(t, 2) } -func TestGetProofsLes3(t *testing.T) { testGetProofs(t, 3) } -func TestGetProofsLes4(t *testing.T) { testGetProofs(t, 4) } - -func testGetProofs(t *testing.T, protocol int) { - // Assemble the test environment - netconfig := testnetConfig{ - blocks: 4, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - var proofreqs []ProofReq - proofsV2 := trienode.NewProofSet() - - accounts := []common.Address{bankAddr, userAddr1, userAddr2, signerAddr, {}} - for i := uint64(0); i <= bc.CurrentBlock().Number.Uint64(); i++ { - header := bc.GetHeaderByNumber(i) - trie, _ := trie.New(trie.StateTrieID(header.Root), server.backend.Blockchain().TrieDB()) - - for _, acc := range accounts { - req := ProofReq{ - BHash: header.Hash(), - Key: crypto.Keccak256(acc[:]), - } - proofreqs = append(proofreqs, req) - trie.Prove(crypto.Keccak256(acc[:]), proofsV2) - } - } - // Send the proof request and verify the response - sendRequest(rawPeer.app, GetProofsV2Msg, 42, proofreqs) - if err := expectResponse(rawPeer.app, ProofsV2Msg, 42, testBufLimit, proofsV2.List()); err != nil { - t.Errorf("proofs mismatch: %v", err) - } -} - -// Tests that the stale contract codes can't be retrieved based on account addresses. -func TestGetStaleProofLes2(t *testing.T) { testGetStaleProof(t, 2) } -func TestGetStaleProofLes3(t *testing.T) { testGetStaleProof(t, 3) } -func TestGetStaleProofLes4(t *testing.T) { testGetStaleProof(t, 4) } - -func testGetStaleProof(t *testing.T, protocol int) { - netconfig := testnetConfig{ - blocks: core.TriesInMemory + 4, - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - check := func(number uint64, wantOK bool) { - var ( - header = bc.GetHeaderByNumber(number) - account = crypto.Keccak256(userAddr1.Bytes()) - ) - req := &ProofReq{ - BHash: header.Hash(), - Key: account, - } - sendRequest(rawPeer.app, GetProofsV2Msg, 42, []*ProofReq{req}) - - var expected []rlp.RawValue - if wantOK { - proofsV2 := trienode.NewProofSet() - t, _ := trie.New(trie.StateTrieID(header.Root), server.backend.Blockchain().TrieDB()) - t.Prove(account, proofsV2) - expected = proofsV2.List() - } - if err := expectResponse(rawPeer.app, ProofsV2Msg, 42, testBufLimit, expected); err != nil { - t.Errorf("codes mismatch: %v", err) - } - } - check(0, false) // Non-exist proof - check(2, false) // Stale proof - check(bc.CurrentHeader().Number.Uint64(), true) // Fresh proof -} - -// Tests that CHT proofs can be correctly retrieved. -func TestGetCHTProofsLes2(t *testing.T) { testGetCHTProofs(t, 2) } -func TestGetCHTProofsLes3(t *testing.T) { testGetCHTProofs(t, 3) } -func TestGetCHTProofsLes4(t *testing.T) { testGetCHTProofs(t, 4) } - -func testGetCHTProofs(t *testing.T, protocol int) { - var ( - config = light.TestServerIndexerConfig - waitIndexers = func(cIndexer, bIndexer, btIndexer *core.ChainIndexer) { - for { - cs, _, _ := cIndexer.Sections() - if cs >= 1 { - break - } - time.Sleep(10 * time.Millisecond) - } - } - netconfig = testnetConfig{ - blocks: int(config.ChtSize + config.ChtConfirms), - protocol: protocol, - indexFn: waitIndexers, - nopruning: true, - } - ) - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - // Assemble the proofs from the different protocols - header := bc.GetHeaderByNumber(config.ChtSize - 1) - rlp, _ := rlp.EncodeToBytes(header) - - key := make([]byte, 8) - binary.BigEndian.PutUint64(key, config.ChtSize-1) - - proofsV2 := HelperTrieResps{ - AuxData: [][]byte{rlp}, - } - root := light.GetChtRoot(server.db, 0, bc.GetHeaderByNumber(config.ChtSize-1).Hash()) - trie, _ := trie.New(trie.TrieID(root), trie.NewDatabase(rawdb.NewTable(server.db, string(rawdb.ChtTablePrefix)), trie.HashDefaults)) - trie.Prove(key, &proofsV2.Proofs) - // Assemble the requests for the different protocols - requestsV2 := []HelperTrieReq{{ - Type: htCanonical, - TrieIdx: 0, - Key: key, - AuxReq: htAuxHeader, - }} - // Send the proof request and verify the response - sendRequest(rawPeer.app, GetHelperTrieProofsMsg, 42, requestsV2) - if err := expectResponse(rawPeer.app, HelperTrieProofsMsg, 42, testBufLimit, proofsV2); err != nil { - t.Errorf("proofs mismatch: %v", err) - } -} - -func TestGetBloombitsProofsLes2(t *testing.T) { testGetBloombitsProofs(t, 2) } -func TestGetBloombitsProofsLes3(t *testing.T) { testGetBloombitsProofs(t, 3) } -func TestGetBloombitsProofsLes4(t *testing.T) { testGetBloombitsProofs(t, 4) } - -// Tests that bloombits proofs can be correctly retrieved. -func testGetBloombitsProofs(t *testing.T, protocol int) { - var ( - config = light.TestServerIndexerConfig - waitIndexers = func(cIndexer, bIndexer, btIndexer *core.ChainIndexer) { - for { - bts, _, _ := btIndexer.Sections() - if bts >= 1 { - break - } - time.Sleep(10 * time.Millisecond) - } - } - netconfig = testnetConfig{ - blocks: int(config.BloomTrieSize + config.BloomTrieConfirms), - protocol: protocol, - indexFn: waitIndexers, - nopruning: true, - } - ) - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - bc := server.handler.blockchain - - // Request and verify each bit of the bloom bits proofs - for bit := 0; bit < 2048; bit++ { - // Assemble the request and proofs for the bloombits - key := make([]byte, 10) - - binary.BigEndian.PutUint16(key[:2], uint16(bit)) - // Only the first bloom section has data. - binary.BigEndian.PutUint64(key[2:], 0) - - requests := []HelperTrieReq{{ - Type: htBloomBits, - TrieIdx: 0, - Key: key, - }} - var proofs HelperTrieResps - - root := light.GetBloomTrieRoot(server.db, 0, bc.GetHeaderByNumber(config.BloomTrieSize-1).Hash()) - trie, _ := trie.New(trie.TrieID(root), trie.NewDatabase(rawdb.NewTable(server.db, string(rawdb.BloomTrieTablePrefix)), trie.HashDefaults)) - trie.Prove(key, &proofs.Proofs) - - // Send the proof request and verify the response - sendRequest(rawPeer.app, GetHelperTrieProofsMsg, 42, requests) - if err := expectResponse(rawPeer.app, HelperTrieProofsMsg, 42, testBufLimit, proofs); err != nil { - t.Errorf("bit %d: proofs mismatch: %v", bit, err) - } - } -} - -func TestTransactionStatusLes2(t *testing.T) { testTransactionStatus(t, lpv2) } -func TestTransactionStatusLes3(t *testing.T) { testTransactionStatus(t, lpv3) } -func TestTransactionStatusLes4(t *testing.T) { testTransactionStatus(t, lpv4) } - -func testTransactionStatus(t *testing.T, protocol int) { - netconfig := testnetConfig{ - protocol: protocol, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - server.handler.addTxsSync = true - - chain := server.handler.blockchain - - var reqID uint64 - - test := func(tx *types.Transaction, send bool, expStatus light.TxStatus) { - t.Helper() - - reqID++ - if send { - sendRequest(rawPeer.app, SendTxV2Msg, reqID, types.Transactions{tx}) - } else { - sendRequest(rawPeer.app, GetTxStatusMsg, reqID, []common.Hash{tx.Hash()}) - } - if err := expectResponse(rawPeer.app, TxStatusMsg, reqID, testBufLimit, []light.TxStatus{expStatus}); err != nil { - t.Errorf("transaction status mismatch: %v", err) - } - } - signer := types.HomesteadSigner{} - - // test error status by sending an underpriced transaction - tx0, _ := types.SignTx(types.NewTransaction(0, userAddr1, big.NewInt(10000), params.TxGas, nil, nil), signer, bankKey) - test(tx0, true, light.TxStatus{Status: txpool.TxStatusUnknown, Error: "transaction underpriced: tip needed 1, tip permitted 0"}) - - tx1, _ := types.SignTx(types.NewTransaction(0, userAddr1, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, bankKey) - test(tx1, false, light.TxStatus{Status: txpool.TxStatusUnknown}) // query before sending, should be unknown - test(tx1, true, light.TxStatus{Status: txpool.TxStatusPending}) // send valid processable tx, should return pending - test(tx1, true, light.TxStatus{Status: txpool.TxStatusPending}) // adding it again should not return an error - - tx2, _ := types.SignTx(types.NewTransaction(1, userAddr1, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, bankKey) - tx3, _ := types.SignTx(types.NewTransaction(2, userAddr1, big.NewInt(10000), params.TxGas, big.NewInt(100000000000), nil), signer, bankKey) - // send transactions in the wrong order, tx3 should be queued - test(tx3, true, light.TxStatus{Status: txpool.TxStatusQueued}) - test(tx2, true, light.TxStatus{Status: txpool.TxStatusPending}) - // query again, now tx3 should be pending too - test(tx3, false, light.TxStatus{Status: txpool.TxStatusPending}) - - // generate and add a block with tx1 and tx2 included - gchain, _ := core.GenerateChain(params.TestChainConfig, chain.GetBlockByNumber(0), ethash.NewFaker(), server.db, 1, func(i int, block *core.BlockGen) { - block.AddTx(tx1) - block.AddTx(tx2) - }) - if _, err := chain.InsertChain(gchain); err != nil { - panic(err) - } - // wait until TxPool processes the inserted block - for i := 0; i < 10; i++ { - if pending, _ := server.handler.txpool.Stats(); pending == 1 { - break - } - time.Sleep(100 * time.Millisecond) - } - if pending, _ := server.handler.txpool.Stats(); pending != 1 { - t.Fatalf("pending count mismatch: have %d, want 1", pending) - } - // Discard new block announcement - msg, _ := rawPeer.app.ReadMsg() - msg.Discard() - - // check if their status is included now - block1hash := rawdb.ReadCanonicalHash(server.db, 1) - test(tx1, false, light.TxStatus{Status: txpool.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 0}}) - - test(tx2, false, light.TxStatus{Status: txpool.TxStatusIncluded, Lookup: &rawdb.LegacyTxLookupEntry{BlockHash: block1hash, BlockIndex: 1, Index: 1}}) - - // create a reorg that rolls them back - gchain, _ = core.GenerateChain(params.TestChainConfig, chain.GetBlockByNumber(0), ethash.NewFaker(), server.db, 2, func(i int, block *core.BlockGen) {}) - if _, err := chain.InsertChain(gchain); err != nil { - panic(err) - } - // wait until TxPool processes the reorg - for i := 0; i < 10; i++ { - if pending, _ := server.handler.txpool.Stats(); pending == 3 { - break - } - time.Sleep(100 * time.Millisecond) - } - if pending, _ := server.handler.txpool.Stats(); pending != 3 { - t.Fatalf("pending count mismatch: have %d, want 3", pending) - } - // Discard new block announcement - msg, _ = rawPeer.app.ReadMsg() - msg.Discard() - - // check if their status is pending again - test(tx1, false, light.TxStatus{Status: txpool.TxStatusPending}) - test(tx2, false, light.TxStatus{Status: txpool.TxStatusPending}) -} - -func TestStopResumeLES3(t *testing.T) { testStopResume(t, lpv3) } -func TestStopResumeLES4(t *testing.T) { testStopResume(t, lpv4) } - -func testStopResume(t *testing.T, protocol int) { - netconfig := testnetConfig{ - protocol: protocol, - simClock: true, - nopruning: true, - } - server, _, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - server.handler.server.costTracker.testing = true - server.handler.server.costTracker.testCostList = testCostList(testBufLimit / 10) - - rawPeer, closePeer, _ := server.newRawPeer(t, "peer", protocol) - defer closePeer() - - var ( - reqID uint64 - expBuf = testBufLimit - testCost = testBufLimit / 10 - ) - header := server.handler.blockchain.CurrentHeader() - req := func() { - reqID++ - sendRequest(rawPeer.app, GetBlockHeadersMsg, reqID, &GetBlockHeadersData{Origin: hashOrNumber{Hash: header.Hash()}, Amount: 1}) - } - for i := 1; i <= 5; i++ { - // send requests while we still have enough buffer and expect a response - for expBuf >= testCost { - req() - expBuf -= testCost - if err := expectResponse(rawPeer.app, BlockHeadersMsg, reqID, expBuf, []*types.Header{header}); err != nil { - t.Errorf("expected response and failed: %v", err) - } - } - // send some more requests in excess and expect a single StopMsg - c := i - for c > 0 { - req() - c-- - } - if err := p2p.ExpectMsg(rawPeer.app, StopMsg, nil); err != nil { - t.Errorf("expected StopMsg and failed: %v", err) - } - // wait until the buffer is recharged by half of the limit - wait := testBufLimit / testBufRecharge / 2 - server.clock.(*mclock.Simulated).Run(time.Millisecond * time.Duration(wait)) - - // expect a ResumeMsg with the partially recharged buffer value - expBuf += testBufRecharge * wait - if err := p2p.ExpectMsg(rawPeer.app, ResumeMsg, expBuf); err != nil { - t.Errorf("expected ResumeMsg and failed: %v", err) - } - } -} diff --git a/les/metrics.go b/les/metrics.go deleted file mode 100644 index 07d3133c95..0000000000 --- a/les/metrics.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2016 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 les - -import ( - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" -) - -var ( - miscInPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/total", nil) - miscInTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/total", nil) - miscInHeaderPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/header", nil) - miscInHeaderTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/header", nil) - miscInBodyPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/body", nil) - miscInBodyTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/body", nil) - miscInCodePacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/code", nil) - miscInCodeTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/code", nil) - miscInReceiptPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/receipt", nil) - miscInReceiptTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/receipt", nil) - miscInTrieProofPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/proof", nil) - miscInTrieProofTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/proof", nil) - miscInHelperTriePacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/helperTrie", nil) - miscInHelperTrieTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/helperTrie", nil) - miscInTxsPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/txs", nil) - miscInTxsTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/txs", nil) - miscInTxStatusPacketsMeter = metrics.NewRegisteredMeter("les/misc/in/packets/txStatus", nil) - miscInTxStatusTrafficMeter = metrics.NewRegisteredMeter("les/misc/in/traffic/txStatus", nil) - - miscOutPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/total", nil) - miscOutTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/total", nil) - miscOutHeaderPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/header", nil) - miscOutHeaderTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/header", nil) - miscOutBodyPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/body", nil) - miscOutBodyTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/body", nil) - miscOutCodePacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/code", nil) - miscOutCodeTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/code", nil) - miscOutReceiptPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/receipt", nil) - miscOutReceiptTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/receipt", nil) - miscOutTrieProofPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/proof", nil) - miscOutTrieProofTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/proof", nil) - miscOutHelperTriePacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/helperTrie", nil) - miscOutHelperTrieTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/helperTrie", nil) - miscOutTxsPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/txs", nil) - miscOutTxsTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/txs", nil) - miscOutTxStatusPacketsMeter = metrics.NewRegisteredMeter("les/misc/out/packets/txStatus", nil) - miscOutTxStatusTrafficMeter = metrics.NewRegisteredMeter("les/misc/out/traffic/txStatus", nil) - - miscServingTimeHeaderTimer = metrics.NewRegisteredTimer("les/misc/serve/header", nil) - miscServingTimeBodyTimer = metrics.NewRegisteredTimer("les/misc/serve/body", nil) - miscServingTimeCodeTimer = metrics.NewRegisteredTimer("les/misc/serve/code", nil) - miscServingTimeReceiptTimer = metrics.NewRegisteredTimer("les/misc/serve/receipt", nil) - miscServingTimeTrieProofTimer = metrics.NewRegisteredTimer("les/misc/serve/proof", nil) - miscServingTimeHelperTrieTimer = metrics.NewRegisteredTimer("les/misc/serve/helperTrie", nil) - miscServingTimeTxTimer = metrics.NewRegisteredTimer("les/misc/serve/txs", nil) - miscServingTimeTxStatusTimer = metrics.NewRegisteredTimer("les/misc/serve/txStatus", nil) - - connectionTimer = metrics.NewRegisteredTimer("les/connection/duration", nil) - serverConnectionGauge = metrics.NewRegisteredGauge("les/connection/server", nil) - - totalCapacityGauge = metrics.NewRegisteredGauge("les/server/totalCapacity", nil) - totalRechargeGauge = metrics.NewRegisteredGauge("les/server/totalRecharge", nil) - blockProcessingTimer = metrics.NewRegisteredTimer("les/server/blockProcessingTime", nil) - - requestServedMeter = metrics.NewRegisteredMeter("les/server/req/avgServedTime", nil) - requestServedTimer = metrics.NewRegisteredTimer("les/server/req/servedTime", nil) - requestEstimatedMeter = metrics.NewRegisteredMeter("les/server/req/avgEstimatedTime", nil) - requestEstimatedTimer = metrics.NewRegisteredTimer("les/server/req/estimatedTime", nil) - relativeCostHistogram = metrics.NewRegisteredHistogram("les/server/req/relative", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostHeaderHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/header", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostBodyHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/body", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostReceiptHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/receipt", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostCodeHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/code", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostProofHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/proof", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostHelperProofHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/helperTrie", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostSendTxHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/txs", nil, metrics.NewExpDecaySample(1028, 0.015)) - relativeCostTxStatusHistogram = metrics.NewRegisteredHistogram("les/server/req/relative/txStatus", nil, metrics.NewExpDecaySample(1028, 0.015)) - - globalFactorGauge = metrics.NewRegisteredGauge("les/server/globalFactor", nil) - recentServedGauge = metrics.NewRegisteredGauge("les/server/recentRequestServed", nil) - recentEstimatedGauge = metrics.NewRegisteredGauge("les/server/recentRequestEstimated", nil) - sqServedGauge = metrics.NewRegisteredGauge("les/server/servingQueue/served", nil) - sqQueuedGauge = metrics.NewRegisteredGauge("les/server/servingQueue/queued", nil) - - clientFreezeMeter = metrics.NewRegisteredMeter("les/server/clientEvent/freeze", nil) - clientErrorMeter = metrics.NewRegisteredMeter("les/server/clientEvent/error", nil) - - requestRTT = metrics.NewRegisteredTimer("les/client/req/rtt", nil) - requestSendDelay = metrics.NewRegisteredTimer("les/client/req/sendDelay", nil) - - serverSelectableGauge = metrics.NewRegisteredGauge("les/client/serverPool/selectable", nil) - serverDialedMeter = metrics.NewRegisteredMeter("les/client/serverPool/dialed", nil) - serverConnectedGauge = metrics.NewRegisteredGauge("les/client/serverPool/connected", nil) - sessionValueMeter = metrics.NewRegisteredMeter("les/client/serverPool/sessionValue", nil) - totalValueGauge = metrics.NewRegisteredGauge("les/client/serverPool/totalValue", nil) - suggestedTimeoutGauge = metrics.NewRegisteredGauge("les/client/serverPool/timeout", nil) -) - -// meteredMsgReadWriter is a wrapper around a p2p.MsgReadWriter, capable of -// accumulating the above defined metrics based on the data stream contents. -type meteredMsgReadWriter struct { - p2p.MsgReadWriter // Wrapped message stream to meter - version int // Protocol version to select correct meters -} - -// newMeteredMsgWriter wraps a p2p MsgReadWriter with metering support. If the -// metrics system is disabled, this function returns the original object. -func newMeteredMsgWriter(rw p2p.MsgReadWriter, version int) p2p.MsgReadWriter { - if !metrics.Enabled { - return rw - } - return &meteredMsgReadWriter{MsgReadWriter: rw, version: version} -} - -func (rw *meteredMsgReadWriter) ReadMsg() (p2p.Msg, error) { - // Read the message and short circuit in case of an error - msg, err := rw.MsgReadWriter.ReadMsg() - if err != nil { - return msg, err - } - // Account for the data traffic - packets, traffic := miscInPacketsMeter, miscInTrafficMeter - packets.Mark(1) - traffic.Mark(int64(msg.Size)) - - return msg, err -} - -func (rw *meteredMsgReadWriter) WriteMsg(msg p2p.Msg) error { - // Account for the data traffic - packets, traffic := miscOutPacketsMeter, miscOutTrafficMeter - packets.Mark(1) - traffic.Mark(int64(msg.Size)) - - // Send the packet to the p2p layer - return rw.MsgReadWriter.WriteMsg(msg) -} diff --git a/les/odr.go b/les/odr.go deleted file mode 100644 index 943b05fdfc..0000000000 --- a/les/odr.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2016 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 les - -import ( - "context" - "math/rand" - "sort" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/light" -) - -// LesOdr implements light.OdrBackend -type LesOdr struct { - db ethdb.Database - indexerConfig *light.IndexerConfig - chtIndexer, bloomTrieIndexer, bloomIndexer *core.ChainIndexer - peers *serverPeerSet - retriever *retrieveManager - stop chan struct{} -} - -func NewLesOdr(db ethdb.Database, config *light.IndexerConfig, peers *serverPeerSet, retriever *retrieveManager) *LesOdr { - return &LesOdr{ - db: db, - indexerConfig: config, - peers: peers, - retriever: retriever, - stop: make(chan struct{}), - } -} - -// Stop cancels all pending retrievals -func (odr *LesOdr) Stop() { - close(odr.stop) -} - -// Database returns the backing database -func (odr *LesOdr) Database() ethdb.Database { - return odr.db -} - -// SetIndexers adds the necessary chain indexers to the ODR backend -func (odr *LesOdr) SetIndexers(chtIndexer, bloomTrieIndexer, bloomIndexer *core.ChainIndexer) { - odr.chtIndexer = chtIndexer - odr.bloomTrieIndexer = bloomTrieIndexer - odr.bloomIndexer = bloomIndexer -} - -// ChtIndexer returns the CHT chain indexer -func (odr *LesOdr) ChtIndexer() *core.ChainIndexer { - return odr.chtIndexer -} - -// BloomTrieIndexer returns the bloom trie chain indexer -func (odr *LesOdr) BloomTrieIndexer() *core.ChainIndexer { - return odr.bloomTrieIndexer -} - -// BloomIndexer returns the bloombits chain indexer -func (odr *LesOdr) BloomIndexer() *core.ChainIndexer { - return odr.bloomIndexer -} - -// IndexerConfig returns the indexer config. -func (odr *LesOdr) IndexerConfig() *light.IndexerConfig { - return odr.indexerConfig -} - -const ( - MsgBlockHeaders = iota - MsgBlockBodies - MsgCode - MsgReceipts - MsgProofsV2 - MsgHelperTrieProofs - MsgTxStatus -) - -// Msg encodes a LES message that delivers reply data for a request -type Msg struct { - MsgType int - ReqID uint64 - Obj interface{} -} - -// peerByTxHistory is a heap.Interface implementation which can sort -// the peerset by transaction history. -type peerByTxHistory []*serverPeer - -func (h peerByTxHistory) Len() int { return len(h) } -func (h peerByTxHistory) Less(i, j int) bool { - if h[i].txHistory == txIndexUnlimited { - return false - } - if h[j].txHistory == txIndexUnlimited { - return true - } - return h[i].txHistory < h[j].txHistory -} -func (h peerByTxHistory) Swap(i, j int) { h[i], h[j] = h[j], h[i] } - -const ( - maxTxStatusRetry = 3 // The maximum retries will be made for tx status request. - maxTxStatusCandidates = 5 // The maximum les servers the tx status requests will be sent to. -) - -// RetrieveTxStatus retrieves the transaction status from the LES network. -// There is no guarantee in the LES protocol that the mined transaction will -// be retrieved back for sure because of different reasons(the transaction -// is unindexed, the malicious server doesn't reply it deliberately, etc). -// Therefore, unretrieved transactions(UNKNOWN) will receive a certain number -// of retries, thus giving a weak guarantee. -func (odr *LesOdr) RetrieveTxStatus(ctx context.Context, req *light.TxStatusRequest) error { - // Sort according to the transaction history supported by the peer and - // select the peers with longest history. - var ( - retries int - peers []*serverPeer - missing = len(req.Hashes) - result = make([]light.TxStatus, len(req.Hashes)) - canSend = make(map[string]bool) - ) - for _, peer := range odr.peers.allPeers() { - if peer.txHistory == txIndexDisabled { - continue - } - peers = append(peers, peer) - } - sort.Sort(sort.Reverse(peerByTxHistory(peers))) - for i := 0; i < maxTxStatusCandidates && i < len(peers); i++ { - canSend[peers[i].id] = true - } - // Send out the request and assemble the result. - for { - if retries >= maxTxStatusRetry || len(canSend) == 0 { - break - } - var ( - // Deep copy the request, so that the partial result won't be mixed. - req = &TxStatusRequest{Hashes: req.Hashes} - id = rand.Uint64() - distreq = &distReq{ - getCost: func(dp distPeer) uint64 { return req.GetCost(dp.(*serverPeer)) }, - canSend: func(dp distPeer) bool { return canSend[dp.(*serverPeer).id] }, - request: func(dp distPeer) func() { - p := dp.(*serverPeer) - p.fcServer.QueuedRequest(id, req.GetCost(p)) - delete(canSend, p.id) - return func() { req.Request(id, p) } - }, - } - ) - if err := odr.retriever.retrieve(ctx, id, distreq, func(p distPeer, msg *Msg) error { return req.Validate(odr.db, msg) }, odr.stop); err != nil { - return err - } - // Collect the response and assemble them to the final result. - // All the response is not verifiable, so always pick the first - // one we get. - for index, status := range req.Status { - if result[index].Status != txpool.TxStatusUnknown { - continue - } - if status.Status == txpool.TxStatusUnknown { - continue - } - result[index], missing = status, missing-1 - } - // Abort the procedure if all the status are retrieved - if missing == 0 { - break - } - retries += 1 - } - req.Status = result - return nil -} - -// Retrieve tries to fetch an object from the LES network. It's a common API -// for most of the LES requests except for the TxStatusRequest which needs -// the additional retry mechanism. -// If the network retrieval was successful, it stores the object in local db. -func (odr *LesOdr) Retrieve(ctx context.Context, req light.OdrRequest) (err error) { - lreq := LesRequest(req) - - reqID := rand.Uint64() - rq := &distReq{ - getCost: func(dp distPeer) uint64 { - return lreq.GetCost(dp.(*serverPeer)) - }, - canSend: func(dp distPeer) bool { - p := dp.(*serverPeer) - if !p.onlyAnnounce { - return lreq.CanSend(p) - } - return false - }, - request: func(dp distPeer) func() { - p := dp.(*serverPeer) - cost := lreq.GetCost(p) - p.fcServer.QueuedRequest(reqID, cost) - return func() { lreq.Request(reqID, p) } - }, - } - - defer func(sent mclock.AbsTime) { - if err != nil { - return - } - requestRTT.Update(time.Duration(mclock.Now() - sent)) - }(mclock.Now()) - - if err := odr.retriever.retrieve(ctx, reqID, rq, func(p distPeer, msg *Msg) error { return lreq.Validate(odr.db, msg) }, odr.stop); err != nil { - return err - } - req.StoreResult(odr.db) - return nil -} diff --git a/les/odr_requests.go b/les/odr_requests.go deleted file mode 100644 index c907018590..0000000000 --- a/les/odr_requests.go +++ /dev/null @@ -1,537 +0,0 @@ -// Copyright 2016 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 les - -import ( - "encoding/binary" - "errors" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -var ( - errInvalidMessageType = errors.New("invalid message type") - errInvalidEntryCount = errors.New("invalid number of response entries") - errHeaderUnavailable = errors.New("header unavailable") - errTxHashMismatch = errors.New("transaction hash mismatch") - errUncleHashMismatch = errors.New("uncle hash mismatch") - errReceiptHashMismatch = errors.New("receipt hash mismatch") - errDataHashMismatch = errors.New("data hash mismatch") - errCHTHashMismatch = errors.New("cht hash mismatch") - errCHTNumberMismatch = errors.New("cht number mismatch") - errUselessNodes = errors.New("useless nodes in merkle proof nodeset") -) - -type LesOdrRequest interface { - GetCost(*serverPeer) uint64 - CanSend(*serverPeer) bool - Request(uint64, *serverPeer) error - Validate(ethdb.Database, *Msg) error -} - -func LesRequest(req light.OdrRequest) LesOdrRequest { - switch r := req.(type) { - case *light.BlockRequest: - return (*BlockRequest)(r) - case *light.ReceiptsRequest: - return (*ReceiptsRequest)(r) - case *light.TrieRequest: - return (*TrieRequest)(r) - case *light.CodeRequest: - return (*CodeRequest)(r) - case *light.ChtRequest: - return (*ChtRequest)(r) - case *light.BloomRequest: - return (*BloomRequest)(r) - case *light.TxStatusRequest: - return (*TxStatusRequest)(r) - default: - return nil - } -} - -// BlockRequest is the ODR request type for block bodies -type BlockRequest light.BlockRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *BlockRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetBlockBodiesMsg, 1) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *BlockRequest) CanSend(peer *serverPeer) bool { - return peer.HasBlock(r.Hash, r.Number, false) -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *BlockRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting block body", "hash", r.Hash) - return peer.requestBodies(reqID, []common.Hash{r.Hash}) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *BlockRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating block body", "hash", r.Hash) - - // Ensure we have a correct message with a single block body - if msg.MsgType != MsgBlockBodies { - return errInvalidMessageType - } - bodies := msg.Obj.([]*types.Body) - if len(bodies) != 1 { - return errInvalidEntryCount - } - body := bodies[0] - - // Retrieve our stored header and validate block content against it - if r.Header == nil { - r.Header = rawdb.ReadHeader(db, r.Hash, r.Number) - } - if r.Header == nil { - return errHeaderUnavailable - } - if r.Header.TxHash != types.DeriveSha(types.Transactions(body.Transactions), trie.NewStackTrie(nil)) { - return errTxHashMismatch - } - if r.Header.UncleHash != types.CalcUncleHash(body.Uncles) { - return errUncleHashMismatch - } - // Validations passed, encode and store RLP - data, err := rlp.EncodeToBytes(body) - if err != nil { - return err - } - r.Rlp = data - return nil -} - -// ReceiptsRequest is the ODR request type for block receipts by block hash -type ReceiptsRequest light.ReceiptsRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *ReceiptsRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetReceiptsMsg, 1) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *ReceiptsRequest) CanSend(peer *serverPeer) bool { - return peer.HasBlock(r.Hash, r.Number, false) -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *ReceiptsRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting block receipts", "hash", r.Hash) - return peer.requestReceipts(reqID, []common.Hash{r.Hash}) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *ReceiptsRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating block receipts", "hash", r.Hash) - - // Ensure we have a correct message with a single block receipt - if msg.MsgType != MsgReceipts { - return errInvalidMessageType - } - receipts := msg.Obj.([]types.Receipts) - if len(receipts) != 1 { - return errInvalidEntryCount - } - receipt := receipts[0] - - // Retrieve our stored header and validate receipt content against it - if r.Header == nil { - r.Header = rawdb.ReadHeader(db, r.Hash, r.Number) - } - if r.Header == nil { - return errHeaderUnavailable - } - if r.Header.ReceiptHash != types.DeriveSha(receipt, trie.NewStackTrie(nil)) { - return errReceiptHashMismatch - } - // Validations passed, store and return - r.Receipts = receipt - return nil -} - -type ProofReq struct { - BHash common.Hash - AccountAddress, Key []byte - FromLevel uint -} - -// ODR request type for state/storage trie entries, see LesOdrRequest interface -type TrieRequest light.TrieRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *TrieRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetProofsV2Msg, 1) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *TrieRequest) CanSend(peer *serverPeer) bool { - return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber, true) -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *TrieRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting trie proof", "root", r.Id.Root, "key", r.Key) - req := ProofReq{ - BHash: r.Id.BlockHash, - AccountAddress: r.Id.AccountAddress, - Key: r.Key, - } - return peer.requestProofs(reqID, []ProofReq{req}) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *TrieRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating trie proof", "root", r.Id.Root, "key", r.Key) - - if msg.MsgType != MsgProofsV2 { - return errInvalidMessageType - } - proofs := msg.Obj.(trienode.ProofList) - // Verify the proof and store if checks out - nodeSet := proofs.Set() - reads := &readTraceDB{db: nodeSet} - if _, err := trie.VerifyProof(r.Id.Root, r.Key, reads); err != nil { - return fmt.Errorf("merkle proof verification failed: %v", err) - } - // check if all nodes have been read by VerifyProof - if len(reads.reads) != nodeSet.KeyCount() { - return errUselessNodes - } - r.Proof = nodeSet - return nil -} - -type CodeReq struct { - BHash common.Hash - AccountAddress []byte -} - -// CodeRequest is the ODR request type for node data (used for retrieving contract code), see LesOdrRequest interface -type CodeRequest light.CodeRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *CodeRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetCodeMsg, 1) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *CodeRequest) CanSend(peer *serverPeer) bool { - return peer.HasBlock(r.Id.BlockHash, r.Id.BlockNumber, true) -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *CodeRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting code data", "hash", r.Hash) - req := CodeReq{ - BHash: r.Id.BlockHash, - AccountAddress: r.Id.AccountAddress, - } - return peer.requestCode(reqID, []CodeReq{req}) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *CodeRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating code data", "hash", r.Hash) - - // Ensure we have a correct message with a single code element - if msg.MsgType != MsgCode { - return errInvalidMessageType - } - reply := msg.Obj.([][]byte) - if len(reply) != 1 { - return errInvalidEntryCount - } - data := reply[0] - - // Verify the data and store if checks out - if hash := crypto.Keccak256Hash(data); r.Hash != hash { - return errDataHashMismatch - } - r.Data = data - return nil -} - -const ( - // helper trie type constants - htCanonical = iota // Canonical hash trie - htBloomBits // BloomBits trie - - // helper trie auxiliary types - // htAuxNone = 1 ; deprecated number, used in les2/3 previously. - htAuxHeader = 2 // applicable for htCanonical, requests for relevant headers -) - -type HelperTrieReq struct { - Type uint - TrieIdx uint64 - Key []byte - FromLevel, AuxReq uint -} - -type HelperTrieResps struct { // describes all responses, not just a single one - Proofs trienode.ProofList - AuxData [][]byte -} - -// ChtRequest is the ODR request type for requesting headers by Canonical Hash Trie, see LesOdrRequest interface -type ChtRequest light.ChtRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *ChtRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetHelperTrieProofsMsg, 1) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *ChtRequest) CanSend(peer *serverPeer) bool { - peer.lock.RLock() - defer peer.lock.RUnlock() - - return peer.headInfo.Number >= r.Config.ChtConfirms && r.ChtNum <= (peer.headInfo.Number-r.Config.ChtConfirms)/r.Config.ChtSize -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *ChtRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting CHT", "cht", r.ChtNum, "block", r.BlockNum) - var encNum [8]byte - binary.BigEndian.PutUint64(encNum[:], r.BlockNum) - req := HelperTrieReq{ - Type: htCanonical, - TrieIdx: r.ChtNum, - Key: encNum[:], - AuxReq: htAuxHeader, - } - return peer.requestHelperTrieProofs(reqID, []HelperTrieReq{req}) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *ChtRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating CHT", "cht", r.ChtNum, "block", r.BlockNum) - - if msg.MsgType != MsgHelperTrieProofs { - return errInvalidMessageType - } - resp := msg.Obj.(HelperTrieResps) - if len(resp.AuxData) != 1 { - return errInvalidEntryCount - } - nodeSet := resp.Proofs.Set() - headerEnc := resp.AuxData[0] - if len(headerEnc) == 0 { - return errHeaderUnavailable - } - header := new(types.Header) - if err := rlp.DecodeBytes(headerEnc, header); err != nil { - return errHeaderUnavailable - } - // Verify the CHT - var ( - node light.ChtNode - encNumber [8]byte - ) - binary.BigEndian.PutUint64(encNumber[:], r.BlockNum) - - reads := &readTraceDB{db: nodeSet} - value, err := trie.VerifyProof(r.ChtRoot, encNumber[:], reads) - if err != nil { - return fmt.Errorf("merkle proof verification failed: %v", err) - } - if len(reads.reads) != nodeSet.KeyCount() { - return errUselessNodes - } - if err := rlp.DecodeBytes(value, &node); err != nil { - return err - } - if node.Hash != header.Hash() { - return errCHTHashMismatch - } - if r.BlockNum != header.Number.Uint64() { - return errCHTNumberMismatch - } - // Verifications passed, store and return - r.Header = header - r.Proof = nodeSet - r.Td = node.Td - return nil -} - -type BloomReq struct { - BloomTrieNum, BitIdx, SectionIndex, FromLevel uint64 -} - -// BloomRequest is the ODR request type for requesting headers by Canonical Hash Trie, see LesOdrRequest interface -type BloomRequest light.BloomRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *BloomRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetHelperTrieProofsMsg, len(r.SectionIndexList)) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *BloomRequest) CanSend(peer *serverPeer) bool { - peer.lock.RLock() - defer peer.lock.RUnlock() - - if peer.version < lpv2 { - return false - } - return peer.headInfo.Number >= r.Config.BloomTrieConfirms && r.BloomTrieNum <= (peer.headInfo.Number-r.Config.BloomTrieConfirms)/r.Config.BloomTrieSize -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *BloomRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting BloomBits", "bloomTrie", r.BloomTrieNum, "bitIdx", r.BitIdx, "sections", r.SectionIndexList) - reqs := make([]HelperTrieReq, len(r.SectionIndexList)) - - var encNumber [10]byte - binary.BigEndian.PutUint16(encNumber[:2], uint16(r.BitIdx)) - - for i, sectionIdx := range r.SectionIndexList { - binary.BigEndian.PutUint64(encNumber[2:], sectionIdx) - reqs[i] = HelperTrieReq{ - Type: htBloomBits, - TrieIdx: r.BloomTrieNum, - Key: common.CopyBytes(encNumber[:]), - } - } - return peer.requestHelperTrieProofs(reqID, reqs) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *BloomRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating BloomBits", "bloomTrie", r.BloomTrieNum, "bitIdx", r.BitIdx, "sections", r.SectionIndexList) - - // Ensure we have a correct message with a single proof element - if msg.MsgType != MsgHelperTrieProofs { - return errInvalidMessageType - } - resps := msg.Obj.(HelperTrieResps) - proofs := resps.Proofs - nodeSet := proofs.Set() - reads := &readTraceDB{db: nodeSet} - - r.BloomBits = make([][]byte, len(r.SectionIndexList)) - - // Verify the proofs - var encNumber [10]byte - binary.BigEndian.PutUint16(encNumber[:2], uint16(r.BitIdx)) - - for i, idx := range r.SectionIndexList { - binary.BigEndian.PutUint64(encNumber[2:], idx) - value, err := trie.VerifyProof(r.BloomTrieRoot, encNumber[:], reads) - if err != nil { - return err - } - r.BloomBits[i] = value - } - - if len(reads.reads) != nodeSet.KeyCount() { - return errUselessNodes - } - r.Proofs = nodeSet - return nil -} - -// TxStatusRequest is the ODR request type for transaction status -type TxStatusRequest light.TxStatusRequest - -// GetCost returns the cost of the given ODR request according to the serving -// peer's cost table (implementation of LesOdrRequest) -func (r *TxStatusRequest) GetCost(peer *serverPeer) uint64 { - return peer.getRequestCost(GetTxStatusMsg, len(r.Hashes)) -} - -// CanSend tells if a certain peer is suitable for serving the given request -func (r *TxStatusRequest) CanSend(peer *serverPeer) bool { - return peer.txHistory != txIndexDisabled -} - -// Request sends an ODR request to the LES network (implementation of LesOdrRequest) -func (r *TxStatusRequest) Request(reqID uint64, peer *serverPeer) error { - peer.Log().Debug("Requesting transaction status", "count", len(r.Hashes)) - return peer.requestTxStatus(reqID, r.Hashes) -} - -// Validate processes an ODR request reply message from the LES network -// returns true and stores results in memory if the message was a valid reply -// to the request (implementation of LesOdrRequest) -func (r *TxStatusRequest) Validate(db ethdb.Database, msg *Msg) error { - log.Debug("Validating transaction status", "count", len(r.Hashes)) - - if msg.MsgType != MsgTxStatus { - return errInvalidMessageType - } - status := msg.Obj.([]light.TxStatus) - if len(status) != len(r.Hashes) { - return errInvalidEntryCount - } - r.Status = status - return nil -} - -// readTraceDB stores the keys of database reads. We use this to check that received node -// sets contain only the trie nodes necessary to make proofs pass. -type readTraceDB struct { - db ethdb.KeyValueReader - reads map[string]struct{} -} - -// Get returns a stored node -func (db *readTraceDB) Get(k []byte) ([]byte, error) { - if db.reads == nil { - db.reads = make(map[string]struct{}) - } - db.reads[string(k)] = struct{}{} - return db.db.Get(k) -} - -// Has returns true if the node set contains the given key -func (db *readTraceDB) Has(key []byte) (bool, error) { - _, err := db.Get(key) - return err == nil, nil -} diff --git a/les/odr_test.go b/les/odr_test.go deleted file mode 100644 index 69824a92dd..0000000000 --- a/les/odr_test.go +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright 2016 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 les - -// Note: these tests are disabled now because they cannot work with the old sync -// mechanism removed but will be useful again once the PoS ultralight mode is implemented - -/* -import ( - "bytes" - "context" - "crypto/rand" - "fmt" - "math/big" - "reflect" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" -) - -type odrTestFn func(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte - -func TestOdrGetBlockLes2(t *testing.T) { testOdr(t, 2, 1, true, odrGetBlock) } -func TestOdrGetBlockLes3(t *testing.T) { testOdr(t, 3, 1, true, odrGetBlock) } -func TestOdrGetBlockLes4(t *testing.T) { testOdr(t, 4, 1, true, odrGetBlock) } - -func odrGetBlock(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { - var block *types.Block - if bc != nil { - block = bc.GetBlockByHash(bhash) - } else { - block, _ = lc.GetBlockByHash(ctx, bhash) - } - if block == nil { - return nil - } - rlp, _ := rlp.EncodeToBytes(block) - return rlp -} - -func TestOdrGetReceiptsLes2(t *testing.T) { testOdr(t, 2, 1, true, odrGetReceipts) } -func TestOdrGetReceiptsLes3(t *testing.T) { testOdr(t, 3, 1, true, odrGetReceipts) } -func TestOdrGetReceiptsLes4(t *testing.T) { testOdr(t, 4, 1, true, odrGetReceipts) } - -func odrGetReceipts(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { - var receipts types.Receipts - if bc != nil { - if number := rawdb.ReadHeaderNumber(db, bhash); number != nil { - if header := rawdb.ReadHeader(db, bhash, *number); header != nil { - receipts = rawdb.ReadReceipts(db, bhash, *number, header.Time, config) - } - } - } else { - if number := rawdb.ReadHeaderNumber(db, bhash); number != nil { - receipts, _ = light.GetBlockReceipts(ctx, lc.Odr(), bhash, *number) - } - } - if receipts == nil { - return nil - } - rlp, _ := rlp.EncodeToBytes(receipts) - return rlp -} - -func TestOdrAccountsLes2(t *testing.T) { testOdr(t, 2, 1, true, odrAccounts) } -func TestOdrAccountsLes3(t *testing.T) { testOdr(t, 3, 1, true, odrAccounts) } -func TestOdrAccountsLes4(t *testing.T) { testOdr(t, 4, 1, true, odrAccounts) } - -func odrAccounts(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { - dummyAddr := common.HexToAddress("1234567812345678123456781234567812345678") - acc := []common.Address{bankAddr, userAddr1, userAddr2, dummyAddr} - - var ( - res []byte - st *state.StateDB - err error - ) - for _, addr := range acc { - if bc != nil { - header := bc.GetHeaderByHash(bhash) - st, err = state.New(header.Root, bc.StateCache(), nil) - } else { - header := lc.GetHeaderByHash(bhash) - st = light.NewState(ctx, header, lc.Odr()) - } - if err == nil { - bal := st.GetBalance(addr) - rlp, _ := rlp.EncodeToBytes(bal) - res = append(res, rlp...) - } - } - return res -} - -func TestOdrContractCallLes2(t *testing.T) { testOdr(t, 2, 2, true, odrContractCall) } -func TestOdrContractCallLes3(t *testing.T) { testOdr(t, 3, 2, true, odrContractCall) } -func TestOdrContractCallLes4(t *testing.T) { testOdr(t, 4, 2, true, odrContractCall) } - -func odrContractCall(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { - data := common.Hex2Bytes("60CD26850000000000000000000000000000000000000000000000000000000000000000") - - var res []byte - for i := 0; i < 3; i++ { - data[35] = byte(i) - if bc != nil { - header := bc.GetHeaderByHash(bhash) - statedb, err := state.New(header.Root, bc.StateCache(), nil) - - if err == nil { - from := statedb.GetOrNewStateObject(bankAddr) - from.SetBalance(math.MaxBig256) - - msg := &core.Message{ - From: from.Address(), - To: &testContractAddr, - Value: new(big.Int), - GasLimit: 100000, - GasPrice: big.NewInt(params.InitialBaseFee), - GasFeeCap: big.NewInt(params.InitialBaseFee), - GasTipCap: new(big.Int), - Data: data, - SkipAccountChecks: true, - } - - context := core.NewEVMBlockContext(header, bc, nil) - txContext := core.NewEVMTxContext(msg) - vmenv := vm.NewEVM(context, txContext, statedb, config, vm.Config{NoBaseFee: true}) - - //vmenv := core.NewEnv(statedb, config, bc, msg, header, vm.Config{}) - gp := new(core.GasPool).AddGas(math.MaxUint64) - result, _ := core.ApplyMessage(vmenv, msg, gp) - res = append(res, result.Return()...) - } - } else { - header := lc.GetHeaderByHash(bhash) - state := light.NewState(ctx, header, lc.Odr()) - state.SetBalance(bankAddr, math.MaxBig256) - msg := &core.Message{ - From: bankAddr, - To: &testContractAddr, - Value: new(big.Int), - GasLimit: 100000, - GasPrice: big.NewInt(params.InitialBaseFee), - GasFeeCap: big.NewInt(params.InitialBaseFee), - GasTipCap: new(big.Int), - Data: data, - SkipAccountChecks: true, - } - context := core.NewEVMBlockContext(header, lc, nil) - txContext := core.NewEVMTxContext(msg) - vmenv := vm.NewEVM(context, txContext, state, config, vm.Config{NoBaseFee: true}) - gp := new(core.GasPool).AddGas(math.MaxUint64) - result, _ := core.ApplyMessage(vmenv, msg, gp) - if state.Error() == nil { - res = append(res, result.Return()...) - } - } - } - return res -} - -func TestOdrTxStatusLes2(t *testing.T) { testOdr(t, 2, 1, false, odrTxStatus) } -func TestOdrTxStatusLes3(t *testing.T) { testOdr(t, 3, 1, false, odrTxStatus) } -func TestOdrTxStatusLes4(t *testing.T) { testOdr(t, 4, 1, false, odrTxStatus) } - -func odrTxStatus(ctx context.Context, db ethdb.Database, config *params.ChainConfig, bc *core.BlockChain, lc *light.LightChain, bhash common.Hash) []byte { - var txs types.Transactions - if bc != nil { - block := bc.GetBlockByHash(bhash) - txs = block.Transactions() - } else { - if block, _ := lc.GetBlockByHash(ctx, bhash); block != nil { - btxs := block.Transactions() - txs = make(types.Transactions, len(btxs)) - for i, tx := range btxs { - var err error - txs[i], _, _, _, err = light.GetTransaction(ctx, lc.Odr(), tx.Hash()) - if err != nil { - return nil - } - } - } - } - rlp, _ := rlp.EncodeToBytes(txs) - return rlp -} - -// testOdr tests odr requests whose validation guaranteed by block headers. -func testOdr(t *testing.T, protocol int, expFail uint64, checkCached bool, fn odrTestFn) { - // Assemble the test environment - netconfig := testnetConfig{ - blocks: 4, - protocol: protocol, - connect: true, - nopruning: true, - } - server, client, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - // Ensure the client has synced all necessary data. - clientHead := client.handler.backend.blockchain.CurrentHeader() - if clientHead.Number.Uint64() != 4 { - t.Fatalf("Failed to sync the chain with server, head: %v", clientHead.Number.Uint64()) - } - // Disable the mechanism that we will wait a few time for request - // even there is no suitable peer to send right now. - waitForPeers = 0 - - test := func(expFail uint64) { - // Mark this as a helper to put the failures at the correct lines - t.Helper() - - for i := uint64(0); i <= server.handler.blockchain.CurrentHeader().Number.Uint64(); i++ { - bhash := rawdb.ReadCanonicalHash(server.db, i) - b1 := fn(light.NoOdr, server.db, server.handler.server.chainConfig, server.handler.blockchain, nil, bhash) - - // Set the timeout as 1 second here, ensure there is enough time - // for travis to make the action. - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - b2 := fn(ctx, client.db, client.handler.backend.chainConfig, nil, client.handler.backend.blockchain, bhash) - cancel() - - eq := bytes.Equal(b1, b2) - exp := i < expFail - if exp && !eq { - t.Fatalf("odr mismatch: have %x, want %x", b2, b1) - } - if !exp && eq { - t.Fatalf("unexpected odr match") - } - } - } - - // expect retrievals to fail (except genesis block) without a les peer - client.handler.backend.peers.lock.Lock() - client.peer.speer.hasBlockHook = func(common.Hash, uint64, bool) bool { return false } - client.handler.backend.peers.lock.Unlock() - test(expFail) - - // expect all retrievals to pass - client.handler.backend.peers.lock.Lock() - client.peer.speer.hasBlockHook = func(common.Hash, uint64, bool) bool { return true } - client.handler.backend.peers.lock.Unlock() - test(5) - - // still expect all retrievals to pass, now data should be cached locally - if checkCached { - client.handler.backend.peers.unregister(client.peer.speer.id) - time.Sleep(time.Millisecond * 10) // ensure that all peerSetNotify callbacks are executed - test(5) - } -} - -func TestGetTxStatusFromUnindexedPeersLES4(t *testing.T) { testGetTxStatusFromUnindexedPeers(t, lpv4) } - -func testGetTxStatusFromUnindexedPeers(t *testing.T, protocol int) { - var ( - blocks = 8 - netconfig = testnetConfig{ - blocks: blocks, - protocol: protocol, - nopruning: true, - } - ) - server, client, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - // Iterate the chain, create the tx indexes locally - var ( - testHash common.Hash - testStatus light.TxStatus - - txs = make(map[common.Hash]*types.Transaction) // Transaction objects set - blockNumbers = make(map[common.Hash]uint64) // Transaction hash to block number mappings - blockHashes = make(map[common.Hash]common.Hash) // Transaction hash to block hash mappings - intraIndex = make(map[common.Hash]uint64) // Transaction intra-index in block - ) - for number := uint64(1); number < server.backend.Blockchain().CurrentBlock().Number.Uint64(); number++ { - block := server.backend.Blockchain().GetBlockByNumber(number) - if block == nil { - t.Fatalf("Failed to retrieve block %d", number) - } - for index, tx := range block.Transactions() { - txs[tx.Hash()] = tx - blockNumbers[tx.Hash()] = number - blockHashes[tx.Hash()] = block.Hash() - intraIndex[tx.Hash()] = uint64(index) - - if testHash == (common.Hash{}) { - testHash = tx.Hash() - testStatus = light.TxStatus{ - Status: txpool.TxStatusIncluded, - Lookup: &rawdb.LegacyTxLookupEntry{ - BlockHash: block.Hash(), - BlockIndex: block.NumberU64(), - Index: uint64(index), - }, - } - } - } - } - // serveMsg processes incoming GetTxStatusMsg and sends the response back. - serveMsg := func(peer *testPeer, txLookup uint64) error { - msg, err := peer.app.ReadMsg() - if err != nil { - return err - } - if msg.Code != GetTxStatusMsg { - return fmt.Errorf("message code mismatch: got %d, expected %d", msg.Code, GetTxStatusMsg) - } - var r GetTxStatusPacket - if err := msg.Decode(&r); err != nil { - return err - } - stats := make([]light.TxStatus, len(r.Hashes)) - for i, hash := range r.Hashes { - number, exist := blockNumbers[hash] - if !exist { - continue // Filter out unknown transactions - } - min := uint64(blocks) - txLookup - if txLookup != txIndexUnlimited && (txLookup == txIndexDisabled || number < min) { - continue // Filter out unindexed transactions - } - stats[i].Status = txpool.TxStatusIncluded - stats[i].Lookup = &rawdb.LegacyTxLookupEntry{ - BlockHash: blockHashes[hash], - BlockIndex: number, - Index: intraIndex[hash], - } - } - data, _ := rlp.EncodeToBytes(stats) - reply := &reply{peer.app, TxStatusMsg, r.ReqID, data} - reply.send(testBufLimit) - return nil - } - - var testspecs = []struct { - peers int - txLookups []uint64 - txs []common.Hash - results []light.TxStatus - }{ - // Retrieve mined transaction from the empty peerset - { - peers: 0, - txLookups: []uint64{}, - txs: []common.Hash{testHash}, - results: []light.TxStatus{{}}, - }, - // Retrieve unknown transaction from the full peers - { - peers: 3, - txLookups: []uint64{txIndexUnlimited, txIndexUnlimited, txIndexUnlimited}, - txs: []common.Hash{randomHash()}, - results: []light.TxStatus{{}}, - }, - // Retrieve mined transaction from the full peers - { - peers: 3, - txLookups: []uint64{txIndexUnlimited, txIndexUnlimited, txIndexUnlimited}, - txs: []common.Hash{testHash}, - results: []light.TxStatus{testStatus}, - }, - // Retrieve mixed transactions from the full peers - { - peers: 3, - txLookups: []uint64{txIndexUnlimited, txIndexUnlimited, txIndexUnlimited}, - txs: []common.Hash{randomHash(), testHash}, - results: []light.TxStatus{{}, testStatus}, - }, - // Retrieve mixed transactions from unindexed peer(but the target is still available) - { - peers: 3, - txLookups: []uint64{uint64(blocks) - testStatus.Lookup.BlockIndex, uint64(blocks) - testStatus.Lookup.BlockIndex - 1, uint64(blocks) - testStatus.Lookup.BlockIndex - 2}, - txs: []common.Hash{randomHash(), testHash}, - results: []light.TxStatus{{}, testStatus}, - }, - // Retrieve mixed transactions from unindexed peer(but the target is not available) - { - peers: 3, - txLookups: []uint64{uint64(blocks) - testStatus.Lookup.BlockIndex - 1, uint64(blocks) - testStatus.Lookup.BlockIndex - 1, uint64(blocks) - testStatus.Lookup.BlockIndex - 2}, - txs: []common.Hash{randomHash(), testHash}, - results: []light.TxStatus{{}, {}}, - }, - } - for _, testspec := range testspecs { - // Create a bunch of server peers with different tx history - var ( - closeFns []func() - ) - for i := 0; i < testspec.peers; i++ { - peer, closePeer, _ := client.newRawPeer(t, fmt.Sprintf("server-%d", i), protocol, testspec.txLookups[i]) - closeFns = append(closeFns, closePeer) - - // Create a one-time routine for serving message - go func(i int, peer *testPeer, lookup uint64) { - serveMsg(peer, lookup) - }(i, peer, testspec.txLookups[i]) - } - - // Send out the GetTxStatus requests, compare the result with - // expected value. - r := &light.TxStatusRequest{Hashes: testspec.txs} - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - err := client.handler.backend.odr.RetrieveTxStatus(ctx, r) - if err != nil { - t.Errorf("Failed to retrieve tx status %v", err) - } else { - if !reflect.DeepEqual(testspec.results, r.Status) { - t.Errorf("Result mismatch, diff") - } - } - - // Close all connected peers and start the next round - for _, closeFn := range closeFns { - closeFn() - } - } -} - -// randomHash generates a random blob of data and returns it as a hash. -func randomHash() common.Hash { - var hash common.Hash - if n, err := rand.Read(hash[:]); n != common.HashLength || err != nil { - panic(err) - } - return hash -} -*/ diff --git a/les/peer.go b/les/peer.go deleted file mode 100644 index b38a393d4c..0000000000 --- a/les/peer.go +++ /dev/null @@ -1,1362 +0,0 @@ -// Copyright 2016 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 les - -import ( - "crypto/ecdsa" - "errors" - "fmt" - "math/big" - "math/rand" - "net" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/les/flowcontrol" - "github.com/ethereum/go-ethereum/les/utils" - vfc "github.com/ethereum/go-ethereum/les/vflux/client" - vfs "github.com/ethereum/go-ethereum/les/vflux/server" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -var ( - errClosed = errors.New("peer set is closed") - errAlreadyRegistered = errors.New("peer is already registered") - errNotRegistered = errors.New("peer is not registered") -) - -const ( - maxRequestErrors = 20 // number of invalid requests tolerated (makes the protocol less brittle but still avoids spam) - maxResponseErrors = 50 // number of invalid responses tolerated (makes the protocol less brittle but still avoids spam) - - allowedUpdateBytes = 100000 // initial/maximum allowed update size - allowedUpdateRate = time.Millisecond * 10 // time constant for recharging one byte of allowance - - freezeTimeBase = time.Millisecond * 700 // fixed component of client freeze time - freezeTimeRandom = time.Millisecond * 600 // random component of client freeze time - freezeCheckPeriod = time.Millisecond * 100 // buffer value recheck period after initial freeze time has elapsed - - // If the total encoded size of a sent transaction batch is over txSizeCostLimit - // per transaction then the request cost is calculated as proportional to the - // encoded size instead of the transaction count - txSizeCostLimit = 0x4000 - - // handshakeTimeout is the timeout LES handshake will be treated as failed. - handshakeTimeout = 5 * time.Second -) - -const ( - announceTypeNone = iota - announceTypeSimple - announceTypeSigned -) - -type keyValueEntry struct { - Key string - Value rlp.RawValue -} - -type keyValueList []keyValueEntry -type keyValueMap map[string]rlp.RawValue - -func (l keyValueList) add(key string, val interface{}) keyValueList { - var entry keyValueEntry - entry.Key = key - if val == nil { - val = uint64(0) - } - enc, err := rlp.EncodeToBytes(val) - if err == nil { - entry.Value = enc - } - return append(l, entry) -} - -func (l keyValueList) decode() (keyValueMap, uint64) { - m := make(keyValueMap) - var size uint64 - for _, entry := range l { - m[entry.Key] = entry.Value - size += uint64(len(entry.Key)) + uint64(len(entry.Value)) + 8 - } - return m, size -} - -func (m keyValueMap) get(key string, val interface{}) error { - enc, ok := m[key] - if !ok { - return errResp(ErrMissingKey, "%s", key) - } - if val == nil { - return nil - } - return rlp.DecodeBytes(enc, val) -} - -// peerCommons contains fields needed by both server peer and client peer. -type peerCommons struct { - *p2p.Peer - rw p2p.MsgReadWriter - - id string // Peer identity. - version int // Protocol version negotiated. - network uint64 // Network ID being on. - frozen atomic.Bool // Flag whether the peer is frozen. - announceType uint64 // New block announcement type. - serving atomic.Bool // The status indicates the peer is served. - headInfo blockInfo // Last announced block information. - - // Background task queue for caching peer tasks and executing in order. - sendQueue *utils.ExecQueue - - // Flow control agreement. - fcParams flowcontrol.ServerParams // The config for token bucket. - fcCosts requestCostTable // The Maximum request cost table. - - closeCh chan struct{} - lock sync.RWMutex // Lock used to protect all thread-sensitive fields. -} - -// isFrozen returns true if the client is frozen or the server has put our -// client in frozen state -func (p *peerCommons) isFrozen() bool { - return p.frozen.Load() -} - -// canQueue returns an indicator whether the peer can queue an operation. -func (p *peerCommons) canQueue() bool { - return p.sendQueue.CanQueue() && !p.isFrozen() -} - -// queueSend caches a peer operation in the background task queue. -// Please ensure to check `canQueue` before call this function -func (p *peerCommons) queueSend(f func()) bool { - return p.sendQueue.Queue(f) -} - -// String implements fmt.Stringer. -func (p *peerCommons) String() string { - return fmt.Sprintf("Peer %s [%s]", p.id, fmt.Sprintf("les/%d", p.version)) -} - -// PeerInfo represents a short summary of the `eth` sub-protocol metadata known -// about a connected peer. -type PeerInfo struct { - Version int `json:"version"` // Ethereum protocol version negotiated - Difficulty *big.Int `json:"difficulty"` // Total difficulty of the peer's blockchain - Head string `json:"head"` // SHA3 hash of the peer's best owned block -} - -// Info gathers and returns a collection of metadata known about a peer. -func (p *peerCommons) Info() *PeerInfo { - return &PeerInfo{ - Version: p.version, - Difficulty: p.Td(), - Head: fmt.Sprintf("%x", p.Head()), - } -} - -// Head retrieves a copy of the current head (most recent) hash of the peer. -func (p *peerCommons) Head() (hash common.Hash) { - p.lock.RLock() - defer p.lock.RUnlock() - - return p.headInfo.Hash -} - -// Td retrieves the current total difficulty of a peer. -func (p *peerCommons) Td() *big.Int { - p.lock.RLock() - defer p.lock.RUnlock() - - return new(big.Int).Set(p.headInfo.Td) -} - -// HeadAndTd retrieves the current head hash and total difficulty of a peer. -func (p *peerCommons) HeadAndTd() (hash common.Hash, td *big.Int) { - p.lock.RLock() - defer p.lock.RUnlock() - - return p.headInfo.Hash, new(big.Int).Set(p.headInfo.Td) -} - -// sendReceiveHandshake exchanges handshake packet with remote peer and returns any error -// if failed to send or receive packet. -func (p *peerCommons) sendReceiveHandshake(sendList keyValueList) (keyValueList, error) { - var ( - errc = make(chan error, 2) - recvList keyValueList - ) - // Send out own handshake in a new thread - go func() { - errc <- p2p.Send(p.rw, StatusMsg, &sendList) - }() - go func() { - // In the mean time retrieve the remote status message - msg, err := p.rw.ReadMsg() - if err != nil { - errc <- err - return - } - if msg.Code != StatusMsg { - errc <- errResp(ErrNoStatusMsg, "first msg has code %x (!= %x)", msg.Code, StatusMsg) - return - } - if msg.Size > ProtocolMaxMsgSize { - errc <- errResp(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize) - return - } - // Decode the handshake - if err := msg.Decode(&recvList); err != nil { - errc <- errResp(ErrDecode, "msg %v: %v", msg, err) - return - } - errc <- nil - }() - timeout := time.NewTimer(handshakeTimeout) - defer timeout.Stop() - for i := 0; i < 2; i++ { - select { - case err := <-errc: - if err != nil { - return nil, err - } - case <-timeout.C: - return nil, p2p.DiscReadTimeout - } - } - return recvList, nil -} - -// handshake executes the les protocol handshake, negotiating version number, -// network IDs, difficulties, head and genesis blocks. Besides the basic handshake -// fields, server and client can exchange and resolve some specified fields through -// two callback functions. -func (p *peerCommons) handshake(td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, forkID forkid.ID, forkFilter forkid.Filter, sendCallback func(*keyValueList), recvCallback func(keyValueMap) error) error { - p.lock.Lock() - defer p.lock.Unlock() - - var send keyValueList - - // Add some basic handshake fields - send = send.add("protocolVersion", uint64(p.version)) - send = send.add("networkId", p.network) - // Note: the head info announced at handshake is only used in case of server peers - // but dummy values are still announced by clients for compatibility with older servers - send = send.add("headTd", td) - send = send.add("headHash", head) - send = send.add("headNum", headNum) - send = send.add("genesisHash", genesis) - - // If the protocol version is beyond les4, then pass the forkID - // as well. Check http://eips.ethereum.org/EIPS/eip-2124 for more - // spec detail. - if p.version >= lpv4 { - send = send.add("forkID", forkID) - } - // Add client-specified or server-specified fields - if sendCallback != nil { - sendCallback(&send) - } - // Exchange the handshake packet and resolve the received one. - recvList, err := p.sendReceiveHandshake(send) - if err != nil { - return err - } - recv, size := recvList.decode() - if size > allowedUpdateBytes { - return errResp(ErrRequestRejected, "") - } - var rGenesis common.Hash - var rVersion, rNetwork uint64 - if err := recv.get("protocolVersion", &rVersion); err != nil { - return err - } - if err := recv.get("networkId", &rNetwork); err != nil { - return err - } - if err := recv.get("genesisHash", &rGenesis); err != nil { - return err - } - if rGenesis != genesis { - return errResp(ErrGenesisBlockMismatch, "%x (!= %x)", rGenesis[:8], genesis[:8]) - } - if rNetwork != p.network { - return errResp(ErrNetworkIdMismatch, "%d (!= %d)", rNetwork, p.network) - } - if int(rVersion) != p.version { - return errResp(ErrProtocolVersionMismatch, "%d (!= %d)", rVersion, p.version) - } - // Check forkID if the protocol version is beyond the les4 - if p.version >= lpv4 { - var forkID forkid.ID - if err := recv.get("forkID", &forkID); err != nil { - return err - } - if err := forkFilter(forkID); err != nil { - return errResp(ErrForkIDRejected, "%v", err) - } - } - if recvCallback != nil { - return recvCallback(recv) - } - return nil -} - -// close closes the channel and notifies all background routines to exit. -func (p *peerCommons) close() { - close(p.closeCh) - p.sendQueue.Quit() -} - -// serverPeer represents each node to which the client is connected. -// The node here refers to the les server. -type serverPeer struct { - peerCommons - - // Status fields - trusted bool // The flag whether the server is selected as trusted server. - onlyAnnounce bool // The flag whether the server sends announcement only. - chainSince, chainRecent uint64 // The range of chain server peer can serve. - stateSince, stateRecent uint64 // The range of state server peer can serve. - txHistory uint64 // The length of available tx history, 0 means all, 1 means disabled - - fcServer *flowcontrol.ServerNode // Client side mirror token bucket. - vtLock sync.Mutex - nodeValueTracker *vfc.NodeValueTracker - sentReqs map[uint64]sentReqEntry - - // Statistics - errCount utils.LinearExpiredValue // Counter the invalid responses server has replied - updateCount uint64 - updateTime mclock.AbsTime - - // Test callback hooks - hasBlockHook func(common.Hash, uint64, bool) bool // Used to determine whether the server has the specified block. -} - -func newServerPeer(version int, network uint64, trusted bool, p *p2p.Peer, rw p2p.MsgReadWriter) *serverPeer { - return &serverPeer{ - peerCommons: peerCommons{ - Peer: p, - rw: rw, - id: p.ID().String(), - version: version, - network: network, - sendQueue: utils.NewExecQueue(100), - closeCh: make(chan struct{}), - }, - trusted: trusted, - errCount: utils.LinearExpiredValue{Rate: mclock.AbsTime(time.Hour)}, - } -} - -// rejectUpdate returns true if a parameter update has to be rejected because -// the size and/or rate of updates exceed the capacity limitation -func (p *serverPeer) rejectUpdate(size uint64) bool { - now := mclock.Now() - if p.updateCount == 0 { - p.updateTime = now - } else { - dt := now - p.updateTime - p.updateTime = now - - r := uint64(dt / mclock.AbsTime(allowedUpdateRate)) - if p.updateCount > r { - p.updateCount -= r - } else { - p.updateCount = 0 - } - } - p.updateCount += size - return p.updateCount > allowedUpdateBytes -} - -// freeze processes Stop messages from the given server and set the status as -// frozen. -func (p *serverPeer) freeze() { - if p.frozen.CompareAndSwap(false, true) { - p.sendQueue.Clear() - } -} - -// unfreeze processes Resume messages from the given server and set the status -// as unfrozen. -func (p *serverPeer) unfreeze() { - p.frozen.Store(false) -} - -// sendRequest send a request to the server based on the given message type -// and content. -func sendRequest(w p2p.MsgWriter, msgcode, reqID uint64, data interface{}) error { - type req struct { - ReqID uint64 - Data interface{} - } - return p2p.Send(w, msgcode, &req{reqID, data}) -} - -func (p *serverPeer) sendRequest(msgcode, reqID uint64, data interface{}, amount int) error { - p.sentRequest(reqID, uint32(msgcode), uint32(amount)) - return sendRequest(p.rw, msgcode, reqID, data) -} - -// requestHeadersByHash fetches a batch of blocks' headers corresponding to the -// specified header query, based on the hash of an origin block. -func (p *serverPeer) requestHeadersByHash(reqID uint64, origin common.Hash, amount int, skip int, reverse bool) error { - p.Log().Debug("Fetching batch of headers", "count", amount, "fromhash", origin, "skip", skip, "reverse", reverse) - return p.sendRequest(GetBlockHeadersMsg, reqID, &GetBlockHeadersData{Origin: hashOrNumber{Hash: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}, amount) -} - -// requestHeadersByNumber fetches a batch of blocks' headers corresponding to the -// specified header query, based on the number of an origin block. -func (p *serverPeer) requestHeadersByNumber(reqID, origin uint64, amount int, skip int, reverse bool) error { - p.Log().Debug("Fetching batch of headers", "count", amount, "fromnum", origin, "skip", skip, "reverse", reverse) - return p.sendRequest(GetBlockHeadersMsg, reqID, &GetBlockHeadersData{Origin: hashOrNumber{Number: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}, amount) -} - -// requestBodies fetches a batch of blocks' bodies corresponding to the hashes -// specified. -func (p *serverPeer) requestBodies(reqID uint64, hashes []common.Hash) error { - p.Log().Debug("Fetching batch of block bodies", "count", len(hashes)) - return p.sendRequest(GetBlockBodiesMsg, reqID, hashes, len(hashes)) -} - -// requestCode fetches a batch of arbitrary data from a node's known state -// data, corresponding to the specified hashes. -func (p *serverPeer) requestCode(reqID uint64, reqs []CodeReq) error { - p.Log().Debug("Fetching batch of codes", "count", len(reqs)) - return p.sendRequest(GetCodeMsg, reqID, reqs, len(reqs)) -} - -// requestReceipts fetches a batch of transaction receipts from a remote node. -func (p *serverPeer) requestReceipts(reqID uint64, hashes []common.Hash) error { - p.Log().Debug("Fetching batch of receipts", "count", len(hashes)) - return p.sendRequest(GetReceiptsMsg, reqID, hashes, len(hashes)) -} - -// requestProofs fetches a batch of merkle proofs from a remote node. -func (p *serverPeer) requestProofs(reqID uint64, reqs []ProofReq) error { - p.Log().Debug("Fetching batch of proofs", "count", len(reqs)) - return p.sendRequest(GetProofsV2Msg, reqID, reqs, len(reqs)) -} - -// requestHelperTrieProofs fetches a batch of HelperTrie merkle proofs from a remote node. -func (p *serverPeer) requestHelperTrieProofs(reqID uint64, reqs []HelperTrieReq) error { - p.Log().Debug("Fetching batch of HelperTrie proofs", "count", len(reqs)) - return p.sendRequest(GetHelperTrieProofsMsg, reqID, reqs, len(reqs)) -} - -// requestTxStatus fetches a batch of transaction status records from a remote node. -func (p *serverPeer) requestTxStatus(reqID uint64, txHashes []common.Hash) error { - p.Log().Debug("Requesting transaction status", "count", len(txHashes)) - return p.sendRequest(GetTxStatusMsg, reqID, txHashes, len(txHashes)) -} - -// sendTxs creates a reply with a batch of transactions to be added to the remote transaction pool. -func (p *serverPeer) sendTxs(reqID uint64, amount int, txs rlp.RawValue) error { - p.Log().Debug("Sending batch of transactions", "amount", amount, "size", len(txs)) - sizeFactor := (len(txs) + txSizeCostLimit/2) / txSizeCostLimit - if sizeFactor > amount { - amount = sizeFactor - } - return p.sendRequest(SendTxV2Msg, reqID, txs, amount) -} - -// waitBefore implements distPeer interface -func (p *serverPeer) waitBefore(maxCost uint64) (time.Duration, float64) { - return p.fcServer.CanSend(maxCost) -} - -// getRequestCost returns an estimated request cost according to the flow control -// rules negotiated between the server and the client. -func (p *serverPeer) getRequestCost(msgcode uint64, amount int) uint64 { - p.lock.RLock() - defer p.lock.RUnlock() - - costs := p.fcCosts[msgcode] - if costs == nil { - return 0 - } - cost := costs.baseCost + costs.reqCost*uint64(amount) - if cost > p.fcParams.BufLimit { - cost = p.fcParams.BufLimit - } - return cost -} - -// getTxRelayCost returns an estimated relay cost according to the flow control -// rules negotiated between the server and the client. -func (p *serverPeer) getTxRelayCost(amount, size int) uint64 { - p.lock.RLock() - defer p.lock.RUnlock() - - costs := p.fcCosts[SendTxV2Msg] - if costs == nil { - return 0 - } - cost := costs.baseCost + costs.reqCost*uint64(amount) - sizeCost := costs.baseCost + costs.reqCost*uint64(size)/txSizeCostLimit - if sizeCost > cost { - cost = sizeCost - } - if cost > p.fcParams.BufLimit { - cost = p.fcParams.BufLimit - } - return cost -} - -// HasBlock checks if the peer has a given block -func (p *serverPeer) HasBlock(hash common.Hash, number uint64, hasState bool) bool { - p.lock.RLock() - defer p.lock.RUnlock() - - if p.hasBlockHook != nil { - return p.hasBlockHook(hash, number, hasState) - } - head := p.headInfo.Number - var since, recent uint64 - if hasState { - since = p.stateSince - recent = p.stateRecent - } else { - since = p.chainSince - recent = p.chainRecent - } - return head >= number && number >= since && (recent == 0 || number+recent+4 > head) -} - -// updateFlowControl updates the flow control parameters belonging to the server -// node if the announced key/value set contains relevant fields -func (p *serverPeer) updateFlowControl(update keyValueMap) { - p.lock.Lock() - defer p.lock.Unlock() - - // If any of the flow control params is nil, refuse to update. - var params flowcontrol.ServerParams - if update.get("flowControl/BL", ¶ms.BufLimit) == nil && update.get("flowControl/MRR", ¶ms.MinRecharge) == nil { - // todo can light client set a minimal acceptable flow control params? - p.fcParams = params - p.fcServer.UpdateParams(params) - } - var MRC RequestCostList - if update.get("flowControl/MRC", &MRC) == nil { - costUpdate := MRC.decode(ProtocolLengths[uint(p.version)]) - for code, cost := range costUpdate { - p.fcCosts[code] = cost - } - } -} - -// updateHead updates the head information based on the announcement from -// the peer. -func (p *serverPeer) updateHead(hash common.Hash, number uint64, td *big.Int) { - p.lock.Lock() - defer p.lock.Unlock() - - p.headInfo = blockInfo{Hash: hash, Number: number, Td: td} -} - -// Handshake executes the les protocol handshake, negotiating version number, -// network IDs and genesis blocks. -func (p *serverPeer) Handshake(genesis common.Hash, forkid forkid.ID, forkFilter forkid.Filter) error { - // Note: there is no need to share local head with a server but older servers still - // require these fields so we announce zero values. - return p.handshake(common.Big0, common.Hash{}, 0, genesis, forkid, forkFilter, func(lists *keyValueList) { - // Add some client-specific handshake fields - // - // Enable signed announcement randomly even the server is not trusted. - p.announceType = announceTypeSimple - if p.trusted { - p.announceType = announceTypeSigned - } - *lists = (*lists).add("announceType", p.announceType) - }, func(recv keyValueMap) error { - var ( - rHash common.Hash - rNum uint64 - rTd *big.Int - ) - if err := recv.get("headTd", &rTd); err != nil { - return err - } - if err := recv.get("headHash", &rHash); err != nil { - return err - } - if err := recv.get("headNum", &rNum); err != nil { - return err - } - p.headInfo = blockInfo{Hash: rHash, Number: rNum, Td: rTd} - if recv.get("serveChainSince", &p.chainSince) != nil { - p.onlyAnnounce = true - } - if recv.get("serveRecentChain", &p.chainRecent) != nil { - p.chainRecent = 0 - } - if recv.get("serveStateSince", &p.stateSince) != nil { - p.onlyAnnounce = true - } - if recv.get("serveRecentState", &p.stateRecent) != nil { - p.stateRecent = 0 - } - if recv.get("txRelay", nil) != nil { - p.onlyAnnounce = true - } - if p.version >= lpv4 { - var recentTx uint - if err := recv.get("recentTxLookup", &recentTx); err != nil { - return err - } - p.txHistory = uint64(recentTx) - } else { - // The weak assumption is held here that legacy les server(les2,3) - // has unlimited transaction history. The les serving in these legacy - // versions is disabled if the transaction is unindexed. - p.txHistory = txIndexUnlimited - } - if p.onlyAnnounce && !p.trusted { - return errResp(ErrUselessPeer, "peer cannot serve requests") - } - // Parse flow control handshake packet. - var sParams flowcontrol.ServerParams - if err := recv.get("flowControl/BL", &sParams.BufLimit); err != nil { - return err - } - if err := recv.get("flowControl/MRR", &sParams.MinRecharge); err != nil { - return err - } - var MRC RequestCostList - if err := recv.get("flowControl/MRC", &MRC); err != nil { - return err - } - p.fcParams = sParams - p.fcServer = flowcontrol.NewServerNode(sParams, &mclock.System{}) - p.fcCosts = MRC.decode(ProtocolLengths[uint(p.version)]) - - if !p.onlyAnnounce { - for msgCode := range reqAvgTimeCost { - if p.fcCosts[msgCode] == nil { - return errResp(ErrUselessPeer, "peer does not support message %d", msgCode) - } - } - } - return nil - }) -} - -// setValueTracker sets the value tracker references for connected servers. Note that the -// references should be removed upon disconnection by setValueTracker(nil, nil). -func (p *serverPeer) setValueTracker(nvt *vfc.NodeValueTracker) { - p.vtLock.Lock() - p.nodeValueTracker = nvt - if nvt != nil { - p.sentReqs = make(map[uint64]sentReqEntry) - } else { - p.sentReqs = nil - } - p.vtLock.Unlock() -} - -// updateVtParams updates the server's price table in the value tracker. -func (p *serverPeer) updateVtParams() { - p.vtLock.Lock() - defer p.vtLock.Unlock() - - if p.nodeValueTracker == nil { - return - } - reqCosts := make([]uint64, len(requestList)) - for code, costs := range p.fcCosts { - if m, ok := requestMapping[uint32(code)]; ok { - reqCosts[m.first] = costs.baseCost + costs.reqCost - if m.rest != -1 { - reqCosts[m.rest] = costs.reqCost - } - } - } - p.nodeValueTracker.UpdateCosts(reqCosts) -} - -// sentReqEntry remembers sent requests and their sending times -type sentReqEntry struct { - reqType, amount uint32 - at mclock.AbsTime -} - -// sentRequest marks a request sent at the current moment to this server. -func (p *serverPeer) sentRequest(id uint64, reqType, amount uint32) { - p.vtLock.Lock() - if p.sentReqs != nil { - p.sentReqs[id] = sentReqEntry{reqType, amount, mclock.Now()} - } - p.vtLock.Unlock() -} - -// answeredRequest marks a request answered at the current moment by this server. -func (p *serverPeer) answeredRequest(id uint64) { - p.vtLock.Lock() - if p.sentReqs == nil { - p.vtLock.Unlock() - return - } - e, ok := p.sentReqs[id] - delete(p.sentReqs, id) - nvt := p.nodeValueTracker - p.vtLock.Unlock() - if !ok { - return - } - var ( - vtReqs [2]vfc.ServedRequest - reqCount int - ) - m := requestMapping[e.reqType] - if m.rest == -1 || e.amount <= 1 { - reqCount = 1 - vtReqs[0] = vfc.ServedRequest{ReqType: uint32(m.first), Amount: e.amount} - } else { - reqCount = 2 - vtReqs[0] = vfc.ServedRequest{ReqType: uint32(m.first), Amount: 1} - vtReqs[1] = vfc.ServedRequest{ReqType: uint32(m.rest), Amount: e.amount - 1} - } - dt := time.Duration(mclock.Now() - e.at) - nvt.Served(vtReqs[:reqCount], dt) -} - -// clientPeer represents each node to which the les server is connected. -// The node here refers to the light client. -type clientPeer struct { - peerCommons - - // responseLock ensures that responses are queued in the same order as - // RequestProcessed is called - responseLock sync.Mutex - responseCount uint64 // Counter to generate an unique id for request processing. - - balance vfs.ConnectedBalance - - // invalidLock is used for protecting invalidCount. - invalidLock sync.RWMutex - invalidCount utils.LinearExpiredValue // Counter the invalid request the client peer has made. - - capacity uint64 - // lastAnnounce is the last broadcast created by the server; may be newer than the last head - // sent to the specific client (stored in headInfo) if capacity is zero. In this case the - // latest head is sent when the client gains non-zero capacity. - lastAnnounce announceData - - connectedAt mclock.AbsTime - server bool - errCh chan error - fcClient *flowcontrol.ClientNode // Server side mirror token bucket. -} - -func newClientPeer(version int, network uint64, p *p2p.Peer, rw p2p.MsgReadWriter) *clientPeer { - return &clientPeer{ - peerCommons: peerCommons{ - Peer: p, - rw: rw, - id: p.ID().String(), - version: version, - network: network, - sendQueue: utils.NewExecQueue(100), - closeCh: make(chan struct{}), - }, - invalidCount: utils.LinearExpiredValue{Rate: mclock.AbsTime(time.Hour)}, - errCh: make(chan error, 1), - } -} - -// FreeClientId returns a string identifier for the peer. Multiple peers with -// the same identifier can not be connected in free mode simultaneously. -func (p *clientPeer) FreeClientId() string { - if addr, ok := p.RemoteAddr().(*net.TCPAddr); ok { - if addr.IP.IsLoopback() { - // using peer id instead of loopback ip address allows multiple free - // connections from local machine to own server - return p.id - } else { - return addr.IP.String() - } - } - return p.id -} - -// sendStop notifies the client about being in frozen state -func (p *clientPeer) sendStop() error { - return p2p.Send(p.rw, StopMsg, struct{}{}) -} - -// sendResume notifies the client about getting out of frozen state -func (p *clientPeer) sendResume(bv uint64) error { - return p2p.Send(p.rw, ResumeMsg, bv) -} - -// freeze temporarily puts the client in a frozen state which means all unprocessed -// and subsequent requests are dropped. Unfreezing happens automatically after a short -// time if the client's buffer value is at least in the slightly positive region. -// The client is also notified about being frozen/unfrozen with a Stop/Resume message. -func (p *clientPeer) freeze() { - if p.version < lpv3 { - // if Stop/Resume is not supported then just drop the peer after setting - // its frozen status permanently - p.frozen.Store(true) - p.Peer.Disconnect(p2p.DiscUselessPeer) - return - } - if !p.frozen.Swap(true) { - go func() { - p.sendStop() - time.Sleep(freezeTimeBase + time.Duration(rand.Int63n(int64(freezeTimeRandom)))) - for { - bufValue, bufLimit := p.fcClient.BufferStatus() - if bufLimit == 0 { - return - } - if bufValue <= bufLimit/8 { - time.Sleep(freezeCheckPeriod) - continue - } - p.frozen.Store(false) - p.sendResume(bufValue) - return - } - }() - } -} - -// reply struct represents a reply with the actual data already RLP encoded and -// only the bv (buffer value) missing. This allows the serving mechanism to -// calculate the bv value which depends on the data size before sending the reply. -type reply struct { - w p2p.MsgWriter - msgcode, reqID uint64 - data rlp.RawValue -} - -// send sends the reply with the calculated buffer value -func (r *reply) send(bv uint64) error { - type resp struct { - ReqID, BV uint64 - Data rlp.RawValue - } - return p2p.Send(r.w, r.msgcode, &resp{r.reqID, bv, r.data}) -} - -// size returns the RLP encoded size of the message data -func (r *reply) size() uint32 { - return uint32(len(r.data)) -} - -// replyBlockHeaders creates a reply with a batch of block headers -func (p *clientPeer) replyBlockHeaders(reqID uint64, headers []*types.Header) *reply { - data, _ := rlp.EncodeToBytes(headers) - return &reply{p.rw, BlockHeadersMsg, reqID, data} -} - -// replyBlockBodiesRLP creates a reply with a batch of block contents from -// an already RLP encoded format. -func (p *clientPeer) replyBlockBodiesRLP(reqID uint64, bodies []rlp.RawValue) *reply { - data, _ := rlp.EncodeToBytes(bodies) - return &reply{p.rw, BlockBodiesMsg, reqID, data} -} - -// replyCode creates a reply with a batch of arbitrary internal data, corresponding to the -// hashes requested. -func (p *clientPeer) replyCode(reqID uint64, codes [][]byte) *reply { - data, _ := rlp.EncodeToBytes(codes) - return &reply{p.rw, CodeMsg, reqID, data} -} - -// replyReceiptsRLP creates a reply with a batch of transaction receipts, corresponding to the -// ones requested from an already RLP encoded format. -func (p *clientPeer) replyReceiptsRLP(reqID uint64, receipts []rlp.RawValue) *reply { - data, _ := rlp.EncodeToBytes(receipts) - return &reply{p.rw, ReceiptsMsg, reqID, data} -} - -// replyProofsV2 creates a reply with a batch of merkle proofs, corresponding to the ones requested. -func (p *clientPeer) replyProofsV2(reqID uint64, proofs trienode.ProofList) *reply { - data, _ := rlp.EncodeToBytes(proofs) - return &reply{p.rw, ProofsV2Msg, reqID, data} -} - -// replyHelperTrieProofs creates a reply with a batch of HelperTrie proofs, corresponding to the ones requested. -func (p *clientPeer) replyHelperTrieProofs(reqID uint64, resp HelperTrieResps) *reply { - data, _ := rlp.EncodeToBytes(resp) - return &reply{p.rw, HelperTrieProofsMsg, reqID, data} -} - -// replyTxStatus creates a reply with a batch of transaction status records, corresponding to the ones requested. -func (p *clientPeer) replyTxStatus(reqID uint64, stats []light.TxStatus) *reply { - data, _ := rlp.EncodeToBytes(stats) - return &reply{p.rw, TxStatusMsg, reqID, data} -} - -// sendAnnounce announces the availability of a number of blocks through -// a hash notification. -func (p *clientPeer) sendAnnounce(request announceData) error { - return p2p.Send(p.rw, AnnounceMsg, request) -} - -// InactiveAllowance implements vfs.clientPeer -func (p *clientPeer) InactiveAllowance() time.Duration { - return 0 // will return more than zero for les/5 clients -} - -// getCapacity returns the current capacity of the peer -func (p *clientPeer) getCapacity() uint64 { - p.lock.RLock() - defer p.lock.RUnlock() - - return p.capacity -} - -// UpdateCapacity updates the request serving capacity assigned to a given client -// and also sends an announcement about the updated flow control parameters. -// Note: UpdateCapacity implements vfs.clientPeer and should not block. The requested -// parameter is true if the callback was initiated by ClientPool.SetCapacity on the given peer. -func (p *clientPeer) UpdateCapacity(newCap uint64, requested bool) { - p.lock.Lock() - defer p.lock.Unlock() - - if newCap != p.fcParams.MinRecharge { - p.fcParams = flowcontrol.ServerParams{MinRecharge: newCap, BufLimit: newCap * bufLimitRatio} - p.fcClient.UpdateParams(p.fcParams) - var kvList keyValueList - kvList = kvList.add("flowControl/MRR", newCap) - kvList = kvList.add("flowControl/BL", newCap*bufLimitRatio) - p.queueSend(func() { p.sendAnnounce(announceData{Update: kvList}) }) - } - - if p.capacity == 0 && newCap != 0 { - p.sendLastAnnounce() - } - p.capacity = newCap -} - -// announceOrStore sends the given head announcement to the client if the client is -// active (capacity != 0) and the same announcement hasn't been sent before. If the -// client is inactive the announcement is stored and sent later if the client is -// activated again. -func (p *clientPeer) announceOrStore(announce announceData) { - p.lock.Lock() - defer p.lock.Unlock() - - p.lastAnnounce = announce - if p.capacity != 0 { - p.sendLastAnnounce() - } -} - -// announce sends the given head announcement to the client if it hasn't been sent before -func (p *clientPeer) sendLastAnnounce() { - if p.lastAnnounce.Td == nil { - return - } - if p.headInfo.Td == nil || p.lastAnnounce.Td.Cmp(p.headInfo.Td) > 0 { - if !p.queueSend(func() { p.sendAnnounce(p.lastAnnounce) }) { - p.Log().Debug("Dropped announcement because queue is full", "number", p.lastAnnounce.Number, "hash", p.lastAnnounce.Hash) - } else { - p.Log().Debug("Sent announcement", "number", p.lastAnnounce.Number, "hash", p.lastAnnounce.Hash) - } - p.headInfo = blockInfo{Hash: p.lastAnnounce.Hash, Number: p.lastAnnounce.Number, Td: p.lastAnnounce.Td} - } -} - -// Handshake executes the les protocol handshake, negotiating version number, -// network IDs, difficulties, head and genesis blocks. -func (p *clientPeer) Handshake(td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, forkID forkid.ID, forkFilter forkid.Filter, server *LesServer) error { - recentTx := server.handler.blockchain.TxLookupLimit() - if recentTx != txIndexUnlimited { - if recentTx < blockSafetyMargin { - recentTx = txIndexDisabled - } else { - recentTx -= blockSafetyMargin - txIndexRecentOffset - } - } - if recentTx != txIndexUnlimited && p.version < lpv4 { - return errors.New("cannot serve old clients without a complete tx index") - } - // Note: clientPeer.headInfo should contain the last head announced to the client by us. - // The values announced in the handshake are dummy values for compatibility reasons and should be ignored. - p.headInfo = blockInfo{Hash: head, Number: headNum, Td: td} - return p.handshake(td, head, headNum, genesis, forkID, forkFilter, func(lists *keyValueList) { - // Add some information which services server can offer. - *lists = (*lists).add("serveHeaders", nil) - *lists = (*lists).add("serveChainSince", uint64(0)) - *lists = (*lists).add("serveStateSince", uint64(0)) - - // If local ethereum node is running in archive mode, advertise ourselves we have - // all version state data. Otherwise only recent state is available. - stateRecent := uint64(core.TriesInMemory - blockSafetyMargin) - if server.archiveMode { - stateRecent = 0 - } - *lists = (*lists).add("serveRecentState", stateRecent) - *lists = (*lists).add("txRelay", nil) - if p.version >= lpv4 { - *lists = (*lists).add("recentTxLookup", recentTx) - } - *lists = (*lists).add("flowControl/BL", server.defParams.BufLimit) - *lists = (*lists).add("flowControl/MRR", server.defParams.MinRecharge) - - var costList RequestCostList - if server.costTracker.testCostList != nil { - costList = server.costTracker.testCostList - } else { - costList = server.costTracker.makeCostList(server.costTracker.globalFactor()) - } - *lists = (*lists).add("flowControl/MRC", costList) - p.fcCosts = costList.decode(ProtocolLengths[uint(p.version)]) - p.fcParams = server.defParams - }, func(recv keyValueMap) error { - p.server = recv.get("flowControl/MRR", nil) == nil - if p.server { - p.announceType = announceTypeNone // connected to another server, send no messages - } else { - if recv.get("announceType", &p.announceType) != nil { - // set default announceType on server side - p.announceType = announceTypeSimple - } - } - return nil - }) -} - -func (p *clientPeer) bumpInvalid() { - p.invalidLock.Lock() - p.invalidCount.Add(1, mclock.Now()) - p.invalidLock.Unlock() -} - -func (p *clientPeer) getInvalid() uint64 { - p.invalidLock.RLock() - defer p.invalidLock.RUnlock() - return p.invalidCount.Value(mclock.Now()) -} - -// Disconnect implements vfs.clientPeer -func (p *clientPeer) Disconnect() { - p.Peer.Disconnect(p2p.DiscRequested) -} - -// serverPeerSubscriber is an interface to notify services about added or -// removed server peers -type serverPeerSubscriber interface { - registerPeer(*serverPeer) - unregisterPeer(*serverPeer) -} - -// serverPeerSet represents the set of active server peers currently -// participating in the Light Ethereum sub-protocol. -type serverPeerSet struct { - peers map[string]*serverPeer - // subscribers is a batch of subscribers and peerset will notify - // these subscribers when the peerset changes(new server peer is - // added or removed) - subscribers []serverPeerSubscriber - closed bool - lock sync.RWMutex -} - -// newServerPeerSet creates a new peer set to track the active server peers. -func newServerPeerSet() *serverPeerSet { - return &serverPeerSet{peers: make(map[string]*serverPeer)} -} - -// subscribe adds a service to be notified about added or removed -// peers and also register all active peers into the given service. -func (ps *serverPeerSet) subscribe(sub serverPeerSubscriber) { - ps.lock.Lock() - defer ps.lock.Unlock() - - ps.subscribers = append(ps.subscribers, sub) - for _, p := range ps.peers { - sub.registerPeer(p) - } -} - -// register adds a new server peer into the set, or returns an error if the -// peer is already known. -func (ps *serverPeerSet) register(peer *serverPeer) error { - ps.lock.Lock() - defer ps.lock.Unlock() - - if ps.closed { - return errClosed - } - if _, exist := ps.peers[peer.id]; exist { - return errAlreadyRegistered - } - ps.peers[peer.id] = peer - for _, sub := range ps.subscribers { - sub.registerPeer(peer) - } - return nil -} - -// unregister removes a remote peer from the active set, disabling any further -// actions to/from that particular entity. It also initiates disconnection at -// the networking layer. -func (ps *serverPeerSet) unregister(id string) error { - ps.lock.Lock() - defer ps.lock.Unlock() - - p, ok := ps.peers[id] - if !ok { - return errNotRegistered - } - delete(ps.peers, id) - for _, sub := range ps.subscribers { - sub.unregisterPeer(p) - } - p.Peer.Disconnect(p2p.DiscRequested) - return nil -} - -// ids returns a list of all registered peer IDs -func (ps *serverPeerSet) ids() []string { - ps.lock.RLock() - defer ps.lock.RUnlock() - - var ids []string - for id := range ps.peers { - ids = append(ids, id) - } - return ids -} - -// peer retrieves the registered peer with the given id. -func (ps *serverPeerSet) peer(id string) *serverPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return ps.peers[id] -} - -// len returns if the current number of peers in the set. -func (ps *serverPeerSet) len() int { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return len(ps.peers) -} - -// allServerPeers returns all server peers in a list. -func (ps *serverPeerSet) allPeers() []*serverPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - list := make([]*serverPeer, 0, len(ps.peers)) - for _, p := range ps.peers { - list = append(list, p) - } - return list -} - -// close disconnects all peers. No new peers can be registered -// after close has returned. -func (ps *serverPeerSet) close() { - ps.lock.Lock() - defer ps.lock.Unlock() - - for _, p := range ps.peers { - p.Disconnect(p2p.DiscQuitting) - } - ps.closed = true -} - -// clientPeerSet represents the set of active client peers currently -// participating in the Light Ethereum sub-protocol. -type clientPeerSet struct { - peers map[enode.ID]*clientPeer - lock sync.RWMutex - closed bool - - privateKey *ecdsa.PrivateKey - lastAnnounce, signedAnnounce announceData -} - -// newClientPeerSet creates a new peer set to track the client peers. -func newClientPeerSet() *clientPeerSet { - return &clientPeerSet{peers: make(map[enode.ID]*clientPeer)} -} - -// register adds a new peer into the peer set, or returns an error if the -// peer is already known. -func (ps *clientPeerSet) register(peer *clientPeer) error { - ps.lock.Lock() - defer ps.lock.Unlock() - - if ps.closed { - return errClosed - } - if _, exist := ps.peers[peer.ID()]; exist { - return errAlreadyRegistered - } - ps.peers[peer.ID()] = peer - ps.announceOrStore(peer) - return nil -} - -// unregister removes a remote peer from the peer set, disabling any further -// actions to/from that particular entity. It also initiates disconnection -// at the networking layer. -func (ps *clientPeerSet) unregister(id enode.ID) error { - ps.lock.Lock() - defer ps.lock.Unlock() - - p, ok := ps.peers[id] - if !ok { - return errNotRegistered - } - delete(ps.peers, id) - p.Peer.Disconnect(p2p.DiscRequested) - return nil -} - -// ids returns a list of all registered peer IDs -func (ps *clientPeerSet) ids() []enode.ID { - ps.lock.RLock() - defer ps.lock.RUnlock() - - var ids []enode.ID - for id := range ps.peers { - ids = append(ids, id) - } - return ids -} - -// peer retrieves the registered peer with the given id. -func (ps *clientPeerSet) peer(id enode.ID) *clientPeer { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return ps.peers[id] -} - -// setSignerKey sets the signer key for signed announcements. Should be called before -// starting the protocol handler. -func (ps *clientPeerSet) setSignerKey(privateKey *ecdsa.PrivateKey) { - ps.privateKey = privateKey -} - -// broadcast sends the given announcements to all active peers -func (ps *clientPeerSet) broadcast(announce announceData) { - ps.lock.Lock() - defer ps.lock.Unlock() - - ps.lastAnnounce = announce - for _, peer := range ps.peers { - ps.announceOrStore(peer) - } -} - -// announceOrStore sends the requested type of announcement to the given peer or stores -// it for later if the peer is inactive (capacity == 0). -func (ps *clientPeerSet) announceOrStore(p *clientPeer) { - if ps.lastAnnounce.Td == nil { - return - } - switch p.announceType { - case announceTypeSimple: - p.announceOrStore(ps.lastAnnounce) - case announceTypeSigned: - if ps.signedAnnounce.Hash != ps.lastAnnounce.Hash { - ps.signedAnnounce = ps.lastAnnounce - ps.signedAnnounce.sign(ps.privateKey) - } - p.announceOrStore(ps.signedAnnounce) - } -} - -// close disconnects all peers. No new peers can be registered -// after close has returned. -func (ps *clientPeerSet) close() { - ps.lock.Lock() - defer ps.lock.Unlock() - - for _, p := range ps.peers { - p.Peer.Disconnect(p2p.DiscQuitting) - } - ps.closed = true -} - -// serverSet is a special set which contains all connected les servers. -// Les servers will also be discovered by discovery protocol because they -// also run the LES protocol. We can't drop them although they are useless -// for us(server) but for other protocols(e.g. ETH) upon the devp2p they -// may be useful. -type serverSet struct { - lock sync.Mutex - set map[string]*clientPeer - closed bool -} - -func newServerSet() *serverSet { - return &serverSet{set: make(map[string]*clientPeer)} -} - -func (s *serverSet) register(peer *clientPeer) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.closed { - return errClosed - } - if _, exist := s.set[peer.id]; exist { - return errAlreadyRegistered - } - s.set[peer.id] = peer - return nil -} - -func (s *serverSet) unregister(peer *clientPeer) error { - s.lock.Lock() - defer s.lock.Unlock() - - if s.closed { - return errClosed - } - if _, exist := s.set[peer.id]; !exist { - return errNotRegistered - } - delete(s.set, peer.id) - peer.Peer.Disconnect(p2p.DiscQuitting) - return nil -} - -func (s *serverSet) close() { - s.lock.Lock() - defer s.lock.Unlock() - - for _, p := range s.set { - p.Peer.Disconnect(p2p.DiscQuitting) - } - s.closed = true -} diff --git a/les/peer_test.go b/les/peer_test.go deleted file mode 100644 index d6ca0eac7c..0000000000 --- a/les/peer_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2019 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 les - -import ( - "crypto/rand" - "errors" - "math/big" - "reflect" - "sort" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" -) - -type testServerPeerSub struct { - regCh chan *serverPeer - unregCh chan *serverPeer -} - -func newTestServerPeerSub() *testServerPeerSub { - return &testServerPeerSub{ - regCh: make(chan *serverPeer, 1), - unregCh: make(chan *serverPeer, 1), - } -} - -func (t *testServerPeerSub) registerPeer(p *serverPeer) { t.regCh <- p } -func (t *testServerPeerSub) unregisterPeer(p *serverPeer) { t.unregCh <- p } - -func TestPeerSubscription(t *testing.T) { - peers := newServerPeerSet() - defer peers.close() - - checkIds := func(expect []string) { - given := peers.ids() - if len(given) == 0 && len(expect) == 0 { - return - } - sort.Strings(given) - sort.Strings(expect) - if !reflect.DeepEqual(given, expect) { - t.Fatalf("all peer ids mismatch, want %v, given %v", expect, given) - } - } - checkPeers := func(peerCh chan *serverPeer) { - select { - case <-peerCh: - case <-time.NewTimer(100 * time.Millisecond).C: - t.Fatalf("timeout, no event received") - } - select { - case <-peerCh: - t.Fatalf("unexpected event received") - case <-time.NewTimer(10 * time.Millisecond).C: - } - } - checkIds([]string{}) - - sub := newTestServerPeerSub() - peers.subscribe(sub) - - // Generate a random id and create the peer - var id enode.ID - rand.Read(id[:]) - peer := newServerPeer(2, NetworkId, false, p2p.NewPeer(id, "name", nil), nil) - peers.register(peer) - - checkIds([]string{peer.id}) - checkPeers(sub.regCh) - - peers.unregister(peer.id) - checkIds([]string{}) - checkPeers(sub.unregCh) -} - -type fakeChain struct{} - -func (f *fakeChain) Config() *params.ChainConfig { return params.MainnetChainConfig } -func (f *fakeChain) Genesis() *types.Block { - return core.DefaultGenesisBlock().ToBlock() -} -func (f *fakeChain) CurrentHeader() *types.Header { return &types.Header{Number: big.NewInt(10000000)} } - -func TestHandshake(t *testing.T) { - // Create a message pipe to communicate through - app, net := p2p.MsgPipe() - - // Generate a random id and create the peer - var id enode.ID - rand.Read(id[:]) - - peer1 := newClientPeer(2, NetworkId, p2p.NewPeer(id, "name", nil), net) - peer2 := newServerPeer(2, NetworkId, true, p2p.NewPeer(id, "name", nil), app) - - var ( - errCh1 = make(chan error, 1) - errCh2 = make(chan error, 1) - - td = big.NewInt(100) - head = common.HexToHash("deadbeef") - headNum = uint64(10) - genesis = common.HexToHash("cafebabe") - - chain1, chain2 = &fakeChain{}, &fakeChain{} - forkID1 = forkid.NewID(chain1.Config(), chain1.Genesis(), chain1.CurrentHeader().Number.Uint64(), chain1.CurrentHeader().Time) - forkID2 = forkid.NewID(chain2.Config(), chain2.Genesis(), chain2.CurrentHeader().Number.Uint64(), chain2.CurrentHeader().Time) - filter1, filter2 = forkid.NewFilter(chain1), forkid.NewFilter(chain2) - ) - - go func() { - errCh1 <- peer1.handshake(td, head, headNum, genesis, forkID1, filter1, func(list *keyValueList) { - var announceType uint64 = announceTypeSigned - *list = (*list).add("announceType", announceType) - }, nil) - }() - go func() { - errCh2 <- peer2.handshake(td, head, headNum, genesis, forkID2, filter2, nil, func(recv keyValueMap) error { - var reqType uint64 - err := recv.get("announceType", &reqType) - if err != nil { - return err - } - if reqType != announceTypeSigned { - return errors.New("expected announceTypeSigned") - } - return nil - }) - }() - - for i := 0; i < 2; i++ { - select { - case err := <-errCh1: - if err != nil { - t.Fatalf("handshake failed, %v", err) - } - case err := <-errCh2: - if err != nil { - t.Fatalf("handshake failed, %v", err) - } - case <-time.After(time.Second): - t.Fatalf("timeout") - } - } -} diff --git a/les/protocol.go b/les/protocol.go deleted file mode 100644 index cfebdbfb9a..0000000000 --- a/les/protocol.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright 2016 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 les - -import ( - "crypto/ecdsa" - "errors" - "fmt" - "io" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - vfc "github.com/ethereum/go-ethereum/les/vflux/client" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -// Constants to match up protocol versions and messages -const ( - lpv2 = 2 - lpv3 = 3 - lpv4 = 4 -) - -// Supported versions of the les protocol (first is primary) -var ( - ClientProtocolVersions = []uint{lpv2, lpv3, lpv4} - ServerProtocolVersions = []uint{lpv2, lpv3, lpv4} -) - -// ProtocolLengths is the number of implemented message corresponding to different protocol versions. -var ProtocolLengths = map[uint]uint64{lpv2: 22, lpv3: 24, lpv4: 24} - -const ( - NetworkId = 1 - ProtocolMaxMsgSize = 10 * 1024 * 1024 // Maximum cap on the size of a protocol message - blockSafetyMargin = 4 // safety margin applied to block ranges specified relative to head block - - txIndexUnlimited = 0 // this value in the "recentTxLookup" handshake field means the entire tx index history is served - txIndexDisabled = 1 // this value means tx index is not served at all - txIndexRecentOffset = 1 // txIndexRecentOffset + N in the handshake field means then tx index of the last N blocks is supported -) - -// les protocol message codes -const ( - // Protocol messages inherited from LPV1 - StatusMsg = 0x00 - AnnounceMsg = 0x01 - GetBlockHeadersMsg = 0x02 - BlockHeadersMsg = 0x03 - GetBlockBodiesMsg = 0x04 - BlockBodiesMsg = 0x05 - GetReceiptsMsg = 0x06 - ReceiptsMsg = 0x07 - GetCodeMsg = 0x0a - CodeMsg = 0x0b - // Protocol messages introduced in LPV2 - GetProofsV2Msg = 0x0f - ProofsV2Msg = 0x10 - GetHelperTrieProofsMsg = 0x11 - HelperTrieProofsMsg = 0x12 - SendTxV2Msg = 0x13 - GetTxStatusMsg = 0x14 - TxStatusMsg = 0x15 - // Protocol messages introduced in LPV3 - StopMsg = 0x16 - ResumeMsg = 0x17 -) - -// GetBlockHeadersData represents a block header query (the request ID is not included) -type GetBlockHeadersData struct { - Origin hashOrNumber // Block from which to retrieve headers - Amount uint64 // Maximum number of headers to retrieve - Skip uint64 // Blocks to skip between consecutive headers - Reverse bool // Query direction (false = rising towards latest, true = falling towards genesis) -} - -// GetBlockHeadersPacket represents a block header request -type GetBlockHeadersPacket struct { - ReqID uint64 - Query GetBlockHeadersData -} - -// GetBlockBodiesPacket represents a block body request -type GetBlockBodiesPacket struct { - ReqID uint64 - Hashes []common.Hash -} - -// GetCodePacket represents a contract code request -type GetCodePacket struct { - ReqID uint64 - Reqs []CodeReq -} - -// GetReceiptsPacket represents a block receipts request -type GetReceiptsPacket struct { - ReqID uint64 - Hashes []common.Hash -} - -// GetProofsPacket represents a proof request -type GetProofsPacket struct { - ReqID uint64 - Reqs []ProofReq -} - -// GetHelperTrieProofsPacket represents a helper trie proof request -type GetHelperTrieProofsPacket struct { - ReqID uint64 - Reqs []HelperTrieReq -} - -// SendTxPacket represents a transaction propagation request -type SendTxPacket struct { - ReqID uint64 - Txs []*types.Transaction -} - -// GetTxStatusPacket represents a transaction status query -type GetTxStatusPacket struct { - ReqID uint64 - Hashes []common.Hash -} - -type requestInfo struct { - name string - maxCount uint64 - refBasketFirst, refBasketRest float64 -} - -// reqMapping maps an LES request to one or two vflux service vector entries. -// If rest != -1 and the request type is used with amounts larger than one then the -// first one of the multi-request is mapped to first while the rest is mapped to rest. -type reqMapping struct { - first, rest int -} - -var ( - // requests describes the available LES request types and their initializing amounts - // in the vfc.ValueTracker reference basket. Initial values are estimates - // based on the same values as the server's default cost estimates (reqAvgTimeCost). - requests = map[uint64]requestInfo{ - GetBlockHeadersMsg: {"GetBlockHeaders", MaxHeaderFetch, 10, 1000}, - GetBlockBodiesMsg: {"GetBlockBodies", MaxBodyFetch, 1, 0}, - GetReceiptsMsg: {"GetReceipts", MaxReceiptFetch, 1, 0}, - GetCodeMsg: {"GetCode", MaxCodeFetch, 1, 0}, - GetProofsV2Msg: {"GetProofsV2", MaxProofsFetch, 10, 0}, - GetHelperTrieProofsMsg: {"GetHelperTrieProofs", MaxHelperTrieProofsFetch, 10, 100}, - SendTxV2Msg: {"SendTxV2", MaxTxSend, 1, 0}, - GetTxStatusMsg: {"GetTxStatus", MaxTxStatus, 10, 0}, - } - requestList []vfc.RequestInfo - requestMapping map[uint32]reqMapping -) - -// init creates a request list and mapping between protocol message codes and vflux -// service vector indices. -func init() { - requestMapping = make(map[uint32]reqMapping) - for code, req := range requests { - cost := reqAvgTimeCost[code] - rm := reqMapping{len(requestList), -1} - requestList = append(requestList, vfc.RequestInfo{ - Name: req.name + ".first", - InitAmount: req.refBasketFirst, - InitValue: float64(cost.baseCost + cost.reqCost), - }) - if req.refBasketRest != 0 { - rm.rest = len(requestList) - requestList = append(requestList, vfc.RequestInfo{ - Name: req.name + ".rest", - InitAmount: req.refBasketRest, - InitValue: float64(cost.reqCost), - }) - } - requestMapping[uint32(code)] = rm - } -} - -type errCode int - -const ( - ErrMsgTooLarge = iota - ErrDecode - ErrInvalidMsgCode - ErrProtocolVersionMismatch - ErrNetworkIdMismatch - ErrGenesisBlockMismatch - ErrNoStatusMsg - ErrExtraStatusMsg - ErrSuspendedPeer - ErrUselessPeer - ErrRequestRejected - ErrUnexpectedResponse - ErrInvalidResponse - ErrTooManyTimeouts - ErrMissingKey - ErrForkIDRejected -) - -func (e errCode) String() string { - return errorToString[int(e)] -} - -// XXX change once legacy code is out -var errorToString = map[int]string{ - ErrMsgTooLarge: "Message too long", - ErrDecode: "Invalid message", - ErrInvalidMsgCode: "Invalid message code", - ErrProtocolVersionMismatch: "Protocol version mismatch", - ErrNetworkIdMismatch: "NetworkId mismatch", - ErrGenesisBlockMismatch: "Genesis block mismatch", - ErrNoStatusMsg: "No status message", - ErrExtraStatusMsg: "Extra status message", - ErrSuspendedPeer: "Suspended peer", - ErrRequestRejected: "Request rejected", - ErrUnexpectedResponse: "Unexpected response", - ErrInvalidResponse: "Invalid response", - ErrTooManyTimeouts: "Too many request timeouts", - ErrMissingKey: "Key missing from list", - ErrForkIDRejected: "ForkID rejected", -} - -// announceData is the network packet for the block announcements. -type announceData struct { - Hash common.Hash // Hash of one particular block being announced - Number uint64 // Number of one particular block being announced - Td *big.Int // Total difficulty of one particular block being announced - ReorgDepth uint64 - Update keyValueList -} - -// sanityCheck verifies that the values are reasonable, as a DoS protection -func (a *announceData) sanityCheck() error { - if tdlen := a.Td.BitLen(); tdlen > 100 { - return fmt.Errorf("too large block TD: bitlen %d", tdlen) - } - return nil -} - -// sign adds a signature to the block announcement by the given privKey -func (a *announceData) sign(privKey *ecdsa.PrivateKey) { - rlp, _ := rlp.EncodeToBytes(blockInfo{a.Hash, a.Number, a.Td}) - sig, _ := crypto.Sign(crypto.Keccak256(rlp), privKey) - a.Update = a.Update.add("sign", sig) -} - -// checkSignature verifies if the block announcement has a valid signature by the given pubKey -func (a *announceData) checkSignature(id enode.ID, update keyValueMap) error { - var sig []byte - if err := update.get("sign", &sig); err != nil { - return err - } - rlp, _ := rlp.EncodeToBytes(blockInfo{a.Hash, a.Number, a.Td}) - recPubkey, err := crypto.SigToPub(crypto.Keccak256(rlp), sig) - if err != nil { - return err - } - if id == enode.PubkeyToIDV4(recPubkey) { - return nil - } - return errors.New("wrong signature") -} - -type blockInfo struct { - Hash common.Hash // Hash of one particular block being announced - Number uint64 // Number of one particular block being announced - Td *big.Int // Total difficulty of one particular block being announced -} - -// hashOrNumber is a combined field for specifying an origin block. -type hashOrNumber struct { - Hash common.Hash // Block hash from which to retrieve headers (excludes Number) - Number uint64 // Block hash from which to retrieve headers (excludes Hash) -} - -// EncodeRLP is a specialized encoder for hashOrNumber to encode only one of the -// two contained union fields. -func (hn *hashOrNumber) EncodeRLP(w io.Writer) error { - if hn.Hash == (common.Hash{}) { - return rlp.Encode(w, hn.Number) - } - if hn.Number != 0 { - return fmt.Errorf("both origin hash (%x) and number (%d) provided", hn.Hash, hn.Number) - } - return rlp.Encode(w, hn.Hash) -} - -// DecodeRLP is a specialized decoder for hashOrNumber to decode the contents -// into either a block hash or a block number. -func (hn *hashOrNumber) DecodeRLP(s *rlp.Stream) error { - _, size, err := s.Kind() - switch { - case err != nil: - return err - case size == 32: - hn.Number = 0 - return s.Decode(&hn.Hash) - case size <= 8: - hn.Hash = common.Hash{} - return s.Decode(&hn.Number) - default: - return fmt.Errorf("invalid input size %d for origin", size) - } -} - -// CodeData is the network response packet for a node data retrieval. -type CodeData []struct { - Value []byte -} diff --git a/les/request_test.go b/les/request_test.go deleted file mode 100644 index 5e354b7efd..0000000000 --- a/les/request_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2016 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 les - -// Note: these tests are disabled now because they cannot work with the old sync -// mechanism removed but will be useful again once the PoS ultralight mode is implemented - -/* -import ( - "context" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/light" -) - -var testBankSecureTrieKey = secAddr(bankAddr) - -func secAddr(addr common.Address) []byte { - return crypto.Keccak256(addr[:]) -} - -type accessTestFn func(db ethdb.Database, bhash common.Hash, number uint64) light.OdrRequest - -func TestBlockAccessLes2(t *testing.T) { testAccess(t, 2, tfBlockAccess) } -func TestBlockAccessLes3(t *testing.T) { testAccess(t, 3, tfBlockAccess) } -func TestBlockAccessLes4(t *testing.T) { testAccess(t, 4, tfBlockAccess) } - -func tfBlockAccess(db ethdb.Database, bhash common.Hash, number uint64) light.OdrRequest { - return &light.BlockRequest{Hash: bhash, Number: number} -} - -func TestReceiptsAccessLes2(t *testing.T) { testAccess(t, 2, tfReceiptsAccess) } -func TestReceiptsAccessLes3(t *testing.T) { testAccess(t, 3, tfReceiptsAccess) } -func TestReceiptsAccessLes4(t *testing.T) { testAccess(t, 4, tfReceiptsAccess) } - -func tfReceiptsAccess(db ethdb.Database, bhash common.Hash, number uint64) light.OdrRequest { - return &light.ReceiptsRequest{Hash: bhash, Number: number} -} - -func TestTrieEntryAccessLes2(t *testing.T) { testAccess(t, 2, tfTrieEntryAccess) } -func TestTrieEntryAccessLes3(t *testing.T) { testAccess(t, 3, tfTrieEntryAccess) } -func TestTrieEntryAccessLes4(t *testing.T) { testAccess(t, 4, tfTrieEntryAccess) } - -func tfTrieEntryAccess(db ethdb.Database, bhash common.Hash, number uint64) light.OdrRequest { - if number := rawdb.ReadHeaderNumber(db, bhash); number != nil { - return &light.TrieRequest{Id: light.StateTrieID(rawdb.ReadHeader(db, bhash, *number)), Key: testBankSecureTrieKey} - } - return nil -} - -func TestCodeAccessLes2(t *testing.T) { testAccess(t, 2, tfCodeAccess) } -func TestCodeAccessLes3(t *testing.T) { testAccess(t, 3, tfCodeAccess) } -func TestCodeAccessLes4(t *testing.T) { testAccess(t, 4, tfCodeAccess) } - -func tfCodeAccess(db ethdb.Database, bhash common.Hash, num uint64) light.OdrRequest { - number := rawdb.ReadHeaderNumber(db, bhash) - if number != nil { - return nil - } - header := rawdb.ReadHeader(db, bhash, *number) - if header.Number.Uint64() < testContractDeployed { - return nil - } - sti := light.StateTrieID(header) - ci := light.StorageTrieID(sti, testContractAddr, types.EmptyRootHash) - return &light.CodeRequest{Id: ci, Hash: crypto.Keccak256Hash(testContractCodeDeployed)} -} - -func testAccess(t *testing.T, protocol int, fn accessTestFn) { - // Assemble the test environment - netconfig := testnetConfig{ - blocks: 4, - protocol: protocol, - indexFn: nil, - connect: true, - nopruning: true, - } - server, client, tearDown := newClientServerEnv(t, netconfig) - defer tearDown() - - // Ensure the client has synced all necessary data. - clientHead := client.handler.backend.blockchain.CurrentHeader() - if clientHead.Number.Uint64() != 4 { - t.Fatalf("Failed to sync the chain with server, head: %v", clientHead.Number.Uint64()) - } - - test := func(expFail uint64) { - for i := uint64(0); i <= server.handler.blockchain.CurrentHeader().Number.Uint64(); i++ { - bhash := rawdb.ReadCanonicalHash(server.db, i) - if req := fn(client.db, bhash, i); req != nil { - ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) - - err := client.handler.backend.odr.Retrieve(ctx, req) - cancel() - - got := err == nil - exp := i < expFail - if exp && !got { - t.Errorf("object retrieval failed") - } - if !exp && got { - t.Errorf("unexpected object retrieval success") - } - } - } - } - test(5) -} -*/ diff --git a/les/retrieve.go b/les/retrieve.go deleted file mode 100644 index 728f960a54..0000000000 --- a/les/retrieve.go +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright 2017 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 les - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/ethereum/go-ethereum/light" -) - -var ( - retryQueue = time.Millisecond * 100 - hardRequestTimeout = time.Second * 10 -) - -// retrieveManager is a layer on top of requestDistributor which takes care of -// matching replies by request ID and handles timeouts and resends if necessary. -type retrieveManager struct { - dist *requestDistributor - peers *serverPeerSet - softRequestTimeout func() time.Duration - - lock sync.RWMutex - sentReqs map[uint64]*sentReq -} - -// validatorFunc is a function that processes a reply message -type validatorFunc func(distPeer, *Msg) error - -// sentReq represents a request sent and tracked by retrieveManager -type sentReq struct { - rm *retrieveManager - req *distReq - id uint64 - validate validatorFunc - - eventsCh chan reqPeerEvent - stopCh chan struct{} - stopped bool - err error - - lock sync.RWMutex // protect access to sentTo map - sentTo map[distPeer]sentReqToPeer - - lastReqQueued bool // last request has been queued but not sent - lastReqSentTo distPeer // if not nil then last request has been sent to given peer but not timed out - reqSrtoCount int // number of requests that reached soft (but not hard) timeout -} - -// sentReqToPeer notifies the request-from-peer goroutine (tryRequest) about a response -// delivered by the given peer. Only one delivery is allowed per request per peer, -// after which delivered is set to true, the validity of the response is sent on the -// valid channel and no more responses are accepted. -type sentReqToPeer struct { - delivered, frozen bool - event chan int -} - -// reqPeerEvent is sent by the request-from-peer goroutine (tryRequest) to the -// request state machine (retrieveLoop) through the eventsCh channel. -type reqPeerEvent struct { - event int - peer distPeer -} - -const ( - rpSent = iota // if peer == nil, not sent (no suitable peers) - rpSoftTimeout - rpHardTimeout - rpDeliveredValid - rpDeliveredInvalid - rpNotDelivered -) - -// newRetrieveManager creates the retrieve manager -func newRetrieveManager(peers *serverPeerSet, dist *requestDistributor, srto func() time.Duration) *retrieveManager { - return &retrieveManager{ - peers: peers, - dist: dist, - sentReqs: make(map[uint64]*sentReq), - softRequestTimeout: srto, - } -} - -// retrieve sends a request (to multiple peers if necessary) and waits for an answer -// that is delivered through the deliver function and successfully validated by the -// validator callback. It returns when a valid answer is delivered or the context is -// cancelled. -func (rm *retrieveManager) retrieve(ctx context.Context, reqID uint64, req *distReq, val validatorFunc, shutdown chan struct{}) error { - sentReq := rm.sendReq(reqID, req, val) - select { - case <-sentReq.stopCh: - case <-ctx.Done(): - sentReq.stop(ctx.Err()) - case <-shutdown: - sentReq.stop(errors.New("client is shutting down")) - } - return sentReq.getError() -} - -// sendReq starts a process that keeps trying to retrieve a valid answer for a -// request from any suitable peers until stopped or succeeded. -func (rm *retrieveManager) sendReq(reqID uint64, req *distReq, val validatorFunc) *sentReq { - r := &sentReq{ - rm: rm, - req: req, - id: reqID, - sentTo: make(map[distPeer]sentReqToPeer), - stopCh: make(chan struct{}), - eventsCh: make(chan reqPeerEvent, 10), - validate: val, - } - - canSend := req.canSend - req.canSend = func(p distPeer) bool { - // add an extra check to canSend: the request has not been sent to the same peer before - r.lock.RLock() - _, sent := r.sentTo[p] - r.lock.RUnlock() - return !sent && canSend(p) - } - - request := req.request - req.request = func(p distPeer) func() { - // before actually sending the request, put an entry into the sentTo map - r.lock.Lock() - r.sentTo[p] = sentReqToPeer{delivered: false, frozen: false, event: make(chan int, 1)} - r.lock.Unlock() - return request(p) - } - rm.lock.Lock() - rm.sentReqs[reqID] = r - rm.lock.Unlock() - - go r.retrieveLoop() - return r -} - -// deliver is called by the LES protocol manager to deliver reply messages to waiting requests -func (rm *retrieveManager) deliver(peer distPeer, msg *Msg) error { - rm.lock.RLock() - req, ok := rm.sentReqs[msg.ReqID] - rm.lock.RUnlock() - - if ok { - return req.deliver(peer, msg) - } - return errResp(ErrUnexpectedResponse, "reqID = %v", msg.ReqID) -} - -// frozen is called by the LES protocol manager when a server has suspended its service and we -// should not expect an answer for the requests already sent there -func (rm *retrieveManager) frozen(peer distPeer) { - rm.lock.RLock() - defer rm.lock.RUnlock() - - for _, req := range rm.sentReqs { - req.frozen(peer) - } -} - -// reqStateFn represents a state of the retrieve loop state machine -type reqStateFn func() reqStateFn - -// retrieveLoop is the retrieval state machine event loop -func (r *sentReq) retrieveLoop() { - go r.tryRequest() - r.lastReqQueued = true - state := r.stateRequesting - - for state != nil { - state = state() - } - - r.rm.lock.Lock() - delete(r.rm.sentReqs, r.id) - r.rm.lock.Unlock() -} - -// stateRequesting: a request has been queued or sent recently; when it reaches soft timeout, -// a new request is sent to a new peer -func (r *sentReq) stateRequesting() reqStateFn { - select { - case ev := <-r.eventsCh: - r.update(ev) - switch ev.event { - case rpSent: - if ev.peer == nil { - // request send failed, no more suitable peers - if r.waiting() { - // we are already waiting for sent requests which may succeed so keep waiting - return r.stateNoMorePeers - } - // nothing to wait for, no more peers to ask, return with error - r.stop(light.ErrNoPeers) - // no need to go to stopped state because waiting() already returned false - return nil - } - case rpSoftTimeout: - // last request timed out, try asking a new peer - go r.tryRequest() - r.lastReqQueued = true - return r.stateRequesting - case rpDeliveredInvalid, rpNotDelivered: - // if it was the last sent request (set to nil by update) then start a new one - if !r.lastReqQueued && r.lastReqSentTo == nil { - go r.tryRequest() - r.lastReqQueued = true - } - return r.stateRequesting - case rpDeliveredValid: - r.stop(nil) - return r.stateStopped - } - return r.stateRequesting - case <-r.stopCh: - return r.stateStopped - } -} - -// stateNoMorePeers: could not send more requests because no suitable peers are available. -// Peers may become suitable for a certain request later or new peers may appear so we -// keep trying. -func (r *sentReq) stateNoMorePeers() reqStateFn { - select { - case <-time.After(retryQueue): - go r.tryRequest() - r.lastReqQueued = true - return r.stateRequesting - case ev := <-r.eventsCh: - r.update(ev) - if ev.event == rpDeliveredValid { - r.stop(nil) - return r.stateStopped - } - if r.waiting() { - return r.stateNoMorePeers - } - r.stop(light.ErrNoPeers) - return nil - case <-r.stopCh: - return r.stateStopped - } -} - -// stateStopped: request succeeded or cancelled, just waiting for some peers -// to either answer or time out hard -func (r *sentReq) stateStopped() reqStateFn { - for r.waiting() { - r.update(<-r.eventsCh) - } - return nil -} - -// update updates the queued/sent flags and timed out peers counter according to the event -func (r *sentReq) update(ev reqPeerEvent) { - switch ev.event { - case rpSent: - r.lastReqQueued = false - r.lastReqSentTo = ev.peer - case rpSoftTimeout: - r.lastReqSentTo = nil - r.reqSrtoCount++ - case rpHardTimeout: - r.reqSrtoCount-- - case rpDeliveredValid, rpDeliveredInvalid, rpNotDelivered: - if ev.peer == r.lastReqSentTo { - r.lastReqSentTo = nil - } else { - r.reqSrtoCount-- - } - } -} - -// waiting returns true if the retrieval mechanism is waiting for an answer from -// any peer -func (r *sentReq) waiting() bool { - return r.lastReqQueued || r.lastReqSentTo != nil || r.reqSrtoCount > 0 -} - -// tryRequest tries to send the request to a new peer and waits for it to either -// succeed or time out if it has been sent. It also sends the appropriate reqPeerEvent -// messages to the request's event channel. -func (r *sentReq) tryRequest() { - sent := r.rm.dist.queue(r.req) - var p distPeer - select { - case p = <-sent: - case <-r.stopCh: - if r.rm.dist.cancel(r.req) { - p = nil - } else { - p = <-sent - } - } - - r.eventsCh <- reqPeerEvent{rpSent, p} - if p == nil { - return - } - - hrto := false - - r.lock.RLock() - s, ok := r.sentTo[p] - r.lock.RUnlock() - if !ok { - panic(nil) - } - - defer func() { - pp, ok := p.(*serverPeer) - if hrto && ok { - pp.Log().Debug("Request timed out hard") - if r.rm.peers != nil { - r.rm.peers.unregister(pp.id) - } - } - }() - - select { - case event := <-s.event: - if event == rpNotDelivered { - r.lock.Lock() - delete(r.sentTo, p) - r.lock.Unlock() - } - r.eventsCh <- reqPeerEvent{event, p} - return - case <-time.After(r.rm.softRequestTimeout()): - r.eventsCh <- reqPeerEvent{rpSoftTimeout, p} - } - - select { - case event := <-s.event: - if event == rpNotDelivered { - r.lock.Lock() - delete(r.sentTo, p) - r.lock.Unlock() - } - r.eventsCh <- reqPeerEvent{event, p} - case <-time.After(hardRequestTimeout): - hrto = true - r.eventsCh <- reqPeerEvent{rpHardTimeout, p} - } -} - -// deliver a reply belonging to this request -func (r *sentReq) deliver(peer distPeer, msg *Msg) error { - r.lock.Lock() - defer r.lock.Unlock() - - s, ok := r.sentTo[peer] - if !ok || s.delivered { - return errResp(ErrUnexpectedResponse, "reqID = %v", msg.ReqID) - } - if s.frozen { - return nil - } - valid := r.validate(peer, msg) == nil - r.sentTo[peer] = sentReqToPeer{delivered: true, frozen: false, event: s.event} - if valid { - s.event <- rpDeliveredValid - } else { - s.event <- rpDeliveredInvalid - } - if !valid { - return errResp(ErrInvalidResponse, "reqID = %v", msg.ReqID) - } - return nil -} - -// frozen sends a "not delivered" event to the peer event channel belonging to the -// given peer if the request has been sent there, causing the state machine to not -// expect an answer and potentially even send the request to the same peer again -// when canSend allows it. -func (r *sentReq) frozen(peer distPeer) { - r.lock.Lock() - defer r.lock.Unlock() - - s, ok := r.sentTo[peer] - if ok && !s.delivered && !s.frozen { - r.sentTo[peer] = sentReqToPeer{delivered: false, frozen: true, event: s.event} - s.event <- rpNotDelivered - } -} - -// stop stops the retrieval process and sets an error code that will be returned -// by getError -func (r *sentReq) stop(err error) { - r.lock.Lock() - if !r.stopped { - r.stopped = true - r.err = err - close(r.stopCh) - } - r.lock.Unlock() -} - -// getError returns any retrieval error (either internally generated or set by the -// stop function) after stopCh has been closed -func (r *sentReq) getError() error { - return r.err -} diff --git a/les/server.go b/les/server.go deleted file mode 100644 index d84856c7fb..0000000000 --- a/les/server.go +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2016 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 les - -import ( - "crypto/ecdsa" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/flowcontrol" - vfs "github.com/ethereum/go-ethereum/les/vflux/server" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" -) - -var ( - defaultPosFactors = vfs.PriceFactors{TimeFactor: 0, CapacityFactor: 1, RequestFactor: 1} - defaultNegFactors = vfs.PriceFactors{TimeFactor: 0, CapacityFactor: 1, RequestFactor: 1} -) - -const defaultConnectedBias = time.Minute * 3 - -type ethBackend interface { - ArchiveMode() bool - BlockChain() *core.BlockChain - BloomIndexer() *core.ChainIndexer - ChainDb() ethdb.Database - Synced() bool - TxPool() *txpool.TxPool -} - -type LesServer struct { - lesCommons - - archiveMode bool // Flag whether the ethereum node runs in archive mode. - handler *serverHandler - peers *clientPeerSet - serverset *serverSet - vfluxServer *vfs.Server - privateKey *ecdsa.PrivateKey - - // Flow control and capacity management - fcManager *flowcontrol.ClientManager - costTracker *costTracker - defParams flowcontrol.ServerParams - servingQueue *servingQueue - clientPool *vfs.ClientPool - - minCapacity, maxCapacity uint64 - threadsIdle int // Request serving threads count when system is idle. - threadsBusy int // Request serving threads count when system is busy(block insertion). - - p2pSrv *p2p.Server -} - -func NewLesServer(node *node.Node, e ethBackend, config *ethconfig.Config) (*LesServer, error) { - lesDb, err := node.OpenDatabase("les.server", 0, 0, "eth/db/lesserver/", false) - if err != nil { - return nil, err - } - // Calculate the number of threads used to service the light client - // requests based on the user-specified value. - threads := config.LightServ * 4 / 100 - if threads < 4 { - threads = 4 - } - srv := &LesServer{ - lesCommons: lesCommons{ - genesis: e.BlockChain().Genesis().Hash(), - config: config, - chainConfig: e.BlockChain().Config(), - iConfig: light.DefaultServerIndexerConfig, - chainDb: e.ChainDb(), - lesDb: lesDb, - chainReader: e.BlockChain(), - chtIndexer: light.NewChtIndexer(e.ChainDb(), nil, params.CHTFrequency, params.HelperTrieProcessConfirmations, true), - bloomTrieIndexer: light.NewBloomTrieIndexer(e.ChainDb(), nil, params.BloomBitsBlocks, params.BloomTrieFrequency, true), - closeCh: make(chan struct{}), - }, - archiveMode: e.ArchiveMode(), - peers: newClientPeerSet(), - serverset: newServerSet(), - vfluxServer: vfs.NewServer(time.Millisecond * 10), - fcManager: flowcontrol.NewClientManager(nil, &mclock.System{}), - servingQueue: newServingQueue(int64(time.Millisecond*10), float64(config.LightServ)/100), - threadsBusy: config.LightServ/100 + 1, - threadsIdle: threads, - p2pSrv: node.Server(), - } - issync := e.Synced - if config.LightNoSyncServe { - issync = func() bool { return true } - } - srv.handler = newServerHandler(srv, e.BlockChain(), e.ChainDb(), e.TxPool(), issync) - srv.costTracker, srv.minCapacity = newCostTracker(e.ChainDb(), config) - - // Initialize the bloom trie indexer. - e.BloomIndexer().AddChildIndexer(srv.bloomTrieIndexer) - - // Initialize server capacity management fields. - srv.defParams = flowcontrol.ServerParams{ - BufLimit: srv.minCapacity * bufLimitRatio, - MinRecharge: srv.minCapacity, - } - // LES flow control tries to more or less guarantee the possibility for the - // clients to send a certain amount of requests at any time and get a quick - // response. Most of the clients want this guarantee but don't actually need - // to send requests most of the time. Our goal is to serve as many clients as - // possible while the actually used server capacity does not exceed the limits - totalRecharge := srv.costTracker.totalRecharge() - srv.maxCapacity = srv.minCapacity * uint64(srv.config.LightPeers) - if totalRecharge > srv.maxCapacity { - srv.maxCapacity = totalRecharge - } - srv.fcManager.SetCapacityLimits(srv.minCapacity, srv.maxCapacity, srv.minCapacity*2) - srv.clientPool = vfs.NewClientPool(lesDb, srv.minCapacity, defaultConnectedBias, mclock.System{}, issync) - srv.clientPool.Start() - srv.clientPool.SetDefaultFactors(defaultPosFactors, defaultNegFactors) - srv.vfluxServer.Register(srv.clientPool, "les", "Ethereum light client service") - srv.chtIndexer.Start(e.BlockChain()) - - node.RegisterProtocols(srv.Protocols()) - node.RegisterAPIs(srv.APIs()) - node.RegisterLifecycle(srv) - return srv, nil -} - -func (s *LesServer) APIs() []rpc.API { - return []rpc.API{ - { - Namespace: "les", - Service: NewLightServerAPI(s), - }, - { - Namespace: "debug", - Service: NewDebugAPI(s), - }, - } -} - -func (s *LesServer) Protocols() []p2p.Protocol { - ps := s.makeProtocols(ServerProtocolVersions, s.handler.runPeer, func(id enode.ID) interface{} { - if p := s.peers.peer(id); p != nil { - return p.Info() - } - return nil - }, nil) - // Add "les" ENR entries. - for i := range ps { - ps[i].Attributes = []enr.Entry{&lesEntry{ - VfxVersion: 1, - }} - } - return ps -} - -// Start starts the LES server -func (s *LesServer) Start() error { - s.privateKey = s.p2pSrv.PrivateKey - s.peers.setSignerKey(s.privateKey) - s.handler.start() - s.wg.Add(1) - go s.capacityManagement() - if s.p2pSrv.DiscV5 != nil { - s.p2pSrv.DiscV5.RegisterTalkHandler("vfx", s.vfluxServer.ServeEncoded) - } - return nil -} - -// Stop stops the LES service -func (s *LesServer) Stop() error { - close(s.closeCh) - - s.clientPool.Stop() - if s.serverset != nil { - s.serverset.close() - } - s.peers.close() - s.fcManager.Stop() - s.costTracker.stop() - s.handler.stop() - s.servingQueue.stop() - if s.vfluxServer != nil { - s.vfluxServer.Stop() - } - - // Note, bloom trie indexer is closed by parent bloombits indexer. - if s.chtIndexer != nil { - s.chtIndexer.Close() - } - if s.lesDb != nil { - s.lesDb.Close() - } - s.wg.Wait() - log.Info("Les server stopped") - - return nil -} - -// capacityManagement starts an event handler loop that updates the recharge curve of -// the client manager and adjusts the client pool's size according to the total -// capacity updates coming from the client manager -func (s *LesServer) capacityManagement() { - defer s.wg.Done() - - processCh := make(chan bool, 100) - sub := s.handler.blockchain.SubscribeBlockProcessingEvent(processCh) - defer sub.Unsubscribe() - - totalRechargeCh := make(chan uint64, 100) - totalRecharge := s.costTracker.subscribeTotalRecharge(totalRechargeCh) - - totalCapacityCh := make(chan uint64, 100) - totalCapacity := s.fcManager.SubscribeTotalCapacity(totalCapacityCh) - s.clientPool.SetLimits(uint64(s.config.LightPeers), totalCapacity) - - var ( - busy bool - freePeers uint64 - blockProcess mclock.AbsTime - ) - updateRecharge := func() { - if busy { - s.servingQueue.setThreads(s.threadsBusy) - s.fcManager.SetRechargeCurve(flowcontrol.PieceWiseLinear{{0, 0}, {totalRecharge, totalRecharge}}) - } else { - s.servingQueue.setThreads(s.threadsIdle) - s.fcManager.SetRechargeCurve(flowcontrol.PieceWiseLinear{{0, 0}, {totalRecharge / 10, totalRecharge}, {totalRecharge, totalRecharge}}) - } - } - updateRecharge() - - for { - select { - case busy = <-processCh: - if busy { - blockProcess = mclock.Now() - } else { - blockProcessingTimer.Update(time.Duration(mclock.Now() - blockProcess)) - } - updateRecharge() - case totalRecharge = <-totalRechargeCh: - totalRechargeGauge.Update(int64(totalRecharge)) - updateRecharge() - case totalCapacity = <-totalCapacityCh: - totalCapacityGauge.Update(int64(totalCapacity)) - newFreePeers := totalCapacity / s.minCapacity - if newFreePeers < freePeers && newFreePeers < uint64(s.config.LightPeers) { - log.Warn("Reduced free peer connections", "from", freePeers, "to", newFreePeers) - } - freePeers = newFreePeers - s.clientPool.SetLimits(uint64(s.config.LightPeers), totalCapacity) - case <-s.closeCh: - return - } - } -} diff --git a/les/server_handler.go b/les/server_handler.go deleted file mode 100644 index 5b3505064b..0000000000 --- a/les/server_handler.go +++ /dev/null @@ -1,436 +0,0 @@ -// Copyright 2019 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 les - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/flowcontrol" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/trie" -) - -const ( - softResponseLimit = 2 * 1024 * 1024 // Target maximum size of returned blocks, headers or node data. - estHeaderRlpSize = 500 // Approximate size of an RLP encoded block header - - MaxHeaderFetch = 192 // Amount of block headers to be fetched per retrieval request - MaxBodyFetch = 32 // Amount of block bodies to be fetched per retrieval request - MaxReceiptFetch = 128 // Amount of transaction receipts to allow fetching per request - MaxCodeFetch = 64 // Amount of contract codes to allow fetching per request - MaxProofsFetch = 64 // Amount of merkle proofs to be fetched per retrieval request - MaxHelperTrieProofsFetch = 64 // Amount of helper tries to be fetched per retrieval request - MaxTxSend = 64 // Amount of transactions to be send per request - MaxTxStatus = 256 // Amount of transactions to queried per request -) - -var ( - errTooManyInvalidRequest = errors.New("too many invalid requests made") -) - -// serverHandler is responsible for serving light client and process -// all incoming light requests. -type serverHandler struct { - forkFilter forkid.Filter - blockchain *core.BlockChain - chainDb ethdb.Database - txpool *txpool.TxPool - server *LesServer - - closeCh chan struct{} // Channel used to exit all background routines of handler. - wg sync.WaitGroup // WaitGroup used to track all background routines of handler. - synced func() bool // Callback function used to determine whether local node is synced. - - // Testing fields - addTxsSync bool -} - -func newServerHandler(server *LesServer, blockchain *core.BlockChain, chainDb ethdb.Database, txpool *txpool.TxPool, synced func() bool) *serverHandler { - handler := &serverHandler{ - forkFilter: forkid.NewFilter(blockchain), - server: server, - blockchain: blockchain, - chainDb: chainDb, - txpool: txpool, - closeCh: make(chan struct{}), - synced: synced, - } - return handler -} - -// start starts the server handler. -func (h *serverHandler) start() { - h.wg.Add(1) - go h.broadcastLoop() -} - -// stop stops the server handler. -func (h *serverHandler) stop() { - close(h.closeCh) - h.wg.Wait() -} - -// runPeer is the p2p protocol run function for the given version. -func (h *serverHandler) runPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter) error { - peer := newClientPeer(int(version), h.server.config.NetworkId, p, newMeteredMsgWriter(rw, int(version))) - defer peer.close() - h.wg.Add(1) - defer h.wg.Done() - return h.handle(peer) -} - -func (h *serverHandler) handle(p *clientPeer) error { - p.Log().Debug("Light Ethereum peer connected", "name", p.Name()) - - // Execute the LES handshake - var ( - head = h.blockchain.CurrentHeader() - hash = head.Hash() - number = head.Number.Uint64() - td = h.blockchain.GetTd(hash, number) - forkID = forkid.NewID(h.blockchain.Config(), h.blockchain.Genesis(), number, head.Time) - ) - if err := p.Handshake(td, hash, number, h.blockchain.Genesis().Hash(), forkID, h.forkFilter, h.server); err != nil { - p.Log().Debug("Light Ethereum handshake failed", "err", err) - return err - } - // Connected to another server, no messages expected, just wait for disconnection - if p.server { - if err := h.server.serverset.register(p); err != nil { - return err - } - _, err := p.rw.ReadMsg() - h.server.serverset.unregister(p) - return err - } - // Setup flow control mechanism for the peer - p.fcClient = flowcontrol.NewClientNode(h.server.fcManager, p.fcParams) - defer p.fcClient.Disconnect() - - // Reject light clients if server is not synced. Put this checking here, so - // that "non-synced" les-server peers are still allowed to keep the connection. - if !h.synced() { - p.Log().Debug("Light server not synced, rejecting peer") - return p2p.DiscRequested - } - - // Register the peer into the peerset and clientpool - if err := h.server.peers.register(p); err != nil { - return err - } - if p.balance = h.server.clientPool.Register(p); p.balance == nil { - h.server.peers.unregister(p.ID()) - p.Log().Debug("Client pool already closed") - return p2p.DiscRequested - } - p.connectedAt = mclock.Now() - - var wg sync.WaitGroup // Wait group used to track all in-flight task routines. - defer func() { - wg.Wait() // Ensure all background task routines have exited. - h.server.clientPool.Unregister(p) - h.server.peers.unregister(p.ID()) - p.balance = nil - connectionTimer.Update(time.Duration(mclock.Now() - p.connectedAt)) - }() - - // Mark the peer as being served. - p.serving.Store(true) - defer p.serving.Store(false) - - // Spawn a main loop to handle all incoming messages. - for { - select { - case err := <-p.errCh: - p.Log().Debug("Failed to send light ethereum response", "err", err) - return err - default: - } - if err := h.handleMsg(p, &wg); err != nil { - p.Log().Debug("Light Ethereum message handling failed", "err", err) - return err - } - } -} - -// beforeHandle will do a series of prechecks before handling message. -func (h *serverHandler) beforeHandle(p *clientPeer, reqID, responseCount uint64, msg p2p.Msg, reqCnt uint64, maxCount uint64) (*servingTask, uint64) { - // Ensure that the request sent by client peer is valid - inSizeCost := h.server.costTracker.realCost(0, msg.Size, 0) - if reqCnt == 0 || reqCnt > maxCount { - p.fcClient.OneTimeCost(inSizeCost) - return nil, 0 - } - // Ensure that the client peer complies with the flow control - // rules agreed by both sides. - if p.isFrozen() { - p.fcClient.OneTimeCost(inSizeCost) - return nil, 0 - } - maxCost := p.fcCosts.getMaxCost(msg.Code, reqCnt) - accepted, bufShort, priority := p.fcClient.AcceptRequest(reqID, responseCount, maxCost) - if !accepted { - p.freeze() - p.Log().Error("Request came too early", "remaining", common.PrettyDuration(time.Duration(bufShort*1000000/p.fcParams.MinRecharge))) - p.fcClient.OneTimeCost(inSizeCost) - return nil, 0 - } - // Create a multi-stage task, estimate the time it takes for the task to - // execute, and cache it in the request service queue. - factor := h.server.costTracker.globalFactor() - if factor < 0.001 { - factor = 1 - p.Log().Error("Invalid global cost factor", "factor", factor) - } - maxTime := uint64(float64(maxCost) / factor) - task := h.server.servingQueue.newTask(p, maxTime, priority) - if !task.start() { - p.fcClient.RequestProcessed(reqID, responseCount, maxCost, inSizeCost) - return nil, 0 - } - return task, maxCost -} - -// Afterhandle will perform a series of operations after message handling, -// such as updating flow control data, sending reply, etc. -func (h *serverHandler) afterHandle(p *clientPeer, reqID, responseCount uint64, msg p2p.Msg, maxCost uint64, reqCnt uint64, task *servingTask, reply *reply) { - if reply != nil { - task.done() - } - p.responseLock.Lock() - defer p.responseLock.Unlock() - - // Short circuit if the client is already frozen. - if p.isFrozen() { - realCost := h.server.costTracker.realCost(task.servingTime, msg.Size, 0) - p.fcClient.RequestProcessed(reqID, responseCount, maxCost, realCost) - return - } - // Positive correction buffer value with real cost. - var replySize uint32 - if reply != nil { - replySize = reply.size() - } - var realCost uint64 - if h.server.costTracker.testing { - realCost = maxCost // Assign a fake cost for testing purpose - } else { - realCost = h.server.costTracker.realCost(task.servingTime, msg.Size, replySize) - if realCost > maxCost { - realCost = maxCost - } - } - bv := p.fcClient.RequestProcessed(reqID, responseCount, maxCost, realCost) - if reply != nil { - // Feed cost tracker request serving statistic. - h.server.costTracker.updateStats(msg.Code, reqCnt, task.servingTime, realCost) - // Reduce priority "balance" for the specific peer. - p.balance.RequestServed(realCost) - p.queueSend(func() { - if err := reply.send(bv); err != nil { - select { - case p.errCh <- err: - default: - } - } - }) - } -} - -// handleMsg is invoked whenever an inbound message is received from a remote -// peer. The remote connection is torn down upon returning any error. -func (h *serverHandler) handleMsg(p *clientPeer, wg *sync.WaitGroup) error { - // Read the next message from the remote peer, and ensure it's fully consumed - msg, err := p.rw.ReadMsg() - if err != nil { - return err - } - p.Log().Trace("Light Ethereum message arrived", "code", msg.Code, "bytes", msg.Size) - - // Discard large message which exceeds the limitation. - if msg.Size > ProtocolMaxMsgSize { - clientErrorMeter.Mark(1) - return errResp(ErrMsgTooLarge, "%v > %v", msg.Size, ProtocolMaxMsgSize) - } - defer msg.Discard() - - // Lookup the request handler table, ensure it's supported - // message type by the protocol. - req, ok := Les3[msg.Code] - if !ok { - p.Log().Trace("Received invalid message", "code", msg.Code) - clientErrorMeter.Mark(1) - return errResp(ErrInvalidMsgCode, "%v", msg.Code) - } - p.Log().Trace("Received " + req.Name) - - // Decode the p2p message, resolve the concrete handler for it. - serve, reqID, reqCnt, err := req.Handle(msg) - if err != nil { - clientErrorMeter.Mark(1) - return errResp(ErrDecode, "%v: %v", msg, err) - } - if metrics.EnabledExpensive { - req.InPacketsMeter.Mark(1) - req.InTrafficMeter.Mark(int64(msg.Size)) - } - p.responseCount++ - responseCount := p.responseCount - - // First check this client message complies all rules before - // handling it and return a processor if all checks are passed. - task, maxCost := h.beforeHandle(p, reqID, responseCount, msg, reqCnt, req.MaxCount) - if task == nil { - return nil - } - wg.Add(1) - go func() { - defer wg.Done() - - reply := serve(h, p, task.waitOrStop) - h.afterHandle(p, reqID, responseCount, msg, maxCost, reqCnt, task, reply) - - if metrics.EnabledExpensive { - size := uint32(0) - if reply != nil { - size = reply.size() - } - req.OutPacketsMeter.Mark(1) - req.OutTrafficMeter.Mark(int64(size)) - req.ServingTimeMeter.Update(time.Duration(task.servingTime)) - } - }() - // If the client has made too much invalid request(e.g. request a non-existent data), - // reject them to prevent SPAM attack. - if p.getInvalid() > maxRequestErrors { - clientErrorMeter.Mark(1) - return errTooManyInvalidRequest - } - return nil -} - -// BlockChain implements serverBackend -func (h *serverHandler) BlockChain() *core.BlockChain { - return h.blockchain -} - -// TxPool implements serverBackend -func (h *serverHandler) TxPool() *txpool.TxPool { - return h.txpool -} - -// ArchiveMode implements serverBackend -func (h *serverHandler) ArchiveMode() bool { - return h.server.archiveMode -} - -// AddTxsSync implements serverBackend -func (h *serverHandler) AddTxsSync() bool { - return h.addTxsSync -} - -// getAccount retrieves an account from the state based on root. -func getAccount(triedb *trie.Database, root common.Hash, addr common.Address) (types.StateAccount, error) { - trie, err := trie.NewStateTrie(trie.StateTrieID(root), triedb) - if err != nil { - return types.StateAccount{}, err - } - acc, err := trie.GetAccount(addr) - if err != nil { - return types.StateAccount{}, err - } - if acc == nil { - return types.StateAccount{}, fmt.Errorf("account %#x is not present", addr) - } - return *acc, nil -} - -// GetHelperTrie returns the post-processed trie root for the given trie ID and section index -func (h *serverHandler) GetHelperTrie(typ uint, index uint64) *trie.Trie { - var ( - root common.Hash - prefix string - ) - switch typ { - case htCanonical: - sectionHead := rawdb.ReadCanonicalHash(h.chainDb, (index+1)*h.server.iConfig.ChtSize-1) - root, prefix = light.GetChtRoot(h.chainDb, index, sectionHead), string(rawdb.ChtTablePrefix) - case htBloomBits: - sectionHead := rawdb.ReadCanonicalHash(h.chainDb, (index+1)*h.server.iConfig.BloomTrieSize-1) - root, prefix = light.GetBloomTrieRoot(h.chainDb, index, sectionHead), string(rawdb.BloomTrieTablePrefix) - } - if root == (common.Hash{}) { - return nil - } - triedb := trie.NewDatabase(rawdb.NewTable(h.chainDb, prefix), trie.HashDefaults) - trie, _ := trie.New(trie.TrieID(root), triedb) - return trie -} - -// broadcastLoop broadcasts new block information to all connected light -// clients. According to the agreement between client and server, server should -// only broadcast new announcement if the total difficulty is higher than the -// last one. Besides server will add the signature if client requires. -func (h *serverHandler) broadcastLoop() { - defer h.wg.Done() - - headCh := make(chan core.ChainHeadEvent, 10) - headSub := h.blockchain.SubscribeChainHeadEvent(headCh) - defer headSub.Unsubscribe() - - var ( - lastHead = h.blockchain.CurrentHeader() - lastTd = common.Big0 - ) - for { - select { - case ev := <-headCh: - header := ev.Block.Header() - hash, number := header.Hash(), header.Number.Uint64() - td := h.blockchain.GetTd(hash, number) - if td == nil || td.Cmp(lastTd) <= 0 { - continue - } - var reorg uint64 - if lastHead != nil { - // If a setHead has been performed, the common ancestor can be nil. - if ancestor := rawdb.FindCommonAncestor(h.chainDb, header, lastHead); ancestor != nil { - reorg = lastHead.Number.Uint64() - ancestor.Number.Uint64() - } - } - lastHead, lastTd = header, td - log.Debug("Announcing block to peers", "number", number, "hash", hash, "td", td, "reorg", reorg) - h.server.peers.broadcast(announceData{Hash: hash, Number: number, Td: td, ReorgDepth: reorg}) - case <-h.closeCh: - return - } - } -} diff --git a/les/server_requests.go b/les/server_requests.go deleted file mode 100644 index cc5b601713..0000000000 --- a/les/server_requests.go +++ /dev/null @@ -1,566 +0,0 @@ -// Copyright 2021 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 les - -import ( - "encoding/binary" - "encoding/json" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" - "github.com/ethereum/go-ethereum/trie/trienode" -) - -// serverBackend defines the backend functions needed for serving LES requests -type serverBackend interface { - ArchiveMode() bool - AddTxsSync() bool - BlockChain() *core.BlockChain - TxPool() *txpool.TxPool - GetHelperTrie(typ uint, index uint64) *trie.Trie -} - -// Decoder is implemented by the messages passed to the handler functions -type Decoder interface { - Decode(val interface{}) error -} - -// RequestType is a static struct that describes an LES request type and references -// its handler function. -type RequestType struct { - Name string - MaxCount uint64 - InPacketsMeter, InTrafficMeter, OutPacketsMeter, OutTrafficMeter metrics.Meter - ServingTimeMeter metrics.Timer - Handle func(msg Decoder) (serve serveRequestFn, reqID, amount uint64, err error) -} - -// serveRequestFn is returned by the request handler functions after decoding the request. -// This function does the actual request serving using the supplied backend. waitOrStop is -// called between serving individual request items and may block if the serving process -// needs to be throttled. If it returns false then the process is terminated. -// The reply is not sent by this function yet. The flow control feedback value is supplied -// by the protocol handler when calling the send function of the returned reply struct. -type serveRequestFn func(backend serverBackend, peer *clientPeer, waitOrStop func() bool) *reply - -// Les3 contains the request types supported by les/2 and les/3 -var Les3 = map[uint64]RequestType{ - GetBlockHeadersMsg: { - Name: "block header request", - MaxCount: MaxHeaderFetch, - InPacketsMeter: miscInHeaderPacketsMeter, - InTrafficMeter: miscInHeaderTrafficMeter, - OutPacketsMeter: miscOutHeaderPacketsMeter, - OutTrafficMeter: miscOutHeaderTrafficMeter, - ServingTimeMeter: miscServingTimeHeaderTimer, - Handle: handleGetBlockHeaders, - }, - GetBlockBodiesMsg: { - Name: "block bodies request", - MaxCount: MaxBodyFetch, - InPacketsMeter: miscInBodyPacketsMeter, - InTrafficMeter: miscInBodyTrafficMeter, - OutPacketsMeter: miscOutBodyPacketsMeter, - OutTrafficMeter: miscOutBodyTrafficMeter, - ServingTimeMeter: miscServingTimeBodyTimer, - Handle: handleGetBlockBodies, - }, - GetCodeMsg: { - Name: "code request", - MaxCount: MaxCodeFetch, - InPacketsMeter: miscInCodePacketsMeter, - InTrafficMeter: miscInCodeTrafficMeter, - OutPacketsMeter: miscOutCodePacketsMeter, - OutTrafficMeter: miscOutCodeTrafficMeter, - ServingTimeMeter: miscServingTimeCodeTimer, - Handle: handleGetCode, - }, - GetReceiptsMsg: { - Name: "receipts request", - MaxCount: MaxReceiptFetch, - InPacketsMeter: miscInReceiptPacketsMeter, - InTrafficMeter: miscInReceiptTrafficMeter, - OutPacketsMeter: miscOutReceiptPacketsMeter, - OutTrafficMeter: miscOutReceiptTrafficMeter, - ServingTimeMeter: miscServingTimeReceiptTimer, - Handle: handleGetReceipts, - }, - GetProofsV2Msg: { - Name: "les/2 proofs request", - MaxCount: MaxProofsFetch, - InPacketsMeter: miscInTrieProofPacketsMeter, - InTrafficMeter: miscInTrieProofTrafficMeter, - OutPacketsMeter: miscOutTrieProofPacketsMeter, - OutTrafficMeter: miscOutTrieProofTrafficMeter, - ServingTimeMeter: miscServingTimeTrieProofTimer, - Handle: handleGetProofs, - }, - GetHelperTrieProofsMsg: { - Name: "helper trie proof request", - MaxCount: MaxHelperTrieProofsFetch, - InPacketsMeter: miscInHelperTriePacketsMeter, - InTrafficMeter: miscInHelperTrieTrafficMeter, - OutPacketsMeter: miscOutHelperTriePacketsMeter, - OutTrafficMeter: miscOutHelperTrieTrafficMeter, - ServingTimeMeter: miscServingTimeHelperTrieTimer, - Handle: handleGetHelperTrieProofs, - }, - SendTxV2Msg: { - Name: "new transactions", - MaxCount: MaxTxSend, - InPacketsMeter: miscInTxsPacketsMeter, - InTrafficMeter: miscInTxsTrafficMeter, - OutPacketsMeter: miscOutTxsPacketsMeter, - OutTrafficMeter: miscOutTxsTrafficMeter, - ServingTimeMeter: miscServingTimeTxTimer, - Handle: handleSendTx, - }, - GetTxStatusMsg: { - Name: "transaction status query request", - MaxCount: MaxTxStatus, - InPacketsMeter: miscInTxStatusPacketsMeter, - InTrafficMeter: miscInTxStatusTrafficMeter, - OutPacketsMeter: miscOutTxStatusPacketsMeter, - OutTrafficMeter: miscOutTxStatusTrafficMeter, - ServingTimeMeter: miscServingTimeTxStatusTimer, - Handle: handleGetTxStatus, - }, -} - -// handleGetBlockHeaders handles a block header request -func handleGetBlockHeaders(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetBlockHeadersPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - // Gather headers until the fetch or network limits is reached - var ( - bc = backend.BlockChain() - hashMode = r.Query.Origin.Hash != (common.Hash{}) - first = true - maxNonCanonical = uint64(100) - bytes common.StorageSize - headers []*types.Header - unknown bool - ) - for !unknown && len(headers) < int(r.Query.Amount) && bytes < softResponseLimit { - if !first && !waitOrStop() { - return nil - } - // Retrieve the next header satisfying the r - var origin *types.Header - if hashMode { - if first { - origin = bc.GetHeaderByHash(r.Query.Origin.Hash) - if origin != nil { - r.Query.Origin.Number = origin.Number.Uint64() - } - } else { - origin = bc.GetHeader(r.Query.Origin.Hash, r.Query.Origin.Number) - } - } else { - origin = bc.GetHeaderByNumber(r.Query.Origin.Number) - } - if origin == nil { - break - } - headers = append(headers, origin) - bytes += estHeaderRlpSize - - // Advance to the next header of the r - switch { - case hashMode && r.Query.Reverse: - // Hash based traversal towards the genesis block - ancestor := r.Query.Skip + 1 - if ancestor == 0 { - unknown = true - } else { - r.Query.Origin.Hash, r.Query.Origin.Number = bc.GetAncestor(r.Query.Origin.Hash, r.Query.Origin.Number, ancestor, &maxNonCanonical) - unknown = r.Query.Origin.Hash == common.Hash{} - } - case hashMode && !r.Query.Reverse: - // Hash based traversal towards the leaf block - var ( - current = origin.Number.Uint64() - next = current + r.Query.Skip + 1 - ) - if next <= current { - infos, _ := json.Marshal(p.Peer.Info()) - p.Log().Warn("GetBlockHeaders skip overflow attack", "current", current, "skip", r.Query.Skip, "next", next, "attacker", string(infos)) - unknown = true - } else { - if header := bc.GetHeaderByNumber(next); header != nil { - nextHash := header.Hash() - expOldHash, _ := bc.GetAncestor(nextHash, next, r.Query.Skip+1, &maxNonCanonical) - if expOldHash == r.Query.Origin.Hash { - r.Query.Origin.Hash, r.Query.Origin.Number = nextHash, next - } else { - unknown = true - } - } else { - unknown = true - } - } - case r.Query.Reverse: - // Number based traversal towards the genesis block - if r.Query.Origin.Number >= r.Query.Skip+1 { - r.Query.Origin.Number -= r.Query.Skip + 1 - } else { - unknown = true - } - - case !r.Query.Reverse: - // Number based traversal towards the leaf block - r.Query.Origin.Number += r.Query.Skip + 1 - } - first = false - } - return p.replyBlockHeaders(r.ReqID, headers) - }, r.ReqID, r.Query.Amount, nil -} - -// handleGetBlockBodies handles a block body request -func handleGetBlockBodies(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetBlockBodiesPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - var ( - bytes int - bodies []rlp.RawValue - ) - bc := backend.BlockChain() - for i, hash := range r.Hashes { - if i != 0 && !waitOrStop() { - return nil - } - if bytes >= softResponseLimit { - break - } - body := bc.GetBodyRLP(hash) - if body == nil { - p.bumpInvalid() - continue - } - bodies = append(bodies, body) - bytes += len(body) - } - return p.replyBlockBodiesRLP(r.ReqID, bodies) - }, r.ReqID, uint64(len(r.Hashes)), nil -} - -// handleGetCode handles a contract code request -func handleGetCode(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetCodePacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - var ( - bytes int - data [][]byte - ) - bc := backend.BlockChain() - for i, request := range r.Reqs { - if i != 0 && !waitOrStop() { - return nil - } - // Look up the root hash belonging to the request - header := bc.GetHeaderByHash(request.BHash) - if header == nil { - p.Log().Warn("Failed to retrieve associate header for code", "hash", request.BHash) - p.bumpInvalid() - continue - } - // Refuse to search stale state data in the database since looking for - // a non-exist key is kind of expensive. - local := bc.CurrentHeader().Number.Uint64() - if !backend.ArchiveMode() && header.Number.Uint64()+core.TriesInMemory <= local { - p.Log().Debug("Reject stale code request", "number", header.Number.Uint64(), "head", local) - p.bumpInvalid() - continue - } - address := common.BytesToAddress(request.AccountAddress) - account, err := getAccount(bc.TrieDB(), header.Root, address) - if err != nil { - p.Log().Warn("Failed to retrieve account for code", "block", header.Number, "hash", header.Hash(), "account", address, "err", err) - p.bumpInvalid() - continue - } - code, err := bc.StateCache().ContractCode(address, common.BytesToHash(account.CodeHash)) - if err != nil { - p.Log().Warn("Failed to retrieve account code", "block", header.Number, "hash", header.Hash(), "account", address, "codehash", common.BytesToHash(account.CodeHash), "err", err) - continue - } - // Accumulate the code and abort if enough data was retrieved - data = append(data, code) - if bytes += len(code); bytes >= softResponseLimit { - break - } - } - return p.replyCode(r.ReqID, data) - }, r.ReqID, uint64(len(r.Reqs)), nil -} - -// handleGetReceipts handles a block receipts request -func handleGetReceipts(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetReceiptsPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - var ( - bytes int - receipts []rlp.RawValue - ) - bc := backend.BlockChain() - for i, hash := range r.Hashes { - if i != 0 && !waitOrStop() { - return nil - } - if bytes >= softResponseLimit { - break - } - // Retrieve the requested block's receipts, skipping if unknown to us - results := bc.GetReceiptsByHash(hash) - if results == nil { - if header := bc.GetHeaderByHash(hash); header == nil || header.ReceiptHash != types.EmptyReceiptsHash { - p.bumpInvalid() - continue - } - } - // If known, encode and queue for response packet - if encoded, err := rlp.EncodeToBytes(results); err != nil { - log.Error("Failed to encode receipt", "err", err) - } else { - receipts = append(receipts, encoded) - bytes += len(encoded) - } - } - return p.replyReceiptsRLP(r.ReqID, receipts) - }, r.ReqID, uint64(len(r.Hashes)), nil -} - -// handleGetProofs handles a proof request -func handleGetProofs(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetProofsPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - var ( - lastBHash common.Hash - root common.Hash - header *types.Header - err error - ) - bc := backend.BlockChain() - nodes := trienode.NewProofSet() - - for i, request := range r.Reqs { - if i != 0 && !waitOrStop() { - return nil - } - // Look up the root hash belonging to the request - if request.BHash != lastBHash { - root, lastBHash = common.Hash{}, request.BHash - - if header = bc.GetHeaderByHash(request.BHash); header == nil { - p.Log().Warn("Failed to retrieve header for proof", "hash", request.BHash) - p.bumpInvalid() - continue - } - // Refuse to search stale state data in the database since looking for - // a non-exist key is kind of expensive. - local := bc.CurrentHeader().Number.Uint64() - if !backend.ArchiveMode() && header.Number.Uint64()+core.TriesInMemory <= local { - p.Log().Debug("Reject stale trie request", "number", header.Number.Uint64(), "head", local) - p.bumpInvalid() - continue - } - root = header.Root - } - // If a header lookup failed (non existent), ignore subsequent requests for the same header - if root == (common.Hash{}) { - p.bumpInvalid() - continue - } - // Open the account or storage trie for the request - statedb := bc.StateCache() - - var trie state.Trie - switch len(request.AccountAddress) { - case 0: - // No account key specified, open an account trie - trie, err = statedb.OpenTrie(root) - if trie == nil || err != nil { - p.Log().Warn("Failed to open storage trie for proof", "block", header.Number, "hash", header.Hash(), "root", root, "err", err) - continue - } - default: - // Account key specified, open a storage trie - address := common.BytesToAddress(request.AccountAddress) - account, err := getAccount(bc.TrieDB(), root, address) - if err != nil { - p.Log().Warn("Failed to retrieve account for proof", "block", header.Number, "hash", header.Hash(), "account", address, "err", err) - p.bumpInvalid() - continue - } - trie, err = statedb.OpenStorageTrie(root, address, account.Root, nil) - if trie == nil || err != nil { - p.Log().Warn("Failed to open storage trie for proof", "block", header.Number, "hash", header.Hash(), "account", address, "root", account.Root, "err", err) - continue - } - } - // Prove the user's request from the account or storage trie - if err := trie.Prove(request.Key, nodes); err != nil { - p.Log().Warn("Failed to prove state request", "block", header.Number, "hash", header.Hash(), "err", err) - continue - } - if nodes.DataSize() >= softResponseLimit { - break - } - } - return p.replyProofsV2(r.ReqID, nodes.List()) - }, r.ReqID, uint64(len(r.Reqs)), nil -} - -// handleGetHelperTrieProofs handles a helper trie proof request -func handleGetHelperTrieProofs(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetHelperTrieProofsPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - var ( - lastIdx uint64 - lastType uint - auxTrie *trie.Trie - auxBytes int - auxData [][]byte - ) - bc := backend.BlockChain() - nodes := trienode.NewProofSet() - for i, request := range r.Reqs { - if i != 0 && !waitOrStop() { - return nil - } - if auxTrie == nil || request.Type != lastType || request.TrieIdx != lastIdx { - lastType, lastIdx = request.Type, request.TrieIdx - auxTrie = backend.GetHelperTrie(request.Type, request.TrieIdx) - } - if auxTrie == nil { - return nil - } - // TODO(rjl493456442) short circuit if the proving is failed. - // The original client side code has a dirty hack to retrieve - // the headers with no valid proof. Keep the compatibility for - // legacy les protocol and drop this hack when the les2/3 are - // not supported. - err := auxTrie.Prove(request.Key, nodes) - if p.version >= lpv4 && err != nil { - return nil - } - if request.Type == htCanonical && request.AuxReq == htAuxHeader && len(request.Key) == 8 { - header := bc.GetHeaderByNumber(binary.BigEndian.Uint64(request.Key)) - data, err := rlp.EncodeToBytes(header) - if err != nil { - log.Error("Failed to encode header", "err", err) - return nil - } - auxData = append(auxData, data) - auxBytes += len(data) - } - if nodes.DataSize()+auxBytes >= softResponseLimit { - break - } - } - return p.replyHelperTrieProofs(r.ReqID, HelperTrieResps{Proofs: nodes.List(), AuxData: auxData}) - }, r.ReqID, uint64(len(r.Reqs)), nil -} - -// handleSendTx handles a transaction propagation request -func handleSendTx(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r SendTxPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - amount := uint64(len(r.Txs)) - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - stats := make([]light.TxStatus, len(r.Txs)) - for i, tx := range r.Txs { - if i != 0 && !waitOrStop() { - return nil - } - hash := tx.Hash() - stats[i] = txStatus(backend, hash) - if stats[i].Status == txpool.TxStatusUnknown { - if errs := backend.TxPool().Add([]*types.Transaction{tx}, false, backend.AddTxsSync()); errs[0] != nil { - stats[i].Error = errs[0].Error() - continue - } - stats[i] = txStatus(backend, hash) - } - } - return p.replyTxStatus(r.ReqID, stats) - }, r.ReqID, amount, nil -} - -// handleGetTxStatus handles a transaction status query -func handleGetTxStatus(msg Decoder) (serveRequestFn, uint64, uint64, error) { - var r GetTxStatusPacket - if err := msg.Decode(&r); err != nil { - return nil, 0, 0, err - } - return func(backend serverBackend, p *clientPeer, waitOrStop func() bool) *reply { - stats := make([]light.TxStatus, len(r.Hashes)) - for i, hash := range r.Hashes { - if i != 0 && !waitOrStop() { - return nil - } - stats[i] = txStatus(backend, hash) - } - return p.replyTxStatus(r.ReqID, stats) - }, r.ReqID, uint64(len(r.Hashes)), nil -} - -// txStatus returns the status of a specified transaction. -func txStatus(b serverBackend, hash common.Hash) light.TxStatus { - var stat light.TxStatus - // Looking the transaction in txpool first. - stat.Status = b.TxPool().Status(hash) - - // If the transaction is unknown to the pool, try looking it up locally. - if stat.Status == txpool.TxStatusUnknown { - lookup := b.BlockChain().GetTransactionLookup(hash) - if lookup != nil { - stat.Status = txpool.TxStatusIncluded - stat.Lookup = lookup - } - } - return stat -} diff --git a/les/servingqueue.go b/les/servingqueue.go deleted file mode 100644 index b258fc3caf..0000000000 --- a/les/servingqueue.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2019 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 les - -import ( - "sync" - "sync/atomic" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/common/prque" - "golang.org/x/exp/slices" -) - -// servingQueue allows running tasks in a limited number of threads and puts the -// waiting tasks in a priority queue -type servingQueue struct { - recentTime, queuedTime uint64 - servingTimeDiff atomic.Uint64 - burstLimit, burstDropLimit uint64 - burstDecRate float64 - lastUpdate mclock.AbsTime - - queueAddCh, queueBestCh chan *servingTask - stopThreadCh, quit chan struct{} - setThreadsCh chan int - - wg sync.WaitGroup - threadCount int // number of currently running threads - queue *prque.Prque[int64, *servingTask] // priority queue for waiting or suspended tasks - best *servingTask // the highest priority task (not included in the queue) - suspendBias int64 // priority bias against suspending an already running task -} - -// servingTask represents a request serving task. Tasks can be implemented to -// run in multiple steps, allowing the serving queue to suspend execution between -// steps if higher priority tasks are entered. The creator of the task should -// set the following fields: -// -// - priority: greater value means higher priority; values can wrap around the int64 range -// - run: execute a single step; return true if finished -// - after: executed after run finishes or returns an error, receives the total serving time -type servingTask struct { - sq *servingQueue - servingTime, timeAdded, maxTime, expTime uint64 - peer *clientPeer - priority int64 - biasAdded bool - token runToken - tokenCh chan runToken -} - -// runToken received by servingTask.start allows the task to run. Closing the -// channel by servingTask.stop signals the thread controller to allow a new task -// to start running. -type runToken chan struct{} - -// start blocks until the task can start and returns true if it is allowed to run. -// Returning false means that the task should be cancelled. -func (t *servingTask) start() bool { - if t.peer.isFrozen() { - return false - } - t.tokenCh = make(chan runToken, 1) - select { - case t.sq.queueAddCh <- t: - case <-t.sq.quit: - return false - } - select { - case t.token = <-t.tokenCh: - case <-t.sq.quit: - return false - } - if t.token == nil { - return false - } - t.servingTime -= uint64(mclock.Now()) - return true -} - -// done signals the thread controller about the task being finished and returns -// the total serving time of the task in nanoseconds. -func (t *servingTask) done() uint64 { - t.servingTime += uint64(mclock.Now()) - close(t.token) - diff := t.servingTime - t.timeAdded - t.timeAdded = t.servingTime - if t.expTime > diff { - t.expTime -= diff - t.sq.servingTimeDiff.Add(t.expTime) - } else { - t.expTime = 0 - } - return t.servingTime -} - -// waitOrStop can be called during the execution of the task. It blocks if there -// is a higher priority task waiting (a bias is applied in favor of the currently -// running task). Returning true means that the execution can be resumed. False -// means the task should be cancelled. -func (t *servingTask) waitOrStop() bool { - t.done() - if !t.biasAdded { - t.priority += t.sq.suspendBias - t.biasAdded = true - } - return t.start() -} - -// newServingQueue returns a new servingQueue -func newServingQueue(suspendBias int64, utilTarget float64) *servingQueue { - sq := &servingQueue{ - queue: prque.New[int64, *servingTask](nil), - suspendBias: suspendBias, - queueAddCh: make(chan *servingTask, 100), - queueBestCh: make(chan *servingTask), - stopThreadCh: make(chan struct{}), - quit: make(chan struct{}), - setThreadsCh: make(chan int, 10), - burstLimit: uint64(utilTarget * bufLimitRatio * 1200000), - burstDropLimit: uint64(utilTarget * bufLimitRatio * 1000000), - burstDecRate: utilTarget, - lastUpdate: mclock.Now(), - } - sq.wg.Add(2) - go sq.queueLoop() - go sq.threadCountLoop() - return sq -} - -// newTask creates a new task with the given priority -func (sq *servingQueue) newTask(peer *clientPeer, maxTime uint64, priority int64) *servingTask { - return &servingTask{ - sq: sq, - peer: peer, - maxTime: maxTime, - expTime: maxTime, - priority: priority, - } -} - -// threadController is started in multiple goroutines and controls the execution -// of tasks. The number of active thread controllers equals the allowed number of -// concurrently running threads. It tries to fetch the highest priority queued -// task first. If there are no queued tasks waiting then it can directly catch -// run tokens from the token channel and allow the corresponding tasks to run -// without entering the priority queue. -func (sq *servingQueue) threadController() { - defer sq.wg.Done() - for { - token := make(runToken) - select { - case best := <-sq.queueBestCh: - best.tokenCh <- token - case <-sq.stopThreadCh: - return - case <-sq.quit: - return - } - select { - case <-sq.stopThreadCh: - return - case <-sq.quit: - return - case <-token: - } - } -} - -// peerTasks lists the tasks received from a given peer when selecting peers to freeze -type peerTasks struct { - peer *clientPeer - list []*servingTask - sumTime uint64 - priority float64 -} - -// freezePeers selects the peers with the worst priority queued tasks and freezes -// them until burstTime goes under burstDropLimit or all peers are frozen -func (sq *servingQueue) freezePeers() { - peerMap := make(map[*clientPeer]*peerTasks) - var peerList []*peerTasks - if sq.best != nil { - sq.queue.Push(sq.best, sq.best.priority) - } - sq.best = nil - for sq.queue.Size() > 0 { - task := sq.queue.PopItem() - tasks := peerMap[task.peer] - if tasks == nil { - bufValue, bufLimit := task.peer.fcClient.BufferStatus() - if bufLimit < 1 { - bufLimit = 1 - } - tasks = &peerTasks{ - peer: task.peer, - priority: float64(bufValue) / float64(bufLimit), // lower value comes first - } - peerMap[task.peer] = tasks - peerList = append(peerList, tasks) - } - tasks.list = append(tasks.list, task) - tasks.sumTime += task.expTime - } - slices.SortFunc(peerList, func(a, b *peerTasks) int { - if a.priority < b.priority { - return -1 - } - if a.priority > b.priority { - return 1 - } - return 0 - }) - drop := true - for _, tasks := range peerList { - if drop { - tasks.peer.freeze() - tasks.peer.fcClient.Freeze() - sq.queuedTime -= tasks.sumTime - sqQueuedGauge.Update(int64(sq.queuedTime)) - clientFreezeMeter.Mark(1) - drop = sq.recentTime+sq.queuedTime > sq.burstDropLimit - for _, task := range tasks.list { - task.tokenCh <- nil - } - } else { - for _, task := range tasks.list { - sq.queue.Push(task, task.priority) - } - } - } - if sq.queue.Size() > 0 { - sq.best = sq.queue.PopItem() - } -} - -// updateRecentTime recalculates the recent serving time value -func (sq *servingQueue) updateRecentTime() { - subTime := sq.servingTimeDiff.Swap(0) - now := mclock.Now() - dt := now - sq.lastUpdate - sq.lastUpdate = now - if dt > 0 { - subTime += uint64(float64(dt) * sq.burstDecRate) - } - if sq.recentTime > subTime { - sq.recentTime -= subTime - } else { - sq.recentTime = 0 - } -} - -// addTask inserts a task into the priority queue -func (sq *servingQueue) addTask(task *servingTask) { - if sq.best == nil { - sq.best = task - } else if task.priority-sq.best.priority > 0 { - sq.queue.Push(sq.best, sq.best.priority) - sq.best = task - } else { - sq.queue.Push(task, task.priority) - } - sq.updateRecentTime() - sq.queuedTime += task.expTime - sqServedGauge.Update(int64(sq.recentTime)) - sqQueuedGauge.Update(int64(sq.queuedTime)) - if sq.recentTime+sq.queuedTime > sq.burstLimit { - sq.freezePeers() - } -} - -// queueLoop is an event loop running in a goroutine. It receives tasks from queueAddCh -// and always tries to send the highest priority task to queueBestCh. Successfully sent -// tasks are removed from the queue. -func (sq *servingQueue) queueLoop() { - defer sq.wg.Done() - for { - if sq.best != nil { - expTime := sq.best.expTime - select { - case task := <-sq.queueAddCh: - sq.addTask(task) - case sq.queueBestCh <- sq.best: - sq.updateRecentTime() - sq.queuedTime -= expTime - sq.recentTime += expTime - sqServedGauge.Update(int64(sq.recentTime)) - sqQueuedGauge.Update(int64(sq.queuedTime)) - if sq.queue.Size() == 0 { - sq.best = nil - } else { - sq.best = sq.queue.PopItem() - } - case <-sq.quit: - return - } - } else { - select { - case task := <-sq.queueAddCh: - sq.addTask(task) - case <-sq.quit: - return - } - } - } -} - -// threadCountLoop is an event loop running in a goroutine. It adjusts the number -// of active thread controller goroutines. -func (sq *servingQueue) threadCountLoop() { - var threadCountTarget int - defer sq.wg.Done() - for { - for threadCountTarget > sq.threadCount { - sq.wg.Add(1) - go sq.threadController() - sq.threadCount++ - } - if threadCountTarget < sq.threadCount { - select { - case threadCountTarget = <-sq.setThreadsCh: - case sq.stopThreadCh <- struct{}{}: - sq.threadCount-- - case <-sq.quit: - return - } - } else { - select { - case threadCountTarget = <-sq.setThreadsCh: - case <-sq.quit: - return - } - } - } -} - -// setThreads sets the allowed processing thread count, suspending tasks as soon as -// possible if necessary. -func (sq *servingQueue) setThreads(threadCount int) { - select { - case sq.setThreadsCh <- threadCount: - case <-sq.quit: - return - } -} - -// stop stops task processing as soon as possible and shuts down the serving queue. -func (sq *servingQueue) stop() { - close(sq.quit) - sq.wg.Wait() -} diff --git a/les/state_accessor.go b/les/state_accessor.go deleted file mode 100644 index 9a8214ac2f..0000000000 --- a/les/state_accessor.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2021 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 les - -import ( - "context" - "errors" - "fmt" - - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/tracers" - "github.com/ethereum/go-ethereum/light" -) - -// noopReleaser is returned in case there is no operation expected -// for releasing state. -var noopReleaser = tracers.StateReleaseFunc(func() {}) - -// stateAtBlock retrieves the state database associated with a certain block. -func (leth *LightEthereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64) (*state.StateDB, tracers.StateReleaseFunc, error) { - return light.NewState(ctx, block.Header(), leth.odr), noopReleaser, nil -} - -// stateAtTransaction returns the execution environment of a certain transaction. -func (leth *LightEthereum) stateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { - // Short circuit if it's genesis block. - if block.NumberU64() == 0 { - return nil, vm.BlockContext{}, nil, nil, errors.New("no transaction in genesis") - } - // Create the parent state database - parent, err := leth.blockchain.GetBlock(ctx, block.ParentHash(), block.NumberU64()-1) - if err != nil { - return nil, vm.BlockContext{}, nil, nil, err - } - statedb, release, err := leth.stateAtBlock(ctx, parent, reexec) - if err != nil { - return nil, vm.BlockContext{}, nil, nil, err - } - if txIndex == 0 && len(block.Transactions()) == 0 { - return nil, vm.BlockContext{}, statedb, release, nil - } - // Recompute transactions up to the target index. - signer := types.MakeSigner(leth.blockchain.Config(), block.Number(), block.Time()) - for idx, tx := range block.Transactions() { - // Assemble the transaction call message and return if the requested offset - msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) - txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), leth.blockchain, nil) - statedb.SetTxContext(tx.Hash(), idx) - if idx == txIndex { - return msg, context, statedb, release, nil - } - // Not yet the searched for transaction, execute on top of the current state - vmenv := vm.NewEVM(context, txContext, statedb, leth.blockchain.Config(), vm.Config{}) - if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { - return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) - } - // Ensure any modifications are committed to the state - // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect - statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) - } - return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) -} diff --git a/les/test_helper.go b/les/test_helper.go deleted file mode 100644 index 6be13eaecd..0000000000 --- a/les/test_helper.go +++ /dev/null @@ -1,626 +0,0 @@ -// Copyright 2019 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 . - -// This file contains some shares testing functionality, common to multiple -// different files and modules being tested. Client based network and Server -// based network can be created easily with available APIs. - -package les - -import ( - "context" - "crypto/rand" - "fmt" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/forkid" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/les/flowcontrol" - vfs "github.com/ethereum/go-ethereum/les/vflux/server" - "github.com/ethereum/go-ethereum/light" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" -) - -var ( - bankKey, _ = crypto.GenerateKey() - bankAddr = crypto.PubkeyToAddress(bankKey.PublicKey) - bankFunds = big.NewInt(1_000_000_000_000_000_000) - - userKey1, _ = crypto.GenerateKey() - userKey2, _ = crypto.GenerateKey() - userAddr1 = crypto.PubkeyToAddress(userKey1.PublicKey) - userAddr2 = crypto.PubkeyToAddress(userKey2.PublicKey) - - testContractAddr common.Address - testContractCode = common.Hex2Bytes("606060405260cc8060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360cd2685146041578063c16431b914606b57603f565b005b6055600480803590602001909190505060a9565b6040518082815260200191505060405180910390f35b60886004808035906020019091908035906020019091905050608a565b005b80600060005083606481101560025790900160005b50819055505b5050565b6000600060005082606481101560025790900160005b5054905060c7565b91905056") - testContractCodeDeployed = testContractCode[16:] - testContractDeployed = uint64(2) - - testEventEmitterCode = common.Hex2Bytes("60606040523415600e57600080fd5b7f57050ab73f6b9ebdd9f76b8d4997793f48cf956e965ee070551b9ca0bb71584e60405160405180910390a160358060476000396000f3006060604052600080fd00a165627a7a723058203f727efcad8b5811f8cb1fc2620ce5e8c63570d697aef968172de296ea3994140029") - - // Checkpoint oracle relative fields - signerKey, _ = crypto.GenerateKey() - signerAddr = crypto.PubkeyToAddress(signerKey.PublicKey) -) - -var ( - // The token bucket buffer limit for testing purpose. - testBufLimit = uint64(1000000) - - // The buffer recharging speed for testing purpose. - testBufRecharge = uint64(1000) -) - -/* -contract test { - - uint256[100] data; - - function Put(uint256 addr, uint256 value) { - data[addr] = value; - } - - function Get(uint256 addr) constant returns (uint256 value) { - return data[addr]; - } -} -*/ - -// prepare pre-commits specified number customized blocks into chain. -func prepare(n int, backend *backends.SimulatedBackend) { - var ( - ctx = context.Background() - signer = types.HomesteadSigner{} - ) - for i := 0; i < n; i++ { - switch i { - case 0: - // Builtin-block - // number: 1 - // txs: 1 - - // bankUser transfers some ether to user1 - nonce, _ := backend.PendingNonceAt(ctx, bankAddr) - tx, _ := types.SignTx(types.NewTransaction(nonce, userAddr1, big.NewInt(10_000_000_000_000_000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, bankKey) - backend.SendTransaction(ctx, tx) - case 1: - // Builtin-block - // number: 2 - // txs: 4 - - bankNonce, _ := backend.PendingNonceAt(ctx, bankAddr) - userNonce1, _ := backend.PendingNonceAt(ctx, userAddr1) - - // bankUser transfers more ether to user1 - tx1, _ := types.SignTx(types.NewTransaction(bankNonce, userAddr1, big.NewInt(1_000_000_000_000_000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, bankKey) - backend.SendTransaction(ctx, tx1) - - // user1 relays ether to user2 - tx2, _ := types.SignTx(types.NewTransaction(userNonce1, userAddr2, big.NewInt(1_000_000_000_000_000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, userKey1) - backend.SendTransaction(ctx, tx2) - - // user1 deploys a test contract - tx3, _ := types.SignTx(types.NewContractCreation(userNonce1+1, big.NewInt(0), 200000, big.NewInt(params.InitialBaseFee), testContractCode), signer, userKey1) - backend.SendTransaction(ctx, tx3) - testContractAddr = crypto.CreateAddress(userAddr1, userNonce1+1) - - // user1 deploys a event contract - tx4, _ := types.SignTx(types.NewContractCreation(userNonce1+2, big.NewInt(0), 200000, big.NewInt(params.InitialBaseFee), testEventEmitterCode), signer, userKey1) - backend.SendTransaction(ctx, tx4) - case 2: - // Builtin-block - // number: 3 - // txs: 2 - - // bankUser transfer some ether to signer - bankNonce, _ := backend.PendingNonceAt(ctx, bankAddr) - tx1, _ := types.SignTx(types.NewTransaction(bankNonce, signerAddr, big.NewInt(1000000000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, bankKey) - backend.SendTransaction(ctx, tx1) - - // invoke test contract - data := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001") - tx2, _ := types.SignTx(types.NewTransaction(bankNonce+1, testContractAddr, big.NewInt(0), 100000, big.NewInt(params.InitialBaseFee), data), signer, bankKey) - backend.SendTransaction(ctx, tx2) - case 3: - // Builtin-block - // number: 4 - // txs: 1 - - // invoke test contract - bankNonce, _ := backend.PendingNonceAt(ctx, bankAddr) - data := common.Hex2Bytes("C16431B900000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002") - tx, _ := types.SignTx(types.NewTransaction(bankNonce, testContractAddr, big.NewInt(0), 100000, big.NewInt(params.InitialBaseFee), data), signer, bankKey) - backend.SendTransaction(ctx, tx) - } - backend.Commit() - } -} - -// testIndexers creates a set of indexers with specified params for testing purpose. -func testIndexers(db ethdb.Database, odr light.OdrBackend, config *light.IndexerConfig, disablePruning bool) []*core.ChainIndexer { - var indexers [3]*core.ChainIndexer - indexers[0] = light.NewChtIndexer(db, odr, config.ChtSize, config.ChtConfirms, disablePruning) - indexers[1] = core.NewBloomIndexer(db, config.BloomSize, config.BloomConfirms) - indexers[2] = light.NewBloomTrieIndexer(db, odr, config.BloomSize, config.BloomTrieSize, disablePruning) - // make bloomTrieIndexer as a child indexer of bloom indexer. - indexers[1].AddChildIndexer(indexers[2]) - return indexers[:] -} - -func newTestClientHandler(backend *backends.SimulatedBackend, odr *LesOdr, indexers []*core.ChainIndexer, db ethdb.Database, peers *serverPeerSet) (*clientHandler, func()) { - var ( - evmux = new(event.TypeMux) - engine = ethash.NewFaker() - gspec = core.Genesis{ - Config: params.AllEthashProtocolChanges, - Alloc: core.GenesisAlloc{bankAddr: {Balance: bankFunds}}, - GasLimit: 100000000, - BaseFee: big.NewInt(params.InitialBaseFee), - } - ) - genesis := gspec.MustCommit(db, trie.NewDatabase(db, trie.HashDefaults)) - chain, _ := light.NewLightChain(odr, gspec.Config, engine) - - client := &LightEthereum{ - lesCommons: lesCommons{ - genesis: genesis.Hash(), - config: ðconfig.Config{LightPeers: 100, NetworkId: NetworkId}, - chainConfig: params.AllEthashProtocolChanges, - iConfig: light.TestClientIndexerConfig, - chainDb: db, - chainReader: chain, - closeCh: make(chan struct{}), - }, - peers: peers, - reqDist: odr.retriever.dist, - retriever: odr.retriever, - odr: odr, - engine: engine, - blockchain: chain, - eventMux: evmux, - merger: consensus.NewMerger(rawdb.NewMemoryDatabase()), - } - client.handler = newClientHandler(client) - - return client.handler, func() { - client.handler.stop() - } -} - -func newTestServerHandler(blocks int, indexers []*core.ChainIndexer, db ethdb.Database, clock mclock.Clock) (*serverHandler, *backends.SimulatedBackend, func()) { - var ( - gspec = core.Genesis{ - Config: params.AllEthashProtocolChanges, - Alloc: core.GenesisAlloc{bankAddr: {Balance: bankFunds}}, - GasLimit: 100000000, - BaseFee: big.NewInt(params.InitialBaseFee), - } - ) - genesis := gspec.MustCommit(db, trie.NewDatabase(db, trie.HashDefaults)) - - // create a simulation backend and pre-commit several customized block to the database. - simulation := backends.NewSimulatedBackendWithDatabase(db, gspec.Alloc, 100000000) - prepare(blocks, simulation) - - txpoolConfig := legacypool.DefaultConfig - txpoolConfig.Journal = "" - - pool := legacypool.New(txpoolConfig, simulation.Blockchain()) - txpool, _ := txpool.New(new(big.Int).SetUint64(txpoolConfig.PriceLimit), simulation.Blockchain(), []txpool.SubPool{pool}) - - server := &LesServer{ - lesCommons: lesCommons{ - genesis: genesis.Hash(), - config: ðconfig.Config{LightPeers: 100, NetworkId: NetworkId}, - chainConfig: params.AllEthashProtocolChanges, - iConfig: light.TestServerIndexerConfig, - chainDb: db, - chainReader: simulation.Blockchain(), - closeCh: make(chan struct{}), - }, - peers: newClientPeerSet(), - servingQueue: newServingQueue(int64(time.Millisecond*10), 1), - defParams: flowcontrol.ServerParams{ - BufLimit: testBufLimit, - MinRecharge: testBufRecharge, - }, - fcManager: flowcontrol.NewClientManager(nil, clock), - } - server.costTracker, server.minCapacity = newCostTracker(db, server.config) - server.costTracker.testCostList = testCostList(0) // Disable flow control mechanism. - server.clientPool = vfs.NewClientPool(db, testBufRecharge, defaultConnectedBias, clock, alwaysTrueFn) - server.clientPool.Start() - server.clientPool.SetLimits(10000, 10000) // Assign enough capacity for clientpool - server.handler = newServerHandler(server, simulation.Blockchain(), db, txpool, func() bool { return true }) - server.servingQueue.setThreads(4) - server.handler.start() - closer := func() { server.Stop() } - return server.handler, simulation, closer -} - -func alwaysTrueFn() bool { - return true -} - -// testPeer is a simulated peer to allow testing direct network calls. -type testPeer struct { - cpeer *clientPeer - speer *serverPeer - - net p2p.MsgReadWriter // Network layer reader/writer to simulate remote messaging - app *p2p.MsgPipeRW // Application layer reader/writer to simulate the local side -} - -// handshakeWithServer executes the handshake with the remote server peer. -func (p *testPeer) handshakeWithServer(t *testing.T, td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, forkID forkid.ID) { - // It only works for the simulated client peer - if p.cpeer == nil { - t.Fatal("handshake for client peer only") - } - var sendList keyValueList - sendList = sendList.add("protocolVersion", uint64(p.cpeer.version)) - sendList = sendList.add("networkId", uint64(NetworkId)) - sendList = sendList.add("headTd", td) - sendList = sendList.add("headHash", head) - sendList = sendList.add("headNum", headNum) - sendList = sendList.add("genesisHash", genesis) - if p.cpeer.version >= lpv4 { - sendList = sendList.add("forkID", &forkID) - } - if err := p2p.ExpectMsg(p.app, StatusMsg, nil); err != nil { - t.Fatalf("status recv: %v", err) - } - if err := p2p.Send(p.app, StatusMsg, &sendList); err != nil { - t.Fatalf("status send: %v", err) - } -} - -// handshakeWithClient executes the handshake with the remote client peer. -// (used by temporarily disabled tests) -/*func (p *testPeer) handshakeWithClient(t *testing.T, td *big.Int, head common.Hash, headNum uint64, genesis common.Hash, forkID forkid.ID, costList RequestCostList, recentTxLookup uint64) { - // It only works for the simulated client peer - if p.speer == nil { - t.Fatal("handshake for server peer only") - } - var sendList keyValueList - sendList = sendList.add("protocolVersion", uint64(p.speer.version)) - sendList = sendList.add("networkId", uint64(NetworkId)) - sendList = sendList.add("headTd", td) - sendList = sendList.add("headHash", head) - sendList = sendList.add("headNum", headNum) - sendList = sendList.add("genesisHash", genesis) - sendList = sendList.add("serveHeaders", nil) - sendList = sendList.add("serveChainSince", uint64(0)) - sendList = sendList.add("serveStateSince", uint64(0)) - sendList = sendList.add("serveRecentState", uint64(core.TriesInMemory-4)) - sendList = sendList.add("txRelay", nil) - sendList = sendList.add("flowControl/BL", testBufLimit) - sendList = sendList.add("flowControl/MRR", testBufRecharge) - sendList = sendList.add("flowControl/MRC", costList) - if p.speer.version >= lpv4 { - sendList = sendList.add("forkID", &forkID) - sendList = sendList.add("recentTxLookup", recentTxLookup) - } - if err := p2p.ExpectMsg(p.app, StatusMsg, nil); err != nil { - t.Fatalf("status recv: %v", err) - } - if err := p2p.Send(p.app, StatusMsg, &sendList); err != nil { - t.Fatalf("status send: %v", err) - } -}*/ - -// close terminates the local side of the peer, notifying the remote protocol -// manager of termination. -func (p *testPeer) close() { - p.app.Close() -} - -func newTestPeerPair(name string, version int, server *serverHandler, client *clientHandler, noInitAnnounce bool) (*testPeer, *testPeer, error) { - // Create a message pipe to communicate through - app, net := p2p.MsgPipe() - - // Generate a random id and create the peer - var id enode.ID - rand.Read(id[:]) - - peer1 := newClientPeer(version, NetworkId, p2p.NewPeer(id, name, nil), net) - peer2 := newServerPeer(version, NetworkId, false, p2p.NewPeer(id, name, nil), app) - - // Start the peer on a new thread - errc1 := make(chan error, 1) - errc2 := make(chan error, 1) - go func() { - select { - case <-server.closeCh: - errc1 <- p2p.DiscQuitting - case errc1 <- server.handle(peer1): - } - }() - go func() { - select { - case <-client.closeCh: - errc2 <- p2p.DiscQuitting - case errc2 <- client.handle(peer2, noInitAnnounce): - } - }() - // Ensure the connection is established or exits when any error occurs - for { - select { - case err := <-errc1: - return nil, nil, fmt.Errorf("failed to establish protocol connection %v", err) - case err := <-errc2: - return nil, nil, fmt.Errorf("failed to establish protocol connection %v", err) - default: - } - if peer1.serving.Load() && peer2.serving.Load() { - break - } - time.Sleep(50 * time.Millisecond) - } - return &testPeer{cpeer: peer1, net: net, app: app}, &testPeer{speer: peer2, net: app, app: net}, nil -} - -type indexerCallback func(*core.ChainIndexer, *core.ChainIndexer, *core.ChainIndexer) - -// testClient represents a client object for testing with necessary auxiliary fields. -type testClient struct { - clock mclock.Clock - db ethdb.Database - peer *testPeer - handler *clientHandler - - chtIndexer *core.ChainIndexer - bloomIndexer *core.ChainIndexer - bloomTrieIndexer *core.ChainIndexer -} - -// newRawPeer creates a new server peer connects to the server and do the handshake. -// (used by temporarily disabled tests) -/*func (client *testClient) newRawPeer(t *testing.T, name string, version int, recentTxLookup uint64) (*testPeer, func(), <-chan error) { - // Create a message pipe to communicate through - app, net := p2p.MsgPipe() - - // Generate a random id and create the peer - var id enode.ID - rand.Read(id[:]) - peer := newServerPeer(version, NetworkId, false, p2p.NewPeer(id, name, nil), net) - - // Start the peer on a new thread - errCh := make(chan error, 1) - go func() { - select { - case <-client.handler.closeCh: - errCh <- p2p.DiscQuitting - case errCh <- client.handler.handle(peer, false): - } - }() - tp := &testPeer{ - app: app, - net: net, - speer: peer, - } - var ( - genesis = client.handler.backend.blockchain.Genesis() - head = client.handler.backend.blockchain.CurrentHeader() - td = client.handler.backend.blockchain.GetTd(head.Hash(), head.Number.Uint64()) - ) - forkID := forkid.NewID(client.handler.backend.blockchain.Config(), genesis.Hash(), head.Number.Uint64(), head.Time) - tp.handshakeWithClient(t, td, head.Hash(), head.Number.Uint64(), genesis.Hash(), forkID, testCostList(0), recentTxLookup) // disable flow control by default - - // Ensure the connection is established or exits when any error occurs - for { - select { - case <-errCh: - return nil, nil, nil - default: - } - if peer.serving.Load() { - break - } - time.Sleep(50 * time.Millisecond) - } - closePeer := func() { - tp.speer.close() - tp.close() - } - return tp, closePeer, errCh -}*/ - -// testServer represents a server object for testing with necessary auxiliary fields. -type testServer struct { - clock mclock.Clock - backend *backends.SimulatedBackend - db ethdb.Database - peer *testPeer - handler *serverHandler - - chtIndexer *core.ChainIndexer - bloomIndexer *core.ChainIndexer - bloomTrieIndexer *core.ChainIndexer -} - -// newRawPeer creates a new client peer connects to the server and do the handshake. -func (server *testServer) newRawPeer(t *testing.T, name string, version int) (*testPeer, func(), <-chan error) { - // Create a message pipe to communicate through - app, net := p2p.MsgPipe() - - // Generate a random id and create the peer - var id enode.ID - rand.Read(id[:]) - peer := newClientPeer(version, NetworkId, p2p.NewPeer(id, name, nil), net) - - // Start the peer on a new thread - errCh := make(chan error, 1) - go func() { - select { - case <-server.handler.closeCh: - errCh <- p2p.DiscQuitting - case errCh <- server.handler.handle(peer): - } - }() - tp := &testPeer{ - app: app, - net: net, - cpeer: peer, - } - var ( - genesis = server.handler.blockchain.Genesis() - head = server.handler.blockchain.CurrentHeader() - td = server.handler.blockchain.GetTd(head.Hash(), head.Number.Uint64()) - ) - forkID := forkid.NewID(server.handler.blockchain.Config(), genesis, head.Number.Uint64(), head.Time) - tp.handshakeWithServer(t, td, head.Hash(), head.Number.Uint64(), genesis.Hash(), forkID) - - // Ensure the connection is established or exits when any error occurs - for { - select { - case <-errCh: - return nil, nil, nil - default: - } - if peer.serving.Load() { - break - } - time.Sleep(50 * time.Millisecond) - } - closePeer := func() { - tp.cpeer.close() - tp.close() - } - return tp, closePeer, errCh -} - -// testnetConfig wraps all the configurations for testing network. -type testnetConfig struct { - blocks int - protocol int - indexFn indexerCallback - simClock bool - connect bool - nopruning bool -} - -func newClientServerEnv(t *testing.T, config testnetConfig) (*testServer, *testClient, func()) { - var ( - sdb = rawdb.NewMemoryDatabase() - cdb = rawdb.NewMemoryDatabase() - speers = newServerPeerSet() - ) - var clock mclock.Clock = &mclock.System{} - if config.simClock { - clock = &mclock.Simulated{} - } - dist := newRequestDistributor(speers, clock) - rm := newRetrieveManager(speers, dist, func() time.Duration { return time.Millisecond * 500 }) - odr := NewLesOdr(cdb, light.TestClientIndexerConfig, speers, rm) - - sindexers := testIndexers(sdb, nil, light.TestServerIndexerConfig, true) - cIndexers := testIndexers(cdb, odr, light.TestClientIndexerConfig, config.nopruning) - - scIndexer, sbIndexer, sbtIndexer := sindexers[0], sindexers[1], sindexers[2] - ccIndexer, cbIndexer, cbtIndexer := cIndexers[0], cIndexers[1], cIndexers[2] - odr.SetIndexers(ccIndexer, cbIndexer, cbtIndexer) - - server, b, serverClose := newTestServerHandler(config.blocks, sindexers, sdb, clock) - client, clientClose := newTestClientHandler(b, odr, cIndexers, cdb, speers) - - scIndexer.Start(server.blockchain) - sbIndexer.Start(server.blockchain) - ccIndexer.Start(client.backend.blockchain) - cbIndexer.Start(client.backend.blockchain) - - if config.indexFn != nil { - config.indexFn(scIndexer, sbIndexer, sbtIndexer) - } - var ( - err error - speer, cpeer *testPeer - ) - if config.connect { - done := make(chan struct{}) - cpeer, speer, err = newTestPeerPair("peer", config.protocol, server, client, false) - if err != nil { - t.Fatalf("Failed to connect testing peers %v", err) - } - select { - case <-done: - case <-time.After(10 * time.Second): - t.Fatal("test peer did not connect and sync within 3s") - } - } - s := &testServer{ - clock: clock, - backend: b, - db: sdb, - peer: cpeer, - handler: server, - chtIndexer: scIndexer, - bloomIndexer: sbIndexer, - bloomTrieIndexer: sbtIndexer, - } - c := &testClient{ - clock: clock, - db: cdb, - peer: speer, - handler: client, - chtIndexer: ccIndexer, - bloomIndexer: cbIndexer, - bloomTrieIndexer: cbtIndexer, - } - teardown := func() { - if config.connect { - speer.close() - cpeer.close() - cpeer.cpeer.close() - speer.speer.close() - } - ccIndexer.Close() - cbIndexer.Close() - scIndexer.Close() - sbIndexer.Close() - dist.close() - serverClose() - b.Close() - clientClose() - } - return s, c, teardown -} - -// NewFuzzerPeer creates a client peer for test purposes, and also returns -// a function to close the peer: this is needed to avoid goroutine leaks in the -// exec queue. -func NewFuzzerPeer(version int) (p *clientPeer, closer func()) { - p = newClientPeer(version, 0, p2p.NewPeer(enode.ID{}, "", nil), nil) - return p, func() { p.peerCommons.close() } -} diff --git a/les/txrelay.go b/les/txrelay.go deleted file mode 100644 index 40a51fb76f..0000000000 --- a/les/txrelay.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2016 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 les - -import ( - "context" - "math/rand" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" -) - -type lesTxRelay struct { - txSent map[common.Hash]*types.Transaction - txPending map[common.Hash]struct{} - peerList []*serverPeer - peerStartPos int - lock sync.Mutex - stop chan struct{} - - retriever *retrieveManager -} - -func newLesTxRelay(ps *serverPeerSet, retriever *retrieveManager) *lesTxRelay { - r := &lesTxRelay{ - txSent: make(map[common.Hash]*types.Transaction), - txPending: make(map[common.Hash]struct{}), - retriever: retriever, - stop: make(chan struct{}), - } - ps.subscribe(r) - return r -} - -func (ltrx *lesTxRelay) Stop() { - close(ltrx.stop) -} - -func (ltrx *lesTxRelay) registerPeer(p *serverPeer) { - ltrx.lock.Lock() - defer ltrx.lock.Unlock() - - // Short circuit if the peer is announce only. - if p.onlyAnnounce { - return - } - ltrx.peerList = append(ltrx.peerList, p) -} - -func (ltrx *lesTxRelay) unregisterPeer(p *serverPeer) { - ltrx.lock.Lock() - defer ltrx.lock.Unlock() - - for i, peer := range ltrx.peerList { - if peer == p { - // Remove from the peer list - ltrx.peerList = append(ltrx.peerList[:i], ltrx.peerList[i+1:]...) - return - } - } -} - -// send sends a list of transactions to at most a given number of peers. -func (ltrx *lesTxRelay) send(txs types.Transactions, count int) { - sendTo := make(map[*serverPeer]types.Transactions) - - ltrx.peerStartPos++ // rotate the starting position of the peer list - if ltrx.peerStartPos >= len(ltrx.peerList) { - ltrx.peerStartPos = 0 - } - - for _, tx := range txs { - hash := tx.Hash() - _, ok := ltrx.txSent[hash] - if !ok { - ltrx.txSent[hash] = tx - ltrx.txPending[hash] = struct{}{} - } - if len(ltrx.peerList) > 0 { - cnt := count - pos := ltrx.peerStartPos - for { - peer := ltrx.peerList[pos] - sendTo[peer] = append(sendTo[peer], tx) - cnt-- - if cnt == 0 { - break // sent it to the desired number of peers - } - pos++ - if pos == len(ltrx.peerList) { - pos = 0 - } - if pos == ltrx.peerStartPos { - break // tried all available peers - } - } - } - } - - for p, list := range sendTo { - pp := p - ll := list - enc, _ := rlp.EncodeToBytes(ll) - - reqID := rand.Uint64() - rq := &distReq{ - getCost: func(dp distPeer) uint64 { - peer := dp.(*serverPeer) - return peer.getTxRelayCost(len(ll), len(enc)) - }, - canSend: func(dp distPeer) bool { - return !dp.(*serverPeer).onlyAnnounce && dp.(*serverPeer) == pp - }, - request: func(dp distPeer) func() { - peer := dp.(*serverPeer) - cost := peer.getTxRelayCost(len(ll), len(enc)) - peer.fcServer.QueuedRequest(reqID, cost) - return func() { peer.sendTxs(reqID, len(ll), enc) } - }, - } - go ltrx.retriever.retrieve(context.Background(), reqID, rq, func(p distPeer, msg *Msg) error { return nil }, ltrx.stop) - } -} - -func (ltrx *lesTxRelay) Send(txs types.Transactions) { - ltrx.lock.Lock() - defer ltrx.lock.Unlock() - - ltrx.send(txs, 3) -} - -func (ltrx *lesTxRelay) NewHead(head common.Hash, mined []common.Hash, rollback []common.Hash) { - ltrx.lock.Lock() - defer ltrx.lock.Unlock() - - for _, hash := range mined { - delete(ltrx.txPending, hash) - } - - for _, hash := range rollback { - ltrx.txPending[hash] = struct{}{} - } - - if len(ltrx.txPending) > 0 { - txs := make(types.Transactions, len(ltrx.txPending)) - i := 0 - for hash := range ltrx.txPending { - txs[i] = ltrx.txSent[hash] - i++ - } - ltrx.send(txs, 1) - } -} - -func (ltrx *lesTxRelay) Discard(hashes []common.Hash) { - ltrx.lock.Lock() - defer ltrx.lock.Unlock() - - for _, hash := range hashes { - delete(ltrx.txSent, hash) - delete(ltrx.txPending, hash) - } -} diff --git a/les/utils/exec_queue.go b/les/utils/exec_queue.go deleted file mode 100644 index 5942b06ec0..0000000000 --- a/les/utils/exec_queue.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020 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 utils - -import "sync" - -// ExecQueue implements a queue that executes function calls in a single thread, -// in the same order as they have been queued. -type ExecQueue struct { - mu sync.Mutex - cond *sync.Cond - funcs []func() - closeWait chan struct{} -} - -// NewExecQueue creates a new execution Queue. -func NewExecQueue(capacity int) *ExecQueue { - q := &ExecQueue{funcs: make([]func(), 0, capacity)} - q.cond = sync.NewCond(&q.mu) - go q.loop() - return q -} - -func (q *ExecQueue) loop() { - for f := q.waitNext(false); f != nil; f = q.waitNext(true) { - f() - } - close(q.closeWait) -} - -func (q *ExecQueue) waitNext(drop bool) (f func()) { - q.mu.Lock() - if drop && len(q.funcs) > 0 { - // Remove the function that just executed. We do this here instead of when - // dequeuing so len(q.funcs) includes the function that is running. - q.funcs = append(q.funcs[:0], q.funcs[1:]...) - } - for !q.isClosed() { - if len(q.funcs) > 0 { - f = q.funcs[0] - break - } - q.cond.Wait() - } - q.mu.Unlock() - return f -} - -func (q *ExecQueue) isClosed() bool { - return q.closeWait != nil -} - -// CanQueue returns true if more function calls can be added to the execution Queue. -func (q *ExecQueue) CanQueue() bool { - q.mu.Lock() - ok := !q.isClosed() && len(q.funcs) < cap(q.funcs) - q.mu.Unlock() - return ok -} - -// Queue adds a function call to the execution Queue. Returns true if successful. -func (q *ExecQueue) Queue(f func()) bool { - q.mu.Lock() - ok := !q.isClosed() && len(q.funcs) < cap(q.funcs) - if ok { - q.funcs = append(q.funcs, f) - q.cond.Signal() - } - q.mu.Unlock() - return ok -} - -// Clear drops all queued functions. -func (q *ExecQueue) Clear() { - q.mu.Lock() - q.funcs = q.funcs[:0] - q.mu.Unlock() -} - -// Quit stops the exec Queue. -// -// Quit waits for the current execution to finish before returning. -func (q *ExecQueue) Quit() { - q.mu.Lock() - if !q.isClosed() { - q.closeWait = make(chan struct{}) - q.cond.Signal() - } - q.mu.Unlock() - <-q.closeWait -} diff --git a/les/utils/exec_queue_test.go b/les/utils/exec_queue_test.go deleted file mode 100644 index 98601c4486..0000000000 --- a/les/utils/exec_queue_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2017 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 utils - -import "testing" - -func TestExecQueue(t *testing.T) { - var ( - N = 10000 - q = NewExecQueue(N) - counter int - execd = make(chan int) - testexit = make(chan struct{}) - ) - defer q.Quit() - defer close(testexit) - - check := func(state string, wantOK bool) { - c := counter - counter++ - qf := func() { - select { - case execd <- c: - case <-testexit: - } - } - if q.CanQueue() != wantOK { - t.Fatalf("CanQueue() == %t for %s", !wantOK, state) - } - if q.Queue(qf) != wantOK { - t.Fatalf("Queue() == %t for %s", !wantOK, state) - } - } - - for i := 0; i < N; i++ { - check("queue below cap", true) - } - check("full queue", false) - for i := 0; i < N; i++ { - if c := <-execd; c != i { - t.Fatal("execution out of order") - } - } - q.Quit() - check("closed queue", false) -} diff --git a/les/utils/expiredvalue.go b/les/utils/expiredvalue.go deleted file mode 100644 index 099b61d053..0000000000 --- a/les/utils/expiredvalue.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2020 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 utils - -import ( - "math" - "sync" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -// ExpiredValue is a scalar value that is continuously expired (decreased -// exponentially) based on the provided logarithmic expiration offset value. -// -// The formula for value calculation is: base*2^(exp-logOffset). In order to -// simplify the calculation of ExpiredValue, its value is expressed in the form -// of an exponent with a base of 2. -// -// Also here is a trick to reduce a lot of calculations. In theory, when a value X -// decays over time and then a new value Y is added, the final result should be -// X*2^(exp-logOffset)+Y. However it's very hard to represent in memory. -// So the trick is using the idea of inflation instead of exponential decay. At this -// moment the temporary value becomes: X*2^exp+Y*2^logOffset_1, apply the exponential -// decay when we actually want to calculate the value. -// -// e.g. -// t0: V = 100 -// t1: add 30, inflationary value is: 100 + 30/0.3, 0.3 is the decay coefficient -// t2: get value, decay coefficient is 0.2 now, final result is: 200*0.2 = 40 -type ExpiredValue struct { - Base, Exp uint64 // rlp encoding works by default -} - -// ExpirationFactor is calculated from logOffset. 1 <= Factor < 2 and Factor*2^Exp -// describes the multiplier applicable for additions and the divider for readouts. -// If logOffset changes slowly then it saves some expensive operations to not calculate -// them for each addition and readout but cache this intermediate form for some time. -// It is also useful for structures where multiple values are expired with the same -// Expirer. -type ExpirationFactor struct { - Exp uint64 - Factor float64 -} - -// ExpFactor calculates ExpirationFactor based on logOffset -func ExpFactor(logOffset Fixed64) ExpirationFactor { - return ExpirationFactor{Exp: logOffset.ToUint64(), Factor: logOffset.Fraction().Pow2()} -} - -// Value calculates the expired value based on a floating point base and integer -// power-of-2 exponent. This function should be used by multi-value expired structures. -func (e ExpirationFactor) Value(base float64, exp uint64) float64 { - return base / e.Factor * math.Pow(2, float64(int64(exp-e.Exp))) -} - -// Value calculates the value at the given moment. -func (e ExpiredValue) Value(logOffset Fixed64) uint64 { - offset := Uint64ToFixed64(e.Exp) - logOffset - return uint64(float64(e.Base) * offset.Pow2()) -} - -// Add adds a signed value at the given moment -func (e *ExpiredValue) Add(amount int64, logOffset Fixed64) int64 { - integer, frac := logOffset.ToUint64(), logOffset.Fraction() - factor := frac.Pow2() - base := factor * float64(amount) - if integer < e.Exp { - base /= math.Pow(2, float64(e.Exp-integer)) - } - if integer > e.Exp { - e.Base >>= (integer - e.Exp) - e.Exp = integer - } - if base >= 0 || uint64(-base) <= e.Base { - // The conversion from negative float64 to - // uint64 is undefined in golang, and doesn't - // work with ARMv8. More details at: - // https://github.com/golang/go/issues/43047 - if base >= 0 { - e.Base += uint64(base) - } else { - e.Base -= uint64(-base) - } - return amount - } - net := int64(-float64(e.Base) / factor) - e.Base = 0 - return net -} - -// AddExp adds another ExpiredValue -func (e *ExpiredValue) AddExp(a ExpiredValue) { - if e.Exp > a.Exp { - a.Base >>= (e.Exp - a.Exp) - } - if e.Exp < a.Exp { - e.Base >>= (a.Exp - e.Exp) - e.Exp = a.Exp - } - e.Base += a.Base -} - -// SubExp subtracts another ExpiredValue -func (e *ExpiredValue) SubExp(a ExpiredValue) { - if e.Exp > a.Exp { - a.Base >>= (e.Exp - a.Exp) - } - if e.Exp < a.Exp { - e.Base >>= (a.Exp - e.Exp) - e.Exp = a.Exp - } - if e.Base > a.Base { - e.Base -= a.Base - } else { - e.Base = 0 - } -} - -// IsZero returns true if the value is zero -func (e *ExpiredValue) IsZero() bool { - return e.Base == 0 -} - -// LinearExpiredValue is very similar with the expiredValue which the value -// will continuously expired. But the different part is it's expired linearly. -type LinearExpiredValue struct { - Offset uint64 // The latest time offset - Val uint64 // The remaining value, can never be negative - Rate mclock.AbsTime `rlp:"-"` // Expiration rate(by nanosecond), will ignored by RLP -} - -// Value calculates the value at the given moment. This function always has the -// assumption that the given timestamp shouldn't less than the recorded one. -func (e LinearExpiredValue) Value(now mclock.AbsTime) uint64 { - offset := uint64(now / e.Rate) - if e.Offset < offset { - diff := offset - e.Offset - if e.Val >= diff { - e.Val -= diff - } else { - e.Val = 0 - } - } - return e.Val -} - -// Add adds a signed value at the given moment. This function always has the -// assumption that the given timestamp shouldn't less than the recorded one. -func (e *LinearExpiredValue) Add(amount int64, now mclock.AbsTime) uint64 { - offset := uint64(now / e.Rate) - if e.Offset < offset { - diff := offset - e.Offset - if e.Val >= diff { - e.Val -= diff - } else { - e.Val = 0 - } - e.Offset = offset - } - if amount < 0 && uint64(-amount) > e.Val { - e.Val = 0 - } else { - e.Val = uint64(int64(e.Val) + amount) - } - return e.Val -} - -// ValueExpirer controls value expiration rate -type ValueExpirer interface { - SetRate(now mclock.AbsTime, rate float64) - SetLogOffset(now mclock.AbsTime, logOffset Fixed64) - LogOffset(now mclock.AbsTime) Fixed64 -} - -// Expirer changes logOffset with a linear rate which can be changed during operation. -// It is not thread safe, if access by multiple goroutines is needed then it should be -// encapsulated into a locked structure. -// Note that if neither SetRate nor SetLogOffset are used during operation then LogOffset -// is thread safe. -type Expirer struct { - lock sync.RWMutex - logOffset Fixed64 - rate float64 - lastUpdate mclock.AbsTime -} - -// SetRate changes the expiration rate which is the inverse of the time constant in -// nanoseconds. -func (e *Expirer) SetRate(now mclock.AbsTime, rate float64) { - e.lock.Lock() - defer e.lock.Unlock() - - dt := now - e.lastUpdate - if dt > 0 { - e.logOffset += Fixed64(logToFixedFactor * float64(dt) * e.rate) - } - e.lastUpdate = now - e.rate = rate -} - -// SetLogOffset sets logOffset instantly. -func (e *Expirer) SetLogOffset(now mclock.AbsTime, logOffset Fixed64) { - e.lock.Lock() - defer e.lock.Unlock() - - e.lastUpdate = now - e.logOffset = logOffset -} - -// LogOffset returns the current logarithmic offset. -func (e *Expirer) LogOffset(now mclock.AbsTime) Fixed64 { - e.lock.RLock() - defer e.lock.RUnlock() - - dt := now - e.lastUpdate - if dt <= 0 { - return e.logOffset - } - return e.logOffset + Fixed64(logToFixedFactor*float64(dt)*e.rate) -} - -// fixedFactor is the fixed point multiplier factor used by Fixed64. -const fixedFactor = 0x1000000 - -// Fixed64 implements 64-bit fixed point arithmetic functions. -type Fixed64 int64 - -// Uint64ToFixed64 converts uint64 integer to Fixed64 format. -func Uint64ToFixed64(f uint64) Fixed64 { - return Fixed64(f * fixedFactor) -} - -// Float64ToFixed64 converts float64 to Fixed64 format. -func Float64ToFixed64(f float64) Fixed64 { - return Fixed64(f * fixedFactor) -} - -// ToUint64 converts Fixed64 format to uint64. -func (f64 Fixed64) ToUint64() uint64 { - return uint64(f64) / fixedFactor -} - -// Fraction returns the fractional part of a Fixed64 value. -func (f64 Fixed64) Fraction() Fixed64 { - return f64 % fixedFactor -} - -var ( - logToFixedFactor = float64(fixedFactor) / math.Log(2) - fixedToLogFactor = math.Log(2) / float64(fixedFactor) -) - -// Pow2 returns the base 2 power of the fixed point value. -func (f64 Fixed64) Pow2() float64 { - return math.Exp(float64(f64) * fixedToLogFactor) -} diff --git a/les/utils/expiredvalue_test.go b/les/utils/expiredvalue_test.go deleted file mode 100644 index 1c751d8cc6..0000000000 --- a/les/utils/expiredvalue_test.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2020 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 utils - -import ( - "testing" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -func TestValueExpiration(t *testing.T) { - var cases = []struct { - input ExpiredValue - timeOffset Fixed64 - expect uint64 - }{ - {ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(0), 128}, - {ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(1), 64}, - {ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(2), 32}, - {ExpiredValue{Base: 128, Exp: 2}, Uint64ToFixed64(2), 128}, - {ExpiredValue{Base: 128, Exp: 2}, Uint64ToFixed64(3), 64}, - } - for _, c := range cases { - if got := c.input.Value(c.timeOffset); got != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got) - } - } -} - -func TestValueAddition(t *testing.T) { - var cases = []struct { - input ExpiredValue - addend int64 - timeOffset Fixed64 - expect uint64 - expectNet int64 - }{ - // Addition - {ExpiredValue{Base: 128, Exp: 0}, 128, Uint64ToFixed64(0), 256, 128}, - {ExpiredValue{Base: 128, Exp: 2}, 128, Uint64ToFixed64(0), 640, 128}, - - // Addition with offset - {ExpiredValue{Base: 128, Exp: 0}, 128, Uint64ToFixed64(1), 192, 128}, - {ExpiredValue{Base: 128, Exp: 2}, 128, Uint64ToFixed64(1), 384, 128}, - {ExpiredValue{Base: 128, Exp: 2}, 128, Uint64ToFixed64(3), 192, 128}, - - // Subtraction - {ExpiredValue{Base: 128, Exp: 0}, -64, Uint64ToFixed64(0), 64, -64}, - {ExpiredValue{Base: 128, Exp: 0}, -128, Uint64ToFixed64(0), 0, -128}, - {ExpiredValue{Base: 128, Exp: 0}, -192, Uint64ToFixed64(0), 0, -128}, - - // Subtraction with offset - {ExpiredValue{Base: 128, Exp: 0}, -64, Uint64ToFixed64(1), 0, -64}, - {ExpiredValue{Base: 128, Exp: 0}, -128, Uint64ToFixed64(1), 0, -64}, - {ExpiredValue{Base: 128, Exp: 2}, -128, Uint64ToFixed64(1), 128, -128}, - {ExpiredValue{Base: 128, Exp: 2}, -128, Uint64ToFixed64(2), 0, -128}, - } - for _, c := range cases { - if net := c.input.Add(c.addend, c.timeOffset); net != c.expectNet { - t.Fatalf("Net amount mismatch, want=%d, got=%d", c.expectNet, net) - } - if got := c.input.Value(c.timeOffset); got != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got) - } - } -} - -func TestExpiredValueAddition(t *testing.T) { - var cases = []struct { - input ExpiredValue - another ExpiredValue - timeOffset Fixed64 - expect uint64 - }{ - {ExpiredValue{Base: 128, Exp: 0}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(0), 256}, - {ExpiredValue{Base: 128, Exp: 1}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(0), 384}, - {ExpiredValue{Base: 128, Exp: 0}, ExpiredValue{Base: 128, Exp: 1}, Uint64ToFixed64(0), 384}, - {ExpiredValue{Base: 128, Exp: 0}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(1), 128}, - } - for _, c := range cases { - c.input.AddExp(c.another) - if got := c.input.Value(c.timeOffset); got != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got) - } - } -} - -func TestExpiredValueSubtraction(t *testing.T) { - var cases = []struct { - input ExpiredValue - another ExpiredValue - timeOffset Fixed64 - expect uint64 - }{ - {ExpiredValue{Base: 128, Exp: 0}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(0), 0}, - {ExpiredValue{Base: 128, Exp: 0}, ExpiredValue{Base: 128, Exp: 1}, Uint64ToFixed64(0), 0}, - {ExpiredValue{Base: 128, Exp: 1}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(0), 128}, - {ExpiredValue{Base: 128, Exp: 1}, ExpiredValue{Base: 128, Exp: 0}, Uint64ToFixed64(1), 64}, - } - for _, c := range cases { - c.input.SubExp(c.another) - if got := c.input.Value(c.timeOffset); got != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, got) - } - } -} - -func TestLinearExpiredValue(t *testing.T) { - var cases = []struct { - value LinearExpiredValue - now mclock.AbsTime - expect uint64 - }{ - {LinearExpiredValue{ - Offset: 0, - Val: 0, - Rate: mclock.AbsTime(1), - }, 0, 0}, - - {LinearExpiredValue{ - Offset: 1, - Val: 1, - Rate: mclock.AbsTime(1), - }, 0, 1}, - - {LinearExpiredValue{ - Offset: 1, - Val: 1, - Rate: mclock.AbsTime(1), - }, mclock.AbsTime(2), 0}, - - {LinearExpiredValue{ - Offset: 1, - Val: 1, - Rate: mclock.AbsTime(1), - }, mclock.AbsTime(3), 0}, - } - for _, c := range cases { - if value := c.value.Value(c.now); value != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, value) - } - } -} - -func TestLinearExpiredAddition(t *testing.T) { - var cases = []struct { - value LinearExpiredValue - amount int64 - now mclock.AbsTime - expect uint64 - }{ - {LinearExpiredValue{ - Offset: 0, - Val: 0, - Rate: mclock.AbsTime(1), - }, -1, 0, 0}, - - {LinearExpiredValue{ - Offset: 1, - Val: 1, - Rate: mclock.AbsTime(1), - }, -1, 0, 0}, - - {LinearExpiredValue{ - Offset: 1, - Val: 2, - Rate: mclock.AbsTime(1), - }, -1, mclock.AbsTime(2), 0}, - - {LinearExpiredValue{ - Offset: 1, - Val: 2, - Rate: mclock.AbsTime(1), - }, -2, mclock.AbsTime(2), 0}, - } - for _, c := range cases { - if value := c.value.Add(c.amount, c.now); value != c.expect { - t.Fatalf("Value mismatch, want=%d, got=%d", c.expect, value) - } - } -} diff --git a/les/utils/limiter.go b/les/utils/limiter.go deleted file mode 100644 index 70b7ff64f7..0000000000 --- a/les/utils/limiter.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2021 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 utils - -import ( - "sync" - - "github.com/ethereum/go-ethereum/p2p/enode" - "golang.org/x/exp/slices" -) - -const maxSelectionWeight = 1000000000 // maximum selection weight of each individual node/address group - -// Limiter protects a network request serving mechanism from denial-of-service attacks. -// It limits the total amount of resources used for serving requests while ensuring that -// the most valuable connections always have a reasonable chance of being served. -type Limiter struct { - lock sync.Mutex - cond *sync.Cond - quit bool - - nodes map[enode.ID]*nodeQueue - addresses map[string]*addressGroup - addressSelect, valueSelect *WeightedRandomSelect - maxValue float64 - maxCost, sumCost, sumCostLimit uint - selectAddressNext bool -} - -// nodeQueue represents queued requests coming from a single node ID -type nodeQueue struct { - queue []request // always nil if penaltyCost != 0 - id enode.ID - address string - value float64 - flatWeight, valueWeight uint64 // current selection weights in the address/value selectors - sumCost uint // summed cost of requests queued by the node - penaltyCost uint // cumulative cost of dropped requests since last processed request - groupIndex int -} - -// addressGroup is a group of node IDs that have sent their last requests from the same -// network address -type addressGroup struct { - nodes []*nodeQueue - nodeSelect *WeightedRandomSelect - sumFlatWeight, groupWeight uint64 -} - -// request represents an incoming request scheduled for processing -type request struct { - process chan chan struct{} - cost uint -} - -// flatWeight distributes weights equally between each active network address -func flatWeight(item interface{}) uint64 { return item.(*nodeQueue).flatWeight } - -// add adds the node queue to the address group. It is the caller's responsibility to -// add the address group to the address map and the address selector if it wasn't -// there before. -func (ag *addressGroup) add(nq *nodeQueue) { - if nq.groupIndex != -1 { - panic("added node queue is already in an address group") - } - l := len(ag.nodes) - nq.groupIndex = l - ag.nodes = append(ag.nodes, nq) - ag.sumFlatWeight += nq.flatWeight - ag.groupWeight = ag.sumFlatWeight / uint64(l+1) - ag.nodeSelect.Update(ag.nodes[l]) -} - -// update updates the selection weight of the node queue inside the address group. -// It is the caller's responsibility to update the group's selection weight in the -// address selector. -func (ag *addressGroup) update(nq *nodeQueue, weight uint64) { - if nq.groupIndex == -1 || nq.groupIndex >= len(ag.nodes) || ag.nodes[nq.groupIndex] != nq { - panic("updated node queue is not in this address group") - } - ag.sumFlatWeight += weight - nq.flatWeight - nq.flatWeight = weight - ag.groupWeight = ag.sumFlatWeight / uint64(len(ag.nodes)) - ag.nodeSelect.Update(nq) -} - -// remove removes the node queue from the address group. It is the caller's responsibility -// to remove the address group from the address map if it is empty. -func (ag *addressGroup) remove(nq *nodeQueue) { - if nq.groupIndex == -1 || nq.groupIndex >= len(ag.nodes) || ag.nodes[nq.groupIndex] != nq { - panic("removed node queue is not in this address group") - } - - l := len(ag.nodes) - 1 - if nq.groupIndex != l { - ag.nodes[nq.groupIndex] = ag.nodes[l] - ag.nodes[nq.groupIndex].groupIndex = nq.groupIndex - } - nq.groupIndex = -1 - ag.nodes = ag.nodes[:l] - ag.sumFlatWeight -= nq.flatWeight - if l >= 1 { - ag.groupWeight = ag.sumFlatWeight / uint64(l) - } else { - ag.groupWeight = 0 - } - ag.nodeSelect.Remove(nq) -} - -// choose selects one of the node queues belonging to the address group -func (ag *addressGroup) choose() *nodeQueue { - return ag.nodeSelect.Choose().(*nodeQueue) -} - -// NewLimiter creates a new Limiter -func NewLimiter(sumCostLimit uint) *Limiter { - l := &Limiter{ - addressSelect: NewWeightedRandomSelect(func(item interface{}) uint64 { return item.(*addressGroup).groupWeight }), - valueSelect: NewWeightedRandomSelect(func(item interface{}) uint64 { return item.(*nodeQueue).valueWeight }), - nodes: make(map[enode.ID]*nodeQueue), - addresses: make(map[string]*addressGroup), - sumCostLimit: sumCostLimit, - } - l.cond = sync.NewCond(&l.lock) - go l.processLoop() - return l -} - -// selectionWeights calculates the selection weights of a node for both the address and -// the value selector. The selection weight depends on the next request cost or the -// summed cost of recently dropped requests. -func (l *Limiter) selectionWeights(reqCost uint, value float64) (flatWeight, valueWeight uint64) { - if value > l.maxValue { - l.maxValue = value - } - if value > 0 { - // normalize value to <= 1 - value /= l.maxValue - } - if reqCost > l.maxCost { - l.maxCost = reqCost - } - relCost := float64(reqCost) / float64(l.maxCost) - var f float64 - if relCost <= 0.001 { - f = 1 - } else { - f = 0.001 / relCost - } - f *= maxSelectionWeight - flatWeight, valueWeight = uint64(f), uint64(f*value) - if flatWeight == 0 { - flatWeight = 1 - } - return -} - -// Add adds a new request to the node queue belonging to the given id. Value belongs -// to the requesting node. A higher value gives the request a higher chance of being -// served quickly in case of heavy load or a DDoS attack. Cost is a rough estimate -// of the serving cost of the request. A lower cost also gives the request a -// better chance. -func (l *Limiter) Add(id enode.ID, address string, value float64, reqCost uint) chan chan struct{} { - l.lock.Lock() - defer l.lock.Unlock() - - process := make(chan chan struct{}, 1) - if l.quit { - close(process) - return process - } - if reqCost == 0 { - reqCost = 1 - } - if nq, ok := l.nodes[id]; ok { - if nq.queue != nil { - nq.queue = append(nq.queue, request{process, reqCost}) - nq.sumCost += reqCost - nq.value = value - if address != nq.address { - // known id sending request from a new address, move to different address group - l.removeFromGroup(nq) - l.addToGroup(nq, address) - } - } else { - // already waiting on a penalty, just add to the penalty cost and drop the request - nq.penaltyCost += reqCost - l.update(nq) - close(process) - return process - } - } else { - nq := &nodeQueue{ - queue: []request{{process, reqCost}}, - id: id, - value: value, - sumCost: reqCost, - groupIndex: -1, - } - nq.flatWeight, nq.valueWeight = l.selectionWeights(reqCost, value) - if len(l.nodes) == 0 { - l.cond.Signal() - } - l.nodes[id] = nq - if nq.valueWeight != 0 { - l.valueSelect.Update(nq) - } - l.addToGroup(nq, address) - } - l.sumCost += reqCost - if l.sumCost > l.sumCostLimit { - l.dropRequests() - } - return process -} - -// update updates the selection weights of the node queue -func (l *Limiter) update(nq *nodeQueue) { - var cost uint - if nq.queue != nil { - cost = nq.queue[0].cost - } else { - cost = nq.penaltyCost - } - flatWeight, valueWeight := l.selectionWeights(cost, nq.value) - ag := l.addresses[nq.address] - ag.update(nq, flatWeight) - l.addressSelect.Update(ag) - nq.valueWeight = valueWeight - l.valueSelect.Update(nq) -} - -// addToGroup adds the node queue to the given address group. The group is created if -// it does not exist yet. -func (l *Limiter) addToGroup(nq *nodeQueue, address string) { - nq.address = address - ag := l.addresses[address] - if ag == nil { - ag = &addressGroup{nodeSelect: NewWeightedRandomSelect(flatWeight)} - l.addresses[address] = ag - } - ag.add(nq) - l.addressSelect.Update(ag) -} - -// removeFromGroup removes the node queue from its address group -func (l *Limiter) removeFromGroup(nq *nodeQueue) { - ag := l.addresses[nq.address] - ag.remove(nq) - if len(ag.nodes) == 0 { - delete(l.addresses, nq.address) - } - l.addressSelect.Update(ag) -} - -// remove removes the node queue from its address group, the nodes map and the value -// selector -func (l *Limiter) remove(nq *nodeQueue) { - l.removeFromGroup(nq) - if nq.valueWeight != 0 { - l.valueSelect.Remove(nq) - } - delete(l.nodes, nq.id) -} - -// choose selects the next node queue to process. -func (l *Limiter) choose() *nodeQueue { - if l.valueSelect.IsEmpty() || l.selectAddressNext { - if ag, ok := l.addressSelect.Choose().(*addressGroup); ok { - l.selectAddressNext = false - return ag.choose() - } - } - nq, _ := l.valueSelect.Choose().(*nodeQueue) - l.selectAddressNext = true - return nq -} - -// processLoop processes requests sequentially -func (l *Limiter) processLoop() { - l.lock.Lock() - defer l.lock.Unlock() - - for { - if l.quit { - for _, nq := range l.nodes { - for _, request := range nq.queue { - close(request.process) - } - } - return - } - nq := l.choose() - if nq == nil { - l.cond.Wait() - continue - } - if nq.queue != nil { - request := nq.queue[0] - nq.queue = nq.queue[1:] - nq.sumCost -= request.cost - l.sumCost -= request.cost - l.lock.Unlock() - ch := make(chan struct{}) - request.process <- ch - <-ch - l.lock.Lock() - if len(nq.queue) > 0 { - l.update(nq) - } else { - l.remove(nq) - } - } else { - // penalized queue removed, next request will be added to a clean queue - l.remove(nq) - } - } -} - -// Stop stops the processing loop. All queued and future requests are rejected. -func (l *Limiter) Stop() { - l.lock.Lock() - defer l.lock.Unlock() - - l.quit = true - l.cond.Signal() -} - -type dropListItem struct { - nq *nodeQueue - priority float64 -} - -// dropRequests selects the nodes with the highest queued request cost to selection -// weight ratio and drops their queued request. The empty node queues stay in the -// selectors with a low selection weight in order to penalize these nodes. -func (l *Limiter) dropRequests() { - var ( - sumValue float64 - list []dropListItem - ) - for _, nq := range l.nodes { - sumValue += nq.value - } - for _, nq := range l.nodes { - if nq.sumCost == 0 { - continue - } - w := 1 / float64(len(l.addresses)*len(l.addresses[nq.address].nodes)) - if sumValue > 0 { - w += nq.value / sumValue - } - list = append(list, dropListItem{ - nq: nq, - priority: w / float64(nq.sumCost), - }) - } - slices.SortFunc(list, func(a, b dropListItem) int { - if a.priority < b.priority { - return -1 - } - if a.priority < b.priority { - return 1 - } - return 0 - }) - for _, item := range list { - for _, request := range item.nq.queue { - close(request.process) - } - // make the queue penalized; no more requests are accepted until the node is - // selected based on the penalty cost which is the cumulative cost of all dropped - // requests. This ensures that sending excess requests is always penalized - // and incentivizes the sender to stop for a while if no replies are received. - item.nq.queue = nil - item.nq.penaltyCost = item.nq.sumCost - l.sumCost -= item.nq.sumCost // penalty costs are not counted in sumCost - item.nq.sumCost = 0 - l.update(item.nq) - if l.sumCost <= l.sumCostLimit/2 { - return - } - } -} diff --git a/les/utils/limiter_test.go b/les/utils/limiter_test.go deleted file mode 100644 index c031b21de5..0000000000 --- a/les/utils/limiter_test.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2021 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 utils - -import ( - "crypto/rand" - "testing" - - "github.com/ethereum/go-ethereum/p2p/enode" -) - -const ( - ltTolerance = 0.03 - ltRounds = 7 -) - -type ( - ltNode struct { - addr, id int - value, exp float64 - cost uint - reqRate float64 - reqMax, runCount int - lastTotalCost uint - - served, dropped int - } - - ltResult struct { - node *ltNode - ch chan struct{} - } - - limTest struct { - limiter *Limiter - results chan ltResult - runCount int - expCost, totalCost uint - } -) - -func (lt *limTest) request(n *ltNode) { - var ( - address string - id enode.ID - ) - if n.addr >= 0 { - address = string([]byte{byte(n.addr)}) - } else { - var b [32]byte - rand.Read(b[:]) - address = string(b[:]) - } - if n.id >= 0 { - id = enode.ID{byte(n.id)} - } else { - rand.Read(id[:]) - } - lt.runCount++ - n.runCount++ - cch := lt.limiter.Add(id, address, n.value, n.cost) - go func() { - lt.results <- ltResult{n, <-cch} - }() -} - -func (lt *limTest) moreRequests(n *ltNode) { - maxStart := int(float64(lt.totalCost-n.lastTotalCost) * n.reqRate) - if maxStart != 0 { - n.lastTotalCost = lt.totalCost - } - for n.reqMax > n.runCount && maxStart > 0 { - lt.request(n) - maxStart-- - } -} - -func (lt *limTest) process() { - res := <-lt.results - lt.runCount-- - res.node.runCount-- - if res.ch != nil { - res.node.served++ - if res.node.exp != 0 { - lt.expCost += res.node.cost - } - lt.totalCost += res.node.cost - close(res.ch) - } else { - res.node.dropped++ - } -} - -func TestLimiter(t *testing.T) { - limTests := [][]*ltNode{ - { // one id from an individual address and two ids from a shared address - {addr: 0, id: 0, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.5}, - {addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, - {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, - }, - { // varying request costs - {addr: 0, id: 0, value: 0, cost: 10, reqRate: 0.2, reqMax: 1, exp: 0.5}, - {addr: 1, id: 1, value: 0, cost: 3, reqRate: 0.5, reqMax: 1, exp: 0.25}, - {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, - }, - { // different request rate - {addr: 0, id: 0, value: 0, cost: 1, reqRate: 2, reqMax: 2, exp: 0.5}, - {addr: 1, id: 1, value: 0, cost: 1, reqRate: 10, reqMax: 10, exp: 0.25}, - {addr: 1, id: 2, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25}, - }, - { // adding value - {addr: 0, id: 0, value: 3, cost: 1, reqRate: 1, reqMax: 1, exp: (0.5 + 0.3) / 2}, - {addr: 1, id: 1, value: 0, cost: 1, reqRate: 1, reqMax: 1, exp: 0.25 / 2}, - {addr: 1, id: 2, value: 7, cost: 1, reqRate: 1, reqMax: 1, exp: (0.25 + 0.7) / 2}, - }, - { // DoS attack from a single address with a single id - {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 3, id: 3, value: 0, cost: 1, reqRate: 10, reqMax: 1000000000, exp: 0}, - }, - { // DoS attack from a single address with different ids - {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 3, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, - }, - { // DDoS attack from different addresses with a single id - {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: -1, id: 3, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, - }, - { // DDoS attack from different addresses with different ids - {addr: 0, id: 0, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 1, id: 1, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: 2, id: 2, value: 1, cost: 1, reqRate: 1, reqMax: 1, exp: 0.3333}, - {addr: -1, id: -1, value: 0, cost: 1, reqRate: 1, reqMax: 1000000000, exp: 0}, - }, - } - - lt := &limTest{ - limiter: NewLimiter(100), - results: make(chan ltResult), - } - for _, test := range limTests { - lt.expCost, lt.totalCost = 0, 0 - iterCount := 10000 - for j := 0; j < ltRounds; j++ { - // try to reach expected target range in multiple rounds with increasing iteration counts - last := j == ltRounds-1 - for _, n := range test { - lt.request(n) - } - for i := 0; i < iterCount; i++ { - lt.process() - for _, n := range test { - lt.moreRequests(n) - } - } - for lt.runCount > 0 { - lt.process() - } - if spamRatio := 1 - float64(lt.expCost)/float64(lt.totalCost); spamRatio > 0.5*(1+ltTolerance) { - t.Errorf("Spam ratio too high (%f)", spamRatio) - } - fail, success := false, true - for _, n := range test { - if n.exp != 0 { - if n.dropped > 0 { - t.Errorf("Dropped %d requests of non-spam node", n.dropped) - fail = true - } - r := float64(n.served) * float64(n.cost) / float64(lt.expCost) - if r < n.exp*(1-ltTolerance) || r > n.exp*(1+ltTolerance) { - if last { - // print error only if the target is still not reached in the last round - t.Errorf("Request ratio (%f) does not match expected value (%f)", r, n.exp) - } - success = false - } - } - } - if fail || success { - break - } - // neither failed nor succeeded; try more iterations to reach probability targets - iterCount *= 2 - } - } - lt.limiter.Stop() -} diff --git a/les/utils/timeutils.go b/les/utils/timeutils.go deleted file mode 100644 index 62a4285d15..0000000000 --- a/les/utils/timeutils.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2020 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 utils - -import ( - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -type UpdateTimer struct { - clock mclock.Clock - lock sync.Mutex - last mclock.AbsTime - threshold time.Duration -} - -func NewUpdateTimer(clock mclock.Clock, threshold time.Duration) *UpdateTimer { - // We don't accept the update threshold less than 0. - if threshold < 0 { - return nil - } - // Don't panic for lazy users - if clock == nil { - clock = mclock.System{} - } - return &UpdateTimer{ - clock: clock, - last: clock.Now(), - threshold: threshold, - } -} - -func (t *UpdateTimer) Update(callback func(diff time.Duration) bool) bool { - return t.UpdateAt(t.clock.Now(), callback) -} - -func (t *UpdateTimer) UpdateAt(at mclock.AbsTime, callback func(diff time.Duration) bool) bool { - t.lock.Lock() - defer t.lock.Unlock() - - diff := time.Duration(at - t.last) - if diff < 0 { - diff = 0 - } - if diff < t.threshold { - return false - } - if callback(diff) { - t.last = at - return true - } - return false -} diff --git a/les/utils/timeutils_test.go b/les/utils/timeutils_test.go deleted file mode 100644 index b219d0439d..0000000000 --- a/les/utils/timeutils_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2020 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 utils - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -func TestUpdateTimer(t *testing.T) { - timer := NewUpdateTimer(mclock.System{}, -1) - if timer != nil { - t.Fatalf("Create update timer with negative threshold") - } - sim := &mclock.Simulated{} - timer = NewUpdateTimer(sim, time.Second) - if updated := timer.Update(func(diff time.Duration) bool { return true }); updated { - t.Fatalf("Update the clock without reaching the threshold") - } - sim.Run(time.Second) - if updated := timer.Update(func(diff time.Duration) bool { return true }); !updated { - t.Fatalf("Doesn't update the clock when reaching the threshold") - } - if updated := timer.UpdateAt(sim.Now().Add(time.Second), func(diff time.Duration) bool { return true }); !updated { - t.Fatalf("Doesn't update the clock when reaching the threshold") - } - timer = NewUpdateTimer(sim, 0) - if updated := timer.Update(func(diff time.Duration) bool { return true }); !updated { - t.Fatalf("Doesn't update the clock without threshold limitaion") - } -} diff --git a/les/utils/weighted_select.go b/les/utils/weighted_select.go deleted file mode 100644 index 486b00820a..0000000000 --- a/les/utils/weighted_select.go +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2016 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 utils - -import ( - "math" - "math/rand" - - "github.com/ethereum/go-ethereum/log" -) - -type ( - // WeightedRandomSelect is capable of weighted random selection from a set of items - WeightedRandomSelect struct { - root *wrsNode - idx map[WrsItem]int - wfn WeightFn - } - WrsItem interface{} - WeightFn func(interface{}) uint64 -) - -// NewWeightedRandomSelect returns a new WeightedRandomSelect structure -func NewWeightedRandomSelect(wfn WeightFn) *WeightedRandomSelect { - return &WeightedRandomSelect{root: &wrsNode{maxItems: wrsBranches}, idx: make(map[WrsItem]int), wfn: wfn} -} - -// Update updates an item's weight, adds it if it was non-existent or removes it if -// the new weight is zero. Note that explicitly updating decreasing weights is not necessary. -func (w *WeightedRandomSelect) Update(item WrsItem) { - w.setWeight(item, w.wfn(item)) -} - -// Remove removes an item from the set -func (w *WeightedRandomSelect) Remove(item WrsItem) { - w.setWeight(item, 0) -} - -// IsEmpty returns true if the set is empty -func (w *WeightedRandomSelect) IsEmpty() bool { - return w.root.sumCost == 0 -} - -// setWeight sets an item's weight to a specific value (removes it if zero) -func (w *WeightedRandomSelect) setWeight(item WrsItem, weight uint64) { - if weight > math.MaxInt64-w.root.sumCost { - // old weight is still included in sumCost, remove and check again - w.setWeight(item, 0) - if weight > math.MaxInt64-w.root.sumCost { - log.Error("WeightedRandomSelect overflow", "sumCost", w.root.sumCost, "new weight", weight) - weight = math.MaxInt64 - w.root.sumCost - } - } - idx, ok := w.idx[item] - if ok { - w.root.setWeight(idx, weight) - if weight == 0 { - delete(w.idx, item) - } - } else { - if weight != 0 { - if w.root.itemCnt == w.root.maxItems { - // add a new level - newRoot := &wrsNode{sumCost: w.root.sumCost, itemCnt: w.root.itemCnt, level: w.root.level + 1, maxItems: w.root.maxItems * wrsBranches} - newRoot.items[0] = w.root - newRoot.weights[0] = w.root.sumCost - w.root = newRoot - } - w.idx[item] = w.root.insert(item, weight) - } - } -} - -// Choose randomly selects an item from the set, with a chance proportional to its -// current weight. If the weight of the chosen element has been decreased since the -// last stored value, returns it with a newWeight/oldWeight chance, otherwise just -// updates its weight and selects another one -func (w *WeightedRandomSelect) Choose() WrsItem { - for { - if w.root.sumCost == 0 { - return nil - } - val := uint64(rand.Int63n(int64(w.root.sumCost))) - choice, lastWeight := w.root.choose(val) - weight := w.wfn(choice) - if weight != lastWeight { - w.setWeight(choice, weight) - } - if weight >= lastWeight || uint64(rand.Int63n(int64(lastWeight))) < weight { - return choice - } - } -} - -const wrsBranches = 8 // max number of branches in the wrsNode tree - -// wrsNode is a node of a tree structure that can store WrsItems or further wrsNodes. -type wrsNode struct { - items [wrsBranches]interface{} - weights [wrsBranches]uint64 - sumCost uint64 - level, itemCnt, maxItems int -} - -// insert recursively inserts a new item to the tree and returns the item index -func (n *wrsNode) insert(item WrsItem, weight uint64) int { - branch := 0 - for n.items[branch] != nil && (n.level == 0 || n.items[branch].(*wrsNode).itemCnt == n.items[branch].(*wrsNode).maxItems) { - branch++ - if branch == wrsBranches { - panic(nil) - } - } - n.itemCnt++ - n.sumCost += weight - n.weights[branch] += weight - if n.level == 0 { - n.items[branch] = item - return branch - } - var subNode *wrsNode - if n.items[branch] == nil { - subNode = &wrsNode{maxItems: n.maxItems / wrsBranches, level: n.level - 1} - n.items[branch] = subNode - } else { - subNode = n.items[branch].(*wrsNode) - } - subIdx := subNode.insert(item, weight) - return subNode.maxItems*branch + subIdx -} - -// setWeight updates the weight of a certain item (which should exist) and returns -// the change of the last weight value stored in the tree -func (n *wrsNode) setWeight(idx int, weight uint64) uint64 { - if n.level == 0 { - oldWeight := n.weights[idx] - n.weights[idx] = weight - diff := weight - oldWeight - n.sumCost += diff - if weight == 0 { - n.items[idx] = nil - n.itemCnt-- - } - return diff - } - branchItems := n.maxItems / wrsBranches - branch := idx / branchItems - diff := n.items[branch].(*wrsNode).setWeight(idx-branch*branchItems, weight) - n.weights[branch] += diff - n.sumCost += diff - if weight == 0 { - n.itemCnt-- - } - return diff -} - -// choose recursively selects an item from the tree and returns it along with its weight -func (n *wrsNode) choose(val uint64) (WrsItem, uint64) { - for i, w := range n.weights { - if val < w { - if n.level == 0 { - return n.items[i].(WrsItem), n.weights[i] - } - return n.items[i].(*wrsNode).choose(val) - } - val -= w - } - panic(nil) -} diff --git a/les/utils/weighted_select_test.go b/les/utils/weighted_select_test.go deleted file mode 100644 index 3e1c0ad987..0000000000 --- a/les/utils/weighted_select_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2016 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 utils - -import ( - "math/rand" - "testing" -) - -type testWrsItem struct { - idx int - widx *int -} - -func testWeight(i interface{}) uint64 { - t := i.(*testWrsItem) - w := *t.widx - if w == -1 || w == t.idx { - return uint64(t.idx + 1) - } - return 0 -} - -func TestWeightedRandomSelect(t *testing.T) { - testFn := func(cnt int) { - s := NewWeightedRandomSelect(testWeight) - w := -1 - list := make([]testWrsItem, cnt) - for i := range list { - list[i] = testWrsItem{idx: i, widx: &w} - s.Update(&list[i]) - } - w = rand.Intn(cnt) - c := s.Choose() - if c == nil { - t.Errorf("expected item, got nil") - } else { - if c.(*testWrsItem).idx != w { - t.Errorf("expected another item") - } - } - w = -2 - if s.Choose() != nil { - t.Errorf("expected nil, got item") - } - } - testFn(1) - testFn(10) - testFn(100) - testFn(1000) - testFn(10000) - testFn(100000) - testFn(1000000) -} diff --git a/les/vflux/client/api.go b/les/vflux/client/api.go deleted file mode 100644 index 135273ef96..0000000000 --- a/les/vflux/client/api.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2020 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 client - -import ( - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -// PrivateClientAPI implements the vflux client side API -type PrivateClientAPI struct { - vt *ValueTracker -} - -// NewPrivateClientAPI creates a PrivateClientAPI -func NewPrivateClientAPI(vt *ValueTracker) *PrivateClientAPI { - return &PrivateClientAPI{vt} -} - -// parseNodeStr converts either an enode address or a plain hex node id to enode.ID -func parseNodeStr(nodeStr string) (enode.ID, error) { - if id, err := enode.ParseID(nodeStr); err == nil { - return id, nil - } - if node, err := enode.Parse(enode.ValidSchemes, nodeStr); err == nil { - return node.ID(), nil - } else { - return enode.ID{}, err - } -} - -// RequestStats returns the current contents of the reference request basket, with -// request values meaning average per request rather than total. -func (api *PrivateClientAPI) RequestStats() []RequestStatsItem { - return api.vt.RequestStats() -} - -// Distribution returns a distribution as a series of (X, Y) chart coordinates, -// where the X axis is the response time in seconds while the Y axis is the amount of -// service value received with a response time close to the X coordinate. -// The distribution is optionally normalized to a sum of 1. -// If nodeStr == "" then the global distribution is returned, otherwise the individual -// distribution of the specified server node. -func (api *PrivateClientAPI) Distribution(nodeStr string, normalized bool) (RtDistribution, error) { - var expFactor utils.ExpirationFactor - if !normalized { - expFactor = utils.ExpFactor(api.vt.StatsExpirer().LogOffset(mclock.Now())) - } - if nodeStr == "" { - return api.vt.RtStats().Distribution(normalized, expFactor), nil - } - if id, err := parseNodeStr(nodeStr); err == nil { - return api.vt.GetNode(id).RtStats().Distribution(normalized, expFactor), nil - } else { - return RtDistribution{}, err - } -} - -// Timeout suggests a timeout value based on either the global distribution or the -// distribution of the specified node. The parameter is the desired rate of timeouts -// assuming a similar distribution in the future. -// Note that the actual timeout should have a sensible minimum bound so that operating -// under ideal working conditions for a long time (for example, using a local server -// with very low response times) will not make it very hard for the system to accommodate -// longer response times in the future. -func (api *PrivateClientAPI) Timeout(nodeStr string, failRate float64) (float64, error) { - if nodeStr == "" { - return float64(api.vt.RtStats().Timeout(failRate)) / float64(time.Second), nil - } - if id, err := parseNodeStr(nodeStr); err == nil { - return float64(api.vt.GetNode(id).RtStats().Timeout(failRate)) / float64(time.Second), nil - } else { - return 0, err - } -} - -// Value calculates the total service value provided either globally or by the specified -// server node, using a weight function based on the given timeout. -func (api *PrivateClientAPI) Value(nodeStr string, timeout float64) (float64, error) { - wt := TimeoutWeights(time.Duration(timeout * float64(time.Second))) - expFactor := utils.ExpFactor(api.vt.StatsExpirer().LogOffset(mclock.Now())) - if nodeStr == "" { - return api.vt.RtStats().Value(wt, expFactor), nil - } - if id, err := parseNodeStr(nodeStr); err == nil { - return api.vt.GetNode(id).RtStats().Value(wt, expFactor), nil - } else { - return 0, err - } -} diff --git a/les/vflux/client/fillset.go b/les/vflux/client/fillset.go deleted file mode 100644 index 0da850bcac..0000000000 --- a/les/vflux/client/fillset.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2020 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 client - -import ( - "sync" - - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -// FillSet tries to read nodes from an input iterator and add them to a node set by -// setting the specified node state flag(s) until the size of the set reaches the target. -// Note that other mechanisms (like other FillSet instances reading from different inputs) -// can also set the same flag(s) and FillSet will always care about the total number of -// nodes having those flags. -type FillSet struct { - lock sync.Mutex - cond *sync.Cond - ns *nodestate.NodeStateMachine - input enode.Iterator - closed bool - flags nodestate.Flags - count, target int -} - -// NewFillSet creates a new FillSet -func NewFillSet(ns *nodestate.NodeStateMachine, input enode.Iterator, flags nodestate.Flags) *FillSet { - fs := &FillSet{ - ns: ns, - input: input, - flags: flags, - } - fs.cond = sync.NewCond(&fs.lock) - - ns.SubscribeState(flags, func(n *enode.Node, oldState, newState nodestate.Flags) { - fs.lock.Lock() - if oldState.Equals(flags) { - fs.count-- - } - if newState.Equals(flags) { - fs.count++ - } - if fs.target > fs.count { - fs.cond.Signal() - } - fs.lock.Unlock() - }) - - go fs.readLoop() - return fs -} - -// readLoop keeps reading nodes from the input and setting the specified flags for them -// whenever the node set size is under the current target -func (fs *FillSet) readLoop() { - for { - fs.lock.Lock() - for fs.target <= fs.count && !fs.closed { - fs.cond.Wait() - } - - fs.lock.Unlock() - if !fs.input.Next() { - return - } - fs.ns.SetState(fs.input.Node(), fs.flags, nodestate.Flags{}, 0) - } -} - -// SetTarget sets the current target for node set size. If the previous target was not -// reached and FillSet was still waiting for the next node from the input then the next -// incoming node will be added to the set regardless of the target. This ensures that -// all nodes coming from the input are eventually added to the set. -func (fs *FillSet) SetTarget(target int) { - fs.lock.Lock() - defer fs.lock.Unlock() - - fs.target = target - if fs.target > fs.count { - fs.cond.Signal() - } -} - -// Close shuts FillSet down and closes the input iterator -func (fs *FillSet) Close() { - fs.lock.Lock() - defer fs.lock.Unlock() - - fs.closed = true - fs.input.Close() - fs.cond.Signal() -} diff --git a/les/vflux/client/fillset_test.go b/les/vflux/client/fillset_test.go deleted file mode 100644 index 9a5a2a98a8..0000000000 --- a/les/vflux/client/fillset_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2020 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 client - -import ( - "crypto/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -type testIter struct { - waitCh chan struct{} - nodeCh chan *enode.Node - node *enode.Node -} - -func (i *testIter) Next() bool { - if _, ok := <-i.waitCh; !ok { - return false - } - i.node = <-i.nodeCh - return true -} - -func (i *testIter) Node() *enode.Node { - return i.node -} - -func (i *testIter) Close() { - close(i.waitCh) -} - -func (i *testIter) push() { - var id enode.ID - rand.Read(id[:]) - i.nodeCh <- enode.SignNull(new(enr.Record), id) -} - -func (i *testIter) waiting(timeout time.Duration) bool { - select { - case i.waitCh <- struct{}{}: - return true - case <-time.After(timeout): - return false - } -} - -func TestFillSet(t *testing.T) { - t.Parallel() - - ns := nodestate.NewNodeStateMachine(nil, nil, &mclock.Simulated{}, testSetup) - iter := &testIter{ - waitCh: make(chan struct{}), - nodeCh: make(chan *enode.Node), - } - fs := NewFillSet(ns, iter, sfTest1) - ns.Start() - - expWaiting := func(i int, push bool) { - for ; i > 0; i-- { - if !iter.waiting(time.Second * 10) { - t.Fatalf("FillSet not waiting for new nodes") - } - if push { - iter.push() - } - } - } - - expNotWaiting := func() { - if iter.waiting(time.Millisecond * 100) { - t.Fatalf("FillSet unexpectedly waiting for new nodes") - } - } - - expNotWaiting() - fs.SetTarget(3) - expWaiting(3, true) - expNotWaiting() - fs.SetTarget(100) - expWaiting(2, true) - expWaiting(1, false) - // lower the target before the previous one has been filled up - fs.SetTarget(0) - iter.push() - expNotWaiting() - fs.SetTarget(10) - expWaiting(4, true) - expNotWaiting() - // remove all previously set flags - ns.ForEach(sfTest1, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { - ns.SetState(node, nodestate.Flags{}, sfTest1, 0) - }) - // now expect FillSet to fill the set up again with 10 new nodes - expWaiting(10, true) - expNotWaiting() - - fs.Close() - ns.Stop() -} diff --git a/les/vflux/client/queueiterator.go b/les/vflux/client/queueiterator.go deleted file mode 100644 index ad3f8df5bb..0000000000 --- a/les/vflux/client/queueiterator.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2020 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 client - -import ( - "sync" - - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -// QueueIterator returns nodes from the specified selectable set in the same order as -// they entered the set. -type QueueIterator struct { - lock sync.Mutex - cond *sync.Cond - - ns *nodestate.NodeStateMachine - queue []*enode.Node - nextNode *enode.Node - waitCallback func(bool) - fifo, closed bool -} - -// NewQueueIterator creates a new QueueIterator. Nodes are selectable if they have all the required -// and none of the disabled flags set. When a node is selected the selectedFlag is set which also -// disables further selectability until it is removed or times out. -func NewQueueIterator(ns *nodestate.NodeStateMachine, requireFlags, disableFlags nodestate.Flags, fifo bool, waitCallback func(bool)) *QueueIterator { - qi := &QueueIterator{ - ns: ns, - fifo: fifo, - waitCallback: waitCallback, - } - qi.cond = sync.NewCond(&qi.lock) - - ns.SubscribeState(requireFlags.Or(disableFlags), func(n *enode.Node, oldState, newState nodestate.Flags) { - oldMatch := oldState.HasAll(requireFlags) && oldState.HasNone(disableFlags) - newMatch := newState.HasAll(requireFlags) && newState.HasNone(disableFlags) - if newMatch == oldMatch { - return - } - - qi.lock.Lock() - defer qi.lock.Unlock() - - if newMatch { - qi.queue = append(qi.queue, n) - } else { - id := n.ID() - for i, qn := range qi.queue { - if qn.ID() == id { - copy(qi.queue[i:len(qi.queue)-1], qi.queue[i+1:]) - qi.queue = qi.queue[:len(qi.queue)-1] - break - } - } - } - qi.cond.Signal() - }) - return qi -} - -// Next moves to the next selectable node. -func (qi *QueueIterator) Next() bool { - qi.lock.Lock() - if !qi.closed && len(qi.queue) == 0 { - if qi.waitCallback != nil { - qi.waitCallback(true) - } - for !qi.closed && len(qi.queue) == 0 { - qi.cond.Wait() - } - if qi.waitCallback != nil { - qi.waitCallback(false) - } - } - if qi.closed { - qi.nextNode = nil - qi.lock.Unlock() - return false - } - // Move to the next node in queue. - if qi.fifo { - qi.nextNode = qi.queue[0] - copy(qi.queue[:len(qi.queue)-1], qi.queue[1:]) - qi.queue = qi.queue[:len(qi.queue)-1] - } else { - qi.nextNode = qi.queue[len(qi.queue)-1] - qi.queue = qi.queue[:len(qi.queue)-1] - } - qi.lock.Unlock() - return true -} - -// Close ends the iterator. -func (qi *QueueIterator) Close() { - qi.lock.Lock() - qi.closed = true - qi.lock.Unlock() - qi.cond.Signal() -} - -// Node returns the current node. -func (qi *QueueIterator) Node() *enode.Node { - qi.lock.Lock() - defer qi.lock.Unlock() - - return qi.nextNode -} diff --git a/les/vflux/client/queueiterator_test.go b/les/vflux/client/queueiterator_test.go deleted file mode 100644 index c7cb649082..0000000000 --- a/les/vflux/client/queueiterator_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2020 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 client - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -func testNode(i int) *enode.Node { - return enode.SignNull(new(enr.Record), testNodeID(i)) -} - -func TestQueueIteratorFIFO(t *testing.T) { - t.Parallel() - - testQueueIterator(t, true) -} - -func TestQueueIteratorLIFO(t *testing.T) { - t.Parallel() - - testQueueIterator(t, false) -} - -func testQueueIterator(t *testing.T, fifo bool) { - ns := nodestate.NewNodeStateMachine(nil, nil, &mclock.Simulated{}, testSetup) - qi := NewQueueIterator(ns, sfTest2, sfTest3.Or(sfTest4), fifo, nil) - ns.Start() - for i := 1; i <= iterTestNodeCount; i++ { - ns.SetState(testNode(i), sfTest1, nodestate.Flags{}, 0) - } - next := func() int { - ch := make(chan struct{}) - go func() { - qi.Next() - close(ch) - }() - select { - case <-ch: - case <-time.After(time.Second * 5): - t.Fatalf("Iterator.Next() timeout") - } - node := qi.Node() - ns.SetState(node, sfTest4, nodestate.Flags{}, 0) - return testNodeIndex(node.ID()) - } - exp := func(i int) { - n := next() - if n != i { - t.Errorf("Wrong item returned by iterator (expected %d, got %d)", i, n) - } - } - explist := func(list []int) { - for i := range list { - if fifo { - exp(list[i]) - } else { - exp(list[len(list)-1-i]) - } - } - } - - ns.SetState(testNode(1), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(2), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(3), sfTest2, nodestate.Flags{}, 0) - explist([]int{1, 2, 3}) - ns.SetState(testNode(4), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(5), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(6), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(5), sfTest3, nodestate.Flags{}, 0) - explist([]int{4, 6}) - ns.SetState(testNode(1), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(2), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(3), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(2), sfTest3, nodestate.Flags{}, 0) - ns.SetState(testNode(2), nodestate.Flags{}, sfTest3, 0) - explist([]int{1, 3, 2}) - ns.Stop() -} diff --git a/les/vflux/client/requestbasket.go b/les/vflux/client/requestbasket.go deleted file mode 100644 index 55d4b165df..0000000000 --- a/les/vflux/client/requestbasket.go +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2020 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 client - -import ( - "io" - - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/rlp" -) - -const basketFactor = 1000000 // reference basket amount and value scale factor - -// referenceBasket keeps track of global request usage statistics and the usual prices -// of each used request type relative to each other. The amounts in the basket are scaled -// up by basketFactor because of the exponential expiration of long-term statistical data. -// Values are scaled so that the sum of all amounts and the sum of all values are equal. -// -// reqValues represent the internal relative value estimates for each request type and are -// calculated as value / amount. The average reqValue of all used requests is 1. -// In other words: SUM(refBasket[type].amount * reqValue[type]) = SUM(refBasket[type].amount) -type referenceBasket struct { - basket requestBasket - reqValues []float64 // contents are read only, new slice is created for each update -} - -// serverBasket collects served request amount and value statistics for a single server. -// -// Values are gradually transferred to the global reference basket with a long time -// constant so that each server basket represents long term usage and price statistics. -// When the transferred part is added to the reference basket the values are scaled so -// that their sum equals the total value calculated according to the previous reqValues. -// The ratio of request values coming from the server basket represent the pricing of -// the specific server and modify the global estimates with a weight proportional to -// the amount of service provided by the server. -type serverBasket struct { - basket requestBasket - rvFactor float64 -} - -type ( - // requestBasket holds amounts and values for each request type. - // These values are exponentially expired (see utils.ExpiredValue). The power of 2 - // exponent is applicable to all values within. - requestBasket struct { - items []basketItem - exp uint64 - } - // basketItem holds amount and value for a single request type. Value is the total - // relative request value accumulated for served requests while amount is the counter - // for each request type. - // Note that these values are both scaled up by basketFactor because of the exponential - // expiration. - basketItem struct { - amount, value uint64 - } -) - -// setExp sets the power of 2 exponent of the structure, scaling base values (the amounts -// and request values) up or down if necessary. -func (b *requestBasket) setExp(exp uint64) { - if exp > b.exp { - shift := exp - b.exp - for i, item := range b.items { - item.amount >>= shift - item.value >>= shift - b.items[i] = item - } - b.exp = exp - } - if exp < b.exp { - shift := b.exp - exp - for i, item := range b.items { - item.amount <<= shift - item.value <<= shift - b.items[i] = item - } - b.exp = exp - } -} - -// init initializes a new server basket with the given service vector size (number of -// different request types) -func (s *serverBasket) init(size int) { - if s.basket.items == nil { - s.basket.items = make([]basketItem, size) - } -} - -// add adds the give type and amount of requests to the basket. Cost is calculated -// according to the server's own cost table. -func (s *serverBasket) add(reqType, reqAmount uint32, reqCost uint64, expFactor utils.ExpirationFactor) { - s.basket.setExp(expFactor.Exp) - i := &s.basket.items[reqType] - i.amount += uint64(float64(uint64(reqAmount)*basketFactor) * expFactor.Factor) - i.value += uint64(float64(reqCost) * s.rvFactor * expFactor.Factor) -} - -// updateRvFactor updates the request value factor that scales server costs into the -// local value dimensions. -func (s *serverBasket) updateRvFactor(rvFactor float64) { - s.rvFactor = rvFactor -} - -// transfer decreases amounts and values in the basket with the given ratio and -// moves the removed amounts into a new basket which is returned and can be added -// to the global reference basket. -func (s *serverBasket) transfer(ratio float64) requestBasket { - res := requestBasket{ - items: make([]basketItem, len(s.basket.items)), - exp: s.basket.exp, - } - for i, v := range s.basket.items { - ta := uint64(float64(v.amount) * ratio) - tv := uint64(float64(v.value) * ratio) - if ta > v.amount { - ta = v.amount - } - if tv > v.value { - tv = v.value - } - s.basket.items[i] = basketItem{v.amount - ta, v.value - tv} - res.items[i] = basketItem{ta, tv} - } - return res -} - -// init initializes the reference basket with the given service vector size (number of -// different request types) -func (r *referenceBasket) init(size int) { - r.reqValues = make([]float64, size) - r.normalize() - r.updateReqValues() -} - -// add adds the transferred part of a server basket to the reference basket while scaling -// value amounts so that their sum equals the total value calculated according to the -// previous reqValues. -func (r *referenceBasket) add(newBasket requestBasket) { - r.basket.setExp(newBasket.exp) - // scale newBasket to match service unit value - var ( - totalCost uint64 - totalValue float64 - ) - for i, v := range newBasket.items { - totalCost += v.value - totalValue += float64(v.amount) * r.reqValues[i] - } - if totalCost > 0 { - // add to reference with scaled values - scaleValues := totalValue / float64(totalCost) - for i, v := range newBasket.items { - r.basket.items[i].amount += v.amount - r.basket.items[i].value += uint64(float64(v.value) * scaleValues) - } - } - r.updateReqValues() -} - -// updateReqValues recalculates reqValues after adding transferred baskets. Note that -// values should be normalized first. -func (r *referenceBasket) updateReqValues() { - r.reqValues = make([]float64, len(r.reqValues)) - for i, b := range r.basket.items { - if b.amount > 0 { - r.reqValues[i] = float64(b.value) / float64(b.amount) - } else { - r.reqValues[i] = 0 - } - } -} - -// normalize ensures that the sum of values equal the sum of amounts in the basket. -func (r *referenceBasket) normalize() { - var sumAmount, sumValue uint64 - for _, b := range r.basket.items { - sumAmount += b.amount - sumValue += b.value - } - add := float64(int64(sumAmount-sumValue)) / float64(sumValue) - for i, b := range r.basket.items { - b.value += uint64(int64(float64(b.value) * add)) - r.basket.items[i] = b - } -} - -// reqValueFactor calculates the request value factor applicable to the server with -// the given announced request cost list -func (r *referenceBasket) reqValueFactor(costList []uint64) float64 { - var ( - totalCost float64 - totalValue uint64 - ) - for i, b := range r.basket.items { - totalCost += float64(costList[i]) * float64(b.amount) // use floats to avoid overflow - totalValue += b.value - } - if totalCost < 1 { - return 0 - } - return float64(totalValue) * basketFactor / totalCost -} - -// EncodeRLP implements rlp.Encoder -func (b *basketItem) EncodeRLP(w io.Writer) error { - return rlp.Encode(w, []interface{}{b.amount, b.value}) -} - -// DecodeRLP implements rlp.Decoder -func (b *basketItem) DecodeRLP(s *rlp.Stream) error { - var item struct { - Amount, Value uint64 - } - if err := s.Decode(&item); err != nil { - return err - } - b.amount, b.value = item.Amount, item.Value - return nil -} - -// EncodeRLP implements rlp.Encoder -func (r *requestBasket) EncodeRLP(w io.Writer) error { - return rlp.Encode(w, []interface{}{r.items, r.exp}) -} - -// DecodeRLP implements rlp.Decoder -func (r *requestBasket) DecodeRLP(s *rlp.Stream) error { - var enc struct { - Items []basketItem - Exp uint64 - } - if err := s.Decode(&enc); err != nil { - return err - } - r.items, r.exp = enc.Items, enc.Exp - return nil -} - -// convertMapping converts a basket loaded from the database into the current format. -// If the available request types and their mapping into the service vector differ from -// the one used when saving the basket then this function reorders old fields and fills -// in previously unknown fields by scaling up amounts and values taken from the -// initialization basket. -func (r requestBasket) convertMapping(oldMapping, newMapping []string, initBasket requestBasket) requestBasket { - nameMap := make(map[string]int) - for i, name := range oldMapping { - nameMap[name] = i - } - rc := requestBasket{items: make([]basketItem, len(newMapping))} - var scale, oldScale, newScale float64 - for i, name := range newMapping { - if ii, ok := nameMap[name]; ok { - rc.items[i] = r.items[ii] - oldScale += float64(initBasket.items[i].amount) * float64(initBasket.items[i].amount) - newScale += float64(rc.items[i].amount) * float64(initBasket.items[i].amount) - } - } - if oldScale > 1e-10 { - scale = newScale / oldScale - } else { - scale = 1 - } - for i, name := range newMapping { - if _, ok := nameMap[name]; !ok { - rc.items[i].amount = uint64(float64(initBasket.items[i].amount) * scale) - rc.items[i].value = uint64(float64(initBasket.items[i].value) * scale) - } - } - return rc -} diff --git a/les/vflux/client/requestbasket_test.go b/les/vflux/client/requestbasket_test.go deleted file mode 100644 index 320d1b4b3e..0000000000 --- a/les/vflux/client/requestbasket_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2020 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 client - -import ( - "math/rand" - "testing" - - "github.com/ethereum/go-ethereum/les/utils" -) - -func checkU64(t *testing.T, name string, value, exp uint64) { - if value != exp { - t.Errorf("Incorrect value for %s: got %d, expected %d", name, value, exp) - } -} - -func checkF64(t *testing.T, name string, value, exp, tol float64) { - if value < exp-tol || value > exp+tol { - t.Errorf("Incorrect value for %s: got %f, expected %f", name, value, exp) - } -} - -func TestServerBasket(t *testing.T) { - t.Parallel() - - var s serverBasket - s.init(2) - // add some requests with different request value factors - s.updateRvFactor(1) - noexp := utils.ExpirationFactor{Factor: 1} - s.add(0, 1000, 10000, noexp) - s.add(1, 3000, 60000, noexp) - s.updateRvFactor(10) - s.add(0, 4000, 4000, noexp) - s.add(1, 2000, 4000, noexp) - s.updateRvFactor(10) - // check basket contents directly - checkU64(t, "s.basket[0].amount", s.basket.items[0].amount, 5000*basketFactor) - checkU64(t, "s.basket[0].value", s.basket.items[0].value, 50000) - checkU64(t, "s.basket[1].amount", s.basket.items[1].amount, 5000*basketFactor) - checkU64(t, "s.basket[1].value", s.basket.items[1].value, 100000) - // transfer 50% of the contents of the basket - transfer1 := s.transfer(0.5) - checkU64(t, "transfer1[0].amount", transfer1.items[0].amount, 2500*basketFactor) - checkU64(t, "transfer1[0].value", transfer1.items[0].value, 25000) - checkU64(t, "transfer1[1].amount", transfer1.items[1].amount, 2500*basketFactor) - checkU64(t, "transfer1[1].value", transfer1.items[1].value, 50000) - // add more requests - s.updateRvFactor(100) - s.add(0, 1000, 100, noexp) - // transfer 25% of the contents of the basket - transfer2 := s.transfer(0.25) - checkU64(t, "transfer2[0].amount", transfer2.items[0].amount, (2500+1000)/4*basketFactor) - checkU64(t, "transfer2[0].value", transfer2.items[0].value, (25000+10000)/4) - checkU64(t, "transfer2[1].amount", transfer2.items[1].amount, 2500/4*basketFactor) - checkU64(t, "transfer2[1].value", transfer2.items[1].value, 50000/4) -} - -func TestConvertMapping(t *testing.T) { - t.Parallel() - - b := requestBasket{items: []basketItem{{3, 3}, {1, 1}, {2, 2}}} - oldMap := []string{"req3", "req1", "req2"} - newMap := []string{"req1", "req2", "req3", "req4"} - init := requestBasket{items: []basketItem{{2, 2}, {4, 4}, {6, 6}, {8, 8}}} - bc := b.convertMapping(oldMap, newMap, init) - checkU64(t, "bc[0].amount", bc.items[0].amount, 1) - checkU64(t, "bc[1].amount", bc.items[1].amount, 2) - checkU64(t, "bc[2].amount", bc.items[2].amount, 3) - checkU64(t, "bc[3].amount", bc.items[3].amount, 4) // 8 should be scaled down to 4 -} - -func TestReqValueFactor(t *testing.T) { - t.Parallel() - - var ref referenceBasket - ref.basket = requestBasket{items: make([]basketItem, 4)} - for i := range ref.basket.items { - ref.basket.items[i].amount = uint64(i+1) * basketFactor - ref.basket.items[i].value = uint64(i+1) * basketFactor - } - ref.init(4) - rvf := ref.reqValueFactor([]uint64{1000, 2000, 3000, 4000}) - // expected value is (1000000+2000000+3000000+4000000) / (1*1000+2*2000+3*3000+4*4000) = 10000000/30000 = 333.333 - checkF64(t, "reqValueFactor", rvf, 333.333, 1) -} - -func TestNormalize(t *testing.T) { - t.Parallel() - - for cycle := 0; cycle < 100; cycle += 1 { - // Initialize data for testing - valueRange, lower := 1000000, 1000000 - ref := referenceBasket{basket: requestBasket{items: make([]basketItem, 10)}} - for i := 0; i < 10; i++ { - ref.basket.items[i].amount = uint64(rand.Intn(valueRange) + lower) - ref.basket.items[i].value = uint64(rand.Intn(valueRange) + lower) - } - ref.normalize() - - // Check whether SUM(amount) ~= SUM(value) - var sumAmount, sumValue uint64 - for i := 0; i < 10; i++ { - sumAmount += ref.basket.items[i].amount - sumValue += ref.basket.items[i].value - } - var epsilon = 0.01 - if float64(sumAmount)*(1+epsilon) < float64(sumValue) || float64(sumAmount)*(1-epsilon) > float64(sumValue) { - t.Fatalf("Failed to normalize sumAmount: %d sumValue: %d", sumAmount, sumValue) - } - } -} - -func TestReqValueAdjustment(t *testing.T) { - t.Parallel() - - var s1, s2 serverBasket - s1.init(3) - s2.init(3) - cost1 := []uint64{30000, 60000, 90000} - cost2 := []uint64{100000, 200000, 300000} - var ref referenceBasket - ref.basket = requestBasket{items: make([]basketItem, 3)} - for i := range ref.basket.items { - ref.basket.items[i].amount = 123 * basketFactor - ref.basket.items[i].value = 123 * basketFactor - } - ref.init(3) - // initial reqValues are expected to be {1, 1, 1} - checkF64(t, "reqValues[0]", ref.reqValues[0], 1, 0.01) - checkF64(t, "reqValues[1]", ref.reqValues[1], 1, 0.01) - checkF64(t, "reqValues[2]", ref.reqValues[2], 1, 0.01) - var logOffset utils.Fixed64 - for period := 0; period < 1000; period++ { - exp := utils.ExpFactor(logOffset) - s1.updateRvFactor(ref.reqValueFactor(cost1)) - s2.updateRvFactor(ref.reqValueFactor(cost2)) - // throw in random requests into each basket using their internal pricing - for i := 0; i < 1000; i++ { - reqType, reqAmount := uint32(rand.Intn(3)), uint32(rand.Intn(10)+1) - reqCost := uint64(reqAmount) * cost1[reqType] - s1.add(reqType, reqAmount, reqCost, exp) - reqType, reqAmount = uint32(rand.Intn(3)), uint32(rand.Intn(10)+1) - reqCost = uint64(reqAmount) * cost2[reqType] - s2.add(reqType, reqAmount, reqCost, exp) - } - ref.add(s1.transfer(0.1)) - ref.add(s2.transfer(0.1)) - ref.normalize() - ref.updateReqValues() - logOffset += utils.Float64ToFixed64(0.1) - } - checkF64(t, "reqValues[0]", ref.reqValues[0], 0.5, 0.01) - checkF64(t, "reqValues[1]", ref.reqValues[1], 1, 0.01) - checkF64(t, "reqValues[2]", ref.reqValues[2], 1.5, 0.01) -} diff --git a/les/vflux/client/serverpool.go b/les/vflux/client/serverpool.go deleted file mode 100644 index 271d6e0224..0000000000 --- a/les/vflux/client/serverpool.go +++ /dev/null @@ -1,605 +0,0 @@ -// Copyright 2021 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 client - -import ( - "errors" - "math/rand" - "reflect" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - minTimeout = time.Millisecond * 500 // minimum request timeout suggested by the server pool - timeoutRefresh = time.Second * 5 // recalculate timeout if older than this - dialCost = 10000 // cost of a TCP dial (used for known node selection weight calculation) - dialWaitStep = 1.5 // exponential multiplier of redial wait time when no value was provided by the server - queryCost = 500 // cost of a UDP pre-negotiation query - queryWaitStep = 1.02 // exponential multiplier of redial wait time when no value was provided by the server - waitThreshold = time.Hour * 2000 // drop node if waiting time is over the threshold - nodeWeightMul = 1000000 // multiplier constant for node weight calculation - nodeWeightThreshold = 100 // minimum weight for keeping a node in the known (valuable) set - minRedialWait = 10 // minimum redial wait time in seconds - preNegLimit = 5 // maximum number of simultaneous pre-negotiation queries - warnQueryFails = 20 // number of consecutive UDP query failures before we print a warning - maxQueryFails = 100 // number of consecutive UDP query failures when then chance of skipping a query reaches 50% -) - -// ServerPool provides a node iterator for dial candidates. The output is a mix of newly discovered -// nodes, a weighted random selection of known (previously valuable) nodes and trusted/paid nodes. -type ServerPool struct { - clock mclock.Clock - unixTime func() int64 - db ethdb.KeyValueStore - - ns *nodestate.NodeStateMachine - vt *ValueTracker - mixer *enode.FairMix - mixSources []enode.Iterator - dialIterator enode.Iterator - validSchemes enr.IdentityScheme - trustedURLs []string - fillSet *FillSet - started, queryFails uint32 - - timeoutLock sync.RWMutex - timeout time.Duration - timeWeights ResponseTimeWeights - timeoutRefreshed mclock.AbsTime - - suggestedTimeoutGauge, totalValueGauge metrics.Gauge - sessionValueMeter metrics.Meter -} - -// nodeHistory keeps track of dial costs which determine node weight together with the -// service value calculated by ValueTracker. -type nodeHistory struct { - dialCost utils.ExpiredValue - redialWaitStart, redialWaitEnd int64 // unix time (seconds) -} - -type nodeHistoryEnc struct { - DialCost utils.ExpiredValue - RedialWaitStart, RedialWaitEnd uint64 -} - -// QueryFunc sends a pre-negotiation query and blocks until a response arrives or timeout occurs. -// It returns 1 if the remote node has confirmed that connection is possible, 0 if not -// possible and -1 if no response arrived (timeout). -type QueryFunc func(*enode.Node) int - -var ( - clientSetup = &nodestate.Setup{Version: 2} - sfHasValue = clientSetup.NewPersistentFlag("hasValue") - sfQuery = clientSetup.NewFlag("query") - sfCanDial = clientSetup.NewFlag("canDial") - sfDialing = clientSetup.NewFlag("dialed") - sfWaitDialTimeout = clientSetup.NewFlag("dialTimeout") - sfConnected = clientSetup.NewFlag("connected") - sfRedialWait = clientSetup.NewFlag("redialWait") - sfAlwaysConnect = clientSetup.NewFlag("alwaysConnect") - sfDialProcess = nodestate.MergeFlags(sfQuery, sfCanDial, sfDialing, sfConnected, sfRedialWait) - - sfiNodeHistory = clientSetup.NewPersistentField("nodeHistory", reflect.TypeOf(nodeHistory{}), - func(field interface{}) ([]byte, error) { - if n, ok := field.(nodeHistory); ok { - ne := nodeHistoryEnc{ - DialCost: n.dialCost, - RedialWaitStart: uint64(n.redialWaitStart), - RedialWaitEnd: uint64(n.redialWaitEnd), - } - enc, err := rlp.EncodeToBytes(&ne) - return enc, err - } - return nil, errors.New("invalid field type") - }, - func(enc []byte) (interface{}, error) { - var ne nodeHistoryEnc - err := rlp.DecodeBytes(enc, &ne) - n := nodeHistory{ - dialCost: ne.DialCost, - redialWaitStart: int64(ne.RedialWaitStart), - redialWaitEnd: int64(ne.RedialWaitEnd), - } - return n, err - }, - ) - sfiNodeWeight = clientSetup.NewField("nodeWeight", reflect.TypeOf(uint64(0))) - sfiConnectedStats = clientSetup.NewField("connectedStats", reflect.TypeOf(ResponseTimeStats{})) - sfiLocalAddress = clientSetup.NewPersistentField("localAddress", reflect.TypeOf(&enr.Record{}), - func(field interface{}) ([]byte, error) { - if enr, ok := field.(*enr.Record); ok { - enc, err := rlp.EncodeToBytes(enr) - return enc, err - } - return nil, errors.New("invalid field type") - }, - func(enc []byte) (interface{}, error) { - var enr enr.Record - if err := rlp.DecodeBytes(enc, &enr); err != nil { - return nil, err - } - return &enr, nil - }, - ) -) - -// NewServerPool creates a new server pool -func NewServerPool(db ethdb.KeyValueStore, dbKey []byte, mixTimeout time.Duration, query QueryFunc, clock mclock.Clock, trustedURLs []string, requestList []RequestInfo) (*ServerPool, enode.Iterator) { - s := &ServerPool{ - db: db, - clock: clock, - unixTime: func() int64 { return time.Now().Unix() }, - validSchemes: enode.ValidSchemes, - trustedURLs: trustedURLs, - vt: NewValueTracker(db, &mclock.System{}, requestList, time.Minute, 1/float64(time.Hour), 1/float64(time.Hour*100), 1/float64(time.Hour*1000)), - ns: nodestate.NewNodeStateMachine(db, []byte(string(dbKey)+"ns:"), clock, clientSetup), - } - s.recalTimeout() - s.mixer = enode.NewFairMix(mixTimeout) - knownSelector := NewWrsIterator(s.ns, sfHasValue, sfDialProcess, sfiNodeWeight) - alwaysConnect := NewQueueIterator(s.ns, sfAlwaysConnect, sfDialProcess, true, nil) - s.mixSources = append(s.mixSources, knownSelector) - s.mixSources = append(s.mixSources, alwaysConnect) - - s.dialIterator = s.mixer - if query != nil { - s.dialIterator = s.addPreNegFilter(s.dialIterator, query) - } - - s.ns.SubscribeState(nodestate.MergeFlags(sfWaitDialTimeout, sfConnected), func(n *enode.Node, oldState, newState nodestate.Flags) { - if oldState.Equals(sfWaitDialTimeout) && newState.IsEmpty() { - // dial timeout, no connection - s.setRedialWait(n, dialCost, dialWaitStep) - s.ns.SetStateSub(n, nodestate.Flags{}, sfDialing, 0) - } - }) - - return s, &serverPoolIterator{ - dialIterator: s.dialIterator, - nextFn: func(node *enode.Node) { - s.ns.Operation(func() { - s.ns.SetStateSub(node, sfDialing, sfCanDial, 0) - s.ns.SetStateSub(node, sfWaitDialTimeout, nodestate.Flags{}, time.Second*10) - }) - }, - nodeFn: s.DialNode, - } -} - -type serverPoolIterator struct { - dialIterator enode.Iterator - nextFn func(*enode.Node) - nodeFn func(*enode.Node) *enode.Node -} - -// Next implements enode.Iterator -func (s *serverPoolIterator) Next() bool { - if s.dialIterator.Next() { - s.nextFn(s.dialIterator.Node()) - return true - } - return false -} - -// Node implements enode.Iterator -func (s *serverPoolIterator) Node() *enode.Node { - return s.nodeFn(s.dialIterator.Node()) -} - -// Close implements enode.Iterator -func (s *serverPoolIterator) Close() { - s.dialIterator.Close() -} - -// AddMetrics adds metrics to the server pool. Should be called before Start(). -func (s *ServerPool) AddMetrics( - suggestedTimeoutGauge, totalValueGauge, serverSelectableGauge, serverConnectedGauge metrics.Gauge, - sessionValueMeter, serverDialedMeter metrics.Meter) { - s.suggestedTimeoutGauge = suggestedTimeoutGauge - s.totalValueGauge = totalValueGauge - s.sessionValueMeter = sessionValueMeter - if serverSelectableGauge != nil { - s.ns.AddLogMetrics(sfHasValue, sfDialProcess, "selectable", nil, nil, serverSelectableGauge) - } - if serverDialedMeter != nil { - s.ns.AddLogMetrics(sfDialing, nodestate.Flags{}, "dialed", serverDialedMeter, nil, nil) - } - if serverConnectedGauge != nil { - s.ns.AddLogMetrics(sfConnected, nodestate.Flags{}, "connected", nil, nil, serverConnectedGauge) - } -} - -// AddSource adds a node discovery source to the server pool (should be called before start) -func (s *ServerPool) AddSource(source enode.Iterator) { - if source != nil { - s.mixSources = append(s.mixSources, source) - } -} - -// addPreNegFilter installs a node filter mechanism that performs a pre-negotiation query. -// Nodes that are filtered out and does not appear on the output iterator are put back -// into redialWait state. -func (s *ServerPool) addPreNegFilter(input enode.Iterator, query QueryFunc) enode.Iterator { - s.fillSet = NewFillSet(s.ns, input, sfQuery) - s.ns.SubscribeState(sfDialProcess, func(n *enode.Node, oldState, newState nodestate.Flags) { - if !newState.Equals(sfQuery) { - if newState.HasAll(sfQuery) { - // remove query flag if the node is already somewhere in the dial process - s.ns.SetStateSub(n, nodestate.Flags{}, sfQuery, 0) - } - return - } - fails := atomic.LoadUint32(&s.queryFails) - failMax := fails - if failMax > maxQueryFails { - failMax = maxQueryFails - } - if rand.Intn(maxQueryFails*2) < int(failMax) { - // skip pre-negotiation with increasing chance, max 50% - // this ensures that the client can operate even if UDP is not working at all - s.ns.SetStateSub(n, sfCanDial, nodestate.Flags{}, time.Second*10) - // set canDial before resetting queried so that FillSet will not read more - // candidates unnecessarily - s.ns.SetStateSub(n, nodestate.Flags{}, sfQuery, 0) - return - } - go func() { - q := query(n) - if q == -1 { - atomic.AddUint32(&s.queryFails, 1) - fails++ - if fails%warnQueryFails == 0 { - // warn if a large number of consecutive queries have failed - log.Warn("UDP connection queries failed", "count", fails) - } - } else { - atomic.StoreUint32(&s.queryFails, 0) - } - s.ns.Operation(func() { - // we are no longer running in the operation that the callback belongs to, start a new one because of setRedialWait - if q == 1 { - s.ns.SetStateSub(n, sfCanDial, nodestate.Flags{}, time.Second*10) - } else { - s.setRedialWait(n, queryCost, queryWaitStep) - } - s.ns.SetStateSub(n, nodestate.Flags{}, sfQuery, 0) - }) - }() - }) - return NewQueueIterator(s.ns, sfCanDial, nodestate.Flags{}, false, func(waiting bool) { - if waiting { - s.fillSet.SetTarget(preNegLimit) - } else { - s.fillSet.SetTarget(0) - } - }) -} - -// Start starts the server pool. Note that NodeStateMachine should be started first. -func (s *ServerPool) Start() { - s.ns.Start() - for _, iter := range s.mixSources { - // add sources to mixer at startup because the mixer instantly tries to read them - // which should only happen after NodeStateMachine has been started - s.mixer.AddSource(iter) - } - for _, url := range s.trustedURLs { - if node, err := enode.Parse(s.validSchemes, url); err == nil { - s.ns.SetState(node, sfAlwaysConnect, nodestate.Flags{}, 0) - } else { - log.Error("Invalid trusted server URL", "url", url, "error", err) - } - } - unixTime := s.unixTime() - s.ns.Operation(func() { - s.ns.ForEach(sfHasValue, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { - s.calculateWeight(node) - if n, ok := s.ns.GetField(node, sfiNodeHistory).(nodeHistory); ok && n.redialWaitEnd > unixTime { - wait := n.redialWaitEnd - unixTime - lastWait := n.redialWaitEnd - n.redialWaitStart - if wait > lastWait { - // if the time until expiration is larger than the last suggested - // waiting time then the system clock was probably adjusted - wait = lastWait - } - s.ns.SetStateSub(node, sfRedialWait, nodestate.Flags{}, time.Duration(wait)*time.Second) - } - }) - }) - atomic.StoreUint32(&s.started, 1) -} - -// Stop stops the server pool -func (s *ServerPool) Stop() { - if s.fillSet != nil { - s.fillSet.Close() - } - s.ns.Operation(func() { - s.ns.ForEach(sfConnected, nodestate.Flags{}, func(n *enode.Node, state nodestate.Flags) { - // recalculate weight of connected nodes in order to update hasValue flag if necessary - s.calculateWeight(n) - }) - }) - s.ns.Stop() - s.vt.Stop() -} - -// RegisterNode implements serverPeerSubscriber -func (s *ServerPool) RegisterNode(node *enode.Node) (*NodeValueTracker, error) { - if atomic.LoadUint32(&s.started) == 0 { - return nil, errors.New("server pool not started yet") - } - nvt := s.vt.Register(node.ID()) - s.ns.Operation(func() { - s.ns.SetStateSub(node, sfConnected, sfDialing.Or(sfWaitDialTimeout), 0) - s.ns.SetFieldSub(node, sfiConnectedStats, nvt.RtStats()) - if node.IP().IsLoopback() { - s.ns.SetFieldSub(node, sfiLocalAddress, node.Record()) - } - }) - return nvt, nil -} - -// UnregisterNode implements serverPeerSubscriber -func (s *ServerPool) UnregisterNode(node *enode.Node) { - s.ns.Operation(func() { - s.setRedialWait(node, dialCost, dialWaitStep) - s.ns.SetStateSub(node, nodestate.Flags{}, sfConnected, 0) - s.ns.SetFieldSub(node, sfiConnectedStats, nil) - }) - s.vt.Unregister(node.ID()) -} - -// recalTimeout calculates the current recommended timeout. This value is used by -// the client as a "soft timeout" value. It also affects the service value calculation -// of individual nodes. -func (s *ServerPool) recalTimeout() { - // Use cached result if possible, avoid recalculating too frequently. - s.timeoutLock.RLock() - refreshed := s.timeoutRefreshed - s.timeoutLock.RUnlock() - now := s.clock.Now() - if refreshed != 0 && time.Duration(now-refreshed) < timeoutRefresh { - return - } - // Cached result is stale, recalculate a new one. - rts := s.vt.RtStats() - - // Add a fake statistic here. It is an easy way to initialize with some - // conservative values when the database is new. As soon as we have a - // considerable amount of real stats this small value won't matter. - rts.Add(time.Second*2, 10, s.vt.StatsExpFactor()) - - // Use either 10% failure rate timeout or twice the median response time - // as the recommended timeout. - timeout := minTimeout - if t := rts.Timeout(0.1); t > timeout { - timeout = t - } - if t := rts.Timeout(0.5) * 2; t > timeout { - timeout = t - } - s.timeoutLock.Lock() - if s.timeout != timeout { - s.timeout = timeout - s.timeWeights = TimeoutWeights(s.timeout) - - if s.suggestedTimeoutGauge != nil { - s.suggestedTimeoutGauge.Update(int64(s.timeout / time.Millisecond)) - } - if s.totalValueGauge != nil { - s.totalValueGauge.Update(int64(rts.Value(s.timeWeights, s.vt.StatsExpFactor()))) - } - } - s.timeoutRefreshed = now - s.timeoutLock.Unlock() -} - -// GetTimeout returns the recommended request timeout. -func (s *ServerPool) GetTimeout() time.Duration { - s.recalTimeout() - s.timeoutLock.RLock() - defer s.timeoutLock.RUnlock() - return s.timeout -} - -// getTimeoutAndWeight returns the recommended request timeout as well as the -// response time weight which is necessary to calculate service value. -func (s *ServerPool) getTimeoutAndWeight() (time.Duration, ResponseTimeWeights) { - s.recalTimeout() - s.timeoutLock.RLock() - defer s.timeoutLock.RUnlock() - return s.timeout, s.timeWeights -} - -// addDialCost adds the given amount of dial cost to the node history and returns the current -// amount of total dial cost -func (s *ServerPool) addDialCost(n *nodeHistory, amount int64) uint64 { - logOffset := s.vt.StatsExpirer().LogOffset(s.clock.Now()) - if amount > 0 { - n.dialCost.Add(amount, logOffset) - } - totalDialCost := n.dialCost.Value(logOffset) - if totalDialCost < dialCost { - totalDialCost = dialCost - } - return totalDialCost -} - -// serviceValue returns the service value accumulated in this session and in total -func (s *ServerPool) serviceValue(node *enode.Node) (sessionValue, totalValue float64) { - nvt := s.vt.GetNode(node.ID()) - if nvt == nil { - return 0, 0 - } - currentStats := nvt.RtStats() - _, timeWeights := s.getTimeoutAndWeight() - expFactor := s.vt.StatsExpFactor() - - totalValue = currentStats.Value(timeWeights, expFactor) - if connStats, ok := s.ns.GetField(node, sfiConnectedStats).(ResponseTimeStats); ok { - diff := currentStats - diff.SubStats(&connStats) - sessionValue = diff.Value(timeWeights, expFactor) - if s.sessionValueMeter != nil { - s.sessionValueMeter.Mark(int64(sessionValue)) - } - } - return -} - -// updateWeight calculates the node weight and updates the nodeWeight field and the -// hasValue flag. It also saves the node state if necessary. -// Note: this function should run inside a NodeStateMachine operation -func (s *ServerPool) updateWeight(node *enode.Node, totalValue float64, totalDialCost uint64) { - weight := uint64(totalValue * nodeWeightMul / float64(totalDialCost)) - if weight >= nodeWeightThreshold { - s.ns.SetStateSub(node, sfHasValue, nodestate.Flags{}, 0) - s.ns.SetFieldSub(node, sfiNodeWeight, weight) - } else { - s.ns.SetStateSub(node, nodestate.Flags{}, sfHasValue, 0) - s.ns.SetFieldSub(node, sfiNodeWeight, nil) - s.ns.SetFieldSub(node, sfiNodeHistory, nil) - s.ns.SetFieldSub(node, sfiLocalAddress, nil) - } - s.ns.Persist(node) // saved if node history or hasValue changed -} - -// setRedialWait calculates and sets the redialWait timeout based on the service value -// and dial cost accumulated during the last session/attempt and in total. -// The waiting time is raised exponentially if no service value has been received in order -// to prevent dialing an unresponsive node frequently for a very long time just because it -// was useful in the past. It can still be occasionally dialed though and once it provides -// a significant amount of service value again its waiting time is quickly reduced or reset -// to the minimum. -// Note: node weight is also recalculated and updated by this function. -// Note 2: this function should run inside a NodeStateMachine operation -func (s *ServerPool) setRedialWait(node *enode.Node, addDialCost int64, waitStep float64) { - n, _ := s.ns.GetField(node, sfiNodeHistory).(nodeHistory) - sessionValue, totalValue := s.serviceValue(node) - totalDialCost := s.addDialCost(&n, addDialCost) - - // if the current dial session has yielded at least the average value/dial cost ratio - // then the waiting time should be reset to the minimum. If the session value - // is below average but still positive then timeout is limited to the ratio of - // average / current service value multiplied by the minimum timeout. If the attempt - // was unsuccessful then timeout is raised exponentially without limitation. - // Note: dialCost is used in the formula below even if dial was not attempted at all - // because the pre-negotiation query did not return a positive result. In this case - // the ratio has no meaning anyway and waitFactor is always raised, though in smaller - // steps because queries are cheaper and therefore we can allow more failed attempts. - unixTime := s.unixTime() - plannedTimeout := float64(n.redialWaitEnd - n.redialWaitStart) // last planned redialWait timeout - var actualWait float64 // actual waiting time elapsed - if unixTime > n.redialWaitEnd { - // the planned timeout has elapsed - actualWait = plannedTimeout - } else { - // if the node was redialed earlier then we do not raise the planned timeout - // exponentially because that could lead to the timeout rising very high in - // a short amount of time - // Note that in case of an early redial actualWait also includes the dial - // timeout or connection time of the last attempt but it still serves its - // purpose of preventing the timeout rising quicker than linearly as a function - // of total time elapsed without a successful connection. - actualWait = float64(unixTime - n.redialWaitStart) - } - // raise timeout exponentially if the last planned timeout has elapsed - // (use at least the last planned timeout otherwise) - nextTimeout := actualWait * waitStep - if plannedTimeout > nextTimeout { - nextTimeout = plannedTimeout - } - // we reduce the waiting time if the server has provided service value during the - // connection (but never under the minimum) - a := totalValue * dialCost * float64(minRedialWait) - b := float64(totalDialCost) * sessionValue - if a < b*nextTimeout { - nextTimeout = a / b - } - if nextTimeout < minRedialWait { - nextTimeout = minRedialWait - } - wait := time.Duration(float64(time.Second) * nextTimeout) - if wait < waitThreshold { - n.redialWaitStart = unixTime - n.redialWaitEnd = unixTime + int64(nextTimeout) - s.ns.SetFieldSub(node, sfiNodeHistory, n) - s.ns.SetStateSub(node, sfRedialWait, nodestate.Flags{}, wait) - s.updateWeight(node, totalValue, totalDialCost) - } else { - // discard known node statistics if waiting time is very long because the node - // hasn't been responsive for a very long time - s.ns.SetFieldSub(node, sfiNodeHistory, nil) - s.ns.SetFieldSub(node, sfiNodeWeight, nil) - s.ns.SetStateSub(node, nodestate.Flags{}, sfHasValue, 0) - } -} - -// calculateWeight calculates and sets the node weight without altering the node history. -// This function should be called during startup and shutdown only, otherwise setRedialWait -// will keep the weights updated as the underlying statistics are adjusted. -// Note: this function should run inside a NodeStateMachine operation -func (s *ServerPool) calculateWeight(node *enode.Node) { - n, _ := s.ns.GetField(node, sfiNodeHistory).(nodeHistory) - _, totalValue := s.serviceValue(node) - totalDialCost := s.addDialCost(&n, 0) - s.updateWeight(node, totalValue, totalDialCost) -} - -// API returns the vflux client API -func (s *ServerPool) API() *PrivateClientAPI { - return NewPrivateClientAPI(s.vt) -} - -type dummyIdentity enode.ID - -func (id dummyIdentity) Verify(r *enr.Record, sig []byte) error { return nil } -func (id dummyIdentity) NodeAddr(r *enr.Record) []byte { return id[:] } - -// DialNode replaces the given enode with a locally generated one containing the ENR -// stored in the sfiLocalAddress field if present. This workaround ensures that nodes -// on the local network can be dialed at the local address if a connection has been -// successfully established previously. -// Note that NodeStateMachine always remembers the enode with the latest version of -// the remote signed ENR. ENR filtering should be performed on that version while -// dialNode should be used for dialing the node over TCP or UDP. -func (s *ServerPool) DialNode(n *enode.Node) *enode.Node { - if enr, ok := s.ns.GetField(n, sfiLocalAddress).(*enr.Record); ok { - n, _ := enode.New(dummyIdentity(n.ID()), enr) - return n - } - return n -} - -// Persist immediately stores the state of a node in the node database -func (s *ServerPool) Persist(n *enode.Node) { - s.ns.Persist(n) -} diff --git a/les/vflux/client/serverpool_test.go b/les/vflux/client/serverpool_test.go deleted file mode 100644 index 19d4fe6630..0000000000 --- a/les/vflux/client/serverpool_test.go +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2020 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 client - -import ( - "math/rand" - "strconv" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" -) - -const ( - spTestNodes = 1000 - spTestTarget = 5 - spTestLength = 10000 - spMinTotal = 40000 - spMaxTotal = 50000 -) - -func testNodeID(i int) enode.ID { - return enode.ID{42, byte(i % 256), byte(i / 256)} -} - -func testNodeIndex(id enode.ID) int { - if id[0] != 42 { - return -1 - } - return int(id[1]) + int(id[2])*256 -} - -type ServerPoolTest struct { - db ethdb.KeyValueStore - clock *mclock.Simulated - quit chan chan struct{} - preNeg, preNegFail bool - sp *ServerPool - spi enode.Iterator - input enode.Iterator - testNodes []spTestNode - trusted []string - waitCount, waitEnded int32 - - // preNegLock protects the cycle counter, testNodes list and its connected field - // (accessed from both the main thread and the preNeg callback) - preNegLock sync.Mutex - queryWg *sync.WaitGroup // a new wait group is created each time the simulation is started - stopping bool // stopping avoid calling queryWg.Add after queryWg.Wait - - cycle, conn, servedConn int - serviceCycles, dialCount int - disconnect map[int][]int -} - -type spTestNode struct { - connectCycles, waitCycles int - nextConnCycle, totalConn int - connected, service bool - node *enode.Node -} - -func newServerPoolTest(preNeg, preNegFail bool) *ServerPoolTest { - nodes := make([]*enode.Node, spTestNodes) - for i := range nodes { - nodes[i] = enode.SignNull(&enr.Record{}, testNodeID(i)) - } - return &ServerPoolTest{ - clock: &mclock.Simulated{}, - db: memorydb.New(), - input: enode.CycleNodes(nodes), - testNodes: make([]spTestNode, spTestNodes), - preNeg: preNeg, - preNegFail: preNegFail, - } -} - -func (s *ServerPoolTest) beginWait() { - // ensure that dialIterator and the maximal number of pre-neg queries are not all stuck in a waiting state - for atomic.AddInt32(&s.waitCount, 1) > preNegLimit { - atomic.AddInt32(&s.waitCount, -1) - s.clock.Run(time.Second) - } -} - -func (s *ServerPoolTest) endWait() { - atomic.AddInt32(&s.waitCount, -1) - atomic.AddInt32(&s.waitEnded, 1) -} - -func (s *ServerPoolTest) addTrusted(i int) { - s.trusted = append(s.trusted, enode.SignNull(&enr.Record{}, testNodeID(i)).String()) -} - -func (s *ServerPoolTest) start() { - var testQuery QueryFunc - s.queryWg = new(sync.WaitGroup) - if s.preNeg { - testQuery = func(node *enode.Node) int { - s.preNegLock.Lock() - if s.stopping { - s.preNegLock.Unlock() - return 0 - } - s.queryWg.Add(1) - idx := testNodeIndex(node.ID()) - n := &s.testNodes[idx] - canConnect := !n.connected && n.connectCycles != 0 && s.cycle >= n.nextConnCycle - s.preNegLock.Unlock() - defer s.queryWg.Done() - - if s.preNegFail { - // simulate a scenario where UDP queries never work - s.beginWait() - s.clock.Sleep(time.Second * 5) - s.endWait() - return -1 - } - switch idx % 3 { - case 0: - // pre-neg returns true only if connection is possible - if canConnect { - return 1 - } - return 0 - case 1: - // pre-neg returns true but connection might still fail - return 1 - case 2: - // pre-neg returns true if connection is possible, otherwise timeout (node unresponsive) - if canConnect { - return 1 - } - s.beginWait() - s.clock.Sleep(time.Second * 5) - s.endWait() - return -1 - } - return -1 - } - } - - requestList := make([]RequestInfo, testReqTypes) - for i := range requestList { - requestList[i] = RequestInfo{Name: "testreq" + strconv.Itoa(i), InitAmount: 1, InitValue: 1} - } - - s.sp, s.spi = NewServerPool(s.db, []byte("sp:"), 0, testQuery, s.clock, s.trusted, requestList) - s.sp.AddSource(s.input) - s.sp.validSchemes = enode.ValidSchemesForTesting - s.sp.unixTime = func() int64 { return int64(s.clock.Now()) / int64(time.Second) } - s.disconnect = make(map[int][]int) - s.sp.Start() - s.quit = make(chan chan struct{}) - go func() { - last := int32(-1) - for { - select { - case <-time.After(time.Millisecond * 100): - c := atomic.LoadInt32(&s.waitEnded) - if c == last { - // advance clock if test is stuck (might happen in rare cases) - s.clock.Run(time.Second) - } - last = c - case quit := <-s.quit: - close(quit) - return - } - } - }() -} - -func (s *ServerPoolTest) stop() { - // disable further queries and wait if one is currently running - s.preNegLock.Lock() - s.stopping = true - s.preNegLock.Unlock() - s.queryWg.Wait() - - quit := make(chan struct{}) - s.quit <- quit - <-quit - s.sp.Stop() - s.spi.Close() - s.preNegLock.Lock() - s.stopping = false - s.preNegLock.Unlock() - for i := range s.testNodes { - n := &s.testNodes[i] - if n.connected { - n.totalConn += s.cycle - } - n.connected = false - n.node = nil - n.nextConnCycle = 0 - } - s.conn, s.servedConn = 0, 0 -} - -func (s *ServerPoolTest) run() { - for count := spTestLength; count > 0; count-- { - if dcList := s.disconnect[s.cycle]; dcList != nil { - for _, idx := range dcList { - n := &s.testNodes[idx] - s.sp.UnregisterNode(n.node) - n.totalConn += s.cycle - s.preNegLock.Lock() - n.connected = false - s.preNegLock.Unlock() - n.node = nil - s.conn-- - if n.service { - s.servedConn-- - } - n.nextConnCycle = s.cycle + n.waitCycles - } - delete(s.disconnect, s.cycle) - } - if s.conn < spTestTarget { - s.dialCount++ - s.beginWait() - s.spi.Next() - s.endWait() - dial := s.spi.Node() - id := dial.ID() - idx := testNodeIndex(id) - n := &s.testNodes[idx] - if !n.connected && n.connectCycles != 0 && s.cycle >= n.nextConnCycle { - s.conn++ - if n.service { - s.servedConn++ - } - n.totalConn -= s.cycle - s.preNegLock.Lock() - n.connected = true - s.preNegLock.Unlock() - dc := s.cycle + n.connectCycles - s.disconnect[dc] = append(s.disconnect[dc], idx) - n.node = dial - nv, _ := s.sp.RegisterNode(n.node) - if n.service { - nv.Served([]ServedRequest{{ReqType: 0, Amount: 100}}, 0) - } - } - } - s.serviceCycles += s.servedConn - s.clock.Run(time.Second) - s.preNegLock.Lock() - s.cycle++ - s.preNegLock.Unlock() - } -} - -func (s *ServerPoolTest) setNodes(count, conn, wait int, service, trusted bool) (res []int) { - for ; count > 0; count-- { - idx := rand.Intn(spTestNodes) - for s.testNodes[idx].connectCycles != 0 || s.testNodes[idx].connected { - idx = rand.Intn(spTestNodes) - } - res = append(res, idx) - s.preNegLock.Lock() - s.testNodes[idx] = spTestNode{ - connectCycles: conn, - waitCycles: wait, - service: service, - } - s.preNegLock.Unlock() - if trusted { - s.addTrusted(idx) - } - } - return -} - -func (s *ServerPoolTest) resetNodes() { - for i, n := range s.testNodes { - if n.connected { - n.totalConn += s.cycle - s.sp.UnregisterNode(n.node) - } - s.preNegLock.Lock() - s.testNodes[i] = spTestNode{totalConn: n.totalConn} - s.preNegLock.Unlock() - } - s.conn, s.servedConn = 0, 0 - s.disconnect = make(map[int][]int) - s.trusted = nil -} - -func (s *ServerPoolTest) checkNodes(t *testing.T, nodes []int) { - var sum int - for _, idx := range nodes { - n := &s.testNodes[idx] - if n.connected { - n.totalConn += s.cycle - } - sum += n.totalConn - n.totalConn = 0 - if n.connected { - n.totalConn -= s.cycle - } - } - if sum < spMinTotal || sum > spMaxTotal { - t.Errorf("Total connection amount %d outside expected range %d to %d", sum, spMinTotal, spMaxTotal) - } -} - -func TestServerPool(t *testing.T) { - t.Parallel() - - testServerPool(t, false, false) -} -func TestServerPoolWithPreNeg(t *testing.T) { - t.Parallel() - - testServerPool(t, true, false) -} -func TestServerPoolWithPreNegFail(t *testing.T) { - t.Parallel() - - testServerPool(t, true, true) -} -func testServerPool(t *testing.T, preNeg, fail bool) { - s := newServerPoolTest(preNeg, fail) - nodes := s.setNodes(100, 200, 200, true, false) - s.setNodes(100, 20, 20, false, false) - s.start() - s.run() - s.stop() - s.checkNodes(t, nodes) -} - -func TestServerPoolChangedNodes(t *testing.T) { - t.Parallel() - - testServerPoolChangedNodes(t, false) -} -func TestServerPoolChangedNodesWithPreNeg(t *testing.T) { - t.Parallel() - - testServerPoolChangedNodes(t, true) -} -func testServerPoolChangedNodes(t *testing.T, preNeg bool) { - s := newServerPoolTest(preNeg, false) - nodes := s.setNodes(100, 200, 200, true, false) - s.setNodes(100, 20, 20, false, false) - s.start() - s.run() - s.checkNodes(t, nodes) - for i := 0; i < 3; i++ { - s.resetNodes() - nodes := s.setNodes(100, 200, 200, true, false) - s.setNodes(100, 20, 20, false, false) - s.run() - s.checkNodes(t, nodes) - } - s.stop() -} - -func TestServerPoolRestartNoDiscovery(t *testing.T) { - t.Parallel() - - testServerPoolRestartNoDiscovery(t, false) -} -func TestServerPoolRestartNoDiscoveryWithPreNeg(t *testing.T) { - t.Parallel() - - testServerPoolRestartNoDiscovery(t, true) -} -func testServerPoolRestartNoDiscovery(t *testing.T, preNeg bool) { - s := newServerPoolTest(preNeg, false) - nodes := s.setNodes(100, 200, 200, true, false) - s.setNodes(100, 20, 20, false, false) - s.start() - s.run() - s.stop() - s.checkNodes(t, nodes) - s.input = nil - s.start() - s.run() - s.stop() - s.checkNodes(t, nodes) -} - -func TestServerPoolTrustedNoDiscovery(t *testing.T) { - t.Parallel() - - testServerPoolTrustedNoDiscovery(t, false) -} -func TestServerPoolTrustedNoDiscoveryWithPreNeg(t *testing.T) { - t.Parallel() - - testServerPoolTrustedNoDiscovery(t, true) -} -func testServerPoolTrustedNoDiscovery(t *testing.T, preNeg bool) { - s := newServerPoolTest(preNeg, false) - trusted := s.setNodes(200, 200, 200, true, true) - s.input = nil - s.start() - s.run() - s.stop() - s.checkNodes(t, trusted) -} diff --git a/les/vflux/client/timestats.go b/les/vflux/client/timestats.go deleted file mode 100644 index 7f1ffdbe26..0000000000 --- a/les/vflux/client/timestats.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2020 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 client - -import ( - "io" - "math" - "time" - - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - minResponseTime = time.Millisecond * 50 - maxResponseTime = time.Second * 10 - timeStatLength = 32 - weightScaleFactor = 1000000 -) - -// ResponseTimeStats is the response time distribution of a set of answered requests, -// weighted with request value, either served by a single server or aggregated for -// multiple servers. -// It it a fixed length (timeStatLength) distribution vector with linear interpolation. -// The X axis (the time values) are not linear, they should be transformed with -// TimeToStatScale and StatScaleToTime. -type ( - ResponseTimeStats struct { - stats [timeStatLength]uint64 - exp uint64 - } - ResponseTimeWeights [timeStatLength]float64 -) - -var timeStatsLogFactor = (timeStatLength - 1) / (math.Log(float64(maxResponseTime)/float64(minResponseTime)) + 1) - -// TimeToStatScale converts a response time to a distribution vector index. The index -// is represented by a float64 so that linear interpolation can be applied. -func TimeToStatScale(d time.Duration) float64 { - if d < 0 { - return 0 - } - r := float64(d) / float64(minResponseTime) - if r > 1 { - r = math.Log(r) + 1 - } - r *= timeStatsLogFactor - if r > timeStatLength-1 { - return timeStatLength - 1 - } - return r -} - -// StatScaleToTime converts a distribution vector index to a response time. The index -// is represented by a float64 so that linear interpolation can be applied. -func StatScaleToTime(r float64) time.Duration { - r /= timeStatsLogFactor - if r > 1 { - r = math.Exp(r - 1) - } - return time.Duration(r * float64(minResponseTime)) -} - -// TimeoutWeights calculates the weight function used for calculating service value -// based on the response time distribution of the received service. -// It is based on the request timeout value of the system. It consists of a half cosine -// function starting with 1, crossing zero at timeout and reaching -1 at 2*timeout. -// After 2*timeout the weight is constant -1. -func TimeoutWeights(timeout time.Duration) (res ResponseTimeWeights) { - for i := range res { - t := StatScaleToTime(float64(i)) - if t < 2*timeout { - res[i] = math.Cos(math.Pi / 2 * float64(t) / float64(timeout)) - } else { - res[i] = -1 - } - } - return -} - -// EncodeRLP implements rlp.Encoder -func (rt *ResponseTimeStats) EncodeRLP(w io.Writer) error { - enc := struct { - Stats [timeStatLength]uint64 - Exp uint64 - }{rt.stats, rt.exp} - return rlp.Encode(w, &enc) -} - -// DecodeRLP implements rlp.Decoder -func (rt *ResponseTimeStats) DecodeRLP(s *rlp.Stream) error { - var enc struct { - Stats [timeStatLength]uint64 - Exp uint64 - } - if err := s.Decode(&enc); err != nil { - return err - } - rt.stats, rt.exp = enc.Stats, enc.Exp - return nil -} - -// Add adds a new response time with the given weight to the distribution. -func (rt *ResponseTimeStats) Add(respTime time.Duration, weight float64, expFactor utils.ExpirationFactor) { - rt.setExp(expFactor.Exp) - weight *= expFactor.Factor * weightScaleFactor - r := TimeToStatScale(respTime) - i := int(r) - r -= float64(i) - rt.stats[i] += uint64(weight * (1 - r)) - if i < timeStatLength-1 { - rt.stats[i+1] += uint64(weight * r) - } -} - -// setExp sets the power of 2 exponent of the structure, scaling base values (the vector -// itself) up or down if necessary. -func (rt *ResponseTimeStats) setExp(exp uint64) { - if exp > rt.exp { - shift := exp - rt.exp - for i, v := range rt.stats { - rt.stats[i] = v >> shift - } - rt.exp = exp - } - if exp < rt.exp { - shift := rt.exp - exp - for i, v := range rt.stats { - rt.stats[i] = v << shift - } - rt.exp = exp - } -} - -// Value calculates the total service value based on the given distribution, using the -// specified weight function. -func (rt ResponseTimeStats) Value(weights ResponseTimeWeights, expFactor utils.ExpirationFactor) float64 { - var v float64 - for i, s := range rt.stats { - v += float64(s) * weights[i] - } - if v < 0 { - return 0 - } - return expFactor.Value(v, rt.exp) / weightScaleFactor -} - -// AddStats adds the given ResponseTimeStats to the current one. -func (rt *ResponseTimeStats) AddStats(s *ResponseTimeStats) { - rt.setExp(s.exp) - for i, v := range s.stats { - rt.stats[i] += v - } -} - -// SubStats subtracts the given ResponseTimeStats from the current one. -func (rt *ResponseTimeStats) SubStats(s *ResponseTimeStats) { - rt.setExp(s.exp) - for i, v := range s.stats { - if v < rt.stats[i] { - rt.stats[i] -= v - } else { - rt.stats[i] = 0 - } - } -} - -// Timeout suggests a timeout value based on the previous distribution. The parameter -// is the desired rate of timeouts assuming a similar distribution in the future. -// Note that the actual timeout should have a sensible minimum bound so that operating -// under ideal working conditions for a long time (for example, using a local server -// with very low response times) will not make it very hard for the system to accommodate -// longer response times in the future. -func (rt ResponseTimeStats) Timeout(failRatio float64) time.Duration { - var sum uint64 - for _, v := range rt.stats { - sum += v - } - s := uint64(float64(sum) * failRatio) - i := timeStatLength - 1 - for i > 0 && s >= rt.stats[i] { - s -= rt.stats[i] - i-- - } - r := float64(i) + 0.5 - if rt.stats[i] > 0 { - r -= float64(s) / float64(rt.stats[i]) - } - if r < 0 { - r = 0 - } - th := StatScaleToTime(r) - if th > maxResponseTime { - th = maxResponseTime - } - return th -} - -// RtDistribution represents a distribution as a series of (X, Y) chart coordinates, -// where the X axis is the response time in seconds while the Y axis is the amount of -// service value received with a response time close to the X coordinate. -type RtDistribution [timeStatLength][2]float64 - -// Distribution returns a RtDistribution, optionally normalized to a sum of 1. -func (rt ResponseTimeStats) Distribution(normalized bool, expFactor utils.ExpirationFactor) (res RtDistribution) { - var mul float64 - if normalized { - var sum uint64 - for _, v := range rt.stats { - sum += v - } - if sum > 0 { - mul = 1 / float64(sum) - } - } else { - mul = expFactor.Value(float64(1)/weightScaleFactor, rt.exp) - } - for i, v := range rt.stats { - res[i][0] = float64(StatScaleToTime(float64(i))) / float64(time.Second) - res[i][1] = float64(v) * mul - } - return -} diff --git a/les/vflux/client/timestats_test.go b/les/vflux/client/timestats_test.go deleted file mode 100644 index 80ea2047c6..0000000000 --- a/les/vflux/client/timestats_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2020 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 client - -import ( - "math" - "math/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/les/utils" -) - -func TestTransition(t *testing.T) { - t.Parallel() - - var epsilon = 0.01 - var cases = []time.Duration{ - time.Millisecond, minResponseTime, - time.Second, time.Second * 5, maxResponseTime, - } - for _, c := range cases { - got := StatScaleToTime(TimeToStatScale(c)) - if float64(got)*(1+epsilon) < float64(c) || float64(got)*(1-epsilon) > float64(c) { - t.Fatalf("Failed to transition back") - } - } - // If the time is too large(exceeds the max response time. - got := StatScaleToTime(TimeToStatScale(2 * maxResponseTime)) - if float64(got)*(1+epsilon) < float64(maxResponseTime) || float64(got)*(1-epsilon) > float64(maxResponseTime) { - t.Fatalf("Failed to transition back") - } -} - -var maxResponseWeights = TimeoutWeights(maxResponseTime) - -func TestValue(t *testing.T) { - t.Parallel() - - noexp := utils.ExpirationFactor{Factor: 1} - for i := 0; i < 1000; i++ { - max := minResponseTime + time.Duration(rand.Int63n(int64(maxResponseTime-minResponseTime))) - min := minResponseTime + time.Duration(rand.Int63n(int64(max-minResponseTime))) - timeout := max/2 + time.Duration(rand.Int63n(int64(maxResponseTime-max/2))) - s := makeRangeStats(min, max, 1000, noexp) - value := s.Value(TimeoutWeights(timeout), noexp) - // calculate the average weight (the average of the given range of the half cosine - // weight function). - minx := math.Pi / 2 * float64(min) / float64(timeout) - maxx := math.Pi / 2 * float64(max) / float64(timeout) - avgWeight := (math.Sin(maxx) - math.Sin(minx)) / (maxx - minx) - expv := 1000 * avgWeight - if expv < 0 { - expv = 0 - } - if value < expv-10 || value > expv+10 { - t.Errorf("Value failed (expected %v, got %v)", expv, value) - } - } -} - -func TestAddSubExpire(t *testing.T) { - t.Parallel() - - var ( - sum1, sum2 ResponseTimeStats - sum1ValueExp, sum2ValueExp float64 - logOffset utils.Fixed64 - ) - for i := 0; i < 1000; i++ { - exp := utils.ExpFactor(logOffset) - max := minResponseTime + time.Duration(rand.Int63n(int64(maxResponseTime-minResponseTime))) - min := minResponseTime + time.Duration(rand.Int63n(int64(max-minResponseTime))) - s := makeRangeStats(min, max, 1000, exp) - value := s.Value(maxResponseWeights, exp) - sum1.AddStats(&s) - sum1ValueExp += value - if rand.Intn(2) == 1 { - sum2.AddStats(&s) - sum2ValueExp += value - } - logOffset += utils.Float64ToFixed64(0.001 / math.Log(2)) - sum1ValueExp -= sum1ValueExp * 0.001 - sum2ValueExp -= sum2ValueExp * 0.001 - } - exp := utils.ExpFactor(logOffset) - sum1Value := sum1.Value(maxResponseWeights, exp) - if sum1Value < sum1ValueExp*0.99 || sum1Value > sum1ValueExp*1.01 { - t.Errorf("sum1Value failed (expected %v, got %v)", sum1ValueExp, sum1Value) - } - sum2Value := sum2.Value(maxResponseWeights, exp) - if sum2Value < sum2ValueExp*0.99 || sum2Value > sum2ValueExp*1.01 { - t.Errorf("sum2Value failed (expected %v, got %v)", sum2ValueExp, sum2Value) - } - diff := sum1 - diff.SubStats(&sum2) - diffValue := diff.Value(maxResponseWeights, exp) - diffValueExp := sum1ValueExp - sum2ValueExp - if diffValue < diffValueExp*0.99 || diffValue > diffValueExp*1.01 { - t.Errorf("diffValue failed (expected %v, got %v)", diffValueExp, diffValue) - } -} - -func TestTimeout(t *testing.T) { - t.Parallel() - - testTimeoutRange(t, 0, time.Second) - testTimeoutRange(t, time.Second, time.Second*2) - testTimeoutRange(t, time.Second, maxResponseTime) -} - -func testTimeoutRange(t *testing.T, min, max time.Duration) { - s := makeRangeStats(min, max, 1000, utils.ExpirationFactor{Factor: 1}) - for i := 2; i < 9; i++ { - to := s.Timeout(float64(i) / 10) - exp := max - (max-min)*time.Duration(i)/10 - tol := (max - min) / 50 - if to < exp-tol || to > exp+tol { - t.Errorf("Timeout failed (expected %v, got %v)", exp, to) - } - } -} - -func makeRangeStats(min, max time.Duration, amount float64, exp utils.ExpirationFactor) ResponseTimeStats { - var s ResponseTimeStats - amount /= 1000 - for i := 0; i < 1000; i++ { - s.Add(min+(max-min)*time.Duration(i)/999, amount, exp) - } - return s -} diff --git a/les/vflux/client/valuetracker.go b/les/vflux/client/valuetracker.go deleted file mode 100644 index e0d1010ffe..0000000000 --- a/les/vflux/client/valuetracker.go +++ /dev/null @@ -1,506 +0,0 @@ -// Copyright 2020 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 client - -import ( - "bytes" - "fmt" - "math" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - vtVersion = 1 // database encoding format for ValueTracker - nvtVersion = 1 // database encoding format for NodeValueTracker -) - -var ( - vtKey = []byte("vt:") - vtNodeKey = []byte("vtNode:") -) - -// NodeValueTracker collects service value statistics for a specific server node -type NodeValueTracker struct { - lock sync.Mutex - - vt *ValueTracker - rtStats, lastRtStats ResponseTimeStats - lastTransfer mclock.AbsTime - basket serverBasket - reqCosts []uint64 - reqValues []float64 -} - -// UpdateCosts updates the node value tracker's request cost table -func (nv *NodeValueTracker) UpdateCosts(reqCosts []uint64) { - nv.vt.lock.Lock() - defer nv.vt.lock.Unlock() - - nv.updateCosts(reqCosts, nv.vt.refBasket.reqValues, nv.vt.refBasket.reqValueFactor(reqCosts)) -} - -// updateCosts updates the request cost table of the server. The request value factor -// is also updated based on the given cost table and the current reference basket. -// Note that the contents of the referenced reqValues slice will not change; a new -// reference is passed if the values are updated by ValueTracker. -func (nv *NodeValueTracker) updateCosts(reqCosts []uint64, reqValues []float64, rvFactor float64) { - nv.lock.Lock() - defer nv.lock.Unlock() - - nv.reqCosts = reqCosts - nv.reqValues = reqValues - nv.basket.updateRvFactor(rvFactor) -} - -// transferStats returns request basket and response time statistics that should be -// added to the global statistics. The contents of the server's own request basket are -// gradually transferred to the main reference basket and removed from the server basket -// with the specified transfer rate. -// The response time statistics are retained at both places and therefore the global -// distribution is always the sum of the individual server distributions. -func (nv *NodeValueTracker) transferStats(now mclock.AbsTime, transferRate float64) (requestBasket, ResponseTimeStats) { - nv.lock.Lock() - defer nv.lock.Unlock() - - dt := now - nv.lastTransfer - nv.lastTransfer = now - if dt < 0 { - dt = 0 - } - recentRtStats := nv.rtStats - recentRtStats.SubStats(&nv.lastRtStats) - nv.lastRtStats = nv.rtStats - return nv.basket.transfer(-math.Expm1(-transferRate * float64(dt))), recentRtStats -} - -type ServedRequest struct { - ReqType, Amount uint32 -} - -// Served adds a served request to the node's statistics. An actual request may be composed -// of one or more request types (service vector indices). -func (nv *NodeValueTracker) Served(reqs []ServedRequest, respTime time.Duration) { - nv.vt.statsExpLock.RLock() - expFactor := nv.vt.statsExpFactor - nv.vt.statsExpLock.RUnlock() - - nv.lock.Lock() - defer nv.lock.Unlock() - - var value float64 - for _, r := range reqs { - nv.basket.add(r.ReqType, r.Amount, nv.reqCosts[r.ReqType]*uint64(r.Amount), expFactor) - value += nv.reqValues[r.ReqType] * float64(r.Amount) - } - nv.rtStats.Add(respTime, value, expFactor) -} - -// RtStats returns the node's own response time distribution statistics -func (nv *NodeValueTracker) RtStats() ResponseTimeStats { - nv.lock.Lock() - defer nv.lock.Unlock() - - return nv.rtStats -} - -// ValueTracker coordinates service value calculation for individual servers and updates -// global statistics -type ValueTracker struct { - clock mclock.Clock - lock sync.Mutex - quit chan chan struct{} - db ethdb.KeyValueStore - connected map[enode.ID]*NodeValueTracker - reqTypeCount int - - refBasket referenceBasket - mappings [][]string - currentMapping int - initRefBasket requestBasket - rtStats ResponseTimeStats - - transferRate float64 - statsExpLock sync.RWMutex - statsExpRate, offlineExpRate float64 - statsExpirer utils.Expirer - statsExpFactor utils.ExpirationFactor -} - -type valueTrackerEncV1 struct { - Mappings [][]string - RefBasketMapping uint - RefBasket requestBasket - RtStats ResponseTimeStats - ExpOffset, SavedAt uint64 -} - -type nodeValueTrackerEncV1 struct { - RtStats ResponseTimeStats - ServerBasketMapping uint - ServerBasket requestBasket -} - -// RequestInfo is an initializer structure for the service vector. -type RequestInfo struct { - // Name identifies the request type and is used for re-mapping the service vector if necessary - Name string - // InitAmount and InitValue are used to initialize the reference basket - InitAmount, InitValue float64 -} - -// NewValueTracker creates a new ValueTracker and loads its previously saved state from -// the database if possible. -func NewValueTracker(db ethdb.KeyValueStore, clock mclock.Clock, reqInfo []RequestInfo, updatePeriod time.Duration, transferRate, statsExpRate, offlineExpRate float64) *ValueTracker { - now := clock.Now() - - initRefBasket := requestBasket{items: make([]basketItem, len(reqInfo))} - mapping := make([]string, len(reqInfo)) - - var sumAmount, sumValue float64 - for _, req := range reqInfo { - sumAmount += req.InitAmount - sumValue += req.InitAmount * req.InitValue - } - scaleValues := sumAmount * basketFactor / sumValue - for i, req := range reqInfo { - mapping[i] = req.Name - initRefBasket.items[i].amount = uint64(req.InitAmount * basketFactor) - initRefBasket.items[i].value = uint64(req.InitAmount * req.InitValue * scaleValues) - } - - vt := &ValueTracker{ - clock: clock, - connected: make(map[enode.ID]*NodeValueTracker), - quit: make(chan chan struct{}), - db: db, - reqTypeCount: len(initRefBasket.items), - initRefBasket: initRefBasket, - transferRate: transferRate, - statsExpRate: statsExpRate, - offlineExpRate: offlineExpRate, - } - if vt.loadFromDb(mapping) != nil { - // previous state not saved or invalid, init with default values - vt.refBasket.basket = initRefBasket - vt.mappings = [][]string{mapping} - vt.currentMapping = 0 - } - vt.statsExpirer.SetRate(now, statsExpRate) - vt.refBasket.init(vt.reqTypeCount) - vt.periodicUpdate() - - go func() { - for { - select { - case <-clock.After(updatePeriod): - vt.lock.Lock() - vt.periodicUpdate() - vt.lock.Unlock() - case quit := <-vt.quit: - close(quit) - return - } - } - }() - return vt -} - -// StatsExpirer returns the statistics expirer so that other values can be expired -// with the same rate as the service value statistics. -func (vt *ValueTracker) StatsExpirer() *utils.Expirer { - return &vt.statsExpirer -} - -// StatsExpFactor returns the current expiration factor so that other values can be expired -// with the same rate as the service value statistics. -func (vt *ValueTracker) StatsExpFactor() utils.ExpirationFactor { - vt.statsExpLock.RLock() - defer vt.statsExpLock.RUnlock() - - return vt.statsExpFactor -} - -// loadFromDb loads the value tracker's state from the database and converts saved -// request basket index mapping if it does not match the specified index to name mapping. -func (vt *ValueTracker) loadFromDb(mapping []string) error { - enc, err := vt.db.Get(vtKey) - if err != nil { - return err - } - r := bytes.NewReader(enc) - var version uint - if err := rlp.Decode(r, &version); err != nil { - log.Error("Decoding value tracker state failed", "err", err) - return err - } - if version != vtVersion { - log.Error("Unknown ValueTracker version", "stored", version, "current", nvtVersion) - return fmt.Errorf("unknown ValueTracker version %d (current version is %d)", version, vtVersion) - } - var vte valueTrackerEncV1 - if err := rlp.Decode(r, &vte); err != nil { - log.Error("Decoding value tracker state failed", "err", err) - return err - } - logOffset := utils.Fixed64(vte.ExpOffset) - dt := time.Now().UnixNano() - int64(vte.SavedAt) - if dt > 0 { - logOffset += utils.Float64ToFixed64(float64(dt) * vt.offlineExpRate / math.Log(2)) - } - vt.statsExpirer.SetLogOffset(vt.clock.Now(), logOffset) - vt.rtStats = vte.RtStats - vt.mappings = vte.Mappings - vt.currentMapping = -1 -loop: - for i, m := range vt.mappings { - if len(m) != len(mapping) { - continue loop - } - for j, s := range mapping { - if m[j] != s { - continue loop - } - } - vt.currentMapping = i - break - } - if vt.currentMapping == -1 { - vt.currentMapping = len(vt.mappings) - vt.mappings = append(vt.mappings, mapping) - } - if int(vte.RefBasketMapping) == vt.currentMapping { - vt.refBasket.basket = vte.RefBasket - } else { - if vte.RefBasketMapping >= uint(len(vt.mappings)) { - log.Error("Unknown request basket mapping", "stored", vte.RefBasketMapping, "current", vt.currentMapping) - return fmt.Errorf("unknown request basket mapping %d (current version is %d)", vte.RefBasketMapping, vt.currentMapping) - } - vt.refBasket.basket = vte.RefBasket.convertMapping(vt.mappings[vte.RefBasketMapping], mapping, vt.initRefBasket) - } - return nil -} - -// saveToDb saves the value tracker's state to the database -func (vt *ValueTracker) saveToDb() { - vte := valueTrackerEncV1{ - Mappings: vt.mappings, - RefBasketMapping: uint(vt.currentMapping), - RefBasket: vt.refBasket.basket, - RtStats: vt.rtStats, - ExpOffset: uint64(vt.statsExpirer.LogOffset(vt.clock.Now())), - SavedAt: uint64(time.Now().UnixNano()), - } - enc1, err := rlp.EncodeToBytes(uint(vtVersion)) - if err != nil { - log.Error("Encoding value tracker state failed", "err", err) - return - } - enc2, err := rlp.EncodeToBytes(&vte) - if err != nil { - log.Error("Encoding value tracker state failed", "err", err) - return - } - if err := vt.db.Put(vtKey, append(enc1, enc2...)); err != nil { - log.Error("Saving value tracker state failed", "err", err) - } -} - -// Stop saves the value tracker's state and each loaded node's individual state and -// returns after shutting the internal goroutines down. -func (vt *ValueTracker) Stop() { - quit := make(chan struct{}) - vt.quit <- quit - <-quit - vt.lock.Lock() - vt.periodicUpdate() - for id, nv := range vt.connected { - vt.saveNode(id, nv) - } - vt.connected = nil - vt.saveToDb() - vt.lock.Unlock() -} - -// Register adds a server node to the value tracker -func (vt *ValueTracker) Register(id enode.ID) *NodeValueTracker { - vt.lock.Lock() - defer vt.lock.Unlock() - - if vt.connected == nil { - // ValueTracker has already been stopped - return nil - } - nv := vt.loadOrNewNode(id) - reqTypeCount := len(vt.refBasket.reqValues) - nv.reqCosts = make([]uint64, reqTypeCount) - nv.lastTransfer = vt.clock.Now() - nv.reqValues = vt.refBasket.reqValues - nv.basket.init(reqTypeCount) - - vt.connected[id] = nv - return nv -} - -// Unregister removes a server node from the value tracker -func (vt *ValueTracker) Unregister(id enode.ID) { - vt.lock.Lock() - defer vt.lock.Unlock() - - if nv := vt.connected[id]; nv != nil { - vt.saveNode(id, nv) - delete(vt.connected, id) - } -} - -// GetNode returns an individual server node's value tracker. If it did not exist before -// then a new node is created. -func (vt *ValueTracker) GetNode(id enode.ID) *NodeValueTracker { - vt.lock.Lock() - defer vt.lock.Unlock() - - return vt.loadOrNewNode(id) -} - -// loadOrNewNode returns an individual server node's value tracker. If it did not exist before -// then a new node is created. -func (vt *ValueTracker) loadOrNewNode(id enode.ID) *NodeValueTracker { - if nv, ok := vt.connected[id]; ok { - return nv - } - nv := &NodeValueTracker{vt: vt, lastTransfer: vt.clock.Now()} - enc, err := vt.db.Get(append(vtNodeKey, id[:]...)) - if err != nil { - return nv - } - r := bytes.NewReader(enc) - var version uint - if err := rlp.Decode(r, &version); err != nil { - log.Error("Failed to decode node value tracker", "id", id, "err", err) - return nv - } - if version != nvtVersion { - log.Error("Unknown NodeValueTracker version", "stored", version, "current", nvtVersion) - return nv - } - var nve nodeValueTrackerEncV1 - if err := rlp.Decode(r, &nve); err != nil { - log.Error("Failed to decode node value tracker", "id", id, "err", err) - return nv - } - nv.rtStats = nve.RtStats - nv.lastRtStats = nve.RtStats - if int(nve.ServerBasketMapping) == vt.currentMapping { - nv.basket.basket = nve.ServerBasket - } else { - if nve.ServerBasketMapping >= uint(len(vt.mappings)) { - log.Error("Unknown request basket mapping", "stored", nve.ServerBasketMapping, "current", vt.currentMapping) - return nv - } - nv.basket.basket = nve.ServerBasket.convertMapping(vt.mappings[nve.ServerBasketMapping], vt.mappings[vt.currentMapping], vt.initRefBasket) - } - return nv -} - -// saveNode saves a server node's value tracker to the database -func (vt *ValueTracker) saveNode(id enode.ID, nv *NodeValueTracker) { - recentRtStats := nv.rtStats - recentRtStats.SubStats(&nv.lastRtStats) - vt.rtStats.AddStats(&recentRtStats) - nv.lastRtStats = nv.rtStats - - nve := nodeValueTrackerEncV1{ - RtStats: nv.rtStats, - ServerBasketMapping: uint(vt.currentMapping), - ServerBasket: nv.basket.basket, - } - enc1, err := rlp.EncodeToBytes(uint(nvtVersion)) - if err != nil { - log.Error("Failed to encode service value information", "id", id, "err", err) - return - } - enc2, err := rlp.EncodeToBytes(&nve) - if err != nil { - log.Error("Failed to encode service value information", "id", id, "err", err) - return - } - if err := vt.db.Put(append(vtNodeKey, id[:]...), append(enc1, enc2...)); err != nil { - log.Error("Failed to save service value information", "id", id, "err", err) - } -} - -// RtStats returns the global response time distribution statistics -func (vt *ValueTracker) RtStats() ResponseTimeStats { - vt.lock.Lock() - defer vt.lock.Unlock() - - vt.periodicUpdate() - return vt.rtStats -} - -// periodicUpdate transfers individual node data to the global statistics, normalizes -// the reference basket and updates request values. The global state is also saved to -// the database with each update. -func (vt *ValueTracker) periodicUpdate() { - now := vt.clock.Now() - vt.statsExpLock.Lock() - vt.statsExpFactor = utils.ExpFactor(vt.statsExpirer.LogOffset(now)) - vt.statsExpLock.Unlock() - - for _, nv := range vt.connected { - basket, rtStats := nv.transferStats(now, vt.transferRate) - vt.refBasket.add(basket) - vt.rtStats.AddStats(&rtStats) - } - vt.refBasket.normalize() - vt.refBasket.updateReqValues() - for _, nv := range vt.connected { - nv.updateCosts(nv.reqCosts, vt.refBasket.reqValues, vt.refBasket.reqValueFactor(nv.reqCosts)) - } - vt.saveToDb() -} - -type RequestStatsItem struct { - Name string - ReqAmount, ReqValue float64 -} - -// RequestStats returns the current contents of the reference request basket, with -// request values meaning average per request rather than total. -func (vt *ValueTracker) RequestStats() []RequestStatsItem { - vt.statsExpLock.RLock() - expFactor := vt.statsExpFactor - vt.statsExpLock.RUnlock() - vt.lock.Lock() - defer vt.lock.Unlock() - - vt.periodicUpdate() - res := make([]RequestStatsItem, len(vt.refBasket.basket.items)) - for i, item := range vt.refBasket.basket.items { - res[i].Name = vt.mappings[vt.currentMapping][i] - res[i].ReqAmount = expFactor.Value(float64(item.amount)/basketFactor, vt.refBasket.basket.exp) - res[i].ReqValue = vt.refBasket.reqValues[i] - } - return res -} diff --git a/les/vflux/client/valuetracker_test.go b/les/vflux/client/valuetracker_test.go deleted file mode 100644 index 332d65ee51..0000000000 --- a/les/vflux/client/valuetracker_test.go +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2020 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 client - -import ( - "math" - "math/rand" - "strconv" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/p2p/enode" - - "github.com/ethereum/go-ethereum/les/utils" -) - -const ( - testReqTypes = 3 - testNodeCount = 5 - testReqCount = 10000 - testRounds = 10 -) - -func TestValueTracker(t *testing.T) { - t.Parallel() - - db := memorydb.New() - clock := &mclock.Simulated{} - requestList := make([]RequestInfo, testReqTypes) - relPrices := make([]float64, testReqTypes) - totalAmount := make([]uint64, testReqTypes) - for i := range requestList { - requestList[i] = RequestInfo{Name: "testreq" + strconv.Itoa(i), InitAmount: 1, InitValue: 1} - totalAmount[i] = 1 - relPrices[i] = rand.Float64() + 0.1 - } - nodes := make([]*NodeValueTracker, testNodeCount) - for round := 0; round < testRounds; round++ { - makeRequests := round < testRounds-2 - useExpiration := round == testRounds-1 - var expRate float64 - if useExpiration { - expRate = math.Log(2) / float64(time.Hour*100) - } - - vt := NewValueTracker(db, clock, requestList, time.Minute, 1/float64(time.Hour), expRate, expRate) - updateCosts := func(i int) { - costList := make([]uint64, testReqTypes) - baseCost := rand.Float64()*10000000 + 100000 - for j := range costList { - costList[j] = uint64(baseCost * relPrices[j]) - } - nodes[i].UpdateCosts(costList) - } - for i := range nodes { - nodes[i] = vt.Register(enode.ID{byte(i)}) - updateCosts(i) - } - if makeRequests { - for i := 0; i < testReqCount; i++ { - reqType := rand.Intn(testReqTypes) - reqAmount := rand.Intn(10) + 1 - node := rand.Intn(testNodeCount) - respTime := time.Duration((rand.Float64() + 1) * float64(time.Second) * float64(node+1) / testNodeCount) - totalAmount[reqType] += uint64(reqAmount) - nodes[node].Served([]ServedRequest{{uint32(reqType), uint32(reqAmount)}}, respTime) - clock.Run(time.Second) - } - } else { - clock.Run(time.Hour * 100) - if useExpiration { - for i, a := range totalAmount { - totalAmount[i] = a / 2 - } - } - } - vt.Stop() - var sumrp, sumrv float64 - for i, rp := range relPrices { - sumrp += rp - sumrv += vt.refBasket.reqValues[i] - } - for i, rp := range relPrices { - ratio := vt.refBasket.reqValues[i] * sumrp / (rp * sumrv) - if ratio < 0.99 || ratio > 1.01 { - t.Errorf("reqValues (%v) does not match relPrices (%v)", vt.refBasket.reqValues, relPrices) - break - } - } - exp := utils.ExpFactor(vt.StatsExpirer().LogOffset(clock.Now())) - basketAmount := make([]uint64, testReqTypes) - for i, bi := range vt.refBasket.basket.items { - basketAmount[i] += uint64(exp.Value(float64(bi.amount), vt.refBasket.basket.exp)) - } - if makeRequests { - // if we did not make requests in this round then we expect all amounts to be - // in the reference basket - for _, node := range nodes { - for i, bi := range node.basket.basket.items { - basketAmount[i] += uint64(exp.Value(float64(bi.amount), node.basket.basket.exp)) - } - } - } - for i, a := range basketAmount { - amount := a / basketFactor - if amount+10 < totalAmount[i] || amount > totalAmount[i]+10 { - t.Errorf("totalAmount[%d] mismatch in round %d (expected %d, got %d)", i, round, totalAmount[i], amount) - } - } - var sumValue float64 - for _, node := range nodes { - s := node.RtStats() - sumValue += s.Value(maxResponseWeights, exp) - } - s := vt.RtStats() - mainValue := s.Value(maxResponseWeights, exp) - if sumValue < mainValue-10 || sumValue > mainValue+10 { - t.Errorf("Main rtStats value does not match sum of node rtStats values in round %d (main %v, sum %v)", round, mainValue, sumValue) - } - } -} diff --git a/les/vflux/client/wrsiterator.go b/les/vflux/client/wrsiterator.go deleted file mode 100644 index 1b37cba6e5..0000000000 --- a/les/vflux/client/wrsiterator.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2020 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 client - -import ( - "sync" - - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -// WrsIterator returns nodes from the specified selectable set with a weighted random -// selection. Selection weights are provided by a callback function. -type WrsIterator struct { - lock sync.Mutex - cond *sync.Cond - - ns *nodestate.NodeStateMachine - wrs *utils.WeightedRandomSelect - nextNode *enode.Node - closed bool -} - -// NewWrsIterator creates a new WrsIterator. Nodes are selectable if they have all the required -// and none of the disabled flags set. When a node is selected the selectedFlag is set which also -// disables further selectability until it is removed or times out. -func NewWrsIterator(ns *nodestate.NodeStateMachine, requireFlags, disableFlags nodestate.Flags, weightField nodestate.Field) *WrsIterator { - wfn := func(i interface{}) uint64 { - n := ns.GetNode(i.(enode.ID)) - if n == nil { - return 0 - } - wt, _ := ns.GetField(n, weightField).(uint64) - return wt - } - - w := &WrsIterator{ - ns: ns, - wrs: utils.NewWeightedRandomSelect(wfn), - } - w.cond = sync.NewCond(&w.lock) - - ns.SubscribeField(weightField, func(n *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - if state.HasAll(requireFlags) && state.HasNone(disableFlags) { - w.lock.Lock() - w.wrs.Update(n.ID()) - w.lock.Unlock() - w.cond.Signal() - } - }) - - ns.SubscribeState(requireFlags.Or(disableFlags), func(n *enode.Node, oldState, newState nodestate.Flags) { - oldMatch := oldState.HasAll(requireFlags) && oldState.HasNone(disableFlags) - newMatch := newState.HasAll(requireFlags) && newState.HasNone(disableFlags) - if newMatch == oldMatch { - return - } - - w.lock.Lock() - if newMatch { - w.wrs.Update(n.ID()) - } else { - w.wrs.Remove(n.ID()) - } - w.lock.Unlock() - w.cond.Signal() - }) - return w -} - -// Next selects the next node. -func (w *WrsIterator) Next() bool { - w.nextNode = w.chooseNode() - return w.nextNode != nil -} - -func (w *WrsIterator) chooseNode() *enode.Node { - w.lock.Lock() - defer w.lock.Unlock() - - for { - for !w.closed && w.wrs.IsEmpty() { - w.cond.Wait() - } - if w.closed { - return nil - } - // Choose the next node at random. Even though w.wrs is guaranteed - // non-empty here, Choose might return nil if all items have weight - // zero. - if c := w.wrs.Choose(); c != nil { - id := c.(enode.ID) - w.wrs.Remove(id) - return w.ns.GetNode(id) - } - } -} - -// Close ends the iterator. -func (w *WrsIterator) Close() { - w.lock.Lock() - w.closed = true - w.lock.Unlock() - w.cond.Signal() -} - -// Node returns the current node. -func (w *WrsIterator) Node() *enode.Node { - w.lock.Lock() - defer w.lock.Unlock() - return w.nextNode -} diff --git a/les/vflux/client/wrsiterator_test.go b/les/vflux/client/wrsiterator_test.go deleted file mode 100644 index f6eb2d8813..0000000000 --- a/les/vflux/client/wrsiterator_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020 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 client - -import ( - "reflect" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -var ( - testSetup = &nodestate.Setup{} - sfTest1 = testSetup.NewFlag("test1") - sfTest2 = testSetup.NewFlag("test2") - sfTest3 = testSetup.NewFlag("test3") - sfTest4 = testSetup.NewFlag("test4") - sfiTestWeight = testSetup.NewField("nodeWeight", reflect.TypeOf(uint64(0))) -) - -const iterTestNodeCount = 6 - -func TestWrsIterator(t *testing.T) { - t.Parallel() - - ns := nodestate.NewNodeStateMachine(nil, nil, &mclock.Simulated{}, testSetup) - w := NewWrsIterator(ns, sfTest2, sfTest3.Or(sfTest4), sfiTestWeight) - ns.Start() - for i := 1; i <= iterTestNodeCount; i++ { - ns.SetState(testNode(i), sfTest1, nodestate.Flags{}, 0) - ns.SetField(testNode(i), sfiTestWeight, uint64(1)) - } - next := func() int { - ch := make(chan struct{}) - go func() { - w.Next() - close(ch) - }() - select { - case <-ch: - case <-time.After(time.Second * 5): - t.Fatalf("Iterator.Next() timeout") - } - node := w.Node() - ns.SetState(node, sfTest4, nodestate.Flags{}, 0) - return testNodeIndex(node.ID()) - } - set := make(map[int]bool) - expset := func() { - for len(set) > 0 { - n := next() - if !set[n] { - t.Errorf("Item returned by iterator not in the expected set (got %d)", n) - } - delete(set, n) - } - } - - ns.SetState(testNode(1), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(2), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(3), sfTest2, nodestate.Flags{}, 0) - set[1] = true - set[2] = true - set[3] = true - expset() - ns.SetState(testNode(4), sfTest2, nodestate.Flags{}, 0) - ns.SetState(testNode(5), sfTest2.Or(sfTest3), nodestate.Flags{}, 0) - ns.SetState(testNode(6), sfTest2, nodestate.Flags{}, 0) - set[4] = true - set[6] = true - expset() - ns.SetField(testNode(2), sfiTestWeight, uint64(0)) - ns.SetState(testNode(1), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(2), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(3), nodestate.Flags{}, sfTest4, 0) - set[1] = true - set[3] = true - expset() - ns.SetField(testNode(2), sfiTestWeight, uint64(1)) - ns.SetState(testNode(2), nodestate.Flags{}, sfTest2, 0) - ns.SetState(testNode(1), nodestate.Flags{}, sfTest4, 0) - ns.SetState(testNode(2), sfTest2, sfTest4, 0) - ns.SetState(testNode(3), nodestate.Flags{}, sfTest4, 0) - set[1] = true - set[2] = true - set[3] = true - expset() - ns.Stop() -} diff --git a/les/vflux/requests.go b/les/vflux/requests.go deleted file mode 100644 index 5abae2f537..0000000000 --- a/les/vflux/requests.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2021 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 vflux - -import ( - "errors" - "math" - "math/big" - - "github.com/ethereum/go-ethereum/rlp" -) - -var ErrNoReply = errors.New("no reply for given request") - -const ( - MaxRequestLength = 16 // max number of individual requests in a batch - CapacityQueryName = "cq" - CapacityQueryMaxLen = 16 -) - -type ( - // Request describes a single vflux request inside a batch. Service and request - // type are identified by strings, parameters are RLP encoded. - Request struct { - Service, Name string - Params []byte - } - // Requests are a batch of vflux requests - Requests []Request - - // Replies are the replies to a batch of requests - Replies [][]byte - - // CapacityQueryReq is the encoding format of the capacity query - CapacityQueryReq struct { - Bias uint64 // seconds - AddTokens []IntOrInf - } - // CapacityQueryReply is the encoding format of the response to the capacity query - CapacityQueryReply []uint64 -) - -// Add encodes and adds a new request to the batch -func (r *Requests) Add(service, name string, val interface{}) (int, error) { - enc, err := rlp.EncodeToBytes(val) - if err != nil { - return -1, err - } - *r = append(*r, Request{ - Service: service, - Name: name, - Params: enc, - }) - return len(*r) - 1, nil -} - -// Get decodes the reply to the i-th request in the batch -func (r Replies) Get(i int, val interface{}) error { - if i < 0 || i >= len(r) { - return ErrNoReply - } - return rlp.DecodeBytes(r[i], val) -} - -const ( - IntNonNegative = iota - IntNegative - IntPlusInf - IntMinusInf -) - -// IntOrInf is the encoding format for arbitrary length signed integers that can also -// hold the values of +Inf or -Inf -type IntOrInf struct { - Type uint8 - Value big.Int -} - -// BigInt returns the value as a big.Int or panics if the value is infinity -func (i *IntOrInf) BigInt() *big.Int { - switch i.Type { - case IntNonNegative: - return new(big.Int).Set(&i.Value) - case IntNegative: - return new(big.Int).Neg(&i.Value) - case IntPlusInf: - panic(nil) // caller should check Inf() before trying to convert to big.Int - case IntMinusInf: - panic(nil) - } - return &big.Int{} // invalid type decodes to 0 value -} - -// Inf returns 1 if the value is +Inf, -1 if it is -Inf, 0 otherwise -func (i *IntOrInf) Inf() int { - switch i.Type { - case IntPlusInf: - return 1 - case IntMinusInf: - return -1 - } - return 0 // invalid type decodes to 0 value -} - -// Int64 limits the value between MinInt64 and MaxInt64 (even if it is +-Inf) and returns an int64 type -func (i *IntOrInf) Int64() int64 { - switch i.Type { - case IntNonNegative: - if i.Value.IsInt64() { - return i.Value.Int64() - } else { - return math.MaxInt64 - } - case IntNegative: - if i.Value.IsInt64() { - return -i.Value.Int64() - } else { - return math.MinInt64 - } - case IntPlusInf: - return math.MaxInt64 - case IntMinusInf: - return math.MinInt64 - } - return 0 // invalid type decodes to 0 value -} - -// SetBigInt sets the value to the given big.Int -func (i *IntOrInf) SetBigInt(v *big.Int) { - if v.Sign() >= 0 { - i.Type = IntNonNegative - i.Value.Set(v) - } else { - i.Type = IntNegative - i.Value.Neg(v) - } -} - -// SetInt64 sets the value to the given int64. Note that MaxInt64 translates to +Inf -// while MinInt64 translates to -Inf. -func (i *IntOrInf) SetInt64(v int64) { - if v >= 0 { - if v == math.MaxInt64 { - i.Type = IntPlusInf - } else { - i.Type = IntNonNegative - i.Value.SetInt64(v) - } - } else { - if v == math.MinInt64 { - i.Type = IntMinusInf - } else { - i.Type = IntNegative - i.Value.SetInt64(-v) - } - } -} - -// SetInf sets the value to +Inf or -Inf -func (i *IntOrInf) SetInf(sign int) { - if sign == 1 { - i.Type = IntPlusInf - } else { - i.Type = IntMinusInf - } -} diff --git a/les/vflux/server/balance.go b/les/vflux/server/balance.go deleted file mode 100644 index b09f7bb501..0000000000 --- a/les/vflux/server/balance.go +++ /dev/null @@ -1,693 +0,0 @@ -// Copyright 2020 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 server - -import ( - "errors" - "math" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -var errBalanceOverflow = errors.New("balance overflow") - -const maxBalance = math.MaxInt64 // maximum allowed balance value - -const ( - balanceCallbackUpdate = iota // called when priority drops below the last minimum estimate - balanceCallbackZero // called when priority drops to zero (positive balance exhausted) - balanceCallbackCount // total number of balance callbacks -) - -// PriceFactors determine the pricing policy (may apply either to positive or -// negative balances which may have different factors). -// - TimeFactor is cost unit per nanosecond of connection time -// - CapacityFactor is cost unit per nanosecond of connection time per 1000000 capacity -// - RequestFactor is cost unit per request "realCost" unit -type PriceFactors struct { - TimeFactor, CapacityFactor, RequestFactor float64 -} - -// connectionPrice returns the price of connection per nanosecond at the given capacity -// and the estimated average request cost. -func (p PriceFactors) connectionPrice(cap uint64, avgReqCost float64) float64 { - return p.TimeFactor + float64(cap)*p.CapacityFactor/1000000 + p.RequestFactor*avgReqCost -} - -type ( - // nodePriority interface provides current and estimated future priorities on demand - nodePriority interface { - // priority should return the current priority of the node (higher is better) - priority(cap uint64) int64 - // estimatePriority should return a lower estimate for the minimum of the node priority - // value starting from the current moment until the given time. If the priority goes - // under the returned estimate before the specified moment then it is the caller's - // responsibility to signal with updateFlag. - estimatePriority(cap uint64, addBalance int64, future, bias time.Duration, update bool) int64 - } - - // ReadOnlyBalance provides read-only operations on the node balance - ReadOnlyBalance interface { - nodePriority - GetBalance() (uint64, uint64) - GetRawBalance() (utils.ExpiredValue, utils.ExpiredValue) - GetPriceFactors() (posFactor, negFactor PriceFactors) - } - - // ConnectedBalance provides operations permitted on connected nodes (non-read-only - // operations are not permitted inside a BalanceOperation) - ConnectedBalance interface { - ReadOnlyBalance - SetPriceFactors(posFactor, negFactor PriceFactors) - RequestServed(cost uint64) uint64 - } - - // AtomicBalanceOperator provides operations permitted in an atomic BalanceOperation - AtomicBalanceOperator interface { - ReadOnlyBalance - AddBalance(amount int64) (uint64, uint64, error) - SetBalance(pos, neg uint64) error - } -) - -// nodeBalance keeps track of the positive and negative balances of a connected -// client and calculates actual and projected future priority values. -// Implements nodePriority interface. -type nodeBalance struct { - bt *balanceTracker - lock sync.RWMutex - node *enode.Node - connAddress string - active, hasPriority, setFlags bool - capacity uint64 - balance balance - posFactor, negFactor PriceFactors - sumReqCost uint64 - lastUpdate, nextUpdate, initTime mclock.AbsTime - updateEvent mclock.Timer - // since only a limited and fixed number of callbacks are needed, they are - // stored in a fixed size array ordered by priority threshold. - callbacks [balanceCallbackCount]balanceCallback - // callbackIndex maps balanceCallback constants to callbacks array indexes (-1 if not active) - callbackIndex [balanceCallbackCount]int - callbackCount int // number of active callbacks -} - -// balance represents a pair of positive and negative balances -type balance struct { - pos, neg utils.ExpiredValue - posExp, negExp utils.ValueExpirer -} - -// posValue returns the value of positive balance at a given timestamp. -func (b balance) posValue(now mclock.AbsTime) uint64 { - return b.pos.Value(b.posExp.LogOffset(now)) -} - -// negValue returns the value of negative balance at a given timestamp. -func (b balance) negValue(now mclock.AbsTime) uint64 { - return b.neg.Value(b.negExp.LogOffset(now)) -} - -// addValue adds the value of a given amount to the balance. The original value and -// updated value will also be returned if the addition is successful. -// Returns the error if the given value is too large and the value overflows. -func (b *balance) addValue(now mclock.AbsTime, amount int64, pos bool, force bool) (uint64, uint64, int64, error) { - var ( - val utils.ExpiredValue - offset utils.Fixed64 - ) - if pos { - offset, val = b.posExp.LogOffset(now), b.pos - } else { - offset, val = b.negExp.LogOffset(now), b.neg - } - old := val.Value(offset) - if amount > 0 && (amount > maxBalance || old > maxBalance-uint64(amount)) { - if !force { - return old, 0, 0, errBalanceOverflow - } - val = utils.ExpiredValue{} - amount = maxBalance - } - net := val.Add(amount, offset) - if pos { - b.pos = val - } else { - b.neg = val - } - return old, val.Value(offset), net, nil -} - -// setValue sets the internal balance amount to the given values. Returns the -// error if the given value is too large. -func (b *balance) setValue(now mclock.AbsTime, pos uint64, neg uint64) error { - if pos > maxBalance || neg > maxBalance { - return errBalanceOverflow - } - var pb, nb utils.ExpiredValue - pb.Add(int64(pos), b.posExp.LogOffset(now)) - nb.Add(int64(neg), b.negExp.LogOffset(now)) - b.pos = pb - b.neg = nb - return nil -} - -// balanceCallback represents a single callback that is activated when client priority -// reaches the given threshold -type balanceCallback struct { - id int - threshold int64 - callback func() -} - -// GetBalance returns the current positive and negative balance. -func (n *nodeBalance) GetBalance() (uint64, uint64) { - n.lock.Lock() - defer n.lock.Unlock() - - now := n.bt.clock.Now() - n.updateBalance(now) - return n.balance.posValue(now), n.balance.negValue(now) -} - -// GetRawBalance returns the current positive and negative balance -// but in the raw(expired value) format. -func (n *nodeBalance) GetRawBalance() (utils.ExpiredValue, utils.ExpiredValue) { - n.lock.Lock() - defer n.lock.Unlock() - - now := n.bt.clock.Now() - n.updateBalance(now) - return n.balance.pos, n.balance.neg -} - -// AddBalance adds the given amount to the positive balance and returns the balance -// before and after the operation. Exceeding maxBalance results in an error (balance is -// unchanged) while adding a negative amount higher than the current balance results in -// zero balance. -// Note: this function should run inside a NodeStateMachine operation -func (n *nodeBalance) AddBalance(amount int64) (uint64, uint64, error) { - var ( - err error - old, new uint64 - now = n.bt.clock.Now() - callbacks []func() - setPriority bool - ) - // Operation with holding the lock - n.bt.updateTotalBalance(n, func() bool { - n.updateBalance(now) - if old, new, _, err = n.balance.addValue(now, amount, true, false); err != nil { - return false - } - callbacks, setPriority = n.checkCallbacks(now), n.checkPriorityStatus() - n.storeBalance(true, false) - return true - }) - if err != nil { - return old, old, err - } - // Operation without holding the lock - for _, cb := range callbacks { - cb() - } - if n.setFlags { - if setPriority { - n.bt.ns.SetStateSub(n.node, n.bt.setup.priorityFlag, nodestate.Flags{}, 0) - } - // Note: priority flag is automatically removed by the zero priority callback if necessary - n.signalPriorityUpdate() - } - return old, new, nil -} - -// SetBalance sets the positive and negative balance to the given values -// Note: this function should run inside a NodeStateMachine operation -func (n *nodeBalance) SetBalance(pos, neg uint64) error { - var ( - now = n.bt.clock.Now() - callbacks []func() - setPriority bool - ) - // Operation with holding the lock - n.bt.updateTotalBalance(n, func() bool { - n.updateBalance(now) - if err := n.balance.setValue(now, pos, neg); err != nil { - return false - } - callbacks, setPriority = n.checkCallbacks(now), n.checkPriorityStatus() - n.storeBalance(true, true) - return true - }) - // Operation without holding the lock - for _, cb := range callbacks { - cb() - } - if n.setFlags { - if setPriority { - n.bt.ns.SetStateSub(n.node, n.bt.setup.priorityFlag, nodestate.Flags{}, 0) - } - // Note: priority flag is automatically removed by the zero priority callback if necessary - n.signalPriorityUpdate() - } - return nil -} - -// RequestServed should be called after serving a request for the given peer -func (n *nodeBalance) RequestServed(cost uint64) (newBalance uint64) { - n.lock.Lock() - - var ( - check bool - fcost = float64(cost) - now = n.bt.clock.Now() - ) - n.updateBalance(now) - if !n.balance.pos.IsZero() { - posCost := -int64(fcost * n.posFactor.RequestFactor) - if posCost == 0 { - fcost = 0 - newBalance = n.balance.posValue(now) - } else { - var net int64 - _, newBalance, net, _ = n.balance.addValue(now, posCost, true, false) - if posCost == net { - fcost = 0 - } else { - fcost *= 1 - float64(net)/float64(posCost) - } - check = true - } - } - if fcost > 0 && n.negFactor.RequestFactor != 0 { - n.balance.addValue(now, int64(fcost*n.negFactor.RequestFactor), false, false) - check = true - } - n.sumReqCost += cost - - var callbacks []func() - if check { - callbacks = n.checkCallbacks(now) - } - n.lock.Unlock() - - if callbacks != nil { - n.bt.ns.Operation(func() { - for _, cb := range callbacks { - cb() - } - }) - } - return -} - -// priority returns the actual priority based on the current balance -func (n *nodeBalance) priority(capacity uint64) int64 { - n.lock.Lock() - defer n.lock.Unlock() - - now := n.bt.clock.Now() - n.updateBalance(now) - return n.balanceToPriority(now, n.balance, capacity) -} - -// EstMinPriority gives a lower estimate for the priority at a given time in the future. -// An average request cost per time is assumed that is twice the average cost per time -// in the current session. -// If update is true then a priority callback is added that turns updateFlag on and off -// in case the priority goes below the estimated minimum. -func (n *nodeBalance) estimatePriority(capacity uint64, addBalance int64, future, bias time.Duration, update bool) int64 { - n.lock.Lock() - defer n.lock.Unlock() - - now := n.bt.clock.Now() - n.updateBalance(now) - - b := n.balance // copy the balance - if addBalance != 0 { - b.addValue(now, addBalance, true, true) - } - if future > 0 { - var avgReqCost float64 - dt := time.Duration(n.lastUpdate - n.initTime) - if dt > time.Second { - avgReqCost = float64(n.sumReqCost) * 2 / float64(dt) - } - b = n.reducedBalance(b, now, future, capacity, avgReqCost) - } - if bias > 0 { - b = n.reducedBalance(b, now.Add(future), bias, capacity, 0) - } - pri := n.balanceToPriority(now, b, capacity) - // Ensure that biased estimates are always lower than actual priorities, even if - // the bias is very small. - // This ensures that two nodes will not ping-pong update signals forever if both of - // them have zero estimated priority drop in the projected future. - current := n.balanceToPriority(now, n.balance, capacity) - if pri >= current { - pri = current - 1 - } - if update { - n.addCallback(balanceCallbackUpdate, pri, n.signalPriorityUpdate) - } - return pri -} - -// SetPriceFactors sets the price factors. TimeFactor is the price of a nanosecond of -// connection while RequestFactor is the price of a request cost unit. -func (n *nodeBalance) SetPriceFactors(posFactor, negFactor PriceFactors) { - n.lock.Lock() - now := n.bt.clock.Now() - n.updateBalance(now) - n.posFactor, n.negFactor = posFactor, negFactor - callbacks := n.checkCallbacks(now) - n.lock.Unlock() - if callbacks != nil { - n.bt.ns.Operation(func() { - for _, cb := range callbacks { - cb() - } - }) - } -} - -// GetPriceFactors returns the price factors -func (n *nodeBalance) GetPriceFactors() (posFactor, negFactor PriceFactors) { - n.lock.Lock() - defer n.lock.Unlock() - - return n.posFactor, n.negFactor -} - -// activate starts time/capacity cost deduction. -func (n *nodeBalance) activate() { - n.bt.updateTotalBalance(n, func() bool { - if n.active { - return false - } - n.active = true - n.lastUpdate = n.bt.clock.Now() - return true - }) -} - -// deactivate stops time/capacity cost deduction and saves the balances in the database -func (n *nodeBalance) deactivate() { - n.bt.updateTotalBalance(n, func() bool { - if !n.active { - return false - } - n.updateBalance(n.bt.clock.Now()) - if n.updateEvent != nil { - n.updateEvent.Stop() - n.updateEvent = nil - } - n.storeBalance(true, true) - n.active = false - return true - }) -} - -// updateBalance updates balance based on the time factor -func (n *nodeBalance) updateBalance(now mclock.AbsTime) { - if n.active && now > n.lastUpdate { - n.balance = n.reducedBalance(n.balance, n.lastUpdate, time.Duration(now-n.lastUpdate), n.capacity, 0) - n.lastUpdate = now - } -} - -// storeBalance stores the positive and/or negative balance of the node in the database -func (n *nodeBalance) storeBalance(pos, neg bool) { - if pos { - n.bt.storeBalance(n.node.ID().Bytes(), false, n.balance.pos) - } - if neg { - n.bt.storeBalance([]byte(n.connAddress), true, n.balance.neg) - } -} - -// addCallback sets up a one-time callback to be called when priority reaches -// the threshold. If it has already reached the threshold the callback is called -// immediately. -// Note: should be called while n.lock is held -// Note 2: the callback function runs inside a NodeStateMachine operation -func (n *nodeBalance) addCallback(id int, threshold int64, callback func()) { - n.removeCallback(id) - idx := 0 - for idx < n.callbackCount && threshold > n.callbacks[idx].threshold { - idx++ - } - for i := n.callbackCount - 1; i >= idx; i-- { - n.callbackIndex[n.callbacks[i].id]++ - n.callbacks[i+1] = n.callbacks[i] - } - n.callbackCount++ - n.callbackIndex[id] = idx - n.callbacks[idx] = balanceCallback{id, threshold, callback} - now := n.bt.clock.Now() - n.updateBalance(now) - n.scheduleCheck(now) -} - -// removeCallback removes the given callback and returns true if it was active -// Note: should be called while n.lock is held -func (n *nodeBalance) removeCallback(id int) bool { - idx := n.callbackIndex[id] - if idx == -1 { - return false - } - n.callbackIndex[id] = -1 - for i := idx; i < n.callbackCount-1; i++ { - n.callbackIndex[n.callbacks[i+1].id]-- - n.callbacks[i] = n.callbacks[i+1] - } - n.callbackCount-- - return true -} - -// checkCallbacks checks whether the threshold of any of the active callbacks -// have been reached and returns triggered callbacks. -// Note: checkCallbacks assumes that the balance has been recently updated. -func (n *nodeBalance) checkCallbacks(now mclock.AbsTime) (callbacks []func()) { - if n.callbackCount == 0 || n.capacity == 0 { - return - } - pri := n.balanceToPriority(now, n.balance, n.capacity) - for n.callbackCount != 0 && n.callbacks[n.callbackCount-1].threshold >= pri { - n.callbackCount-- - n.callbackIndex[n.callbacks[n.callbackCount].id] = -1 - callbacks = append(callbacks, n.callbacks[n.callbackCount].callback) - } - n.scheduleCheck(now) - return -} - -// scheduleCheck sets up or updates a scheduled event to ensure that it will be called -// again just after the next threshold has been reached. -func (n *nodeBalance) scheduleCheck(now mclock.AbsTime) { - if n.callbackCount != 0 { - d, ok := n.timeUntil(n.callbacks[n.callbackCount-1].threshold) - if !ok { - n.nextUpdate = 0 - n.updateAfter(0) - return - } - if n.nextUpdate == 0 || n.nextUpdate > now.Add(d) { - if d > time.Second { - // Note: if the scheduled update is not in the very near future then we - // schedule the update a bit earlier. This way we do need to update a few - // extra times but don't need to reschedule every time a processed request - // brings the expected firing time a little bit closer. - d = ((d - time.Second) * 7 / 8) + time.Second - } - n.nextUpdate = now.Add(d) - n.updateAfter(d) - } - } else { - n.nextUpdate = 0 - n.updateAfter(0) - } -} - -// updateAfter schedules a balance update and callback check in the future -func (n *nodeBalance) updateAfter(dt time.Duration) { - if n.updateEvent == nil || n.updateEvent.Stop() { - if dt == 0 { - n.updateEvent = nil - } else { - n.updateEvent = n.bt.clock.AfterFunc(dt, func() { - var callbacks []func() - n.lock.Lock() - if n.callbackCount != 0 { - now := n.bt.clock.Now() - n.updateBalance(now) - callbacks = n.checkCallbacks(now) - } - n.lock.Unlock() - if callbacks != nil { - n.bt.ns.Operation(func() { - for _, cb := range callbacks { - cb() - } - }) - } - }) - } - } -} - -// balanceExhausted should be called when the positive balance is exhausted (priority goes to zero/negative) -// Note: this function should run inside a NodeStateMachine operation -func (n *nodeBalance) balanceExhausted() { - n.lock.Lock() - n.storeBalance(true, false) - n.hasPriority = false - n.lock.Unlock() - if n.setFlags { - n.bt.ns.SetStateSub(n.node, nodestate.Flags{}, n.bt.setup.priorityFlag, 0) - } -} - -// checkPriorityStatus checks whether the node has gained priority status and sets the priority -// callback and flag if necessary. It assumes that the balance has been recently updated. -// Note that the priority flag has to be set by the caller after the mutex has been released. -func (n *nodeBalance) checkPriorityStatus() bool { - if !n.hasPriority && !n.balance.pos.IsZero() { - n.hasPriority = true - n.addCallback(balanceCallbackZero, 0, func() { n.balanceExhausted() }) - return true - } - return false -} - -// signalPriorityUpdate signals that the priority fell below the previous minimum estimate -// Note: this function should run inside a NodeStateMachine operation -func (n *nodeBalance) signalPriorityUpdate() { - n.bt.ns.SetStateSub(n.node, n.bt.setup.updateFlag, nodestate.Flags{}, 0) - n.bt.ns.SetStateSub(n.node, nodestate.Flags{}, n.bt.setup.updateFlag, 0) -} - -// setCapacity updates the capacity value used for priority calculation -// Note: capacity should never be zero -// Note 2: this function should run inside a NodeStateMachine operation -func (n *nodeBalance) setCapacity(capacity uint64) { - n.lock.Lock() - now := n.bt.clock.Now() - n.updateBalance(now) - n.capacity = capacity - callbacks := n.checkCallbacks(now) - n.lock.Unlock() - for _, cb := range callbacks { - cb() - } -} - -// balanceToPriority converts a balance to a priority value. Lower priority means -// first to disconnect. Positive balance translates to positive priority. If positive -// balance is zero then negative balance translates to a negative priority. -func (n *nodeBalance) balanceToPriority(now mclock.AbsTime, b balance, capacity uint64) int64 { - pos := b.posValue(now) - if pos > 0 { - return int64(pos / capacity) - } - return -int64(b.negValue(now)) -} - -// priorityToBalance converts a target priority to a requested balance value. -// If the priority is negative, then minimal negative balance is returned; -// otherwise the minimal positive balance is returned. -func (n *nodeBalance) priorityToBalance(priority int64, capacity uint64) (uint64, uint64) { - if priority > 0 { - return uint64(priority) * n.capacity, 0 - } - return 0, uint64(-priority) -} - -// reducedBalance estimates the reduced balance at a given time in the future based -// on the given balance, the time factor and an estimated average request cost per time ratio -func (n *nodeBalance) reducedBalance(b balance, start mclock.AbsTime, dt time.Duration, capacity uint64, avgReqCost float64) balance { - // since the costs are applied continuously during the dt time period we calculate - // the expiration offset at the middle of the period - var ( - at = start.Add(dt / 2) - dtf = float64(dt) - ) - if !b.pos.IsZero() { - factor := n.posFactor.connectionPrice(capacity, avgReqCost) - diff := -int64(dtf * factor) - _, _, net, _ := b.addValue(at, diff, true, false) - if net == diff { - dtf = 0 - } else { - dtf += float64(net) / factor - } - } - if dtf > 0 { - factor := n.negFactor.connectionPrice(capacity, avgReqCost) - b.addValue(at, int64(dtf*factor), false, false) - } - return b -} - -// timeUntil calculates the remaining time needed to reach a given priority level -// assuming that no requests are processed until then. If the given level is never -// reached then (0, false) is returned. If it has already been reached then (0, true) -// is returned. -// Note: the function assumes that the balance has been recently updated and -// calculates the time starting from the last update. -func (n *nodeBalance) timeUntil(priority int64) (time.Duration, bool) { - var ( - now = n.bt.clock.Now() - pos = n.balance.posValue(now) - targetPos, targetNeg = n.priorityToBalance(priority, n.capacity) - diffTime float64 - ) - if pos > 0 { - timePrice := n.posFactor.connectionPrice(n.capacity, 0) - if timePrice < 1e-100 { - return 0, false - } - if targetPos > 0 { - if targetPos > pos { - return 0, true - } - diffTime = float64(pos-targetPos) / timePrice - return time.Duration(diffTime), true - } else { - diffTime = float64(pos) / timePrice - } - } else { - if targetPos > 0 { - return 0, true - } - } - neg := n.balance.negValue(now) - if targetNeg > neg { - timePrice := n.negFactor.connectionPrice(n.capacity, 0) - if timePrice < 1e-100 { - return 0, false - } - diffTime += float64(targetNeg-neg) / timePrice - } - return time.Duration(diffTime), true -} diff --git a/les/vflux/server/balance_test.go b/les/vflux/server/balance_test.go deleted file mode 100644 index e1ff7bf4e9..0000000000 --- a/les/vflux/server/balance_test.go +++ /dev/null @@ -1,459 +0,0 @@ -// Copyright 2020 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 server - -import ( - "math" - "math/rand" - "reflect" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -type zeroExpirer struct{} - -func (z zeroExpirer) SetRate(now mclock.AbsTime, rate float64) {} -func (z zeroExpirer) SetLogOffset(now mclock.AbsTime, logOffset utils.Fixed64) {} -func (z zeroExpirer) LogOffset(now mclock.AbsTime) utils.Fixed64 { return 0 } - -type balanceTestClient struct{} - -func (client balanceTestClient) FreeClientId() string { return "" } - -type balanceTestSetup struct { - clock *mclock.Simulated - db ethdb.KeyValueStore - ns *nodestate.NodeStateMachine - setup *serverSetup - bt *balanceTracker -} - -func newBalanceTestSetup(db ethdb.KeyValueStore, posExp, negExp utils.ValueExpirer) *balanceTestSetup { - // Initialize and customize the setup for the balance testing - clock := &mclock.Simulated{} - setup := newServerSetup() - setup.clientField = setup.setup.NewField("balanceTestClient", reflect.TypeOf(balanceTestClient{})) - - ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup) - if posExp == nil { - posExp = zeroExpirer{} - } - if negExp == nil { - negExp = zeroExpirer{} - } - if db == nil { - db = memorydb.New() - } - bt := newBalanceTracker(ns, setup, db, clock, posExp, negExp) - ns.Start() - return &balanceTestSetup{ - clock: clock, - db: db, - ns: ns, - setup: setup, - bt: bt, - } -} - -func (b *balanceTestSetup) newNode(capacity uint64) *nodeBalance { - node := enode.SignNull(&enr.Record{}, enode.ID{}) - b.ns.SetField(node, b.setup.clientField, balanceTestClient{}) - if capacity != 0 { - b.ns.SetField(node, b.setup.capacityField, capacity) - } - n, _ := b.ns.GetField(node, b.setup.balanceField).(*nodeBalance) - return n -} - -func (b *balanceTestSetup) setBalance(node *nodeBalance, pos, neg uint64) (err error) { - b.bt.BalanceOperation(node.node.ID(), node.connAddress, func(balance AtomicBalanceOperator) { - err = balance.SetBalance(pos, neg) - }) - return -} - -func (b *balanceTestSetup) addBalance(node *nodeBalance, add int64) (old, new uint64, err error) { - b.bt.BalanceOperation(node.node.ID(), node.connAddress, func(balance AtomicBalanceOperator) { - old, new, err = balance.AddBalance(add) - }) - return -} - -func (b *balanceTestSetup) stop() { - b.bt.stop() - b.ns.Stop() -} - -func TestAddBalance(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - - node := b.newNode(1000) - var inputs = []struct { - delta int64 - expect [2]uint64 - total uint64 - expectErr bool - }{ - {100, [2]uint64{0, 100}, 100, false}, - {-100, [2]uint64{100, 0}, 0, false}, - {-100, [2]uint64{0, 0}, 0, false}, - {1, [2]uint64{0, 1}, 1, false}, - {maxBalance, [2]uint64{0, 0}, 0, true}, - } - for _, i := range inputs { - old, new, err := b.addBalance(node, i.delta) - if i.expectErr { - if err == nil { - t.Fatalf("Expect get error but nil") - } - continue - } else if err != nil { - t.Fatalf("Expect get no error but %v", err) - } - if old != i.expect[0] || new != i.expect[1] { - t.Fatalf("Positive balance mismatch, got %v -> %v", old, new) - } - if b.bt.TotalTokenAmount() != i.total { - t.Fatalf("Total positive balance mismatch, want %v, got %v", i.total, b.bt.TotalTokenAmount()) - } - } -} - -func TestSetBalance(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000) - - var inputs = []struct { - pos, neg uint64 - }{ - {1000, 0}, - {0, 1000}, - {1000, 1000}, - } - for _, i := range inputs { - b.setBalance(node, i.pos, i.neg) - pos, neg := node.GetBalance() - if pos != i.pos { - t.Fatalf("Positive balance mismatch, want %v, got %v", i.pos, pos) - } - if neg != i.neg { - t.Fatalf("Negative balance mismatch, want %v, got %v", i.neg, neg) - } - } -} - -func TestBalanceTimeCost(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000) - - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - b.setBalance(node, uint64(time.Minute), 0) // 1 minute time allowance - - var inputs = []struct { - runTime time.Duration - expPos uint64 - expNeg uint64 - }{ - {time.Second, uint64(time.Second * 59), 0}, - {0, uint64(time.Second * 59), 0}, - {time.Second * 59, 0, 0}, - {time.Second, 0, uint64(time.Second)}, - } - for _, i := range inputs { - b.clock.Run(i.runTime) - if pos, _ := node.GetBalance(); pos != i.expPos { - t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos) - } - if _, neg := node.GetBalance(); neg != i.expNeg { - t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg) - } - } - - b.setBalance(node, uint64(time.Minute), 0) // Refill 1 minute time allowance - for _, i := range inputs { - b.clock.Run(i.runTime) - if pos, _ := node.GetBalance(); pos != i.expPos { - t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos) - } - if _, neg := node.GetBalance(); neg != i.expNeg { - t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg) - } - } -} - -func TestBalanceReqCost(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - - b.setBalance(node, uint64(time.Minute), 0) // 1 minute time serving time allowance - var inputs = []struct { - reqCost uint64 - expPos uint64 - expNeg uint64 - }{ - {uint64(time.Second), uint64(time.Second * 59), 0}, - {0, uint64(time.Second * 59), 0}, - {uint64(time.Second * 59), 0, 0}, - {uint64(time.Second), 0, uint64(time.Second)}, - } - for _, i := range inputs { - node.RequestServed(i.reqCost) - if pos, _ := node.GetBalance(); pos != i.expPos { - t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos) - } - if _, neg := node.GetBalance(); neg != i.expNeg { - t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg) - } - } -} - -func TestBalanceToPriority(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - - var inputs = []struct { - pos uint64 - neg uint64 - priority int64 - }{ - {1000, 0, 1}, - {2000, 0, 2}, // Higher balance, higher priority value - {0, 0, 0}, - {0, 1000, -1000}, - } - for _, i := range inputs { - b.setBalance(node, i.pos, i.neg) - priority := node.priority(1000) - if priority != i.priority { - t.Fatalf("priority mismatch, want %v, got %v", i.priority, priority) - } - } -} - -func TestEstimatedPriority(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000000000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - b.setBalance(node, uint64(time.Minute), 0) - var inputs = []struct { - runTime time.Duration // time cost - futureTime time.Duration // diff of future time - reqCost uint64 // single request cost - priority int64 // expected estimated priority - }{ - {time.Second, time.Second, 0, 58}, - {0, time.Second, 0, 58}, - - // 2 seconds time cost, 1 second estimated time cost, 10^9 request cost, - // 10^9 estimated request cost per second. - {time.Second, time.Second, 1000000000, 55}, - - // 3 seconds time cost, 3 second estimated time cost, 10^9*2 request cost, - // 4*10^9 estimated request cost. - {time.Second, 3 * time.Second, 1000000000, 48}, - - // All positive balance is used up - {time.Second * 55, 0, 0, -1}, - - // 1 minute estimated time cost, 4/58 * 10^9 estimated request cost per sec. - {0, time.Minute, 0, -int64(time.Minute) - int64(time.Second)*120/29}, - } - for _, i := range inputs { - b.clock.Run(i.runTime) - node.RequestServed(i.reqCost) - priority := node.estimatePriority(1000000000, 0, i.futureTime, 0, false) - if priority != i.priority { - t.Fatalf("Estimated priority mismatch, want %v, got %v", i.priority, priority) - } - } -} - -func TestPositiveBalanceCounting(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - - var nodes []*nodeBalance - for i := 0; i < 100; i += 1 { - node := b.newNode(1000000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - nodes = append(nodes, node) - } - - // Allocate service token - var sum uint64 - for i := 0; i < 100; i += 1 { - amount := int64(rand.Intn(100) + 100) - b.addBalance(nodes[i], amount) - sum += uint64(amount) - } - if b.bt.TotalTokenAmount() != sum { - t.Fatalf("Invalid token amount") - } - - // Change client status - for i := 0; i < 100; i += 1 { - if rand.Intn(2) == 0 { - b.ns.SetField(nodes[i].node, b.setup.capacityField, uint64(1)) - } - } - if b.bt.TotalTokenAmount() != sum { - t.Fatalf("Invalid token amount") - } - for i := 0; i < 100; i += 1 { - if rand.Intn(2) == 0 { - b.ns.SetField(nodes[i].node, b.setup.capacityField, uint64(1)) - } - } - if b.bt.TotalTokenAmount() != sum { - t.Fatalf("Invalid token amount") - } -} - -func TestCallbackChecking(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - - var inputs = []struct { - priority int64 - expDiff time.Duration - }{ - {500, time.Millisecond * 500}, - {0, time.Second}, - {-int64(time.Second), 2 * time.Second}, - } - b.setBalance(node, uint64(time.Second), 0) - for _, i := range inputs { - diff, _ := node.timeUntil(i.priority) - if diff != i.expDiff { - t.Fatalf("Time difference mismatch, want %v, got %v", i.expDiff, diff) - } - } -} - -func TestCallback(t *testing.T) { - t.Parallel() - - b := newBalanceTestSetup(nil, nil, nil) - defer b.stop() - node := b.newNode(1000) - node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1}) - - callCh := make(chan struct{}, 1) - b.setBalance(node, uint64(time.Minute), 0) - node.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} }) - - b.clock.Run(time.Minute) - select { - case <-callCh: - case <-time.NewTimer(time.Second).C: - t.Fatalf("Callback hasn't been called yet") - } - - b.setBalance(node, uint64(time.Minute), 0) - node.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} }) - node.removeCallback(balanceCallbackZero) - - b.clock.Run(time.Minute) - select { - case <-callCh: - t.Fatalf("Callback shouldn't be called") - case <-time.NewTimer(time.Millisecond * 100).C: - } -} - -func TestBalancePersistence(t *testing.T) { - t.Parallel() - - posExp := &utils.Expirer{} - negExp := &utils.Expirer{} - posExp.SetRate(0, math.Log(2)/float64(time.Hour*2)) // halves every two hours - negExp.SetRate(0, math.Log(2)/float64(time.Hour)) // halves every hour - setup := newBalanceTestSetup(nil, posExp, negExp) - - exp := func(balance *nodeBalance, expPos, expNeg uint64) { - pos, neg := balance.GetBalance() - if pos != expPos { - t.Fatalf("Positive balance incorrect, want %v, got %v", expPos, pos) - } - if neg != expNeg { - t.Fatalf("Positive balance incorrect, want %v, got %v", expPos, pos) - } - } - expTotal := func(expTotal uint64) { - total := setup.bt.TotalTokenAmount() - if total != expTotal { - t.Fatalf("Total token amount incorrect, want %v, got %v", expTotal, total) - } - } - - expTotal(0) - balance := setup.newNode(0) - expTotal(0) - setup.setBalance(balance, 16000000000, 16000000000) - exp(balance, 16000000000, 16000000000) - expTotal(16000000000) - - setup.clock.Run(time.Hour * 2) - exp(balance, 8000000000, 4000000000) - expTotal(8000000000) - setup.stop() - - // Test the functionalities after restart - setup = newBalanceTestSetup(setup.db, posExp, negExp) - expTotal(8000000000) - balance = setup.newNode(0) - exp(balance, 8000000000, 4000000000) - expTotal(8000000000) - setup.clock.Run(time.Hour * 2) - exp(balance, 4000000000, 1000000000) - expTotal(4000000000) - setup.stop() -} diff --git a/les/vflux/server/balance_tracker.go b/les/vflux/server/balance_tracker.go deleted file mode 100644 index 9695e79638..0000000000 --- a/les/vflux/server/balance_tracker.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2020 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 server - -import ( - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -const ( - posThreshold = 1000000 // minimum positive balance that is persisted in the database - negThreshold = 1000000 // minimum negative balance that is persisted in the database - persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence -) - -// balanceTracker tracks positive and negative balances for connected nodes. -// After clientField is set externally, a nodeBalance is created and previous -// balance values are loaded from the database. Both balances are exponentially expired -// values. Costs are deducted from the positive balance if present, otherwise added to -// the negative balance. If the capacity is non-zero then a time cost is applied -// continuously while individual request costs are applied immediately. -// The two balances are translated into a single priority value that also depends -// on the actual capacity. -type balanceTracker struct { - setup *serverSetup - clock mclock.Clock - lock sync.Mutex - ns *nodestate.NodeStateMachine - ndb *nodeDB - posExp, negExp utils.ValueExpirer - - posExpTC, negExpTC uint64 - defaultPosFactors, defaultNegFactors PriceFactors - - active, inactive utils.ExpiredValue - balanceTimer *utils.UpdateTimer - quit chan struct{} -} - -// newBalanceTracker creates a new balanceTracker -func newBalanceTracker(ns *nodestate.NodeStateMachine, setup *serverSetup, db ethdb.KeyValueStore, clock mclock.Clock, posExp, negExp utils.ValueExpirer) *balanceTracker { - ndb := newNodeDB(db, clock) - bt := &balanceTracker{ - ns: ns, - setup: setup, - ndb: ndb, - clock: clock, - posExp: posExp, - negExp: negExp, - balanceTimer: utils.NewUpdateTimer(clock, time.Second*10), - quit: make(chan struct{}), - } - posOffset, negOffset := bt.ndb.getExpiration() - posExp.SetLogOffset(clock.Now(), posOffset) - negExp.SetLogOffset(clock.Now(), negOffset) - - // Load all persisted balance entries of priority nodes, - // calculate the total number of issued service tokens. - bt.ndb.forEachBalance(false, func(id enode.ID, balance utils.ExpiredValue) bool { - bt.inactive.AddExp(balance) - return true - }) - - ns.SubscribeField(bt.setup.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - n, _ := ns.GetField(node, bt.setup.balanceField).(*nodeBalance) - if n == nil { - return - } - - ov, _ := oldValue.(uint64) - nv, _ := newValue.(uint64) - if ov == 0 && nv != 0 { - n.activate() - } - if nv != 0 { - n.setCapacity(nv) - } - if ov != 0 && nv == 0 { - n.deactivate() - } - }) - ns.SubscribeField(bt.setup.clientField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - type peer interface { - FreeClientId() string - } - if newValue != nil { - n := bt.newNodeBalance(node, newValue.(peer).FreeClientId(), true) - bt.lock.Lock() - n.SetPriceFactors(bt.defaultPosFactors, bt.defaultNegFactors) - bt.lock.Unlock() - ns.SetFieldSub(node, bt.setup.balanceField, n) - } else { - ns.SetStateSub(node, nodestate.Flags{}, bt.setup.priorityFlag, 0) - if b, _ := ns.GetField(node, bt.setup.balanceField).(*nodeBalance); b != nil { - b.deactivate() - } - ns.SetFieldSub(node, bt.setup.balanceField, nil) - } - }) - - // The positive and negative balances of clients are stored in database - // and both of these decay exponentially over time. Delete them if the - // value is small enough. - bt.ndb.evictCallBack = bt.canDropBalance - - go func() { - for { - select { - case <-clock.After(persistExpirationRefresh): - now := clock.Now() - bt.ndb.setExpiration(posExp.LogOffset(now), negExp.LogOffset(now)) - case <-bt.quit: - return - } - } - }() - return bt -} - -// Stop saves expiration offset and unsaved node balances and shuts balanceTracker down -func (bt *balanceTracker) stop() { - now := bt.clock.Now() - bt.ndb.setExpiration(bt.posExp.LogOffset(now), bt.negExp.LogOffset(now)) - close(bt.quit) - bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { - if n, ok := bt.ns.GetField(node, bt.setup.balanceField).(*nodeBalance); ok { - n.lock.Lock() - n.storeBalance(true, true) - n.lock.Unlock() - bt.ns.SetField(node, bt.setup.balanceField, nil) - } - }) - bt.ndb.close() -} - -// TotalTokenAmount returns the current total amount of service tokens in existence -func (bt *balanceTracker) TotalTokenAmount() uint64 { - bt.lock.Lock() - defer bt.lock.Unlock() - - bt.balanceTimer.Update(func(_ time.Duration) bool { - bt.active = utils.ExpiredValue{} - bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { - if n, ok := bt.ns.GetField(node, bt.setup.balanceField).(*nodeBalance); ok && n.active { - pos, _ := n.GetRawBalance() - bt.active.AddExp(pos) - } - }) - return true - }) - total := bt.active - total.AddExp(bt.inactive) - return total.Value(bt.posExp.LogOffset(bt.clock.Now())) -} - -// GetPosBalanceIDs lists node IDs with an associated positive balance -func (bt *balanceTracker) GetPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) { - return bt.ndb.getPosBalanceIDs(start, stop, maxCount) -} - -// SetDefaultFactors sets the default price factors applied to subsequently connected clients -func (bt *balanceTracker) SetDefaultFactors(posFactors, negFactors PriceFactors) { - bt.lock.Lock() - bt.defaultPosFactors = posFactors - bt.defaultNegFactors = negFactors - bt.lock.Unlock() -} - -// SetExpirationTCs sets positive and negative token expiration time constants. -// Specified in seconds, 0 means infinite (no expiration). -func (bt *balanceTracker) SetExpirationTCs(pos, neg uint64) { - bt.lock.Lock() - defer bt.lock.Unlock() - - bt.posExpTC, bt.negExpTC = pos, neg - now := bt.clock.Now() - if pos > 0 { - bt.posExp.SetRate(now, 1/float64(pos*uint64(time.Second))) - } else { - bt.posExp.SetRate(now, 0) - } - if neg > 0 { - bt.negExp.SetRate(now, 1/float64(neg*uint64(time.Second))) - } else { - bt.negExp.SetRate(now, 0) - } -} - -// GetExpirationTCs returns the current positive and negative token expiration -// time constants -func (bt *balanceTracker) GetExpirationTCs() (pos, neg uint64) { - bt.lock.Lock() - defer bt.lock.Unlock() - - return bt.posExpTC, bt.negExpTC -} - -// BalanceOperation allows atomic operations on the balance of a node regardless of whether -// it is currently connected or not -func (bt *balanceTracker) BalanceOperation(id enode.ID, connAddress string, cb func(AtomicBalanceOperator)) { - bt.ns.Operation(func() { - var nb *nodeBalance - if node := bt.ns.GetNode(id); node != nil { - nb, _ = bt.ns.GetField(node, bt.setup.balanceField).(*nodeBalance) - } - if nb == nil { - node := enode.SignNull(&enr.Record{}, id) - nb = bt.newNodeBalance(node, connAddress, false) - } - cb(nb) - }) -} - -// newNodeBalance loads balances from the database and creates a nodeBalance instance -// for the given node. It also sets the priorityFlag and adds balanceCallbackZero if -// the node has a positive balance. -// Note: this function should run inside a NodeStateMachine operation -func (bt *balanceTracker) newNodeBalance(node *enode.Node, connAddress string, setFlags bool) *nodeBalance { - pb := bt.ndb.getOrNewBalance(node.ID().Bytes(), false) - nb := bt.ndb.getOrNewBalance([]byte(connAddress), true) - n := &nodeBalance{ - bt: bt, - node: node, - setFlags: setFlags, - connAddress: connAddress, - balance: balance{pos: pb, neg: nb, posExp: bt.posExp, negExp: bt.negExp}, - initTime: bt.clock.Now(), - lastUpdate: bt.clock.Now(), - } - for i := range n.callbackIndex { - n.callbackIndex[i] = -1 - } - if setFlags && n.checkPriorityStatus() { - n.bt.ns.SetStateSub(n.node, n.bt.setup.priorityFlag, nodestate.Flags{}, 0) - } - return n -} - -// storeBalance stores either a positive or a negative balance in the database -func (bt *balanceTracker) storeBalance(id []byte, neg bool, value utils.ExpiredValue) { - if bt.canDropBalance(bt.clock.Now(), neg, value) { - bt.ndb.delBalance(id, neg) // balance is small enough, drop it directly. - } else { - bt.ndb.setBalance(id, neg, value) - } -} - -// canDropBalance tells whether a positive or negative balance is below the threshold -// and therefore can be dropped from the database -func (bt *balanceTracker) canDropBalance(now mclock.AbsTime, neg bool, b utils.ExpiredValue) bool { - if neg { - return b.Value(bt.negExp.LogOffset(now)) <= negThreshold - } - return b.Value(bt.posExp.LogOffset(now)) <= posThreshold -} - -// updateTotalBalance adjusts the total balance after executing given callback. -func (bt *balanceTracker) updateTotalBalance(n *nodeBalance, callback func() bool) { - bt.lock.Lock() - defer bt.lock.Unlock() - - n.lock.Lock() - defer n.lock.Unlock() - - original, active := n.balance.pos, n.active - if !callback() { - return - } - if active { - bt.active.SubExp(original) - } else { - bt.inactive.SubExp(original) - } - if n.active { - bt.active.AddExp(n.balance.pos) - } else { - bt.inactive.AddExp(n.balance.pos) - } -} diff --git a/les/vflux/server/clientdb.go b/les/vflux/server/clientdb.go deleted file mode 100644 index a39cbec36a..0000000000 --- a/les/vflux/server/clientdb.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2020 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 server - -import ( - "bytes" - "encoding/binary" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/lru" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - balanceCacheLimit = 8192 // the maximum number of cached items in service token balance queue - - // nodeDBVersion is the version identifier of the node data in db - // - // Changelog: - // Version 0 => 1 - // * Replace `lastTotal` with `meta` in positive balance: version 0=>1 - // - // Version 1 => 2 - // * Positive Balance and negative balance is changed: - // * Cumulative time is replaced with expiration - nodeDBVersion = 2 - - // dbCleanupCycle is the cycle of db for useless data cleanup - dbCleanupCycle = time.Hour -) - -var ( - positiveBalancePrefix = []byte("pb:") // dbVersion(uint16 big endian) + positiveBalancePrefix + id -> balance - negativeBalancePrefix = []byte("nb:") // dbVersion(uint16 big endian) + negativeBalancePrefix + ip -> balance - expirationKey = []byte("expiration:") // dbVersion(uint16 big endian) + expirationKey -> posExp, negExp -) - -type nodeDB struct { - db ethdb.KeyValueStore - cache *lru.Cache[string, utils.ExpiredValue] - auxbuf []byte // 37-byte auxiliary buffer for key encoding - verbuf [2]byte // 2-byte auxiliary buffer for db version - evictCallBack func(mclock.AbsTime, bool, utils.ExpiredValue) bool // Callback to determine whether the balance can be evicted. - clock mclock.Clock - closeCh chan struct{} - cleanupHook func() // Test hook used for testing -} - -func newNodeDB(db ethdb.KeyValueStore, clock mclock.Clock) *nodeDB { - ndb := &nodeDB{ - db: db, - cache: lru.NewCache[string, utils.ExpiredValue](balanceCacheLimit), - auxbuf: make([]byte, 37), - clock: clock, - closeCh: make(chan struct{}), - } - binary.BigEndian.PutUint16(ndb.verbuf[:], uint16(nodeDBVersion)) - go ndb.expirer() - return ndb -} - -func (db *nodeDB) close() { - close(db.closeCh) -} - -func (db *nodeDB) getPrefix(neg bool) []byte { - prefix := positiveBalancePrefix - if neg { - prefix = negativeBalancePrefix - } - return append(db.verbuf[:], prefix...) -} - -func (db *nodeDB) key(id []byte, neg bool) []byte { - prefix := positiveBalancePrefix - if neg { - prefix = negativeBalancePrefix - } - if len(prefix)+len(db.verbuf)+len(id) > len(db.auxbuf) { - db.auxbuf = append(db.auxbuf, make([]byte, len(prefix)+len(db.verbuf)+len(id)-len(db.auxbuf))...) - } - copy(db.auxbuf[:len(db.verbuf)], db.verbuf[:]) - copy(db.auxbuf[len(db.verbuf):len(db.verbuf)+len(prefix)], prefix) - copy(db.auxbuf[len(prefix)+len(db.verbuf):len(prefix)+len(db.verbuf)+len(id)], id) - return db.auxbuf[:len(prefix)+len(db.verbuf)+len(id)] -} - -func (db *nodeDB) getExpiration() (utils.Fixed64, utils.Fixed64) { - blob, err := db.db.Get(append(db.verbuf[:], expirationKey...)) - if err != nil || len(blob) != 16 { - return 0, 0 - } - return utils.Fixed64(binary.BigEndian.Uint64(blob[:8])), utils.Fixed64(binary.BigEndian.Uint64(blob[8:16])) -} - -func (db *nodeDB) setExpiration(pos, neg utils.Fixed64) { - var buff [16]byte - binary.BigEndian.PutUint64(buff[:8], uint64(pos)) - binary.BigEndian.PutUint64(buff[8:16], uint64(neg)) - db.db.Put(append(db.verbuf[:], expirationKey...), buff[:16]) -} - -func (db *nodeDB) getOrNewBalance(id []byte, neg bool) utils.ExpiredValue { - key := db.key(id, neg) - item, exist := db.cache.Get(string(key)) - if exist { - return item - } - - var b utils.ExpiredValue - enc, err := db.db.Get(key) - if err != nil || len(enc) == 0 { - return b - } - if err := rlp.DecodeBytes(enc, &b); err != nil { - log.Crit("Failed to decode positive balance", "err", err) - } - db.cache.Add(string(key), b) - return b -} - -func (db *nodeDB) setBalance(id []byte, neg bool, b utils.ExpiredValue) { - key := db.key(id, neg) - enc, err := rlp.EncodeToBytes(&(b)) - if err != nil { - log.Crit("Failed to encode positive balance", "err", err) - } - db.db.Put(key, enc) - db.cache.Add(string(key), b) -} - -func (db *nodeDB) delBalance(id []byte, neg bool) { - key := db.key(id, neg) - db.db.Delete(key) - db.cache.Remove(string(key)) -} - -// getPosBalanceIDs returns a lexicographically ordered list of IDs of accounts -// with a positive balance -func (db *nodeDB) getPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) { - if maxCount <= 0 { - return - } - prefix := db.getPrefix(false) - keylen := len(prefix) + len(enode.ID{}) - - it := db.db.NewIterator(prefix, start.Bytes()) - defer it.Release() - - for it.Next() { - var id enode.ID - if len(it.Key()) != keylen { - return - } - copy(id[:], it.Key()[keylen-len(id):]) - if bytes.Compare(id.Bytes(), stop.Bytes()) >= 0 { - return - } - result = append(result, id) - if len(result) == maxCount { - return - } - } - return -} - -// forEachBalance iterates all balances and passes values to callback. -func (db *nodeDB) forEachBalance(neg bool, callback func(id enode.ID, balance utils.ExpiredValue) bool) { - prefix := db.getPrefix(neg) - keylen := len(prefix) + len(enode.ID{}) - - it := db.db.NewIterator(prefix, nil) - defer it.Release() - - for it.Next() { - var id enode.ID - if len(it.Key()) != keylen { - return - } - copy(id[:], it.Key()[keylen-len(id):]) - - var b utils.ExpiredValue - if err := rlp.DecodeBytes(it.Value(), &b); err != nil { - continue - } - if !callback(id, b) { - return - } - } -} - -func (db *nodeDB) expirer() { - for { - select { - case <-db.clock.After(dbCleanupCycle): - db.expireNodes() - case <-db.closeCh: - return - } - } -} - -// expireNodes iterates the whole node db and checks whether the -// token balances can be deleted. -func (db *nodeDB) expireNodes() { - var ( - visited int - deleted int - start = time.Now() - ) - for _, neg := range []bool{false, true} { - iter := db.db.NewIterator(db.getPrefix(neg), nil) - for iter.Next() { - visited++ - var balance utils.ExpiredValue - if err := rlp.DecodeBytes(iter.Value(), &balance); err != nil { - log.Crit("Failed to decode negative balance", "err", err) - } - if db.evictCallBack != nil && db.evictCallBack(db.clock.Now(), neg, balance) { - deleted++ - db.db.Delete(iter.Key()) - } - } - } - // Invoke testing hook if it's not nil. - if db.cleanupHook != nil { - db.cleanupHook() - } - log.Debug("Expire nodes", "visited", visited, "deleted", deleted, "elapsed", common.PrettyDuration(time.Since(start))) -} diff --git a/les/vflux/server/clientdb_test.go b/les/vflux/server/clientdb_test.go deleted file mode 100644 index caa4384e19..0000000000 --- a/les/vflux/server/clientdb_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2020 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 server - -import ( - "reflect" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/p2p/enode" -) - -func expval(v uint64) utils.ExpiredValue { - return utils.ExpiredValue{Base: v} -} - -func TestNodeDB(t *testing.T) { - t.Parallel() - - ndb := newNodeDB(rawdb.NewMemoryDatabase(), mclock.System{}) - defer ndb.close() - - var cases = []struct { - id enode.ID - ip string - balance utils.ExpiredValue - positive bool - }{ - {enode.ID{0x00, 0x01, 0x02}, "", expval(100), true}, - {enode.ID{0x00, 0x01, 0x02}, "", expval(200), true}, - {enode.ID{}, "127.0.0.1", expval(100), false}, - {enode.ID{}, "127.0.0.1", expval(200), false}, - } - for _, c := range cases { - if c.positive { - ndb.setBalance(c.id.Bytes(), false, c.balance) - if pb := ndb.getOrNewBalance(c.id.Bytes(), false); !reflect.DeepEqual(pb, c.balance) { - t.Fatalf("Positive balance mismatch, want %v, got %v", c.balance, pb) - } - } else { - ndb.setBalance([]byte(c.ip), true, c.balance) - if nb := ndb.getOrNewBalance([]byte(c.ip), true); !reflect.DeepEqual(nb, c.balance) { - t.Fatalf("Negative balance mismatch, want %v, got %v", c.balance, nb) - } - } - } - for _, c := range cases { - if c.positive { - ndb.delBalance(c.id.Bytes(), false) - if pb := ndb.getOrNewBalance(c.id.Bytes(), false); !reflect.DeepEqual(pb, utils.ExpiredValue{}) { - t.Fatalf("Positive balance mismatch, want %v, got %v", utils.ExpiredValue{}, pb) - } - } else { - ndb.delBalance([]byte(c.ip), true) - if nb := ndb.getOrNewBalance([]byte(c.ip), true); !reflect.DeepEqual(nb, utils.ExpiredValue{}) { - t.Fatalf("Negative balance mismatch, want %v, got %v", utils.ExpiredValue{}, nb) - } - } - } - posExp, negExp := utils.Fixed64(1000), utils.Fixed64(2000) - ndb.setExpiration(posExp, negExp) - if pos, neg := ndb.getExpiration(); pos != posExp || neg != negExp { - t.Fatalf("Expiration mismatch, want %v / %v, got %v / %v", posExp, negExp, pos, neg) - } - /* curBalance := currencyBalance{typ: "ETH", amount: 10000} - ndb.setCurrencyBalance(enode.ID{0x01, 0x02}, curBalance) - if got := ndb.getCurrencyBalance(enode.ID{0x01, 0x02}); !reflect.DeepEqual(got, curBalance) { - t.Fatalf("Currency balance mismatch, want %v, got %v", curBalance, got) - }*/ -} - -func TestNodeDBExpiration(t *testing.T) { - t.Parallel() - - var ( - iterated int - done = make(chan struct{}, 1) - ) - callback := func(now mclock.AbsTime, neg bool, b utils.ExpiredValue) bool { - iterated += 1 - return true - } - clock := &mclock.Simulated{} - ndb := newNodeDB(rawdb.NewMemoryDatabase(), clock) - defer ndb.close() - ndb.evictCallBack = callback - ndb.cleanupHook = func() { done <- struct{}{} } - - var cases = []struct { - id []byte - neg bool - balance utils.ExpiredValue - }{ - {[]byte{0x01, 0x02}, false, expval(1)}, - {[]byte{0x03, 0x04}, false, expval(1)}, - {[]byte{0x05, 0x06}, false, expval(1)}, - {[]byte{0x07, 0x08}, false, expval(1)}, - - {[]byte("127.0.0.1"), true, expval(1)}, - {[]byte("127.0.0.2"), true, expval(1)}, - {[]byte("127.0.0.3"), true, expval(1)}, - {[]byte("127.0.0.4"), true, expval(1)}, - } - for _, c := range cases { - ndb.setBalance(c.id, c.neg, c.balance) - } - clock.WaitForTimers(1) - clock.Run(time.Hour + time.Minute) - select { - case <-done: - case <-time.NewTimer(time.Second).C: - t.Fatalf("timeout") - } - if iterated != 8 { - t.Fatalf("Failed to evict useless balances, want %v, got %d", 8, iterated) - } - - for _, c := range cases { - ndb.setBalance(c.id, c.neg, c.balance) - } - clock.WaitForTimers(1) - clock.Run(time.Hour + time.Minute) - select { - case <-done: - case <-time.NewTimer(time.Second).C: - t.Fatalf("timeout") - } - if iterated != 16 { - t.Fatalf("Failed to evict useless balances, want %v, got %d", 16, iterated) - } -} diff --git a/les/vflux/server/clientpool.go b/les/vflux/server/clientpool.go deleted file mode 100644 index a525f86368..0000000000 --- a/les/vflux/server/clientpool.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2021 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 server - -import ( - "errors" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/les/vflux" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" - "github.com/ethereum/go-ethereum/rlp" -) - -var ( - ErrNotConnected = errors.New("client not connected") - ErrNoPriority = errors.New("priority too low to raise capacity") - ErrCantFindMaximum = errors.New("unable to find maximum allowed capacity") -) - -// ClientPool implements a client database that assigns a priority to each client -// based on a positive and negative balance. Positive balance is externally assigned -// to prioritized clients and is decreased with connection time and processed -// requests (unless the price factors are zero). If the positive balance is zero -// then negative balance is accumulated. -// -// Balance tracking and priority calculation for connected clients is done by -// balanceTracker. PriorityQueue ensures that clients with the lowest positive or -// highest negative balance get evicted when the total capacity allowance is full -// and new clients with a better balance want to connect. -// -// Already connected nodes receive a small bias in their favor in order to avoid -// accepting and instantly kicking out clients. In theory, we try to ensure that -// each client can have several minutes of connection time. -// -// Balances of disconnected clients are stored in nodeDB including positive balance -// and negative balance. Both positive balance and negative balance will decrease -// exponentially. If the balance is low enough, then the record will be dropped. -type ClientPool struct { - *priorityPool - *balanceTracker - - setup *serverSetup - clock mclock.Clock - ns *nodestate.NodeStateMachine - synced func() bool - - lock sync.RWMutex - connectedBias time.Duration - - minCap uint64 // the minimal capacity value allowed for any client - capReqNode *enode.Node // node that is requesting capacity change; only used inside NSM operation -} - -// clientPeer represents a peer in the client pool. None of the callbacks should block. -type clientPeer interface { - Node() *enode.Node - FreeClientId() string // unique id for non-priority clients (typically a prefix of the network address) - InactiveAllowance() time.Duration // disconnection timeout for inactive non-priority peers - UpdateCapacity(newCap uint64, requested bool) // signals a capacity update (requested is true if it is a result of a SetCapacity call on the given peer - Disconnect() // initiates disconnection (Unregister should always be called) -} - -// NewClientPool creates a new client pool -func NewClientPool(balanceDb ethdb.KeyValueStore, minCap uint64, connectedBias time.Duration, clock mclock.Clock, synced func() bool) *ClientPool { - setup := newServerSetup() - ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup) - cp := &ClientPool{ - priorityPool: newPriorityPool(ns, setup, clock, minCap, connectedBias, 4, 100), - balanceTracker: newBalanceTracker(ns, setup, balanceDb, clock, &utils.Expirer{}, &utils.Expirer{}), - setup: setup, - ns: ns, - clock: clock, - minCap: minCap, - connectedBias: connectedBias, - synced: synced, - } - - ns.SubscribeState(nodestate.MergeFlags(setup.activeFlag, setup.inactiveFlag, setup.priorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { - if newState.Equals(setup.inactiveFlag) { - // set timeout for non-priority inactive client - var timeout time.Duration - if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { - timeout = c.InactiveAllowance() - } - ns.AddTimeout(node, setup.inactiveFlag, timeout) - } - if oldState.Equals(setup.inactiveFlag) && newState.Equals(setup.inactiveFlag.Or(setup.priorityFlag)) { - ns.SetStateSub(node, setup.inactiveFlag, nodestate.Flags{}, 0) // priority gained; remove timeout - } - if newState.Equals(setup.activeFlag) { - // active with no priority; limit capacity to minCap - cap, _ := ns.GetField(node, setup.capacityField).(uint64) - if cap > minCap { - cp.requestCapacity(node, minCap, minCap, 0) - } - } - if newState.Equals(nodestate.Flags{}) { - if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { - c.Disconnect() - } - } - }) - - ns.SubscribeField(setup.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok { - newCap, _ := newValue.(uint64) - c.UpdateCapacity(newCap, node == cp.capReqNode) - } - }) - - // add metrics - cp.ns.SubscribeState(nodestate.MergeFlags(cp.setup.activeFlag, cp.setup.inactiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { - if oldState.IsEmpty() && !newState.IsEmpty() { - clientConnectedMeter.Mark(1) - } - if !oldState.IsEmpty() && newState.IsEmpty() { - clientDisconnectedMeter.Mark(1) - } - if oldState.HasNone(cp.setup.activeFlag) && oldState.HasAll(cp.setup.activeFlag) { - clientActivatedMeter.Mark(1) - } - if oldState.HasAll(cp.setup.activeFlag) && oldState.HasNone(cp.setup.activeFlag) { - clientDeactivatedMeter.Mark(1) - } - activeCount, activeCap := cp.Active() - totalActiveCountGauge.Update(int64(activeCount)) - totalActiveCapacityGauge.Update(int64(activeCap)) - totalInactiveCountGauge.Update(int64(cp.Inactive())) - }) - return cp -} - -// Start starts the client pool. Should be called before Register/Unregister. -func (cp *ClientPool) Start() { - cp.ns.Start() -} - -// Stop shuts the client pool down. The clientPeer interface callbacks will not be called -// after Stop. Register calls will return nil. -func (cp *ClientPool) Stop() { - cp.balanceTracker.stop() - cp.ns.Stop() -} - -// Register registers the peer into the client pool. If the peer has insufficient -// priority and remains inactive for longer than the allowed timeout then it will be -// disconnected by calling the Disconnect function of the clientPeer interface. -func (cp *ClientPool) Register(peer clientPeer) ConnectedBalance { - cp.ns.SetField(peer.Node(), cp.setup.clientField, peerWrapper{peer}) - balance, _ := cp.ns.GetField(peer.Node(), cp.setup.balanceField).(*nodeBalance) - return balance -} - -// Unregister removes the peer from the client pool -func (cp *ClientPool) Unregister(peer clientPeer) { - cp.ns.SetField(peer.Node(), cp.setup.clientField, nil) -} - -// SetConnectedBias sets the connection bias, which is applied to already connected clients -// So that already connected client won't be kicked out very soon and we can ensure all -// connected clients can have enough time to request or sync some data. -func (cp *ClientPool) SetConnectedBias(bias time.Duration) { - cp.lock.Lock() - cp.connectedBias = bias - cp.setActiveBias(bias) - cp.lock.Unlock() -} - -// SetCapacity sets the assigned capacity of a connected client -func (cp *ClientPool) SetCapacity(node *enode.Node, reqCap uint64, bias time.Duration, requested bool) (capacity uint64, err error) { - cp.lock.RLock() - if cp.connectedBias > bias { - bias = cp.connectedBias - } - cp.lock.RUnlock() - - cp.ns.Operation(func() { - balance, _ := cp.ns.GetField(node, cp.setup.balanceField).(*nodeBalance) - if balance == nil { - err = ErrNotConnected - return - } - capacity, _ = cp.ns.GetField(node, cp.setup.capacityField).(uint64) - if capacity == 0 { - // if the client is inactive then it has insufficient priority for the minimal capacity - // (will be activated automatically with minCap when possible) - return - } - if reqCap < cp.minCap { - // can't request less than minCap; switching between 0 (inactive state) and minCap is - // performed by the server automatically as soon as necessary/possible - reqCap = cp.minCap - } - if reqCap > cp.minCap && cp.ns.GetState(node).HasNone(cp.setup.priorityFlag) { - err = ErrNoPriority - return - } - if reqCap == capacity { - return - } - if requested { - // mark the requested node so that the UpdateCapacity callback can signal - // whether the update is the direct result of a SetCapacity call on the given node - cp.capReqNode = node - defer func() { - cp.capReqNode = nil - }() - } - - var minTarget, maxTarget uint64 - if reqCap > capacity { - // Estimate maximum available capacity at the current priority level and request - // the estimated amount. - // Note: requestCapacity could find the highest available capacity between the - // current and the requested capacity but it could cost a lot of iterations with - // fine step adjustment if the requested capacity is very high. By doing a quick - // estimation of the maximum available capacity based on the capacity curve we - // can limit the number of required iterations. - curve := cp.getCapacityCurve().exclude(node.ID()) - maxTarget = curve.maxCapacity(func(capacity uint64) int64 { - return balance.estimatePriority(capacity, 0, 0, bias, false) - }) - if maxTarget < reqCap { - return - } - maxTarget = reqCap - - // Specify a narrow target range that allows a limited number of fine step - // iterations - minTarget = maxTarget - maxTarget/20 - if minTarget < capacity { - minTarget = capacity - } - } else { - minTarget, maxTarget = reqCap, reqCap - } - if newCap := cp.requestCapacity(node, minTarget, maxTarget, bias); newCap >= minTarget && newCap <= maxTarget { - capacity = newCap - return - } - // we should be able to find the maximum allowed capacity in a few iterations - log.Error("Unable to find maximum allowed capacity") - err = ErrCantFindMaximum - }) - return -} - -// serveCapQuery serves a vflux capacity query. It receives multiple token amount values -// and a bias time value. For each given token amount it calculates the maximum achievable -// capacity in case the amount is added to the balance. -func (cp *ClientPool) serveCapQuery(id enode.ID, freeID string, data []byte) []byte { - var req vflux.CapacityQueryReq - if rlp.DecodeBytes(data, &req) != nil { - return nil - } - if l := len(req.AddTokens); l == 0 || l > vflux.CapacityQueryMaxLen { - return nil - } - result := make(vflux.CapacityQueryReply, len(req.AddTokens)) - if !cp.synced() { - capacityQueryZeroMeter.Mark(1) - reply, _ := rlp.EncodeToBytes(&result) - return reply - } - - bias := time.Second * time.Duration(req.Bias) - cp.lock.RLock() - if cp.connectedBias > bias { - bias = cp.connectedBias - } - cp.lock.RUnlock() - - // use capacityCurve to answer request for multiple newly bought token amounts - curve := cp.getCapacityCurve().exclude(id) - cp.BalanceOperation(id, freeID, func(balance AtomicBalanceOperator) { - pb, _ := balance.GetBalance() - for i, addTokens := range req.AddTokens { - add := addTokens.Int64() - result[i] = curve.maxCapacity(func(capacity uint64) int64 { - return balance.estimatePriority(capacity, add, 0, bias, false) / int64(capacity) - }) - if add <= 0 && uint64(-add) >= pb && result[i] > cp.minCap { - result[i] = cp.minCap - } - if result[i] < cp.minCap { - result[i] = 0 - } - } - }) - // add first result to metrics (don't care about priority client multi-queries yet) - if result[0] == 0 { - capacityQueryZeroMeter.Mark(1) - } else { - capacityQueryNonZeroMeter.Mark(1) - } - reply, _ := rlp.EncodeToBytes(&result) - return reply -} - -// Handle implements Service -func (cp *ClientPool) Handle(id enode.ID, address string, name string, data []byte) []byte { - switch name { - case vflux.CapacityQueryName: - return cp.serveCapQuery(id, address, data) - default: - return nil - } -} diff --git a/les/vflux/server/clientpool_test.go b/les/vflux/server/clientpool_test.go deleted file mode 100644 index 7319be0824..0000000000 --- a/les/vflux/server/clientpool_test.go +++ /dev/null @@ -1,640 +0,0 @@ -// Copyright 2021 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 server - -import ( - "fmt" - "math/rand" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -const defaultConnectedBias = time.Minute * 3 - -func TestClientPoolL10C100Free(t *testing.T) { - t.Parallel() - - testClientPool(t, 10, 100, 0, true) -} - -func TestClientPoolL40C200Free(t *testing.T) { - t.Parallel() - - testClientPool(t, 40, 200, 0, true) -} - -func TestClientPoolL100C300Free(t *testing.T) { - t.Parallel() - - testClientPool(t, 100, 300, 0, true) -} - -func TestClientPoolL10C100P4(t *testing.T) { - t.Parallel() - - testClientPool(t, 10, 100, 4, false) -} - -func TestClientPoolL40C200P30(t *testing.T) { - t.Parallel() - - testClientPool(t, 40, 200, 30, false) -} - -func TestClientPoolL100C300P20(t *testing.T) { - t.Parallel() - - testClientPool(t, 100, 300, 20, false) -} - -const testClientPoolTicks = 100000 - -type poolTestPeer struct { - node *enode.Node - index int - disconnCh chan int - cap uint64 - inactiveAllowed bool -} - -func newPoolTestPeer(i int, disconnCh chan int) *poolTestPeer { - return &poolTestPeer{ - index: i, - disconnCh: disconnCh, - node: enode.SignNull(&enr.Record{}, enode.ID{byte(i % 256), byte(i >> 8)}), - } -} - -func (i *poolTestPeer) Node() *enode.Node { - return i.node -} - -func (i *poolTestPeer) FreeClientId() string { - return fmt.Sprintf("addr #%d", i.index) -} - -func (i *poolTestPeer) InactiveAllowance() time.Duration { - if i.inactiveAllowed { - return time.Second * 10 - } - return 0 -} - -func (i *poolTestPeer) UpdateCapacity(capacity uint64, requested bool) { - i.cap = capacity -} - -func (i *poolTestPeer) Disconnect() { - if i.disconnCh == nil { - return - } - id := i.node.ID() - i.disconnCh <- int(id[0]) + int(id[1])<<8 -} - -func getBalance(pool *ClientPool, p *poolTestPeer) (pos, neg uint64) { - pool.BalanceOperation(p.node.ID(), p.FreeClientId(), func(nb AtomicBalanceOperator) { - pos, neg = nb.GetBalance() - }) - return -} - -func addBalance(pool *ClientPool, id enode.ID, amount int64) { - pool.BalanceOperation(id, "", func(nb AtomicBalanceOperator) { - nb.AddBalance(amount) - }) -} - -func checkDiff(a, b uint64) bool { - maxDiff := (a + b) / 2000 - if maxDiff < 1 { - maxDiff = 1 - } - return a > b+maxDiff || b > a+maxDiff -} - -func connect(pool *ClientPool, peer *poolTestPeer) uint64 { - pool.Register(peer) - return peer.cap -} - -func disconnect(pool *ClientPool, peer *poolTestPeer) { - pool.Unregister(peer) -} - -func alwaysTrueFn() bool { - return true -} - -func testClientPool(t *testing.T, activeLimit, clientCount, paidCount int, randomDisconnect bool) { - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - connected = make([]bool, clientCount) - connTicks = make([]int, clientCount) - disconnCh = make(chan int, clientCount) - pool = NewClientPool(db, 1, 0, &clock, alwaysTrueFn) - ) - pool.Start() - pool.SetExpirationTCs(0, 1000) - - pool.SetLimits(uint64(activeLimit), uint64(activeLimit)) - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - // pool should accept new peers up to its connected limit - for i := 0; i < activeLimit; i++ { - if cap := connect(pool, newPoolTestPeer(i, disconnCh)); cap != 0 { - connected[i] = true - } else { - t.Fatalf("Test peer #%d rejected", i) - } - } - // randomly connect and disconnect peers, expect to have a similar total connection time at the end - for tickCounter := 0; tickCounter < testClientPoolTicks; tickCounter++ { - clock.Run(1 * time.Second) - - if tickCounter == testClientPoolTicks/4 { - // give a positive balance to some of the peers - amount := testClientPoolTicks / 2 * int64(time.Second) // enough for half of the simulation period - for i := 0; i < paidCount; i++ { - addBalance(pool, newPoolTestPeer(i, disconnCh).node.ID(), amount) - } - } - - i := rand.Intn(clientCount) - if connected[i] { - if randomDisconnect { - disconnect(pool, newPoolTestPeer(i, disconnCh)) - connected[i] = false - connTicks[i] += tickCounter - } - } else { - if cap := connect(pool, newPoolTestPeer(i, disconnCh)); cap != 0 { - connected[i] = true - connTicks[i] -= tickCounter - } else { - disconnect(pool, newPoolTestPeer(i, disconnCh)) - } - } - pollDisconnects: - for { - select { - case i := <-disconnCh: - disconnect(pool, newPoolTestPeer(i, disconnCh)) - if connected[i] { - connTicks[i] += tickCounter - connected[i] = false - } - default: - break pollDisconnects - } - } - } - - expTicks := testClientPoolTicks/2*activeLimit/clientCount + testClientPoolTicks/2*(activeLimit-paidCount)/(clientCount-paidCount) - expMin := expTicks - expTicks/5 - expMax := expTicks + expTicks/5 - paidTicks := testClientPoolTicks/2*activeLimit/clientCount + testClientPoolTicks/2 - paidMin := paidTicks - paidTicks/5 - paidMax := paidTicks + paidTicks/5 - - // check if the total connected time of peers are all in the expected range - for i, c := range connected { - if c { - connTicks[i] += testClientPoolTicks - } - min, max := expMin, expMax - if i < paidCount { - // expect a higher amount for clients with a positive balance - min, max = paidMin, paidMax - } - if connTicks[i] < min || connTicks[i] > max { - t.Errorf("Total connected time of test node #%d (%d) outside expected range (%d to %d)", i, connTicks[i], min, max) - } - } - pool.Stop() -} - -func testPriorityConnect(t *testing.T, pool *ClientPool, p *poolTestPeer, cap uint64, expSuccess bool) { - if cap := connect(pool, p); cap == 0 { - if expSuccess { - t.Fatalf("Failed to connect paid client") - } else { - return - } - } - if newCap, _ := pool.SetCapacity(p.node, cap, defaultConnectedBias, true); newCap != cap { - if expSuccess { - t.Fatalf("Failed to raise capacity of paid client") - } else { - return - } - } - if !expSuccess { - t.Fatalf("Should reject high capacity paid client") - } -} - -func TestConnectPaidClient(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - // Add balance for an external client and mark it as paid client - addBalance(pool, newPoolTestPeer(0, nil).node.ID(), int64(time.Minute)) - testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 10, true) -} - -func TestConnectPaidClientToSmallPool(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - // Add balance for an external client and mark it as paid client - addBalance(pool, newPoolTestPeer(0, nil).node.ID(), int64(time.Minute)) - - // connect a fat paid client to pool, should reject it. - testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 100, false) -} - -func TestConnectPaidClientToFullPool(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - for i := 0; i < 10; i++ { - addBalance(pool, newPoolTestPeer(i, nil).node.ID(), int64(time.Second*20)) - connect(pool, newPoolTestPeer(i, nil)) - } - addBalance(pool, newPoolTestPeer(11, nil).node.ID(), int64(time.Second*2)) // Add low balance to new paid client - if cap := connect(pool, newPoolTestPeer(11, nil)); cap != 0 { - t.Fatalf("Low balance paid client should be rejected") - } - clock.Run(time.Second) - addBalance(pool, newPoolTestPeer(12, nil).node.ID(), int64(time.Minute*5)) // Add high balance to new paid client - if cap := connect(pool, newPoolTestPeer(12, nil)); cap == 0 { - t.Fatalf("High balance paid client should be accepted") - } -} - -func TestPaidClientKickedOut(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - kickedCh = make(chan int, 100) - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - pool.SetExpirationTCs(0, 0) - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - for i := 0; i < 10; i++ { - addBalance(pool, newPoolTestPeer(i, kickedCh).node.ID(), 10000000000) // 10 second allowance - connect(pool, newPoolTestPeer(i, kickedCh)) - clock.Run(time.Millisecond) - } - clock.Run(defaultConnectedBias + time.Second*11) - if cap := connect(pool, newPoolTestPeer(11, kickedCh)); cap == 0 { - t.Fatalf("Free client should be accepted") - } - clock.Run(0) - select { - case id := <-kickedCh: - if id != 0 { - t.Fatalf("Kicked client mismatch, want %v, got %v", 0, id) - } - default: - t.Fatalf("timeout") - } -} - -func TestConnectFreeClient(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - if cap := connect(pool, newPoolTestPeer(0, nil)); cap == 0 { - t.Fatalf("Failed to connect free client") - } - testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 2, false) -} - -func TestConnectFreeClientToFullPool(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - for i := 0; i < 10; i++ { - connect(pool, newPoolTestPeer(i, nil)) - } - if cap := connect(pool, newPoolTestPeer(11, nil)); cap != 0 { - t.Fatalf("New free client should be rejected") - } - clock.Run(time.Minute) - if cap := connect(pool, newPoolTestPeer(12, nil)); cap != 0 { - t.Fatalf("New free client should be rejected") - } - clock.Run(time.Millisecond) - clock.Run(4 * time.Minute) - if cap := connect(pool, newPoolTestPeer(13, nil)); cap == 0 { - t.Fatalf("Old client connects more than 5min should be kicked") - } -} - -func TestFreeClientKickedOut(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - kicked = make(chan int, 100) - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - for i := 0; i < 10; i++ { - connect(pool, newPoolTestPeer(i, kicked)) - clock.Run(time.Millisecond) - } - if cap := connect(pool, newPoolTestPeer(10, kicked)); cap != 0 { - t.Fatalf("New free client should be rejected") - } - clock.Run(0) - select { - case <-kicked: - default: - t.Fatalf("timeout") - } - disconnect(pool, newPoolTestPeer(10, kicked)) - clock.Run(5 * time.Minute) - for i := 0; i < 10; i++ { - connect(pool, newPoolTestPeer(i+10, kicked)) - } - clock.Run(0) - - for i := 0; i < 10; i++ { - select { - case id := <-kicked: - if id >= 10 { - t.Fatalf("Old client should be kicked, now got: %d", id) - } - default: - t.Fatalf("timeout") - } - } -} - -func TestPositiveBalanceCalculation(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - kicked = make(chan int, 10) - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - addBalance(pool, newPoolTestPeer(0, kicked).node.ID(), int64(time.Minute*3)) - testPriorityConnect(t, pool, newPoolTestPeer(0, kicked), 10, true) - clock.Run(time.Minute) - - disconnect(pool, newPoolTestPeer(0, kicked)) - pb, _ := getBalance(pool, newPoolTestPeer(0, kicked)) - if checkDiff(pb, uint64(time.Minute*2)) { - t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute*2), pb) - } -} - -func TestDowngradePriorityClient(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - kicked = make(chan int, 10) - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}) - - p := newPoolTestPeer(0, kicked) - addBalance(pool, p.node.ID(), int64(time.Minute)) - testPriorityConnect(t, pool, p, 10, true) - if p.cap != 10 { - t.Fatalf("The capacity of priority peer hasn't been updated, got: %d", p.cap) - } - - clock.Run(time.Minute) // All positive balance should be used up. - time.Sleep(300 * time.Millisecond) // Ensure the callback is called - if p.cap != 1 { - t.Fatalf("The capcacity of peer should be downgraded, got: %d", p.cap) - } - pb, _ := getBalance(pool, newPoolTestPeer(0, kicked)) - if pb != 0 { - t.Fatalf("Positive balance mismatch, want %v, got %v", 0, pb) - } - - addBalance(pool, newPoolTestPeer(0, kicked).node.ID(), int64(time.Minute)) - pb, _ = getBalance(pool, newPoolTestPeer(0, kicked)) - if checkDiff(pb, uint64(time.Minute)) { - t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute), pb) - } -} - -func TestNegativeBalanceCalculation(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetExpirationTCs(0, 3600) - pool.SetLimits(10, uint64(10)) // Total capacity limit is 10 - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1e-3, CapacityFactor: 0, RequestFactor: 1}, PriceFactors{TimeFactor: 1e-3, CapacityFactor: 0, RequestFactor: 1}) - - for i := 0; i < 10; i++ { - connect(pool, newPoolTestPeer(i, nil)) - } - clock.Run(time.Second) - - for i := 0; i < 10; i++ { - disconnect(pool, newPoolTestPeer(i, nil)) - _, nb := getBalance(pool, newPoolTestPeer(i, nil)) - if nb != 0 { - t.Fatalf("Short connection shouldn't be recorded") - } - } - for i := 0; i < 10; i++ { - connect(pool, newPoolTestPeer(i, nil)) - } - clock.Run(time.Minute) - for i := 0; i < 10; i++ { - disconnect(pool, newPoolTestPeer(i, nil)) - _, nb := getBalance(pool, newPoolTestPeer(i, nil)) - exp := uint64(time.Minute) / 1000 - exp -= exp / 120 // correct for negative balance expiration - if checkDiff(nb, exp) { - t.Fatalf("Negative balance mismatch, want %v, got %v", exp, nb) - } - } -} - -func TestInactiveClient(t *testing.T) { - t.Parallel() - - var ( - clock mclock.Simulated - db = rawdb.NewMemoryDatabase() - ) - pool := NewClientPool(db, 1, defaultConnectedBias, &clock, alwaysTrueFn) - pool.Start() - defer pool.Stop() - pool.SetLimits(2, uint64(2)) - - p1 := newPoolTestPeer(1, nil) - p1.inactiveAllowed = true - p2 := newPoolTestPeer(2, nil) - p2.inactiveAllowed = true - p3 := newPoolTestPeer(3, nil) - p3.inactiveAllowed = true - addBalance(pool, p1.node.ID(), 1000*int64(time.Second)) - addBalance(pool, p3.node.ID(), 2000*int64(time.Second)) - // p1: 1000 p2: 0 p3: 2000 - p1.cap = connect(pool, p1) - if p1.cap != 1 { - t.Fatalf("Failed to connect peer #1") - } - p2.cap = connect(pool, p2) - if p2.cap != 1 { - t.Fatalf("Failed to connect peer #2") - } - p3.cap = connect(pool, p3) - if p3.cap != 1 { - t.Fatalf("Failed to connect peer #3") - } - if p2.cap != 0 { - t.Fatalf("Failed to deactivate peer #2") - } - addBalance(pool, p2.node.ID(), 3000*int64(time.Second)) - // p1: 1000 p2: 3000 p3: 2000 - if p2.cap != 1 { - t.Fatalf("Failed to activate peer #2") - } - if p1.cap != 0 { - t.Fatalf("Failed to deactivate peer #1") - } - addBalance(pool, p2.node.ID(), -2500*int64(time.Second)) - // p1: 1000 p2: 500 p3: 2000 - if p1.cap != 1 { - t.Fatalf("Failed to activate peer #1") - } - if p2.cap != 0 { - t.Fatalf("Failed to deactivate peer #2") - } - pool.SetDefaultFactors(PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 0}, PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 0}) - p4 := newPoolTestPeer(4, nil) - addBalance(pool, p4.node.ID(), 1500*int64(time.Second)) - // p1: 1000 p2: 500 p3: 2000 p4: 1500 - p4.cap = connect(pool, p4) - if p4.cap != 1 { - t.Fatalf("Failed to activate peer #4") - } - if p1.cap != 0 { - t.Fatalf("Failed to deactivate peer #1") - } - clock.Run(time.Second * 600) - // manually trigger a check to avoid a long real-time wait - pool.ns.SetState(p1.node, pool.setup.updateFlag, nodestate.Flags{}, 0) - pool.ns.SetState(p1.node, nodestate.Flags{}, pool.setup.updateFlag, 0) - // p1: 1000 p2: 500 p3: 2000 p4: 900 - if p1.cap != 1 { - t.Fatalf("Failed to activate peer #1") - } - if p4.cap != 0 { - t.Fatalf("Failed to deactivate peer #4") - } - disconnect(pool, p2) - disconnect(pool, p4) - addBalance(pool, p1.node.ID(), -1000*int64(time.Second)) - if p1.cap != 1 { - t.Fatalf("Should not deactivate peer #1") - } - if p2.cap != 0 { - t.Fatalf("Should not activate peer #2") - } -} diff --git a/les/vflux/server/metrics.go b/les/vflux/server/metrics.go deleted file mode 100644 index 680aebe2ea..0000000000 --- a/les/vflux/server/metrics.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021 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 server - -import ( - "github.com/ethereum/go-ethereum/metrics" -) - -var ( - totalActiveCapacityGauge = metrics.NewRegisteredGauge("vflux/server/active/capacity", nil) - totalActiveCountGauge = metrics.NewRegisteredGauge("vflux/server/active/count", nil) - totalInactiveCountGauge = metrics.NewRegisteredGauge("vflux/server/inactive/count", nil) - - clientConnectedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/connected", nil) - clientActivatedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/activated", nil) - clientDeactivatedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/deactivated", nil) - clientDisconnectedMeter = metrics.NewRegisteredMeter("vflux/server/clientEvent/disconnected", nil) - - capacityQueryZeroMeter = metrics.NewRegisteredMeter("vflux/server/capQueryZero", nil) - capacityQueryNonZeroMeter = metrics.NewRegisteredMeter("vflux/server/capQueryNonZero", nil) -) diff --git a/les/vflux/server/prioritypool.go b/les/vflux/server/prioritypool.go deleted file mode 100644 index 766026a808..0000000000 --- a/les/vflux/server/prioritypool.go +++ /dev/null @@ -1,695 +0,0 @@ -// Copyright 2020 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 server - -import ( - "math" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/common/prque" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -const ( - lazyQueueRefresh = time.Second * 10 // refresh period of the active queue -) - -// priorityPool handles a set of nodes where each node has a capacity (a scalar value) -// and a priority (which can change over time and can also depend on the capacity). -// A node is active if it has at least the necessary minimal amount of capacity while -// inactive nodes have 0 capacity (values between 0 and the minimum are not allowed). -// The pool ensures that the number and total capacity of all active nodes are limited -// and the highest priority nodes are active at all times (limits can be changed -// during operation with immediate effect). -// -// When activating clients a priority bias is applied in favor of the already active -// nodes in order to avoid nodes quickly alternating between active and inactive states -// when their priorities are close to each other. The bias is specified in terms of -// duration (time) because priorities are expected to usually get lower over time and -// therefore a future minimum prediction (see EstMinPriority) should monotonously -// decrease with the specified time parameter. -// This time bias can be interpreted as minimum expected active time at the given -// capacity (if the threshold priority stays the same). -// -// Nodes in the pool always have either inactiveFlag or activeFlag set. A new node is -// added to the pool by externally setting inactiveFlag. priorityPool can switch a node -// between inactiveFlag and activeFlag at any time. Nodes can be removed from the pool -// by externally resetting both flags. activeFlag should not be set externally. -// -// The highest priority nodes in "inactive" state are moved to "active" state as soon as -// the minimum capacity can be granted for them. The capacity of lower priority active -// nodes is reduced or they are demoted to "inactive" state if their priority is -// insufficient even at minimal capacity. -type priorityPool struct { - setup *serverSetup - ns *nodestate.NodeStateMachine - clock mclock.Clock - lock sync.Mutex - maxCount, maxCap uint64 - minCap uint64 - activeBias time.Duration - capacityStepDiv, fineStepDiv uint64 - - // The snapshot of priority pool for query. - cachedCurve *capacityCurve - ccUpdatedAt mclock.AbsTime - ccUpdateForced bool - - // Runtime status of prioritypool, represents the - // temporary state if tempState is not empty - tempState []*ppNodeInfo - activeCount, activeCap uint64 - activeQueue *prque.LazyQueue[int64, *ppNodeInfo] - inactiveQueue *prque.Prque[int64, *ppNodeInfo] -} - -// ppNodeInfo is the internal node descriptor of priorityPool -type ppNodeInfo struct { - nodePriority nodePriority - node *enode.Node - connected bool - capacity uint64 // only changed when temporary state is committed - activeIndex, inactiveIndex int - - tempState bool // should only be true while the priorityPool lock is held - tempCapacity uint64 // equals capacity when tempState is false - - // the following fields only affect the temporary state and they are set to their - // default value when leaving the temp state - minTarget, stepDiv uint64 - bias time.Duration -} - -// newPriorityPool creates a new priorityPool -func newPriorityPool(ns *nodestate.NodeStateMachine, setup *serverSetup, clock mclock.Clock, minCap uint64, activeBias time.Duration, capacityStepDiv, fineStepDiv uint64) *priorityPool { - pp := &priorityPool{ - setup: setup, - ns: ns, - clock: clock, - inactiveQueue: prque.New[int64, *ppNodeInfo](inactiveSetIndex), - minCap: minCap, - activeBias: activeBias, - capacityStepDiv: capacityStepDiv, - fineStepDiv: fineStepDiv, - } - if pp.activeBias < time.Duration(1) { - pp.activeBias = time.Duration(1) - } - pp.activeQueue = prque.NewLazyQueue(activeSetIndex, activePriority, pp.activeMaxPriority, clock, lazyQueueRefresh) - - ns.SubscribeField(pp.setup.balanceField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - if newValue != nil { - c := &ppNodeInfo{ - node: node, - nodePriority: newValue.(nodePriority), - activeIndex: -1, - inactiveIndex: -1, - } - ns.SetFieldSub(node, pp.setup.queueField, c) - ns.SetStateSub(node, setup.inactiveFlag, nodestate.Flags{}, 0) - } else { - ns.SetStateSub(node, nodestate.Flags{}, pp.setup.activeFlag.Or(pp.setup.inactiveFlag), 0) - if n, _ := pp.ns.GetField(node, pp.setup.queueField).(*ppNodeInfo); n != nil { - pp.disconnectNode(n) - } - ns.SetFieldSub(node, pp.setup.capacityField, nil) - ns.SetFieldSub(node, pp.setup.queueField, nil) - } - }) - ns.SubscribeState(pp.setup.activeFlag.Or(pp.setup.inactiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { - if c, _ := pp.ns.GetField(node, pp.setup.queueField).(*ppNodeInfo); c != nil { - if oldState.IsEmpty() { - pp.connectNode(c) - } - if newState.IsEmpty() { - pp.disconnectNode(c) - } - } - }) - ns.SubscribeState(pp.setup.updateFlag, func(node *enode.Node, oldState, newState nodestate.Flags) { - if !newState.IsEmpty() { - pp.updatePriority(node) - } - }) - return pp -} - -// requestCapacity tries to set the capacity of a connected node to the highest possible -// value inside the given target range. If maxTarget is not reachable then the capacity is -// iteratively reduced in fine steps based on the fineStepDiv parameter until minTarget is reached. -// The function returns the new capacity if successful and the original capacity otherwise. -// Note: this function should run inside a NodeStateMachine operation -func (pp *priorityPool) requestCapacity(node *enode.Node, minTarget, maxTarget uint64, bias time.Duration) uint64 { - pp.lock.Lock() - pp.activeQueue.Refresh() - - if minTarget < pp.minCap { - minTarget = pp.minCap - } - if maxTarget < minTarget { - maxTarget = minTarget - } - if bias < pp.activeBias { - bias = pp.activeBias - } - c, _ := pp.ns.GetField(node, pp.setup.queueField).(*ppNodeInfo) - if c == nil { - log.Error("requestCapacity called for unknown node", "id", node.ID()) - pp.lock.Unlock() - return 0 - } - pp.setTempState(c) - if maxTarget > c.capacity { - pp.setTempStepDiv(c, pp.fineStepDiv) - pp.setTempBias(c, bias) - } - pp.setTempCapacity(c, maxTarget) - c.minTarget = minTarget - pp.removeFromQueues(c) - pp.activeQueue.Push(c) - pp.enforceLimits() - updates := pp.finalizeChanges(c.tempCapacity >= minTarget && c.tempCapacity <= maxTarget && c.tempCapacity != c.capacity) - pp.lock.Unlock() - pp.updateFlags(updates) - return c.capacity -} - -// SetLimits sets the maximum number and total capacity of simultaneously active nodes -func (pp *priorityPool) SetLimits(maxCount, maxCap uint64) { - pp.lock.Lock() - pp.activeQueue.Refresh() - inc := (maxCount > pp.maxCount) || (maxCap > pp.maxCap) - dec := (maxCount < pp.maxCount) || (maxCap < pp.maxCap) - pp.maxCount, pp.maxCap = maxCount, maxCap - - var updates []capUpdate - if dec { - pp.enforceLimits() - updates = pp.finalizeChanges(true) - } - if inc { - updates = append(updates, pp.tryActivate(false)...) - } - pp.lock.Unlock() - pp.ns.Operation(func() { pp.updateFlags(updates) }) -} - -// setActiveBias sets the bias applied when trying to activate inactive nodes -func (pp *priorityPool) setActiveBias(bias time.Duration) { - pp.lock.Lock() - pp.activeBias = bias - if pp.activeBias < time.Duration(1) { - pp.activeBias = time.Duration(1) - } - updates := pp.tryActivate(false) - pp.lock.Unlock() - pp.ns.Operation(func() { pp.updateFlags(updates) }) -} - -// Active returns the number and total capacity of currently active nodes -func (pp *priorityPool) Active() (uint64, uint64) { - pp.lock.Lock() - defer pp.lock.Unlock() - - return pp.activeCount, pp.activeCap -} - -// Inactive returns the number of currently inactive nodes -func (pp *priorityPool) Inactive() int { - pp.lock.Lock() - defer pp.lock.Unlock() - - return pp.inactiveQueue.Size() -} - -// Limits returns the maximum allowed number and total capacity of active nodes -func (pp *priorityPool) Limits() (uint64, uint64) { - pp.lock.Lock() - defer pp.lock.Unlock() - - return pp.maxCount, pp.maxCap -} - -// inactiveSetIndex callback updates ppNodeInfo item index in inactiveQueue -func inactiveSetIndex(a *ppNodeInfo, index int) { - a.inactiveIndex = index -} - -// activeSetIndex callback updates ppNodeInfo item index in activeQueue -func activeSetIndex(a *ppNodeInfo, index int) { - a.activeIndex = index -} - -// invertPriority inverts a priority value. The active queue uses inverted priorities -// because the node on the top is the first to be deactivated. -func invertPriority(p int64) int64 { - if p == math.MinInt64 { - return math.MaxInt64 - } - return -p -} - -// activePriority callback returns actual priority of ppNodeInfo item in activeQueue -func activePriority(c *ppNodeInfo) int64 { - if c.bias == 0 { - return invertPriority(c.nodePriority.priority(c.tempCapacity)) - } else { - return invertPriority(c.nodePriority.estimatePriority(c.tempCapacity, 0, 0, c.bias, true)) - } -} - -// activeMaxPriority callback returns estimated maximum priority of ppNodeInfo item in activeQueue -func (pp *priorityPool) activeMaxPriority(c *ppNodeInfo, until mclock.AbsTime) int64 { - future := time.Duration(until - pp.clock.Now()) - if future < 0 { - future = 0 - } - return invertPriority(c.nodePriority.estimatePriority(c.tempCapacity, 0, future, c.bias, false)) -} - -// inactivePriority callback returns actual priority of ppNodeInfo item in inactiveQueue -func (pp *priorityPool) inactivePriority(p *ppNodeInfo) int64 { - return p.nodePriority.priority(pp.minCap) -} - -// removeFromQueues removes the node from the active/inactive queues -func (pp *priorityPool) removeFromQueues(c *ppNodeInfo) { - if c.activeIndex >= 0 { - pp.activeQueue.Remove(c.activeIndex) - } - if c.inactiveIndex >= 0 { - pp.inactiveQueue.Remove(c.inactiveIndex) - } -} - -// connectNode is called when a new node has been added to the pool (inactiveFlag set) -// Note: this function should run inside a NodeStateMachine operation -func (pp *priorityPool) connectNode(c *ppNodeInfo) { - pp.lock.Lock() - pp.activeQueue.Refresh() - if c.connected { - pp.lock.Unlock() - return - } - c.connected = true - pp.inactiveQueue.Push(c, pp.inactivePriority(c)) - updates := pp.tryActivate(false) - pp.lock.Unlock() - pp.updateFlags(updates) -} - -// disconnectNode is called when a node has been removed from the pool (both inactiveFlag -// and activeFlag reset) -// Note: this function should run inside a NodeStateMachine operation -func (pp *priorityPool) disconnectNode(c *ppNodeInfo) { - pp.lock.Lock() - pp.activeQueue.Refresh() - if !c.connected { - pp.lock.Unlock() - return - } - c.connected = false - pp.removeFromQueues(c) - - var updates []capUpdate - if c.capacity != 0 { - pp.setTempState(c) - pp.setTempCapacity(c, 0) - updates = pp.tryActivate(true) - } - pp.lock.Unlock() - pp.updateFlags(updates) -} - -// setTempState internally puts a node in a temporary state that can either be reverted -// or confirmed later. This temporary state allows changing the capacity of a node and -// moving it between the active and inactive queue. activeFlag/inactiveFlag and -// capacityField are not changed while the changes are still temporary. -func (pp *priorityPool) setTempState(c *ppNodeInfo) { - if c.tempState { - return - } - c.tempState = true - if c.tempCapacity != c.capacity { // should never happen - log.Error("tempCapacity != capacity when entering tempState") - } - // Assign all the defaults to the temp state. - c.minTarget = pp.minCap - c.stepDiv = pp.capacityStepDiv - c.bias = 0 - pp.tempState = append(pp.tempState, c) -} - -// unsetTempState revokes the temp status of the node and reset all internal -// fields to the default value. -func (pp *priorityPool) unsetTempState(c *ppNodeInfo) { - if !c.tempState { - return - } - c.tempState = false - if c.tempCapacity != c.capacity { // should never happen - log.Error("tempCapacity != capacity when leaving tempState") - } - c.minTarget = pp.minCap - c.stepDiv = pp.capacityStepDiv - c.bias = 0 -} - -// setTempCapacity changes the capacity of a node in the temporary state and adjusts -// activeCap and activeCount accordingly. Since this change is performed in the temporary -// state it should be called after setTempState and before finalizeChanges. -func (pp *priorityPool) setTempCapacity(c *ppNodeInfo, cap uint64) { - if !c.tempState { // should never happen - log.Error("Node is not in temporary state") - return - } - pp.activeCap += cap - c.tempCapacity - if c.tempCapacity == 0 { - pp.activeCount++ - } - if cap == 0 { - pp.activeCount-- - } - c.tempCapacity = cap -} - -// setTempBias changes the connection bias of a node in the temporary state. -func (pp *priorityPool) setTempBias(c *ppNodeInfo, bias time.Duration) { - if !c.tempState { // should never happen - log.Error("Node is not in temporary state") - return - } - c.bias = bias -} - -// setTempStepDiv changes the capacity divisor of a node in the temporary state. -func (pp *priorityPool) setTempStepDiv(c *ppNodeInfo, stepDiv uint64) { - if !c.tempState { // should never happen - log.Error("Node is not in temporary state") - return - } - c.stepDiv = stepDiv -} - -// enforceLimits enforces active node count and total capacity limits. It returns the -// lowest active node priority. Note that this function is performed on the temporary -// internal state. -func (pp *priorityPool) enforceLimits() (*ppNodeInfo, int64) { - if pp.activeCap <= pp.maxCap && pp.activeCount <= pp.maxCount { - return nil, math.MinInt64 - } - var ( - lastNode *ppNodeInfo - maxActivePriority int64 - ) - pp.activeQueue.MultiPop(func(c *ppNodeInfo, priority int64) bool { - lastNode = c - pp.setTempState(c) - maxActivePriority = priority - if c.tempCapacity == c.minTarget || pp.activeCount > pp.maxCount { - pp.setTempCapacity(c, 0) - } else { - sub := c.tempCapacity / c.stepDiv - if sub == 0 { - sub = 1 - } - if c.tempCapacity-sub < c.minTarget { - sub = c.tempCapacity - c.minTarget - } - pp.setTempCapacity(c, c.tempCapacity-sub) - pp.activeQueue.Push(c) - } - return pp.activeCap > pp.maxCap || pp.activeCount > pp.maxCount - }) - return lastNode, invertPriority(maxActivePriority) -} - -// finalizeChanges either commits or reverts temporary changes. The necessary capacity -// field and according flag updates are not performed here but returned in a list because -// they should be performed while the mutex is not held. -func (pp *priorityPool) finalizeChanges(commit bool) (updates []capUpdate) { - for _, c := range pp.tempState { - // always remove and push back in order to update biased priority - pp.removeFromQueues(c) - oldCapacity := c.capacity - if commit { - c.capacity = c.tempCapacity - } else { - pp.setTempCapacity(c, c.capacity) // revert activeCount/activeCap - } - pp.unsetTempState(c) - - if c.connected { - if c.capacity != 0 { - pp.activeQueue.Push(c) - } else { - pp.inactiveQueue.Push(c, pp.inactivePriority(c)) - } - if c.capacity != oldCapacity { - updates = append(updates, capUpdate{c.node, oldCapacity, c.capacity}) - } - } - } - pp.tempState = nil - if commit { - pp.ccUpdateForced = true - } - return -} - -// capUpdate describes a capacityField and activeFlag/inactiveFlag update -type capUpdate struct { - node *enode.Node - oldCap, newCap uint64 -} - -// updateFlags performs capacityField and activeFlag/inactiveFlag updates while the -// pool mutex is not held -// Note: this function should run inside a NodeStateMachine operation -func (pp *priorityPool) updateFlags(updates []capUpdate) { - for _, f := range updates { - if f.oldCap == 0 { - pp.ns.SetStateSub(f.node, pp.setup.activeFlag, pp.setup.inactiveFlag, 0) - } - if f.newCap == 0 { - pp.ns.SetStateSub(f.node, pp.setup.inactiveFlag, pp.setup.activeFlag, 0) - pp.ns.SetFieldSub(f.node, pp.setup.capacityField, nil) - } else { - pp.ns.SetFieldSub(f.node, pp.setup.capacityField, f.newCap) - } - } -} - -// tryActivate tries to activate inactive nodes if possible -func (pp *priorityPool) tryActivate(commit bool) []capUpdate { - for pp.inactiveQueue.Size() > 0 { - c := pp.inactiveQueue.PopItem() - pp.setTempState(c) - pp.setTempBias(c, pp.activeBias) - pp.setTempCapacity(c, pp.minCap) - pp.activeQueue.Push(c) - pp.enforceLimits() - if c.tempCapacity > 0 { - commit = true - pp.setTempBias(c, 0) - } else { - break - } - } - pp.ccUpdateForced = true - return pp.finalizeChanges(commit) -} - -// updatePriority gets the current priority value of the given node from the nodePriority -// interface and performs the necessary changes. It is triggered by updateFlag. -// Note: this function should run inside a NodeStateMachine operation -func (pp *priorityPool) updatePriority(node *enode.Node) { - pp.lock.Lock() - pp.activeQueue.Refresh() - c, _ := pp.ns.GetField(node, pp.setup.queueField).(*ppNodeInfo) - if c == nil || !c.connected { - pp.lock.Unlock() - return - } - pp.removeFromQueues(c) - if c.capacity != 0 { - pp.activeQueue.Push(c) - } else { - pp.inactiveQueue.Push(c, pp.inactivePriority(c)) - } - updates := pp.tryActivate(false) - pp.lock.Unlock() - pp.updateFlags(updates) -} - -// capacityCurve is a snapshot of the priority pool contents in a format that can efficiently -// estimate how much capacity could be granted to a given node at a given priority level. -type capacityCurve struct { - points []curvePoint // curve points sorted in descending order of priority - index map[enode.ID][]int // curve point indexes belonging to each node - excludeList []int // curve point indexes of excluded node - excludeFirst bool // true if activeCount == maxCount -} - -type curvePoint struct { - freeCap uint64 // available capacity and node count at the current priority level - nextPri int64 // next priority level where more capacity will be available -} - -// getCapacityCurve returns a new or recently cached capacityCurve based on the contents of the pool -func (pp *priorityPool) getCapacityCurve() *capacityCurve { - pp.lock.Lock() - defer pp.lock.Unlock() - - now := pp.clock.Now() - dt := time.Duration(now - pp.ccUpdatedAt) - if !pp.ccUpdateForced && pp.cachedCurve != nil && dt < time.Second*10 { - return pp.cachedCurve - } - - pp.ccUpdateForced = false - pp.ccUpdatedAt = now - curve := &capacityCurve{ - index: make(map[enode.ID][]int), - } - pp.cachedCurve = curve - - var excludeID enode.ID - excludeFirst := pp.maxCount == pp.activeCount - // reduce node capacities or remove nodes until nothing is left in the queue; - // record the available capacity and the necessary priority after each step - lastPri := int64(math.MinInt64) - for pp.activeCap > 0 { - cp := curvePoint{} - if pp.activeCap > pp.maxCap { - log.Error("Active capacity is greater than allowed maximum", "active", pp.activeCap, "maximum", pp.maxCap) - } else { - cp.freeCap = pp.maxCap - pp.activeCap - } - // temporarily increase activeCap to enforce reducing or removing a node capacity - tempCap := cp.freeCap + 1 - pp.activeCap += tempCap - var next *ppNodeInfo - // enforceLimits removes the lowest priority node if it has minimal capacity, - // otherwise reduces its capacity - next, cp.nextPri = pp.enforceLimits() - if cp.nextPri < lastPri { - // enforce monotonicity which may be broken by continuously changing priorities - cp.nextPri = lastPri - } else { - lastPri = cp.nextPri - } - pp.activeCap -= tempCap - if next == nil { - log.Error("getCapacityCurve: cannot remove next element from the priority queue") - break - } - id := next.node.ID() - if excludeFirst { - // if the node count limit is already reached then mark the node with the - // lowest priority for exclusion - curve.excludeFirst = true - excludeID = id - excludeFirst = false - } - // multiple curve points and therefore multiple indexes may belong to a node - // if it was removed in multiple steps (if its capacity was more than the minimum) - curve.index[id] = append(curve.index[id], len(curve.points)) - curve.points = append(curve.points, cp) - } - // restore original state of the queue - pp.finalizeChanges(false) - curve.points = append(curve.points, curvePoint{ - freeCap: pp.maxCap, - nextPri: math.MaxInt64, - }) - if curve.excludeFirst { - curve.excludeList = curve.index[excludeID] - } - return curve -} - -// exclude returns a capacityCurve with the given node excluded from the original curve -func (cc *capacityCurve) exclude(id enode.ID) *capacityCurve { - if excludeList, ok := cc.index[id]; ok { - // return a new version of the curve (only one excluded node can be selected) - // Note: if the first node was excluded by default (excludeFirst == true) then - // we can forget about that and exclude the node with the given id instead. - return &capacityCurve{ - points: cc.points, - index: cc.index, - excludeList: excludeList, - } - } - return cc -} - -func (cc *capacityCurve) getPoint(i int) curvePoint { - cp := cc.points[i] - if i == 0 && cc.excludeFirst { - cp.freeCap = 0 - return cp - } - for ii := len(cc.excludeList) - 1; ii >= 0; ii-- { - ei := cc.excludeList[ii] - if ei < i { - break - } - e1, e2 := cc.points[ei], cc.points[ei+1] - cp.freeCap += e2.freeCap - e1.freeCap - } - return cp -} - -// maxCapacity calculates the maximum capacity available for a node with a given -// (monotonically decreasing) priority vs. capacity function. Note that if the requesting -// node is already in the pool then it should be excluded from the curve in order to get -// the correct result. -func (cc *capacityCurve) maxCapacity(priority func(cap uint64) int64) uint64 { - min, max := 0, len(cc.points)-1 // the curve always has at least one point - for min < max { - mid := (min + max) / 2 - cp := cc.getPoint(mid) - if cp.freeCap == 0 || priority(cp.freeCap) > cp.nextPri { - min = mid + 1 - } else { - max = mid - } - } - cp2 := cc.getPoint(min) - if cp2.freeCap == 0 || min == 0 { - return cp2.freeCap - } - cp1 := cc.getPoint(min - 1) - if priority(cp2.freeCap) > cp1.nextPri { - return cp2.freeCap - } - minc, maxc := cp1.freeCap, cp2.freeCap-1 - for minc < maxc { - midc := (minc + maxc + 1) / 2 - if midc == 0 || priority(midc) > cp1.nextPri { - minc = midc - } else { - maxc = midc - 1 - } - } - return maxc -} diff --git a/les/vflux/server/prioritypool_test.go b/les/vflux/server/prioritypool_test.go deleted file mode 100644 index 60b7b83bbc..0000000000 --- a/les/vflux/server/prioritypool_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2020 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 server - -import ( - "math/rand" - "reflect" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -const ( - testCapacityStepDiv = 100 - testCapacityToleranceDiv = 10 - testMinCap = 100 -) - -type ppTestClient struct { - node *enode.Node - balance, cap uint64 -} - -func (c *ppTestClient) priority(cap uint64) int64 { - return int64(c.balance / cap) -} - -func (c *ppTestClient) estimatePriority(cap uint64, addBalance int64, future, bias time.Duration, update bool) int64 { - return int64(c.balance / cap) -} - -func TestPriorityPool(t *testing.T) { - t.Parallel() - - clock := &mclock.Simulated{} - setup := newServerSetup() - setup.balanceField = setup.setup.NewField("ppTestClient", reflect.TypeOf(&ppTestClient{})) - ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup) - - ns.SubscribeField(setup.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { - if n := ns.GetField(node, setup.balanceField); n != nil { - c := n.(*ppTestClient) - c.cap = newValue.(uint64) - } - }) - pp := newPriorityPool(ns, setup, clock, testMinCap, 0, testCapacityStepDiv, testCapacityStepDiv) - ns.Start() - pp.SetLimits(100, 1000000) - clients := make([]*ppTestClient, 100) - raise := func(c *ppTestClient) { - for { - var ok bool - ns.Operation(func() { - newCap := c.cap + c.cap/testCapacityStepDiv - ok = pp.requestCapacity(c.node, newCap, newCap, 0) == newCap - }) - if !ok { - return - } - } - } - var sumBalance uint64 - check := func(c *ppTestClient) { - expCap := 1000000 * c.balance / sumBalance - capTol := expCap / testCapacityToleranceDiv - if c.cap < expCap-capTol || c.cap > expCap+capTol { - t.Errorf("Wrong node capacity (expected %d, got %d)", expCap, c.cap) - } - } - - for i := range clients { - c := &ppTestClient{ - node: enode.SignNull(&enr.Record{}, enode.ID{byte(i)}), - balance: 100000000000, - cap: 1000, - } - sumBalance += c.balance - clients[i] = c - ns.SetField(c.node, setup.balanceField, c) - ns.SetState(c.node, setup.inactiveFlag, nodestate.Flags{}, 0) - raise(c) - check(c) - } - - for count := 0; count < 100; count++ { - c := clients[rand.Intn(len(clients))] - oldBalance := c.balance - c.balance = uint64(rand.Int63n(100000000000) + 100000000000) - sumBalance += c.balance - oldBalance - pp.ns.SetState(c.node, setup.updateFlag, nodestate.Flags{}, 0) - pp.ns.SetState(c.node, nodestate.Flags{}, setup.updateFlag, 0) - if c.balance > oldBalance { - raise(c) - } else { - for _, c := range clients { - raise(c) - } - } - // check whether capacities are proportional to balances - for _, c := range clients { - check(c) - } - if count%10 == 0 { - // test available capacity calculation with capacity curve - c = clients[rand.Intn(len(clients))] - curve := pp.getCapacityCurve().exclude(c.node.ID()) - - add := uint64(rand.Int63n(10000000000000)) - c.balance += add - sumBalance += add - expCap := curve.maxCapacity(func(cap uint64) int64 { - return int64(c.balance / cap) - }) - var ok bool - expFail := expCap + 10 - if expFail < testMinCap { - expFail = testMinCap - } - ns.Operation(func() { - ok = pp.requestCapacity(c.node, expFail, expFail, 0) == expFail - }) - if ok { - t.Errorf("Request for more than expected available capacity succeeded") - } - if expCap >= testMinCap { - ns.Operation(func() { - ok = pp.requestCapacity(c.node, expCap, expCap, 0) == expCap - }) - if !ok { - t.Errorf("Request for expected available capacity failed") - } - } - c.balance -= add - sumBalance -= add - pp.ns.SetState(c.node, setup.updateFlag, nodestate.Flags{}, 0) - pp.ns.SetState(c.node, nodestate.Flags{}, setup.updateFlag, 0) - for _, c := range clients { - raise(c) - } - } - } - - ns.Stop() -} - -func TestCapacityCurve(t *testing.T) { - t.Parallel() - - clock := &mclock.Simulated{} - setup := newServerSetup() - setup.balanceField = setup.setup.NewField("ppTestClient", reflect.TypeOf(&ppTestClient{})) - ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup) - - pp := newPriorityPool(ns, setup, clock, 400000, 0, 2, 2) - ns.Start() - pp.SetLimits(10, 10000000) - clients := make([]*ppTestClient, 10) - - for i := range clients { - c := &ppTestClient{ - node: enode.SignNull(&enr.Record{}, enode.ID{byte(i)}), - balance: 100000000000 * uint64(i+1), - cap: 1000000, - } - clients[i] = c - ns.SetField(c.node, setup.balanceField, c) - ns.SetState(c.node, setup.inactiveFlag, nodestate.Flags{}, 0) - ns.Operation(func() { - pp.requestCapacity(c.node, c.cap, c.cap, 0) - }) - } - - curve := pp.getCapacityCurve() - check := func(balance, expCap uint64) { - cap := curve.maxCapacity(func(cap uint64) int64 { - return int64(balance / cap) - }) - var fail bool - if cap == 0 || expCap == 0 { - fail = cap != expCap - } else { - pri := balance / cap - expPri := balance / expCap - fail = pri != expPri && pri != expPri+1 - } - if fail { - t.Errorf("Incorrect capacity for %d balance (got %d, expected %d)", balance, cap, expCap) - } - } - - check(0, 0) - check(10000000000, 100000) - check(50000000000, 500000) - check(100000000000, 1000000) - check(200000000000, 1000000) - check(300000000000, 1500000) - check(450000000000, 1500000) - check(600000000000, 2000000) - check(800000000000, 2000000) - check(1000000000000, 2500000) - - pp.SetLimits(11, 10000000) - curve = pp.getCapacityCurve() - - check(0, 0) - check(10000000000, 100000) - check(50000000000, 500000) - check(150000000000, 750000) - check(200000000000, 1000000) - check(220000000000, 1100000) - check(275000000000, 1100000) - check(375000000000, 1500000) - check(450000000000, 1500000) - check(600000000000, 2000000) - check(800000000000, 2000000) - check(1000000000000, 2500000) - - ns.Stop() -} diff --git a/les/vflux/server/service.go b/les/vflux/server/service.go deleted file mode 100644 index 40515f072e..0000000000 --- a/les/vflux/server/service.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2021 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 server - -import ( - "net" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/les/utils" - "github.com/ethereum/go-ethereum/les/vflux" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/rlp" -) - -type ( - // Server serves vflux requests - Server struct { - limiter *utils.Limiter - lock sync.Mutex - services map[string]*serviceEntry - delayPerRequest time.Duration - } - - // Service is a service registered at the Server and identified by a string id - Service interface { - Handle(id enode.ID, address string, name string, data []byte) []byte // never called concurrently - } - - serviceEntry struct { - id, desc string - backend Service - } -) - -// NewServer creates a new Server -func NewServer(delayPerRequest time.Duration) *Server { - return &Server{ - limiter: utils.NewLimiter(1000), - delayPerRequest: delayPerRequest, - services: make(map[string]*serviceEntry), - } -} - -// Register registers a Service -func (s *Server) Register(b Service, id, desc string) { - srv := &serviceEntry{backend: b, id: id, desc: desc} - if strings.Contains(srv.id, ":") { - // srv.id + ":" will be used as a service database prefix - log.Error("Service ID contains ':'", "id", srv.id) - return - } - s.lock.Lock() - s.services[srv.id] = srv - s.lock.Unlock() -} - -// Serve serves a vflux request batch -// Note: requests are served by the Handle functions of the registered services. Serve -// may be called concurrently but the Handle functions are called sequentially and -// therefore thread safety is guaranteed. -func (s *Server) Serve(id enode.ID, address string, requests vflux.Requests) vflux.Replies { - reqLen := uint(len(requests)) - if reqLen == 0 || reqLen > vflux.MaxRequestLength { - return nil - } - // Note: the value parameter will be supplied by the token sale module (total amount paid) - ch := <-s.limiter.Add(id, address, 0, reqLen) - if ch == nil { - return nil - } - // Note: the limiter ensures that the following section is not running concurrently, - // the lock only protects against contention caused by new service registration - s.lock.Lock() - results := make(vflux.Replies, len(requests)) - for i, req := range requests { - if service := s.services[req.Service]; service != nil { - results[i] = service.backend.Handle(id, address, req.Name, req.Params) - } - } - s.lock.Unlock() - time.Sleep(s.delayPerRequest * time.Duration(reqLen)) - close(ch) - return results -} - -// ServeEncoded serves an encoded vflux request batch and returns the encoded replies -func (s *Server) ServeEncoded(id enode.ID, addr *net.UDPAddr, req []byte) []byte { - var requests vflux.Requests - if err := rlp.DecodeBytes(req, &requests); err != nil { - return nil - } - results := s.Serve(id, addr.String(), requests) - if results == nil { - return nil - } - res, _ := rlp.EncodeToBytes(&results) - return res -} - -// Stop shuts down the server -func (s *Server) Stop() { - s.limiter.Stop() -} diff --git a/les/vflux/server/status.go b/les/vflux/server/status.go deleted file mode 100644 index 2d7e25b684..0000000000 --- a/les/vflux/server/status.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 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 server - -import ( - "reflect" - - "github.com/ethereum/go-ethereum/p2p/nodestate" -) - -type peerWrapper struct{ clientPeer } // the NodeStateMachine type system needs this wrapper - -// serverSetup is a wrapper of the node state machine setup, which contains -// all the created flags and fields used in the vflux server side. -type serverSetup struct { - setup *nodestate.Setup - clientField nodestate.Field // Field contains the client peer handler - - // Flags and fields controlled by balance tracker. BalanceTracker - // is responsible for setting/deleting these flags or fields. - priorityFlag nodestate.Flags // Flag is set if the node has a positive balance - updateFlag nodestate.Flags // Flag is set whenever the node balance is changed(priority changed) - balanceField nodestate.Field // Field contains the client balance for priority calculation - - // Flags and fields controlled by priority queue. Priority queue - // is responsible for setting/deleting these flags or fields. - activeFlag nodestate.Flags // Flag is set if the node is active - inactiveFlag nodestate.Flags // Flag is set if the node is inactive - capacityField nodestate.Field // Field contains the capacity of the node - queueField nodestate.Field // Field contains the information in the priority queue -} - -// newServerSetup initializes the setup for state machine and returns the flags/fields group. -func newServerSetup() *serverSetup { - setup := &serverSetup{setup: &nodestate.Setup{}} - setup.clientField = setup.setup.NewField("client", reflect.TypeOf(peerWrapper{})) - setup.priorityFlag = setup.setup.NewFlag("priority") - setup.updateFlag = setup.setup.NewFlag("update") - setup.balanceField = setup.setup.NewField("balance", reflect.TypeOf(&nodeBalance{})) - setup.activeFlag = setup.setup.NewFlag("active") - setup.inactiveFlag = setup.setup.NewFlag("inactive") - setup.capacityField = setup.setup.NewField("capacity", reflect.TypeOf(uint64(0))) - setup.queueField = setup.setup.NewField("queue", reflect.TypeOf(&ppNodeInfo{})) - return setup -} diff --git a/tests/fuzzers/les/les-fuzzer.go b/tests/fuzzers/les/les-fuzzer.go deleted file mode 100644 index 209dda0bb9..0000000000 --- a/tests/fuzzers/les/les-fuzzer.go +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright 2021 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 les - -import ( - "bytes" - "encoding/binary" - "io" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/txpool" - "github.com/ethereum/go-ethereum/core/txpool/legacypool" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" - l "github.com/ethereum/go-ethereum/les" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" -) - -var ( - bankKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - bankAddr = crypto.PubkeyToAddress(bankKey.PublicKey) - bankFunds = new(big.Int).Mul(big.NewInt(100), big.NewInt(params.Ether)) - - testChainLen = 256 - testContractCode = common.Hex2Bytes("606060405260cc8060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360cd2685146041578063c16431b914606b57603f565b005b6055600480803590602001909190505060a9565b6040518082815260200191505060405180910390f35b60886004808035906020019091908035906020019091905050608a565b005b80600060005083606481101560025790900160005b50819055505b5050565b6000600060005082606481101560025790900160005b5054905060c7565b91905056") - - chain *core.BlockChain - addresses []common.Address - txHashes []common.Hash - - chtTrie *trie.Trie - bloomTrie *trie.Trie - chtKeys [][]byte - bloomKeys [][]byte -) - -func makechain() (bc *core.BlockChain, addresses []common.Address, txHashes []common.Hash) { - gspec := &core.Genesis{ - Config: params.TestChainConfig, - Alloc: core.GenesisAlloc{bankAddr: {Balance: bankFunds}}, - GasLimit: 100000000, - } - signer := types.HomesteadSigner{} - _, blocks, _ := core.GenerateChainWithGenesis(gspec, ethash.NewFaker(), testChainLen, - func(i int, gen *core.BlockGen) { - var ( - tx *types.Transaction - addr common.Address - ) - nonce := uint64(i) - if i%4 == 0 { - tx, _ = types.SignTx(types.NewContractCreation(nonce, big.NewInt(0), 200000, big.NewInt(params.GWei), testContractCode), signer, bankKey) - addr = crypto.CreateAddress(bankAddr, nonce) - } else { - addr = common.BigToAddress(big.NewInt(int64(i))) - tx, _ = types.SignTx(types.NewTransaction(nonce, addr, big.NewInt(10000), params.TxGas, big.NewInt(params.GWei), nil), signer, bankKey) - } - gen.AddTx(tx) - addresses = append(addresses, addr) - txHashes = append(txHashes, tx.Hash()) - }) - bc, _ = core.NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) - if _, err := bc.InsertChain(blocks); err != nil { - panic(err) - } - return -} - -func makeTries() (chtTrie *trie.Trie, bloomTrie *trie.Trie, chtKeys, bloomKeys [][]byte) { - chtTrie = trie.NewEmpty(trie.NewDatabase(rawdb.NewMemoryDatabase(), trie.HashDefaults)) - bloomTrie = trie.NewEmpty(trie.NewDatabase(rawdb.NewMemoryDatabase(), trie.HashDefaults)) - for i := 0; i < testChainLen; i++ { - // The element in CHT is -> - key := make([]byte, 8) - binary.BigEndian.PutUint64(key, uint64(i+1)) - chtTrie.MustUpdate(key, []byte{0x1, 0xf}) - chtKeys = append(chtKeys, key) - - // The element in Bloom trie is <2 byte bit index> + -> bloom - key2 := make([]byte, 10) - binary.BigEndian.PutUint64(key2[2:], uint64(i+1)) - bloomTrie.MustUpdate(key2, []byte{0x2, 0xe}) - bloomKeys = append(bloomKeys, key2) - } - return -} - -func init() { - chain, addresses, txHashes = makechain() - chtTrie, bloomTrie, chtKeys, bloomKeys = makeTries() -} - -type fuzzer struct { - chain *core.BlockChain - pool *txpool.TxPool - - chainLen int - addresses []common.Address - txs []common.Hash - nonce uint64 - - chtKeys [][]byte - bloomKeys [][]byte - chtTrie *trie.Trie - bloomTrie *trie.Trie - - input io.Reader - exhausted bool -} - -func newFuzzer(input []byte) *fuzzer { - pool := legacypool.New(legacypool.DefaultConfig, chain) - txpool, _ := txpool.New(new(big.Int).SetUint64(legacypool.DefaultConfig.PriceLimit), chain, []txpool.SubPool{pool}) - - return &fuzzer{ - chain: chain, - chainLen: testChainLen, - addresses: addresses, - txs: txHashes, - chtTrie: chtTrie, - bloomTrie: bloomTrie, - chtKeys: chtKeys, - bloomKeys: bloomKeys, - nonce: uint64(len(txHashes)), - pool: txpool, - input: bytes.NewReader(input), - } -} - -func (f *fuzzer) read(size int) []byte { - out := make([]byte, size) - if _, err := f.input.Read(out); err != nil { - f.exhausted = true - } - return out -} - -func (f *fuzzer) randomByte() byte { - d := f.read(1) - return d[0] -} - -func (f *fuzzer) randomBool() bool { - d := f.read(1) - return d[0]&1 == 1 -} - -func (f *fuzzer) randomInt(max int) int { - if max == 0 { - return 0 - } - if max <= 256 { - return int(f.randomByte()) % max - } - var a uint16 - if err := binary.Read(f.input, binary.LittleEndian, &a); err != nil { - f.exhausted = true - } - return int(a % uint16(max)) -} - -func (f *fuzzer) randomX(max int) uint64 { - var a uint16 - if err := binary.Read(f.input, binary.LittleEndian, &a); err != nil { - f.exhausted = true - } - if a < 0x8000 { - return uint64(a%uint16(max+1)) - 1 - } - return (uint64(1)<<(a%64+1) - 1) & (uint64(a) * 343897772345826595) -} - -func (f *fuzzer) randomBlockHash() common.Hash { - h := f.chain.GetCanonicalHash(uint64(f.randomInt(3 * f.chainLen))) - if h != (common.Hash{}) { - return h - } - return common.BytesToHash(f.read(common.HashLength)) -} - -func (f *fuzzer) randomAddress() []byte { - i := f.randomInt(3 * len(f.addresses)) - if i < len(f.addresses) { - return f.addresses[i].Bytes() - } - return f.read(common.AddressLength) -} - -func (f *fuzzer) randomCHTTrieKey() []byte { - i := f.randomInt(3 * len(f.chtKeys)) - if i < len(f.chtKeys) { - return f.chtKeys[i] - } - return f.read(8) -} - -func (f *fuzzer) randomBloomTrieKey() []byte { - i := f.randomInt(3 * len(f.bloomKeys)) - if i < len(f.bloomKeys) { - return f.bloomKeys[i] - } - return f.read(10) -} - -func (f *fuzzer) randomTxHash() common.Hash { - i := f.randomInt(3 * len(f.txs)) - if i < len(f.txs) { - return f.txs[i] - } - return common.BytesToHash(f.read(common.HashLength)) -} - -func (f *fuzzer) BlockChain() *core.BlockChain { - return f.chain -} - -func (f *fuzzer) TxPool() *txpool.TxPool { - return f.pool -} - -func (f *fuzzer) ArchiveMode() bool { - return false -} - -func (f *fuzzer) AddTxsSync() bool { - return false -} - -func (f *fuzzer) GetHelperTrie(typ uint, index uint64) *trie.Trie { - if typ == 0 { - return f.chtTrie - } else if typ == 1 { - return f.bloomTrie - } - return nil -} - -type dummyMsg struct { - data []byte -} - -func (d dummyMsg) Decode(val interface{}) error { - return rlp.DecodeBytes(d.data, val) -} - -func (f *fuzzer) doFuzz(msgCode uint64, packet interface{}) { - enc, err := rlp.EncodeToBytes(packet) - if err != nil { - panic(err) - } - version := f.randomInt(3) + 2 // [LES2, LES3, LES4] - peer, closeFn := l.NewFuzzerPeer(version) - defer closeFn() - fn, _, _, err := l.Les3[msgCode].Handle(dummyMsg{enc}) - if err != nil { - panic(err) - } - fn(f, peer, func() bool { return true }) -} - -func fuzz(input []byte) int { - // We expect some large inputs - if len(input) < 100 { - return -1 - } - f := newFuzzer(input) - if f.exhausted { - return -1 - } - for !f.exhausted { - switch f.randomInt(8) { - case 0: - req := &l.GetBlockHeadersPacket{ - Query: l.GetBlockHeadersData{ - Amount: f.randomX(l.MaxHeaderFetch + 1), - Skip: f.randomX(10), - Reverse: f.randomBool(), - }, - } - if f.randomBool() { - req.Query.Origin.Hash = f.randomBlockHash() - } else { - req.Query.Origin.Number = uint64(f.randomInt(f.chainLen * 2)) - } - f.doFuzz(l.GetBlockHeadersMsg, req) - - case 1: - req := &l.GetBlockBodiesPacket{Hashes: make([]common.Hash, f.randomInt(l.MaxBodyFetch+1))} - for i := range req.Hashes { - req.Hashes[i] = f.randomBlockHash() - } - f.doFuzz(l.GetBlockBodiesMsg, req) - - case 2: - req := &l.GetCodePacket{Reqs: make([]l.CodeReq, f.randomInt(l.MaxCodeFetch+1))} - for i := range req.Reqs { - req.Reqs[i] = l.CodeReq{ - BHash: f.randomBlockHash(), - AccountAddress: f.randomAddress(), - } - } - f.doFuzz(l.GetCodeMsg, req) - - case 3: - req := &l.GetReceiptsPacket{Hashes: make([]common.Hash, f.randomInt(l.MaxReceiptFetch+1))} - for i := range req.Hashes { - req.Hashes[i] = f.randomBlockHash() - } - f.doFuzz(l.GetReceiptsMsg, req) - - case 4: - req := &l.GetProofsPacket{Reqs: make([]l.ProofReq, f.randomInt(l.MaxProofsFetch+1))} - for i := range req.Reqs { - if f.randomBool() { - req.Reqs[i] = l.ProofReq{ - BHash: f.randomBlockHash(), - AccountAddress: f.randomAddress(), - Key: f.randomAddress(), - FromLevel: uint(f.randomX(3)), - } - } else { - req.Reqs[i] = l.ProofReq{ - BHash: f.randomBlockHash(), - Key: f.randomAddress(), - FromLevel: uint(f.randomX(3)), - } - } - } - f.doFuzz(l.GetProofsV2Msg, req) - - case 5: - req := &l.GetHelperTrieProofsPacket{Reqs: make([]l.HelperTrieReq, f.randomInt(l.MaxHelperTrieProofsFetch+1))} - for i := range req.Reqs { - switch f.randomInt(3) { - case 0: - // Canonical hash trie - req.Reqs[i] = l.HelperTrieReq{ - Type: 0, - TrieIdx: f.randomX(3), - Key: f.randomCHTTrieKey(), - FromLevel: uint(f.randomX(3)), - AuxReq: uint(2), - } - case 1: - // Bloom trie - req.Reqs[i] = l.HelperTrieReq{ - Type: 1, - TrieIdx: f.randomX(3), - Key: f.randomBloomTrieKey(), - FromLevel: uint(f.randomX(3)), - AuxReq: 0, - } - default: - // Random trie - req.Reqs[i] = l.HelperTrieReq{ - Type: 2, - TrieIdx: f.randomX(3), - Key: f.randomCHTTrieKey(), - FromLevel: uint(f.randomX(3)), - AuxReq: 0, - } - } - } - f.doFuzz(l.GetHelperTrieProofsMsg, req) - - case 6: - req := &l.SendTxPacket{Txs: make([]*types.Transaction, f.randomInt(l.MaxTxSend+1))} - signer := types.HomesteadSigner{} - for i := range req.Txs { - var nonce uint64 - if f.randomBool() { - nonce = uint64(f.randomByte()) - } else { - nonce = f.nonce - f.nonce += 1 - } - req.Txs[i], _ = types.SignTx(types.NewTransaction(nonce, common.Address{}, big.NewInt(10000), params.TxGas, big.NewInt(1000000000*int64(f.randomByte())), nil), signer, bankKey) - } - f.doFuzz(l.SendTxV2Msg, req) - - case 7: - req := &l.GetTxStatusPacket{Hashes: make([]common.Hash, f.randomInt(l.MaxTxStatus+1))} - for i := range req.Hashes { - req.Hashes[i] = f.randomTxHash() - } - f.doFuzz(l.GetTxStatusMsg, req) - } - } - return 0 -} diff --git a/tests/fuzzers/les/les_test.go b/tests/fuzzers/les/les_test.go deleted file mode 100644 index 53af45ceb4..0000000000 --- a/tests/fuzzers/les/les_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 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 les - -import "testing" - -func Fuzz(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - fuzz(data) - }) -} diff --git a/tests/fuzzers/vflux/clientpool-fuzzer.go b/tests/fuzzers/vflux/clientpool-fuzzer.go deleted file mode 100644 index de694a7b3f..0000000000 --- a/tests/fuzzers/vflux/clientpool-fuzzer.go +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright 2021 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 vflux - -import ( - "bytes" - "encoding/binary" - "io" - "math" - "math/big" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/ethereum/go-ethereum/les/vflux" - vfs "github.com/ethereum/go-ethereum/les/vflux/server" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/rlp" -) - -var ( - debugMode = false - doLog = func(msg string, ctx ...interface{}) { - if !debugMode { - return - } - log.Info(msg, ctx...) - } -) - -type fuzzer struct { - peers [256]*clientPeer - disconnectList []*clientPeer - input io.Reader - exhausted bool - activeCount, activeCap uint64 - maxCount, maxCap uint64 -} - -type clientPeer struct { - fuzzer *fuzzer - node *enode.Node - freeID string - timeout time.Duration - - balance vfs.ConnectedBalance - capacity uint64 -} - -func (p *clientPeer) Node() *enode.Node { - return p.node -} - -func (p *clientPeer) FreeClientId() string { - return p.freeID -} - -func (p *clientPeer) InactiveAllowance() time.Duration { - return p.timeout -} - -func (p *clientPeer) UpdateCapacity(newCap uint64, requested bool) { - origin, originTotal := p.capacity, p.fuzzer.activeCap - p.fuzzer.activeCap -= p.capacity - if p.capacity != 0 { - p.fuzzer.activeCount-- - } - p.capacity = newCap - p.fuzzer.activeCap += p.capacity - if p.capacity != 0 { - p.fuzzer.activeCount++ - } - doLog("Update capacity", "peer", p.node.ID(), "origin", origin, "cap", newCap, "origintotal", originTotal, "total", p.fuzzer.activeCap, "requested", requested) -} - -func (p *clientPeer) Disconnect() { - origin, originTotal := p.capacity, p.fuzzer.activeCap - p.fuzzer.disconnectList = append(p.fuzzer.disconnectList, p) - p.fuzzer.activeCap -= p.capacity - if p.capacity != 0 { - p.fuzzer.activeCount-- - } - p.capacity = 0 - p.balance = nil - doLog("Disconnect", "peer", p.node.ID(), "origin", origin, "origintotal", originTotal, "total", p.fuzzer.activeCap) -} - -func newFuzzer(input []byte) *fuzzer { - f := &fuzzer{ - input: bytes.NewReader(input), - } - for i := range f.peers { - f.peers[i] = &clientPeer{ - fuzzer: f, - node: enode.SignNull(new(enr.Record), enode.ID{byte(i)}), - freeID: string([]byte{byte(i)}), - timeout: f.randomDelay(), - } - } - return f -} - -func (f *fuzzer) read(size int) []byte { - out := make([]byte, size) - if _, err := f.input.Read(out); err != nil { - f.exhausted = true - } - return out -} - -func (f *fuzzer) randomByte() byte { - d := f.read(1) - return d[0] -} - -func (f *fuzzer) randomBool() bool { - d := f.read(1) - return d[0]&1 == 1 -} - -func (f *fuzzer) randomInt(max int) int { - if max == 0 { - return 0 - } - if max <= 256 { - return int(f.randomByte()) % max - } - var a uint16 - if err := binary.Read(f.input, binary.LittleEndian, &a); err != nil { - f.exhausted = true - } - return int(a % uint16(max)) -} - -func (f *fuzzer) randomTokenAmount(signed bool) int64 { - x := uint64(f.randomInt(65000)) - x = x * x * x * x - - if signed && (x&1) == 1 { - if x <= math.MaxInt64 { - return -int64(x) - } - return math.MinInt64 - } - if x <= math.MaxInt64 { - return int64(x) - } - return math.MaxInt64 -} - -func (f *fuzzer) randomDelay() time.Duration { - delay := f.randomByte() - if delay < 128 { - return time.Duration(delay) * time.Second - } - return 0 -} - -func (f *fuzzer) randomFactors() vfs.PriceFactors { - return vfs.PriceFactors{ - TimeFactor: float64(f.randomByte()) / 25500, - CapacityFactor: float64(f.randomByte()) / 255, - RequestFactor: float64(f.randomByte()) / 255, - } -} - -func (f *fuzzer) connectedBalanceOp(balance vfs.ConnectedBalance, id enode.ID) { - switch f.randomInt(3) { - case 0: - cost := uint64(f.randomTokenAmount(false)) - balance.RequestServed(cost) - doLog("Serve request cost", "id", id, "amount", cost) - case 1: - posFactor, negFactor := f.randomFactors(), f.randomFactors() - balance.SetPriceFactors(posFactor, negFactor) - doLog("Set price factor", "pos", posFactor, "neg", negFactor) - case 2: - balance.GetBalance() - balance.GetRawBalance() - balance.GetPriceFactors() - } -} - -func (f *fuzzer) atomicBalanceOp(balance vfs.AtomicBalanceOperator, id enode.ID) { - switch f.randomInt(3) { - case 0: - amount := f.randomTokenAmount(true) - balance.AddBalance(amount) - doLog("Add balance", "id", id, "amount", amount) - case 1: - pos, neg := uint64(f.randomTokenAmount(false)), uint64(f.randomTokenAmount(false)) - balance.SetBalance(pos, neg) - doLog("Set balance", "id", id, "pos", pos, "neg", neg) - case 2: - balance.GetBalance() - balance.GetRawBalance() - balance.GetPriceFactors() - } -} - -func fuzzClientPool(input []byte) int { - if len(input) > 10000 { - return -1 - } - f := newFuzzer(input) - if f.exhausted { - return 0 - } - clock := &mclock.Simulated{} - db := memorydb.New() - pool := vfs.NewClientPool(db, 10, f.randomDelay(), clock, func() bool { return true }) - pool.Start() - defer pool.Stop() - - count := 0 - for !f.exhausted && count < 1000 { - count++ - switch f.randomInt(11) { - case 0: - i := int(f.randomByte()) - f.peers[i].balance = pool.Register(f.peers[i]) - doLog("Register peer", "id", f.peers[i].node.ID()) - case 1: - i := int(f.randomByte()) - f.peers[i].Disconnect() - doLog("Disconnect peer", "id", f.peers[i].node.ID()) - case 2: - f.maxCount = uint64(f.randomByte()) - f.maxCap = uint64(f.randomByte()) - f.maxCap *= f.maxCap - - count, cap := pool.Limits() - pool.SetLimits(f.maxCount, f.maxCap) - doLog("Set limits", "maxcount", f.maxCount, "maxcap", f.maxCap, "origincount", count, "oricap", cap) - case 3: - bias := f.randomDelay() - pool.SetConnectedBias(f.randomDelay()) - doLog("Set connection bias", "bias", bias) - case 4: - pos, neg := f.randomFactors(), f.randomFactors() - pool.SetDefaultFactors(pos, neg) - doLog("Set default factors", "pos", pos, "neg", neg) - case 5: - pos, neg := uint64(f.randomInt(50000)), uint64(f.randomInt(50000)) - pool.SetExpirationTCs(pos, neg) - doLog("Set expiration constants", "pos", pos, "neg", neg) - case 6: - var ( - index = f.randomByte() - reqCap = uint64(f.randomByte()) - bias = f.randomDelay() - requested = f.randomBool() - ) - pool.SetCapacity(f.peers[index].node, reqCap, bias, requested) - doLog("Set capacity", "id", f.peers[index].node.ID(), "reqcap", reqCap, "bias", bias, "requested", requested) - case 7: - index := f.randomByte() - if balance := f.peers[index].balance; balance != nil { - f.connectedBalanceOp(balance, f.peers[index].node.ID()) - } - case 8: - index := f.randomByte() - pool.BalanceOperation(f.peers[index].node.ID(), f.peers[index].freeID, func(balance vfs.AtomicBalanceOperator) { - count := f.randomInt(4) - for i := 0; i < count; i++ { - f.atomicBalanceOp(balance, f.peers[index].node.ID()) - } - }) - case 9: - pool.TotalTokenAmount() - pool.GetExpirationTCs() - pool.Active() - pool.Limits() - pool.GetPosBalanceIDs(f.peers[f.randomByte()].node.ID(), f.peers[f.randomByte()].node.ID(), f.randomInt(100)) - case 10: - req := vflux.CapacityQueryReq{ - Bias: uint64(f.randomByte()), - AddTokens: make([]vflux.IntOrInf, f.randomInt(vflux.CapacityQueryMaxLen+1)), - } - for i := range req.AddTokens { - v := vflux.IntOrInf{Type: uint8(f.randomInt(4))} - if v.Type < 2 { - v.Value = *big.NewInt(f.randomTokenAmount(false)) - } - req.AddTokens[i] = v - } - reqEnc, err := rlp.EncodeToBytes(&req) - if err != nil { - panic(err) - } - p := int(f.randomByte()) - if p < len(reqEnc) { - reqEnc[p] = f.randomByte() - } - pool.Handle(f.peers[f.randomByte()].node.ID(), f.peers[f.randomByte()].freeID, vflux.CapacityQueryName, reqEnc) - } - - for _, peer := range f.disconnectList { - pool.Unregister(peer) - doLog("Unregister peer", "id", peer.node.ID()) - } - f.disconnectList = nil - if d := f.randomDelay(); d > 0 { - clock.Run(d) - } - doLog("Clientpool stats in fuzzer", "count", f.activeCap, "maxcount", f.maxCount, "cap", f.activeCap, "maxcap", f.maxCap) - activeCount, activeCap := pool.Active() - doLog("Clientpool stats in pool", "count", activeCount, "cap", activeCap) - if activeCount != f.activeCount || activeCap != f.activeCap { - panic(nil) - } - if f.activeCount > f.maxCount || f.activeCap > f.maxCap { - panic(nil) - } - } - return 0 -} diff --git a/tests/fuzzers/vflux/clientpool_test.go b/tests/fuzzers/vflux/clientpool_test.go deleted file mode 100644 index 40c5f22905..0000000000 --- a/tests/fuzzers/vflux/clientpool_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 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 vflux - -import "testing" - -func FuzzClientPool(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - fuzzClientPool(data) - }) -}