From d5f6ee46201644d70de9cfbeb8f6514f9a110451 Mon Sep 17 00:00:00 2001 From: Rafael Matias Date: Thu, 4 Jul 2019 13:14:10 +0200 Subject: [PATCH] cmd/swarm: make bzzaccount flag optional and add bzzkeyhex flag (#1531) * cmd/swarm: don't require bzzaccount flag * docker: remove setup script because bzzaccount is not required anymore * cmd/swarm: manage account creation/selection when bzzflag is not defined * cmd/swarm: no bzzaccount flag tests * cmd/swarm: use random network ports on tests * cmd/swarm: fix typo * cmd/swarm: add --bzzkeyhex flag * cmd/swarm: use different ipc paths for tests * readme: update example on how to run swarm * cmd/swarm: rename getAccount -> getOrCreateAccount * cmd/swarm: remove unneeded comment * cmd/swarm: use shorter ipc path for test --- Dockerfile | 3 +- Dockerfile.alltools | 3 +- README.md | 58 ++++++-------- cmd/swarm/access.go | 2 +- cmd/swarm/config.go | 6 +- cmd/swarm/config_test.go | 165 +++++++++++++++++++++++++++++++++++++-- cmd/swarm/download.go | 2 +- cmd/swarm/flags.go | 5 ++ cmd/swarm/main.go | 90 ++++++++++++++++----- docker/run.sh | 26 ------ 10 files changed, 266 insertions(+), 94 deletions(-) delete mode 100755 docker/run.sh diff --git a/Dockerfile b/Dockerfile index dc493da14d..75b9373599 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,5 +10,4 @@ FROM alpine:3.9 RUN apk --no-cache add ca-certificates && update-ca-certificates COPY --from=builder /swarm/build/bin/swarm /usr/local/bin/ COPY --from=geth /usr/local/bin/geth /usr/local/bin/ -COPY docker/run.sh /run.sh -ENTRYPOINT ["/run.sh"] +ENTRYPOINT ["/usr/local/bin/swarm"] diff --git a/Dockerfile.alltools b/Dockerfile.alltools index 594333e00c..31837e3db9 100644 --- a/Dockerfile.alltools +++ b/Dockerfile.alltools @@ -10,6 +10,5 @@ FROM alpine:3.9 RUN apk --no-cache add ca-certificates COPY --from=builder /swarm/build/bin/* /usr/local/bin/ COPY --from=geth /usr/local/bin/geth /usr/local/bin/ -COPY docker/run.sh /run.sh COPY docker/run-smoke.sh /run-smoke.sh -ENTRYPOINT ["/run.sh"] +ENTRYPOINT ["/usr/local/bin/swarm"] diff --git a/README.md b/README.md index a13c391ff7..08037cd651 100644 --- a/README.md +++ b/README.md @@ -9,26 +9,26 @@ Swarm is a distributed storage platform and content distribution service, a nati ## Table of Contents -- [Building the source](#building-the-source) -- [Running Swarm](#running-swarm) - - [Verifying that your local Swarm node is running](#verifying-that-your-local-swarm-node-is-running) - - [Ethereum Name Service resolution](#ethereum-name-service-resolution) -- [Documentation](#documentation) -- [Docker](#docker) - - [Docker tags](#docker-tags) - - [Environment variables](#environment-variables) - - [Swarm command line arguments](#swarm-command-line-arguments) -- [Developers Guide](#developers-guide) - - [Go Environment](#go-environment) - - [Vendored Dependencies](#vendored-dependencies) - - [Testing](#testing) - - [Profiling Swarm](#profiling-swarm) - - [Metrics and Instrumentation in Swarm](#metrics-and-instrumentation-in-swarm) - - [Visualizing metrics](#visualizing-metrics) -- [Public Gateways](#public-gateways) -- [Swarm Dapps](#swarm-dapps) -- [Contributing](#contributing) -- [License](#license) +- [Building the source](#Building-the-source) +- [Running Swarm](#Running-Swarm) + - [Verifying that your local Swarm node is running](#Verifying-that-your-local-Swarm-node-is-running) + - [Ethereum Name Service resolution](#Ethereum-Name-Service-resolution) +- [Documentation](#Documentation) +- [Docker](#Docker) + - [Docker tags](#Docker-tags) + - [Environment variables](#Environment-variables) + - [Swarm command line arguments](#Swarm-command-line-arguments) +- [Developers Guide](#Developers-Guide) + - [Go Environment](#Go-Environment) + - [Vendored Dependencies](#Vendored-Dependencies) + - [Testing](#Testing) + - [Profiling Swarm](#Profiling-Swarm) + - [Metrics and Instrumentation in Swarm](#Metrics-and-Instrumentation-in-Swarm) + - [Visualizing metrics](#Visualizing-metrics) +- [Public Gateways](#Public-Gateways) +- [Swarm Dapps](#Swarm-Dapps) +- [Contributing](#Contributing) +- [License](#License) ## Building the source @@ -53,15 +53,11 @@ $ go install github.com/ethersphere/swarm/cmd/swarm ## Running Swarm -Going through all the possible command line flags is out of scope here, but we've enumerated a few common parameter combos to get you up to speed quickly on how you can run your own Swarm node. - -To run Swarm you need an Ethereum account. Download and install [Geth](https://geth.ethereum.org) if you don't have it on your system. You can create a new Ethereum account by running the following command: - ```bash -$ geth account new +$ swarm ``` -You will be prompted for a password: +If you don't have an account yet, then you will be prompted to create one and secure it with a password: ``` Your new account is locked with a password. Please give a password. Do not forget this password. @@ -69,18 +65,12 @@ Passphrase: Repeat passphrase: ``` -Once you have specified the password, the output will be the Ethereum address representing that account. For example: - -``` -Address: {2f1cd699b0bf461dcfbf0098ad8f5587b038f0f1} -``` - -Using this account, connect to Swarm with +If you have multiple accounts created, then you'll have to choose one of the accounts by using the `--bzzaccount` flag. ```bash $ swarm --bzzaccount -# in our example +# example $ swarm --bzzaccount 2f1cd699b0bf461dcfbf0098ad8f5587b038f0f1 ``` diff --git a/cmd/swarm/access.go b/cmd/swarm/access.go index 19cae7f445..735b1d7c5b 100644 --- a/cmd/swarm/access.go +++ b/cmd/swarm/access.go @@ -106,7 +106,7 @@ func accessNewPass(ctx *cli.Context) { accessKey []byte err error ref = args[0] - password = getPassPhrase("", 0, makePasswordList(ctx)) + password = getPassPhrase("", false, 0, makePasswordList(ctx)) dryRun = ctx.Bool(SwarmDryRunFlag.Name) ) accessKey, ae, err = api.DoPassword(ctx, password, salt) diff --git a/cmd/swarm/config.go b/cmd/swarm/config.go index 299d8203e5..09cacf2427 100644 --- a/cmd/swarm/config.go +++ b/cmd/swarm/config.go @@ -17,6 +17,7 @@ package main import ( + "crypto/ecdsa" "errors" "fmt" "io" @@ -60,6 +61,7 @@ var ( const ( SwarmEnvChequebookAddr = "SWARM_CHEQUEBOOK_ADDR" SwarmEnvAccount = "SWARM_ACCOUNT" + SwarmEnvBzzKeyHex = "SWARM_BZZ_KEY_HEX" SwarmEnvListenAddr = "SWARM_LISTEN_ADDR" SwarmEnvPort = "SWARM_PORT" SwarmEnvNetworkID = "SWARM_NETWORK_ID" @@ -121,9 +123,9 @@ func buildConfig(ctx *cli.Context) (config *bzzapi.Config, err error) { //finally, after the configuration build phase is finished, initialize func initSwarmNode(config *bzzapi.Config, stack *node.Node, ctx *cli.Context, nodeconfig *node.Config) error { - //at this point, all vars should be set in the Config //get the account for the provided swarm account - prvkey := getAccount(config.BzzAccount, ctx, stack) + var prvkey *ecdsa.PrivateKey + config.BzzAccount, prvkey = getOrCreateAccount(ctx, stack) //set the resolved config path (geth --datadir) config.Path = expandPath(stack.InstanceDir()) //finally, initialize the configuration diff --git a/cmd/swarm/config_test.go b/cmd/swarm/config_test.go index d0020d3a59..09d4bfff9b 100644 --- a/cmd/swarm/config_test.go +++ b/cmd/swarm/config_test.go @@ -17,6 +17,7 @@ package main import ( + "encoding/hex" "fmt" "io" "io/ioutil" @@ -28,6 +29,8 @@ import ( "github.com/docker/docker/pkg/reexec" "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/rpc" "github.com/ethersphere/swarm" "github.com/ethersphere/swarm/api" @@ -48,6 +51,7 @@ func TestConfigFailsSwapEnabledNoSwapApi(t *testing.T) { flags := []string{ fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", fmt.Sprintf("--%s", SwarmSwapEnabledFlag.Name), } @@ -56,15 +60,161 @@ func TestConfigFailsSwapEnabledNoSwapApi(t *testing.T) { swarm.ExpectExit() } -func TestConfigFailsNoBzzAccount(t *testing.T) { - flags := []string{ - fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", - fmt.Sprintf("--%s", SwarmPortFlag.Name), "54545", +func TestBzzKeyFlag(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatal("unable to generate key") + } + hexKey := hex.EncodeToString(crypto.FromECDSA(key)) + bzzaccount := crypto.PubkeyToAddress(key.PublicKey).Hex() + + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + conf := &node.Config{ + DataDir: dir, + IPCPath: "testbzzkeyflag.ipc", + NoUSB: true, } - swarm := runSwarm(t, flags...) - swarm.Expect("Fatal: " + SwarmErrNoBZZAccount + "\n") - swarm.ExpectExit() + node := &testNode{Dir: dir} + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, + fmt.Sprintf("--%s", utils.IPCPathFlag.Name), conf.IPCPath, + fmt.Sprintf("--%s", SwarmBzzKeyHexFlag.Name), hexKey, + } + + node.Cmd = runSwarm(t, flags...) + defer func() { + node.Shutdown() + }() + + node.Cmd.InputLine(testPassphrase) + + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.BzzAccount != bzzaccount { + t.Fatalf("Expected account to be %s, got %s", bzzaccount, info.BzzAccount) + } +} +func TestEmptyBzzAccountFlagMultipleAccounts(t *testing.T) { + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + // Create two accounts on disk + getTestAccount(t, dir) + getTestAccount(t, dir) + + node := &testNode{Dir: dir} + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, + } + + node.Cmd = runSwarm(t, flags...) + + node.Cmd.ExpectRegexp(fmt.Sprintf("Please choose one of the accounts by running swarm with the --%s flag.", SwarmAccountFlag.Name)) +} + +func TestEmptyBzzAccountFlagSingleAccount(t *testing.T) { + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + conf, account := getTestAccount(t, dir) + node := &testNode{Dir: dir} + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, + fmt.Sprintf("--%s", utils.IPCPathFlag.Name), conf.IPCPath, + } + + node.Cmd = runSwarm(t, flags...) + defer func() { + node.Shutdown() + }() + + node.Cmd.InputLine(testPassphrase) + + // wait for the node to start + for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(50 * time.Millisecond) { + node.Client, err = rpc.Dial(conf.IPCEndpoint()) + if err == nil { + break + } + } + if node.Client == nil { + t.Fatal(err) + } + + // load info + var info swarm.Info + if err := node.Client.Call(&info, "bzz_info"); err != nil { + t.Fatal(err) + } + + if info.BzzAccount != account.Address.Hex() { + t.Fatalf("Expected account to be %s, got %s", account.Address.Hex(), info.BzzAccount) + } +} + +func TestEmptyBzzAccountFlagNoAccountWrongPassword(t *testing.T) { + dir, err := ioutil.TempDir("", "bzztest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + node := &testNode{Dir: dir} + + flags := []string{ + fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", + fmt.Sprintf("--%s", SwarmPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", + fmt.Sprintf("--%s", utils.DataDirFlag.Name), dir, + } + + node.Cmd = runSwarm(t, flags...) + + // Set password + node.Cmd.InputLine("goodpassword") + // Confirm password + node.Cmd.InputLine("wrongpassword") + + node.Cmd.ExpectRegexp("Passphrases do not match") } func TestConfigCmdLineOverrides(t *testing.T) { @@ -86,6 +236,7 @@ func TestConfigCmdLineOverrides(t *testing.T) { flags := []string{ fmt.Sprintf("--%s", SwarmNetworkIdFlag.Name), "42", fmt.Sprintf("--%s", SwarmPortFlag.Name), httpPort, + fmt.Sprintf("--%s", utils.ListenPortFlag.Name), "0", fmt.Sprintf("--%s", SwarmSyncDisabledFlag.Name), fmt.Sprintf("--%s", CorsStringFlag.Name), "*", fmt.Sprintf("--%s", SwarmAccountFlag.Name), account.Address.String(), diff --git a/cmd/swarm/download.go b/cmd/swarm/download.go index b0fd03755f..97cada64cb 100644 --- a/cmd/swarm/download.go +++ b/cmd/swarm/download.go @@ -101,7 +101,7 @@ func download(ctx *cli.Context) { return nil } if passwords := makePasswordList(ctx); passwords != nil { - password := getPassPhrase(fmt.Sprintf("Downloading %s is restricted", uri), 0, passwords) + password := getPassPhrase(fmt.Sprintf("Downloading %s is restricted", uri), false, 0, passwords) err = dl(password) } else { err = dl("") diff --git a/cmd/swarm/flags.go b/cmd/swarm/flags.go index 6093149e3c..0bb0fe2543 100644 --- a/cmd/swarm/flags.go +++ b/cmd/swarm/flags.go @@ -30,6 +30,11 @@ var ( Usage: "Swarm account key file", EnvVar: SwarmEnvAccount, } + SwarmBzzKeyHexFlag = cli.StringFlag{ + Name: "bzzkeyhex", + Usage: "BzzAccount key in hex (for testing)", + EnvVar: SwarmEnvBzzKeyHex, + } SwarmListenAddrFlag = cli.StringFlag{ Name: "httpaddr", Usage: "Swarm HTTP API listening interface", diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go index a3d91a8a00..b57b02847c 100644 --- a/cmd/swarm/main.go +++ b/cmd/swarm/main.go @@ -184,6 +184,7 @@ func init() { SwarmListenAddrFlag, SwarmPortFlag, SwarmAccountFlag, + SwarmBzzKeyHexFlag, SwarmNetworkIdFlag, ChequebookAddrFlag, // upload flags @@ -351,21 +352,62 @@ func registerBzzService(bzzconfig *bzzapi.Config, stack *node.Node) { } } -func getAccount(bzzaccount string, ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { - //an account is mandatory - if bzzaccount == "" { - utils.Fatalf(SwarmErrNoBZZAccount) +// getOrCreateAccount returns the address and associated private key for a bzzaccount +// If no account exists, it will create an account for you. +func getOrCreateAccount(ctx *cli.Context, stack *node.Node) (string, *ecdsa.PrivateKey) { + var bzzaddr string + + // Check if a key was provided + if hexkey := ctx.GlobalString(SwarmBzzKeyHexFlag.Name); hexkey != "" { + key, err := crypto.HexToECDSA(hexkey) + if err != nil { + utils.Fatalf("failed using %s: %v", SwarmBzzKeyHexFlag.Name, err) + } + bzzaddr := crypto.PubkeyToAddress(key.PublicKey).Hex() + log.Info(fmt.Sprintf("Swarm account key loaded from %s", SwarmBzzKeyHexFlag.Name), "address", bzzaddr) + return bzzaddr, key } - // Try to load the arg as a hex key file. - if key, err := crypto.LoadECDSA(bzzaccount); err == nil { - log.Info("Swarm account key loaded", "address", crypto.PubkeyToAddress(key.PublicKey)) - return key - } - // Otherwise try getting it from the keystore. + am := stack.AccountManager() ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) - return decryptStoreAccount(ks, bzzaccount, utils.MakePasswordList(ctx)) + // Check if an address was provided + if bzzaddr = ctx.GlobalString(SwarmAccountFlag.Name); bzzaddr != "" { + // Try to load the arg as a hex key file. + if key, err := crypto.LoadECDSA(bzzaddr); err == nil { + bzzaddr := crypto.PubkeyToAddress(key.PublicKey).Hex() + log.Info("Swarm account key loaded", "address", bzzaddr) + return bzzaddr, key + } + return bzzaddr, decryptStoreAccount(ks, bzzaddr, utils.MakePasswordList(ctx)) + } + + // No address or key were provided + accounts := ks.Accounts() + + switch l := len(accounts); l { + case 0: + // Create an account + log.Info("You don't have an account yet. Creating one...") + password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) + account, err := ks.NewAccount(password) + if err != nil { + utils.Fatalf("failed creating an account: %v", err) + } + bzzaddr = account.Address.Hex() + case 1: + // Use existing account + bzzaddr = accounts[0].Address.Hex() + default: + // Inform user about multiple accounts + log.Info(fmt.Sprintf("Multiple (%d) accounts were found in your keystore.", l)) + for _, a := range accounts { + log.Info(fmt.Sprintf("Account: %s", a.Address.Hex())) + } + utils.Fatalf(fmt.Sprintf("Please choose one of the accounts by running swarm with the --%s flag.", SwarmAccountFlag.Name)) + } + + return bzzaddr, decryptStoreAccount(ks, bzzaddr, utils.MakePasswordList(ctx)) } // getPrivKey returns the private key of the specified bzzaccount @@ -387,7 +429,9 @@ func getPrivKey(ctx *cli.Context) *ecdsa.PrivateKey { } defer stack.Close() - return getAccount(bzzconfig.BzzAccount, ctx, stack) + var privkey *ecdsa.PrivateKey + bzzconfig.BzzAccount, privkey = getOrCreateAccount(ctx, stack) + return privkey } func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []string) *ecdsa.PrivateKey { @@ -412,7 +456,7 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []stri utils.Fatalf("Can't load swarm account key: %v", err) } for i := 0; i < 3; i++ { - password := getPassPhrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i+1), i, passwords) + password := getPassPhrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i+1), false, i, passwords) key, err := keystore.DecryptKey(keyjson, password) if err == nil { return key.PrivateKey @@ -422,18 +466,17 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string, passwords []stri return nil } -// getPassPhrase retrieves the password associated with bzz account, either by fetching -// from a list of pre-loaded passwords, or by requesting it interactively from user. -func getPassPhrase(prompt string, i int, passwords []string) string { - // non-interactive +// getPassPhrase retrieves the password associated with a bzzaccount, either fetched +// from a list of preloaded passphrases, or requested interactively from the user. +func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { + // If a list of passwords was supplied, retrieve from them if len(passwords) > 0 { if i < len(passwords) { return passwords[i] } return passwords[len(passwords)-1] } - - // fallback to interactive mode + // Otherwise prompt the user for the password if prompt != "" { fmt.Println(prompt) } @@ -441,6 +484,15 @@ func getPassPhrase(prompt string, i int, passwords []string) string { if err != nil { utils.Fatalf("Failed to read passphrase: %v", err) } + if confirmation { + confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase confirmation: %v", err) + } + if password != confirm { + utils.Fatalf("Passphrases do not match") + } + } return password } diff --git a/docker/run.sh b/docker/run.sh deleted file mode 100755 index 4be91ba82f..0000000000 --- a/docker/run.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -set -o errexit -set -o pipefail -set -o nounset - -PASSWORD=${PASSWORD:-} -DATADIR=${DATADIR:-/root/.ethereum/} - -if [ "$PASSWORD" == "" ]; then echo "Password must be set, in order to use swarm non-interactively." && exit 1; fi - -echo $PASSWORD > /password - -KEYFILE=`find $DATADIR | grep UTC | head -n 1` || true -if [ ! -f "$KEYFILE" ]; then echo "No keyfile found. Generating..." && geth --datadir $DATADIR --password /password account new; fi -KEYFILE=`find $DATADIR | grep UTC | head -n 1` || true -if [ ! -f "$KEYFILE" ]; then echo "Could not find nor generate a BZZ keyfile." && exit 1; else echo "Found keyfile $KEYFILE"; fi - -VERSION=`swarm version` -echo "Running Swarm:" -echo $VERSION - -export BZZACCOUNT="`echo -n $KEYFILE | tail -c 40`" || true -if [ "$BZZACCOUNT" == "" ]; then echo "Could not parse BZZACCOUNT from keyfile." && exit 1; fi - -exec swarm --bzzaccount=$BZZACCOUNT --password /password --datadir $DATADIR $@ 2>&1