From 02cee32c49e532ea50a18c123640b7efdf239832 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 21 Dec 2020 12:16:33 -0600 Subject: [PATCH 01/13] main,ethclient,ethstats: init faucet attach feature Rel https://github.com/etclabscore/core-geth/issues/258 This begins implementation of the ref'd feature proposal. Currently 'ethstats' support is removed because that package relies heavily on access to a *node.Node. Eventually I want to move that packages demands to something that can be satisfied by an API instead. Date: 2020-12-21 12:16:33-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 329 ++++++++++++++++++++++++----------------- ethclient/ethclient.go | 9 ++ ethstats/ethstats.go | 38 ++++- 3 files changed, 234 insertions(+), 142 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index a8f3a2078d..657250cf53 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -35,7 +35,6 @@ import ( "net/url" "os" "path/filepath" - "reflect" "regexp" "strconv" "strings" @@ -51,8 +50,6 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/ethstats" - "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" @@ -76,13 +73,18 @@ var ( testnetFlag = flag.Bool("chain.testnet", false, "Configure genesis and bootnodes for testnet chain defaults") rinkebyFlag = flag.Bool("chain.rinkeby", false, "Configure genesis and bootnodes for rinkeby chain defaults") goerliFlag = flag.Bool("chain.goerli", false, "Configure genesis and bootnodes for goerli chain defaults") + attachFlag = flag.String("attach", "", "Attach to an IPC or WS endpoint") 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") + + // FIXME(meowsbits): Commented because support has been temporarily removed during WIP. + // The ethstats package is dependent on having access to a *node.Node and a backend implementation. + // IMO these needs should be met by access to an API rather than a full stack (eg. needs a downloader.Downloader... really?). + // 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") @@ -108,27 +110,82 @@ var ( gitDate = "" // Git commit date YYYYMMDD of the release (set via linker flags) ) -func faucetDirFromConfig(chainConfig ctypes.ChainConfigurator) string { +func faucetDirFromChainID(chainID uint64) string { datadir := filepath.Join(os.Getenv("HOME"), ".faucet") - for conf, suff := range map[ctypes.ChainConfigurator]string{ - params.MainnetChainConfig: "", - params.ClassicChainConfig: "classic", - params.SocialChainConfig: "social", - params.EthersocialChainConfig: "ethersocial", - params.MixChainConfig: "mix", - params.RopstenChainConfig: "ropsten", - params.RinkebyChainConfig: "rinkeby", - params.GoerliChainConfig: "goerli", - params.KottiChainConfig: "kotti", - params.MordorChainConfig: "mordor", - } { - if reflect.DeepEqual(chainConfig, conf) && suff != "" { - datadir = filepath.Join(datadir, suff) - } + switch chainID { + case params.MainnetChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "") + case params.ClassicChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "classic") + case params.SocialChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "social") + case params.EthersocialChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "ethersocial") + case params.MixChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "mix") + case params.RopstenChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "ropsten") + case params.RinkebyChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "rinkeby") + case params.GoerliChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "goerli") + case params.KottiChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "kotti") + case params.MordorChainConfig.GetChainID().Uint64(): + return filepath.Join(datadir, "mordor") } return datadir } +func parseChainFlags() (gs *genesisT.Genesis, bs string, netid uint64) { + var configs = []struct { + flag bool + gs *genesisT.Genesis + bs []string + }{ + {*foundationFlag, params.DefaultGenesisBlock(), nil}, + {*classicFlag, params.DefaultClassicGenesisBlock(), nil}, + {*mordorFlag, params.DefaultMordorGenesisBlock(), nil}, + {*socialFlag, params.DefaultSocialGenesisBlock(), params.SocialBootnodes}, + {*ethersocialFlag, params.DefaultEthersocialGenesisBlock(), params.EthersocialBootnodes}, + {*mixFlag, params.DefaultMixGenesisBlock(), params.MixBootnodes}, + {*testnetFlag, params.DefaultRopstenGenesisBlock(), nil}, + {*rinkebyFlag, params.DefaultRinkebyGenesisBlock(), nil}, + {*kottiFlag, params.DefaultKottiGenesisBlock(), nil}, + {*goerliFlag, params.DefaultGoerliGenesisBlock(), nil}, + } + + var bss []string + for _, conf := range configs { + if conf.flag { + gs, bss, netid = conf.gs, conf.bs, *conf.gs.Config.GetNetworkID() + break + } + } + if len(bss) > 0 { + bs = strings.Join(bss, ",") + } + + // allow overrides + if *genesisFlag != "" { + blob, err := ioutil.ReadFile(*genesisFlag) + if err != nil { + log.Crit("Failed to read genesis block contents", "genesis", *genesisFlag, "err", err) + } + gs = new(genesisT.Genesis) + if err = json.Unmarshal(blob, gs); err != nil { + log.Crit("Failed to parse genesis block json", "err", err) + } + } + if *bootFlag != "" { + bs = *bootFlag + } + if *netFlag != 0 { + netid = *netFlag + } + return +} + func main() { // Parse the flags and set up the logger to print everything requested flag.Parse() @@ -176,107 +233,36 @@ func main() { if err != nil { log.Crit("Failed to render the faucet template", "err", err) } + // Load and parse the genesis block requested by the user var genesis *genesisT.Genesis var enodes []*discv5.Node var blob []byte - genesis, *bootFlag, *netFlag = func() (gs *genesisT.Genesis, bs string, netid uint64) { - var configs = []struct { - flag bool - gs *genesisT.Genesis - bs []string - }{ - { - *foundationFlag, - params.DefaultGenesisBlock(), - nil, - }, - { - *classicFlag, - params.DefaultClassicGenesisBlock(), - nil, - }, - { - *mordorFlag, - params.DefaultMordorGenesisBlock(), - nil, - }, - { - *socialFlag, - params.DefaultSocialGenesisBlock(), - params.SocialBootnodes, - }, - { - *ethersocialFlag, - params.DefaultEthersocialGenesisBlock(), - params.EthersocialBootnodes, - }, - { - *mixFlag, - params.DefaultMixGenesisBlock(), - params.MixBootnodes, - }, - { - *testnetFlag, - params.DefaultRopstenGenesisBlock(), - nil, - }, - { - *rinkebyFlag, - params.DefaultRinkebyGenesisBlock(), - nil, - }, - { - *kottiFlag, - params.DefaultKottiGenesisBlock(), - nil, - }, - { - *goerliFlag, - params.DefaultGoerliGenesisBlock(), - nil, - }, - } + // client will be used if the faucet is attaching. If not it won't be touched. + var client *ethclient.Client - var bss []string - for _, conf := range configs { - if conf.flag { - gs, bss, netid = conf.gs, conf.bs, *conf.gs.Config.GetNetworkID() - break - } - } - if len(bss) > 0 { - bs = strings.Join(bss, ",") - } + genesis, *bootFlag, *netFlag = parseChainFlags() - // allow overrides - if *genesisFlag != "" { - blob, err = ioutil.ReadFile(*genesisFlag) - if err != nil { - log.Crit("Failed to read genesis block contents", "genesis", *genesisFlag, "err", err) - } - gs = new(genesisT.Genesis) - if err = json.Unmarshal(blob, gs); err != nil { - log.Crit("Failed to parse genesis block json", "err", err) + if genesis != nil && attachFlag != nil { + log.Crit("Cannot use genesis and attach options simultaneously") + } + + if genesis != nil { + log.Info("configured chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) + + // Convert the bootnodes to internal enode representations + for _, boot := range strings.Split(*bootFlag, ",") { + if url, err := discv5.ParseNode(boot); err == nil { + enodes = append(enodes, url) + } else { + log.Error("Failed to parse bootnode URL", "url", boot, "err", err) } } - if *bootFlag != "" { - bs = *bootFlag - } - if *netFlag != 0 { - netid = *netFlag - } - return - }() - log.Info("configured chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) - - // Convert the bootnodes to internal enode representations - for _, boot := range strings.Split(*bootFlag, ",") { - if url, err := discv5.ParseNode(boot); err == nil { - enodes = append(enodes, url) - } else { - log.Error("Failed to parse bootnode URL", "url", boot, "err", err) + } else { + client, err = ethclient.DialContext(context.Background(), *attachFlag) + if err != nil { + log.Crit("Failed to connect to client", "error", err) } } @@ -286,7 +272,26 @@ func main() { } pass := strings.TrimSuffix(string(blob), "\n") - ks := keystore.NewKeyStore(filepath.Join(faucetDirFromConfig(genesis.Config), "keys"), keystore.StandardScryptN, keystore.StandardScryptP) + // Get the chain id from the genesis or the designated API. + // We'll use this to infer the keystore and chain data directories. + // NOTE: Relying on having chain id immediately available may be fragile. + // IRC eth_chainId can possibly not return the configured value until the block activating the chain id EIP155 is reached. + // See the difference in implementation between ethapi/api.go and eth/api.go #ChainID() methods. + // There's an issue open about this somewhere at ethereum/xxx. + // This could be resolved by creating a -chainid flag to use as a fallback. + chainID := uint64(0) + if genesis != nil { + chainID = genesis.GetChainID().Uint64() + } else { + cid, err := client.ChainID(context.Background()) + if err != nil { + log.Crit("Failed to get chain id from client", "error", err) + } + chainID = cid.Uint64() + } + + keystorePath := filepath.Join(faucetDirFromChainID(chainID), "keys") + ks := keystore.NewKeyStore(keystorePath, keystore.StandardScryptN, keystore.StandardScryptP) if blob, err = ioutil.ReadFile(*accJSONFlag); err != nil { log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err) } @@ -297,13 +302,32 @@ func main() { 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()) + // faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes()) + faucet, err := newFaucet(ks, website.Bytes()) if err != nil { log.Crit("Failed to start faucet", "err", err) } defer faucet.close() + if genesis != nil { + err = faucet.startStack(genesis, *ethPortFlag, enodes, *netFlag) + if err != nil { + log.Crit("Failed to start to stack", "error", err) + } + } else { + faucet.client = client + } + + // See commented ethStats flag for why this is commented. + // Assemble the ethstats monitoring and reporting service' + // if *statsFlag != "" { + // if err := ethstats.New(stack, lesBackend.ApiBackend, ethstats.HeaderAuthorGetterT{}, *statsFlag); err != nil { + // return nil, err + // } + // } + if err := faucet.listenAndServe(*apiPortFlag); err != nil { log.Crit("Failed to launch faucet API", "err", err) } @@ -339,12 +363,13 @@ type faucet struct { lock sync.RWMutex // Lock protecting the faucet's internals } -func newFaucet(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) { +// startStack starts the node stack, ensures peering, and assigns the respective ethclient to the faucet. +func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, network uint64) error { // Assemble the raw devp2p protocol stack stack, err := node.New(&node.Config{ Name: "MultiFaucet", Version: params.VersionWithCommit(gitCommit, gitDate), - DataDir: faucetDirFromConfig(genesis.Config), + DataDir: faucetDirFromChainID(genesis.Config.GetChainID().Uint64()), P2P: p2p.Config{ NAT: nat.Any(), NoDiscovery: true, @@ -355,7 +380,7 @@ func newFaucet(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, netwo }, }) if err != nil { - return nil, err + return err } // Assemble the Ethereum light client protocol @@ -363,6 +388,8 @@ func newFaucet(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, netwo cfg.SyncMode = downloader.LightSync cfg.NetworkId = network cfg.Genesis = genesis + + // NOTE This is broken, fixed with https://github.com/etclabscore/core-geth/pull/249. switch genesis { case params.DefaultClassicGenesisBlock(): utils.SetDNSDiscoveryDefaults2(&cfg, params.ClassicDNSNetwork1) @@ -373,20 +400,18 @@ func newFaucet(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, netwo default: utils.SetDNSDiscoveryDefaults(&cfg, core.GenesisToBlock(genesis, nil).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 + // Commented because this was only used to pass to the ethstats constructor. + /* + lesBackend, err := les.New(stack, &cfg) + if err != nil { + return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err) } - } + */ + // Boot up the client and ensure it connects to bootnodes if err := stack.Start(); err != nil { - return nil, err + return err } for _, boot := range enodes { old, err := enode.Parse(enode.ValidSchemes, boot.String()) @@ -398,25 +423,34 @@ func newFaucet(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, netwo api, err := stack.Attach() if err != nil { stack.Close() - return nil, err + return err } - client := ethclient.NewClient(api) + f.stack = stack + f.client = ethclient.NewClient(api) + return nil +} - return &faucet{ - config: genesis.Config, - stack: stack, - client: client, +func newFaucet(ks *keystore.KeyStore, index []byte) (*faucet, error) { + f := &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 + } + return f, nil } // close terminates the Ethereum connection and tears down the faucet. func (f *faucet) close() error { - return f.stack.Close() + if f.stack != nil { + return f.stack.Close() + } + f.client.Close() + return nil } // listenAndServe registers the HTTP handlers for the faucet and boots it up @@ -492,10 +526,15 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { f.lock.RLock() reqs := f.reqs f.lock.RUnlock() + peerCount, err := f.client.PeerCount(context.Background()) + if err != nil { + log.Warn("Failed to get peer count", "error", err) + return + } if err = send(conn, map[string]interface{}{ "funds": new(big.Int).Div(balance, ether), "funded": nonce, - "peers": f.stack.Server().PeerCount(), + "peers": peerCount, "requests": reqs, }, 3*time.Second); err != nil { log.Warn("Failed to send initial stats to client", "err", err) @@ -623,7 +662,15 @@ func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { 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.GetChainID()) + + // FIXME(meowsbits): Getting the chain id more than once is redundant and can be optimized. + chainId, err := f.client.ChainID(context.Background()) + if err != nil { + log.Warn("Failed to get chain id", "error", err) + return + } + + signed, err := f.keystore.SignTx(f.account, tx, chainId) if err != nil { f.lock.Unlock() if err = sendError(conn, err); err != nil { @@ -746,13 +793,17 @@ func (f *faucet) loop() { 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() + peerCount, err := f.client.PeerCount(context.Background()) + if err != nil { + log.Warn("Failed to get peer count", "error", err) + continue + } for _, conn := range f.conns { if err := send(conn, map[string]interface{}{ "funds": balance, "funded": f.nonce, - "peers": peers, + "peers": peerCount, "requests": f.reqs, }, time.Second); err != nil { log.Warn("Failed to send stats to client", "err", err) diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 8dc34a835e..e8c7c8e7c1 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -547,3 +547,12 @@ func toCallArg(msg ethereum.CallMsg) interface{} { } return arg } + +func (ec *Client) PeerCount(ctx context.Context) (uint64, error) { + var res hexutil.Uint64 + err := ec.c.CallContext(ctx, &res, "net_peerCount") + if err != nil { + return 0, err + } + return uint64(res), nil +} diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go index f82cffab1f..44defadaca 100644 --- a/ethstats/ethstats.go +++ b/ethstats/ethstats.go @@ -32,7 +32,8 @@ import ( "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/clique" + "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" @@ -81,12 +82,43 @@ type fullNodeBackend interface { SuggestPrice(ctx context.Context) (*big.Int, error) } +// HeaderAuthorGetter is established to take the place of the previously demanded consensus.Engine type. +// Requiring an entire consensus engine interface for this one method is overkill. +type HeaderAuthorGetter interface { + Author(header *types.Header) (common.Address, error) +} + +// HeaderAuthorGetterT implements the HeaderAuthorGetter interface. +// It may be used if the ethstats impelementation doesn't have access to an implementation otherwise. +type HeaderAuthorGetterT struct{} + +// Author gets the author of a header differentially if the header is from a Clique or Ethash-based chain. +// It makes the assumption that Clique chains will not set the Mix field and that Ethash chains will. +// Currently the Mix field is "reserved" for the Clique engines, from consensus/clique/consensus.go#verifyHeader: +/* + // Ensure that the mix digest is zero as we don't have fork protection currently + if header.MixDigest != (common.Hash{}) { + return errInvalidMixDigest + } + */ +func (ag HeaderAuthorGetterT) Author(header *types.Header) (common.Address, error) { + // Mix is reserved and currently unused on Clique. + if header.MixDigest == (common.Hash{}) { + c := &clique.Clique{} + return c.Author(header) + } + e := ðash.Ethash{} + return e.Author(header) +} + // Service implements an Ethereum netstats reporting daemon that pushes local // chain statistics up to a monitoring server. type Service struct { + // TODO: Abstract these types to use an API instead of arbitrary library types. server *p2p.Server // Peer-to-peer server to retrieve networking infos backend backend - engine consensus.Engine // Consensus engine to retrieve variadic block fields + + engine HeaderAuthorGetter // Consensus engine to retrieve variadic block fields node string // Name of the node to display on the monitoring page pass string // Password to authorize access to the monitoring page @@ -167,7 +199,7 @@ func parseEthstatsURL(url string) (parts []string, err error) { } // New returns a monitoring service ready for stats reporting. -func New(node *node.Node, backend backend, engine consensus.Engine, url string) error { +func New(node *node.Node, backend backend, engine HeaderAuthorGetter, url string) error { parts, err := parseEthstatsURL(url) if err != nil { return err From 98d34f7bf376554fdcda5a1e922892c965bec592 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 07:47:24 -0600 Subject: [PATCH 02/13] ethstats: revert to use consensus.Engine interface I'm reverting the HeaderAuthorGetter interface solely to reduce diff size and complexity, both for this PR and in general. It doesn't mean I like it. Date: 2020-12-28 07:47:24-06:00 Signed-off-by: meows --- ethstats/ethstats.go | 36 +++--------------------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go index 44defadaca..d1646369c4 100644 --- a/ethstats/ethstats.go +++ b/ethstats/ethstats.go @@ -32,8 +32,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/consensus/clique" - "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" @@ -82,35 +81,6 @@ type fullNodeBackend interface { SuggestPrice(ctx context.Context) (*big.Int, error) } -// HeaderAuthorGetter is established to take the place of the previously demanded consensus.Engine type. -// Requiring an entire consensus engine interface for this one method is overkill. -type HeaderAuthorGetter interface { - Author(header *types.Header) (common.Address, error) -} - -// HeaderAuthorGetterT implements the HeaderAuthorGetter interface. -// It may be used if the ethstats impelementation doesn't have access to an implementation otherwise. -type HeaderAuthorGetterT struct{} - -// Author gets the author of a header differentially if the header is from a Clique or Ethash-based chain. -// It makes the assumption that Clique chains will not set the Mix field and that Ethash chains will. -// Currently the Mix field is "reserved" for the Clique engines, from consensus/clique/consensus.go#verifyHeader: -/* - // Ensure that the mix digest is zero as we don't have fork protection currently - if header.MixDigest != (common.Hash{}) { - return errInvalidMixDigest - } - */ -func (ag HeaderAuthorGetterT) Author(header *types.Header) (common.Address, error) { - // Mix is reserved and currently unused on Clique. - if header.MixDigest == (common.Hash{}) { - c := &clique.Clique{} - return c.Author(header) - } - e := ðash.Ethash{} - return e.Author(header) -} - // Service implements an Ethereum netstats reporting daemon that pushes local // chain statistics up to a monitoring server. type Service struct { @@ -118,7 +88,7 @@ type Service struct { server *p2p.Server // Peer-to-peer server to retrieve networking infos backend backend - engine HeaderAuthorGetter // Consensus engine to retrieve variadic block fields + engine consensus.Engine // Consensus engine to retrieve variadic block fields node string // Name of the node to display on the monitoring page pass string // Password to authorize access to the monitoring page @@ -199,7 +169,7 @@ func parseEthstatsURL(url string) (parts []string, err error) { } // New returns a monitoring service ready for stats reporting. -func New(node *node.Node, backend backend, engine HeaderAuthorGetter, url string) error { +func New(node *node.Node, backend backend, engine consensus.Engine, url string) error { parts, err := parseEthstatsURL(url) if err != nil { return err From fac6afb9bcca5006caa36433efb87c920452b83b Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:05:24 -0600 Subject: [PATCH 03/13] main: handle TODOs around faucet attachery Work was pending resolution of ethstats reporting in conjunction with node attaching. This has been resolved conceptually by make them exclusive; you cannot activate ethstats for the faucet's target attach node (since that node can handle ethstats reporting all by itself). Date: 2020-12-28 08:05:24-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 1ebd3d4b33..3d0d476420 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -50,6 +50,8 @@ import ( "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/ethstats" + "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" @@ -81,10 +83,7 @@ var ( 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") - // FIXME(meowsbits): Commented because support has been temporarily removed during WIP. - // The ethstats package is dependent on having access to a *node.Node and a backend implementation. - // IMO these needs should be met by access to an API rather than a full stack (eg. needs a downloader.Downloader... really?). - // statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string") + 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") @@ -191,6 +190,11 @@ func main() { flag.Parse() log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) + // Audit flag use. + if *statsFlag != "" && *attachFlag != "" { + log.Crit("flags are incompatible", "flags", []string{"ethstats", "attach"}, "values", []*string{statsFlag, attachFlag}) + } + // Construct the payout tiers amounts := make([]string, *tiersFlag) periods := make([]string, *tiersFlag) @@ -252,7 +256,7 @@ func main() { log.Info("configured chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) // Convert the bootnodes to internal enode representations - for _, boot := range utils.SplitAndTrim(*bootFlag) { + for _, boot := range utils.SplitAndTrim(*bootFlag) { if url, err := discv5.ParseNode(boot); err == nil { enodes = append(enodes, url) } else { @@ -322,14 +326,6 @@ func main() { faucet.client = client } - // See commented ethStats flag for why this is commented. - // Assemble the ethstats monitoring and reporting service' - // if *statsFlag != "" { - // if err := ethstats.New(stack, lesBackend.ApiBackend, ethstats.HeaderAuthorGetterT{}, *statsFlag); err != nil { - // return nil, err - // } - // } - if err := faucet.listenAndServe(*apiPortFlag); err != nil { log.Crit("Failed to launch faucet API", "err", err) } @@ -394,6 +390,8 @@ func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv case params.MainnetGenesisHash: if genesis.GetChainID().Uint64() == params.DefaultClassicGenesisBlock().GetChainID().Uint64() { utils.SetDNSDiscoveryDefaults2(&cfg, params.ClassicDNSNetwork1) + } else { + utils.SetDNSDiscoveryDefaults(&cfg, core.GenesisToBlock(genesis, nil).Hash()) } case params.KottiGenesisHash: utils.SetDNSDiscoveryDefaults2(&cfg, params.KottiDNSNetwork1) @@ -403,13 +401,19 @@ func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv utils.SetDNSDiscoveryDefaults(&cfg, core.GenesisToBlock(genesis, nil).Hash()) } - /* log.Info("Config discovery", "urls", cfg.DiscoveryURLs) - lesBackend, err := les.New(stack, &cfg) - if err != nil { - return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err) + + lesBackend, err := les.New(stack, &cfg) + if err != nil { + return fmt.Errorf("Failed to register the Ethereum service: %w", err) + } + + // Assemble the ethstats monitoring and reporting service' + if *statsFlag != "" { + if err := ethstats.New(stack, lesBackend.ApiBackend, lesBackend.Engine(), *statsFlag); err != nil { + return err } - */ + } // Boot up the client and ensure it connects to bootnodes if err := stack.Start(); err != nil { From 95fb625c5cfa8934ed09343c7214208b4c17fa57 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:08:25 -0600 Subject: [PATCH 04/13] main: fix typo in conditional decl Date: 2020-12-28 08:08:25-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 3d0d476420..c6bab1e464 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -248,7 +248,7 @@ func main() { genesis, *bootFlag, *netFlag = parseChainFlags() - if genesis != nil && attachFlag != nil { + if genesis != nil && *attachFlag != "" { log.Crit("Cannot use genesis and attach options simultaneously") } From cfe817906a021fc1a672ce0d888af83849405a24 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:17:13 -0600 Subject: [PATCH 05/13] main: create flag use audit function Make sure flags aren't used in impossible ways. Date: 2020-12-28 08:17:13-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index c6bab1e464..f5dd6a9b64 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -100,6 +100,19 @@ var ( logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") ) +var chainFlags = []*bool{ + foundationFlag, + classicFlag, + mordorFlag, + kottiFlag, + socialFlag, + ethersocialFlag, + mixFlag, + testnetFlag, + rinkebyFlag, + goerliFlag, +} + var ( ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) ) @@ -185,15 +198,32 @@ func parseChainFlags() (gs *genesisT.Genesis, bs string, netid uint64) { return } +// auditFlagUse ensures that exclusive/incompatible flag values are not set. +// If invalid use if found, the program exits with log.Crit. +func auditFlagUse() { + if *statsFlag != "" && *attachFlag != "" { + log.Crit("flags are incompatible", "flags", []string{"ethstats", "attach"}, "values", []*string{statsFlag, attachFlag}) + } + var activeChainFlag *bool + for _, f := range chainFlags { + if *f { + if activeChainFlag != nil { + log.Crit("cannot use two -chain.* flags simultaneously") + } + activeChainFlag = f + } + } + if activeChainFlag != nil && *attachFlag != "" { + log.Crit("cannot use -chain.* with -attach") + } +} + 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)))) - // Audit flag use. - if *statsFlag != "" && *attachFlag != "" { - log.Crit("flags are incompatible", "flags", []string{"ethstats", "attach"}, "values", []*string{statsFlag, attachFlag}) - } + auditFlagUse() // Construct the payout tiers amounts := make([]string, *tiersFlag) @@ -248,10 +278,6 @@ func main() { genesis, *bootFlag, *netFlag = parseChainFlags() - if genesis != nil && *attachFlag != "" { - log.Crit("Cannot use genesis and attach options simultaneously") - } - if genesis != nil { log.Info("configured chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) From 1c1e22e3ca5b8941275e5c93117afa97d120e336 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:24:18 -0600 Subject: [PATCH 06/13] main: add flag exclusive case to audit (-genesis, -attach) Date: 2020-12-28 08:24:18-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index f5dd6a9b64..8bbfbecf46 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -216,6 +216,9 @@ func auditFlagUse() { if activeChainFlag != nil && *attachFlag != "" { log.Crit("cannot use -chain.* with -attach") } + if *genesisFlag != "" && *attachFlag != "" { + log.Crit("cannot use -genesis flag with -attach") + } } func main() { From 84e93911449bb9e480b54c915d37e5361807f44b Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:25:09 -0600 Subject: [PATCH 07/13] main: add info log lines around genesis/attach features Date: 2020-12-28 08:25:09-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 8bbfbecf46..aaaddad28a 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -282,7 +282,7 @@ func main() { genesis, *bootFlag, *netFlag = parseChainFlags() if genesis != nil { - log.Info("configured chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) + log.Info("Using chain/net config", "network id", *netFlag, "bootnodes", *bootFlag, "chain config", fmt.Sprintf("%v", genesis.Config)) // Convert the bootnodes to internal enode representations for _, boot := range utils.SplitAndTrim(*bootFlag) { @@ -293,6 +293,7 @@ func main() { } } } else { + log.Info("Attaching faucet to running client") client, err = ethclient.DialContext(context.Background(), *attachFlag) if err != nil { log.Crit("Failed to connect to client", "error", err) @@ -342,11 +343,12 @@ func main() { // faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes()) faucet, err := newFaucet(ks, website.Bytes()) if err != nil { - log.Crit("Failed to start faucet", "err", err) + log.Crit("Failed to construct faucet", "err", err) } defer faucet.close() if genesis != nil { + log.Info("Starting faucet client stack") err = faucet.startStack(genesis, *ethPortFlag, enodes, *netFlag) if err != nil { log.Crit("Failed to start to stack", "error", err) From dfdc5d8df2f472f95aff43ce48f45ed58c1bef93 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 08:37:41 -0600 Subject: [PATCH 08/13] ethstats: revert unrelated changes Date: 2020-12-28 08:37:41-06:00 Signed-off-by: meows --- ethstats/ethstats.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go index d1646369c4..d731ec9dca 100644 --- a/ethstats/ethstats.go +++ b/ethstats/ethstats.go @@ -84,10 +84,8 @@ type fullNodeBackend interface { // Service implements an Ethereum netstats reporting daemon that pushes local // chain statistics up to a monitoring server. type Service struct { - // TODO: Abstract these types to use an API instead of arbitrary library types. server *p2p.Server // Peer-to-peer server to retrieve networking infos backend backend - engine consensus.Engine // Consensus engine to retrieve variadic block fields node string // Name of the node to display on the monitoring page From 0962062ea2cdcb8e35d2e23e7366271893301eb8 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 10:10:51 -0600 Subject: [PATCH 09/13] ethstats: (lint) goimports Date: 2020-12-28 10:10:51-06:00 Signed-off-by: meows --- ethstats/ethstats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go index d731ec9dca..f82cffab1f 100644 --- a/ethstats/ethstats.go +++ b/ethstats/ethstats.go @@ -86,7 +86,7 @@ type fullNodeBackend interface { type Service struct { server *p2p.Server // Peer-to-peer server to retrieve networking infos backend backend - engine consensus.Engine // Consensus engine to retrieve variadic block fields + engine consensus.Engine // Consensus engine to retrieve variadic block fields node string // Name of the node to display on the monitoring page pass string // Password to authorize access to the monitoring page From c99ed4582f30ed2ca289d43419ffd09a2639be40 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 10:19:05 -0600 Subject: [PATCH 10/13] main: disable NoDiscovery (enable discovery) Faucet behavior previously effectively demanded user configuration of bootnodes manually to enable peering. This disables NoDiscovery, thus enabling discovery. Date: 2020-12-28 10:19:05-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index aaaddad28a..89fac49318 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -401,7 +401,8 @@ func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv DataDir: faucetDirFromChainID(genesis.Config.GetChainID().Uint64()), P2P: p2p.Config{ NAT: nat.Any(), - NoDiscovery: true, + // NoDiscovery is DISABLED to allow the node the find peers without relying on manually configured bootnodes. + // NoDiscovery: true, DiscoveryV5: true, ListenAddr: fmt.Sprintf(":%d", port), MaxPeers: 25, From dc81ae808d4fef351dff60e63ceb53e1533dd9b1 Mon Sep 17 00:00:00 2001 From: meows Date: Mon, 28 Dec 2020 10:22:20 -0600 Subject: [PATCH 11/13] main: (lint) goimports Date: 2020-12-28 10:22:20-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 89fac49318..5b3ea84c4a 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -400,7 +400,7 @@ func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv Version: params.VersionWithCommit(gitCommit, gitDate), DataDir: faucetDirFromChainID(genesis.Config.GetChainID().Uint64()), P2P: p2p.Config{ - NAT: nat.Any(), + NAT: nat.Any(), // NoDiscovery is DISABLED to allow the node the find peers without relying on manually configured bootnodes. // NoDiscovery: true, DiscoveryV5: true, From b78074862257c7943e18df3912893a01d7ba4a6e Mon Sep 17 00:00:00 2001 From: meows Date: Tue, 29 Dec 2020 05:14:04 -0600 Subject: [PATCH 12/13] main: configure datadir with genesis hash too As noted in the comment on line 310, when attaching to a remote client it may be that the API says that chain id is 0 when the client is not yet synced past the EIP155 activation block height. Requesting and switching on the genesis block hash fixes this issue except for the foundation/classic corner case (same genesis, different chain ids). Date: 2020-12-29 05:14:04-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 5b3ea84c4a..990f189b94 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -122,28 +122,29 @@ var ( gitDate = "" // Git commit date YYYYMMDD of the release (set via linker flags) ) -func faucetDirFromChainID(chainID uint64) string { +func faucetDirFromChainIndicators(chainID uint64, genesisHash common.Hash) string { datadir := filepath.Join(os.Getenv("HOME"), ".faucet") - switch chainID { - case params.MainnetChainConfig.GetChainID().Uint64(): + switch genesisHash { + case params.MainnetGenesisHash: + if chainID == params.ClassicChainConfig.GetChainID().Uint64() { + return filepath.Join(datadir, "classic") + } return filepath.Join(datadir, "") - case params.ClassicChainConfig.GetChainID().Uint64(): - return filepath.Join(datadir, "classic") - case params.SocialChainConfig.GetChainID().Uint64(): + case params.SocialGenesisHash: return filepath.Join(datadir, "social") - case params.EthersocialChainConfig.GetChainID().Uint64(): + case params.EthersocialGenesisHash: return filepath.Join(datadir, "ethersocial") - case params.MixChainConfig.GetChainID().Uint64(): + case params.MixGenesisHash: return filepath.Join(datadir, "mix") - case params.RopstenChainConfig.GetChainID().Uint64(): + case params.RopstenGenesisHash: return filepath.Join(datadir, "ropsten") - case params.RinkebyChainConfig.GetChainID().Uint64(): + case params.RinkebyGenesisHash: return filepath.Join(datadir, "rinkeby") - case params.GoerliChainConfig.GetChainID().Uint64(): + case params.GoerliGenesisHash: return filepath.Join(datadir, "goerli") - case params.KottiChainConfig.GetChainID().Uint64(): + case params.KottiGenesisHash: return filepath.Join(datadir, "kotti") - case params.MordorChainConfig.GetChainID().Uint64(): + case params.MordorGenesisHash: return filepath.Join(datadir, "mordor") } return datadir @@ -314,17 +315,24 @@ func main() { // There's an issue open about this somewhere at ethereum/xxx. // This could be resolved by creating a -chainid flag to use as a fallback. chainID := uint64(0) + var genesisHash common.Hash if genesis != nil { chainID = genesis.GetChainID().Uint64() + genesisHash = core.GenesisToBlock(genesis, nil).Hash() } else { cid, err := client.ChainID(context.Background()) if err != nil { log.Crit("Failed to get chain id from client", "error", err) } + genesisBlock, err := client.BlockByNumber(context.Background(), big.NewInt(0)) + if err != nil { + log.Crit("Failed to get genesis block from client", "error", err) + } chainID = cid.Uint64() + genesisHash = genesisBlock.Hash() } - keystorePath := filepath.Join(faucetDirFromChainID(chainID), "keys") + keystorePath := filepath.Join(faucetDirFromChainIndicators(chainID, genesisHash), "keys") ks := keystore.NewKeyStore(keystorePath, keystore.StandardScryptN, keystore.StandardScryptP) if blob, err = ioutil.ReadFile(*accJSONFlag); err != nil { log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err) @@ -394,11 +402,14 @@ type faucet struct { // startStack starts the node stack, ensures peering, and assigns the respective ethclient to the faucet. func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv5.Node, network uint64) error { + + genesisHash := core.GenesisToBlock(genesis, nil).Hash() + // Assemble the raw devp2p protocol stack stack, err := node.New(&node.Config{ Name: "MultiFaucet", Version: params.VersionWithCommit(gitCommit, gitDate), - DataDir: faucetDirFromChainID(genesis.Config.GetChainID().Uint64()), + DataDir: faucetDirFromChainIndicators(genesis.Config.GetChainID().Uint64(), genesisHash), P2P: p2p.Config{ NAT: nat.Any(), // NoDiscovery is DISABLED to allow the node the find peers without relying on manually configured bootnodes. @@ -418,7 +429,7 @@ func (f *faucet) startStack(genesis *genesisT.Genesis, port int, enodes []*discv cfg.SyncMode = downloader.LightSync cfg.NetworkId = network cfg.Genesis = genesis - switch core.GenesisToBlock(genesis, nil).Hash() { + switch genesisHash { case params.MainnetGenesisHash: if genesis.GetChainID().Uint64() == params.DefaultClassicGenesisBlock().GetChainID().Uint64() { utils.SetDNSDiscoveryDefaults2(&cfg, params.ClassicDNSNetwork1) From e3834053fc06adb50b04fa2514c1aac7bab09fa2 Mon Sep 17 00:00:00 2001 From: meows Date: Tue, 29 Dec 2020 05:30:04 -0600 Subject: [PATCH 13/13] main: add -attach.chainid to provide accessible fallback value This value would be useful and necessary only if the target client were unsynced past the EIP155 activation height. ChainID is used to disambiguate ETH/ETC chains. Date: 2020-12-29 05:30:04-06:00 Signed-off-by: meows --- cmd/faucet/faucet.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/faucet/faucet.go b/cmd/faucet/faucet.go index 990f189b94..db55af6125 100644 --- a/cmd/faucet/faucet.go +++ b/cmd/faucet/faucet.go @@ -75,7 +75,9 @@ var ( testnetFlag = flag.Bool("chain.testnet", false, "Configure genesis and bootnodes for testnet chain defaults") rinkebyFlag = flag.Bool("chain.rinkeby", false, "Configure genesis and bootnodes for rinkeby chain defaults") goerliFlag = flag.Bool("chain.goerli", false, "Configure genesis and bootnodes for goerli chain defaults") - attachFlag = flag.String("attach", "", "Attach to an IPC or WS endpoint") + + attachFlag = flag.String("attach", "", "Attach to an IPC or WS endpoint") + attachChainID = flag.Int64("attach.chainid", 0, "Configure fallback chain id value for use in attach mode (used if target does not have value available yet).") genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") @@ -314,6 +316,8 @@ func main() { // See the difference in implementation between ethapi/api.go and eth/api.go #ChainID() methods. // There's an issue open about this somewhere at ethereum/xxx. // This could be resolved by creating a -chainid flag to use as a fallback. + // NOTE(meowsbits): chainID and genesisHash are ONLY used as input for configuring the + // default data directory. This logic must be bypassed when and if a -datadir flag were in use. chainID := uint64(0) var genesisHash common.Hash if genesis != nil { @@ -330,6 +334,15 @@ func main() { } chainID = cid.Uint64() genesisHash = genesisBlock.Hash() + + // ChainID is only REQUIRED to disambiguate ETH/ETC chains. + if chainID == 0 && genesisHash == params.MainnetGenesisHash { + if *attachChainID == 0 { + // Exit with error if disambiguating fallback is unset. + log.Crit("Ambiguous/unavailable chain identity", "recommended solution", "use -attach.chainid to configure a fallback or wait until target client is synced past EIP155 block height") + } + chainID = uint64(*attachChainID) + } } keystorePath := filepath.Join(faucetDirFromChainIndicators(chainID, genesisHash), "keys")