diff --git a/cmd/burrow/main.go b/cmd/burrow/main.go index fa38362061e30a01c4170f4d8b5081f7e9f1ccc7..d1fa13f458c656e2ee27d8a5b00f23116e397d4b 100644 --- a/cmd/burrow/main.go +++ b/cmd/burrow/main.go @@ -14,6 +14,7 @@ import ( "github.com/hyperledger/burrow/genesis" "github.com/hyperledger/burrow/genesis/spec" "github.com/hyperledger/burrow/keys" + "github.com/hyperledger/burrow/keys/mock" "github.com/hyperledger/burrow/logging" logging_config "github.com/hyperledger/burrow/logging/config" "github.com/hyperledger/burrow/logging/config/presets" @@ -79,6 +80,10 @@ func main() { tomlOpt := cmd.BoolOpt("t toml", false, "Emit GenesisSpec as TOML rather than the "+ "default JSON") + baseOpt := cmd.StringsOpt("b base", nil, "Provide a base GenesisSpecs on top of which any "+ + "additional GenesisSpec presets specified by other flags will be merged. GenesisSpecs appearing "+ + "later take precedent over those appearing early if multiple --base flags are provided") + fullOpt := cmd.IntOpt("f full-accounts", 1, "Number of preset Full type accounts") validatorOpt := cmd.IntOpt("v validator-accounts", 0, "Number of preset Validator type accounts") rootOpt := cmd.IntOpt("r root-accounts", 0, "Number of preset Root type accounts") @@ -86,10 +91,19 @@ func main() { participantsOpt := cmd.IntOpt("p participant-accounts", 1, "Number of preset Participant type accounts") chainNameOpt := cmd.StringOpt("n chain-name", "", "Default chain name") - cmd.Spec = "[--full-accounts] [--validator-accounts] [--root-accounts] [--developer-accounts] [--participant-accounts] [--chain-name] [--toml]" + cmd.Spec = "[--base][--full-accounts] [--validator-accounts] [--root-accounts] [--developer-accounts] " + + "[--participant-accounts] [--chain-name] [--toml]" cmd.Action = func() { specs := make([]spec.GenesisSpec, 0, *participantsOpt+*fullOpt) + for _, baseSpec := range *baseOpt { + genesisSpec := new(spec.GenesisSpec) + err := source.FromFile(baseSpec, genesisSpec) + if err != nil { + fatalf("could not read GenesisSpec: %v", err) + } + specs = append(specs, *genesisSpec) + } for i := 0; i < *fullOpt; i++ { specs = append(specs, spec.FullAccount(i)) } @@ -106,7 +120,9 @@ func main() { specs = append(specs, spec.ParticipantAccount(i)) } genesisSpec := spec.MergeGenesisSpecs(specs...) - genesisSpec.ChainName = *chainNameOpt + if *chainNameOpt != "" { + genesisSpec.ChainName = *chainNameOpt + } if *tomlOpt { os.Stdout.WriteString(source.TOMLString(genesisSpec)) } else { @@ -121,18 +137,24 @@ func main() { genesisSpecOpt := cmd.StringOpt("s genesis-spec", "", "A GenesisSpec to use as a template for a GenesisDoc that will be created along with keys") - tomlInOpt := cmd.BoolOpt("t toml-in", false, "Consume GenesisSpec/GenesisDoc as TOML "+ - "rather than the JSON default") + jsonOutOpt := cmd.BoolOpt("j json-out", false, "Emit config in JSON rather than TOML "+ + "suitable for further processing or forming a separate genesis.json GenesisDoc") keysUrlOpt := cmd.StringOpt("k keys-url", "", fmt.Sprintf("Provide keys URL, default: %s", keys.DefaultKeysConfig().URL)) - jsonOutOpt := cmd.BoolOpt("j json-out", false, "Emit config in JSON rather than TOML "+ - "suitable for further processing or forming a separate genesis.json GenesisDoc") + genesisDocOpt := cmd.StringOpt("g genesis-doc", "", "GenesisDoc in JSON or TOML to embed in config") + + generateKeysOpt := cmd.StringOpt("x generate-keys", "", + "File to output containing secret keys as JSON or according to a custom template (see --keys-template). "+ + "Note that using this options means the keys will not be generated in the default keys instance") - genesisDocOpt := cmd.StringOpt("g genesis-doc", "", "GenesisDoc JSON to embed in config") + keysTemplateOpt := cmd.StringOpt("z keys-template", mock.DefaultDumpKeysFormat, + fmt.Sprintf("Go text/template template (left delim: %s right delim: %s) to generate secret keys "+ + "file specified with --generate-keys. Default:\n%s", mock.LeftTemplateDelim, mock.RightTemplateDelim, + mock.DefaultDumpKeysFormat)) - separateGenesisDoc := cmd.StringOpt("s separate-genesis-doc", "", "Emit a separate genesis doc as JSON") + separateGenesisDoc := cmd.StringOpt("s separate-genesis-doc", "", "Emit a separate genesis doc as JSON or TOML") validatorIndexOpt := cmd.IntOpt("v validator-index", -1, "Validator index (in validators list - GenesisSpec or GenesisDoc) from which to set ValidatorAddress") @@ -150,9 +172,9 @@ func main() { chainNameOpt := cmd.StringOpt("n chain-name", "", "Default chain name") - cmd.Spec = "[--keys-url=<keys URL>] [--genesis-spec=<GenesisSpec file> | --genesis-doc=<GenesisDoc file>] " + - "[--separate-genesis-doc=<genesis JSON file>]" + - "[--validator-index=<index>] [--chain-name] [--toml-in] [--json-out] " + + cmd.Spec = "[--keys-url=<keys URL> | (--generate-keys=<secret keys files> [--keys-template=<text template for each key>])] " + + "[--genesis-spec=<GenesisSpec file> | --genesis-doc=<GenesisDoc file>] " + + "[--separate-genesis-doc=<genesis JSON file>] [--validator-index=<index>] [--chain-name] [--json-out] " + "[--logging=<logging program>] [--describe-logging] [--debug]" cmd.Action = func() { @@ -160,7 +182,7 @@ func main() { if *configOpt != "" { // If explicitly given a config file use it as a base: - err := source.FromTOMLFile(*configOpt, conf) + err := source.FromFile(*configOpt, conf) if err != nil { fatalf("could not read base config file (as TOML): %v", err) } @@ -181,26 +203,41 @@ func main() { conf.Keys.URL = *keysUrlOpt } + // Genesis Spec if *genesisSpecOpt != "" { genesisSpec := new(spec.GenesisSpec) - err := fromFile(*genesisSpecOpt, *tomlInOpt, genesisSpec) + err := source.FromFile(*genesisSpecOpt, genesisSpec) if err != nil { fatalf("could not read GenesisSpec: %v", err) } - keyClient := keys.NewKeyClient(conf.Keys.URL, logging.NewNoopLogger()) - conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient) + if *generateKeysOpt != "" { + keyClient := mock.NewMockKeyClient() + conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient) + + secretKeysString, err := keyClient.DumpKeys(*keysTemplateOpt) + if err != nil { + fatalf("Could not dump keys: %v", err) + } + err = ioutil.WriteFile(*generateKeysOpt, []byte(secretKeysString), 0700) + if err != nil { + fatalf("Could not write secret keys: %v", err) + } + } else { + conf.GenesisDoc, err = genesisSpec.GenesisDoc(keys.NewKeyClient(conf.Keys.URL, logging.NewNoopLogger())) + } if err != nil { fatalf("could not realise GenesisSpec: %v", err) } } else if *genesisDocOpt != "" { genesisDoc := new(genesis.GenesisDoc) - err := fromFile(*genesisSpecOpt, *tomlInOpt, genesisDoc) + err := source.FromFile(*genesisSpecOpt, genesisDoc) if err != nil { fatalf("could not read GenesisSpec: %v", err) } conf.GenesisDoc = genesisDoc } + // Which validator am I? if *validatorIndexOpt > -1 { if conf.GenesisDoc == nil { fatalf("Unable to set ValidatorAddress from provided validator-index since no " + @@ -216,6 +253,7 @@ func main() { conf.ValidatorAddress = &conf.GenesisDoc.Validators[0].Address } + // Logging if *loggingOpt != "" { ops := strings.Split(*loggingOpt, ",") sinkConfig, err := presets.BuildSinkConfig(ops...) @@ -288,10 +326,10 @@ func fatalf(format string, args ...interface{}) { func burrowConfigProvider(configFile string) source.ConfigProvider { return source.FirstOf( // Will fail if file doesn't exist, but still skipped it configFile == "" - source.TOMLFile(configFile, false), + source.File(configFile, false), source.Environment(config.DefaultBurrowConfigJSONEnvironmentVariable), // Try working directory - source.TOMLFile(config.DefaultBurrowConfigTOMLFileName, true), + source.File(config.DefaultBurrowConfigTOMLFileName, true), source.Default(config.DefaultBurrowConfig())) } @@ -308,7 +346,7 @@ func genesisDocProvider(genesisFile string, skipNonExistent bool) source.ConfigP "in config cascade, only specify GenesisDoc in one place") } genesisDoc := new(genesis.GenesisDoc) - err := source.FromJSONFile(genesisFile, genesisDoc) + err := source.FromFile(genesisFile, genesisDoc) if err != nil { return err } @@ -316,18 +354,3 @@ func genesisDocProvider(genesisFile string, skipNonExistent bool) source.ConfigP return nil }) } - -func fromFile(file string, toml bool, conf interface{}) (err error) { - if toml { - err = source.FromTOMLFile(file, conf) - if err != nil { - fatalf("could not read GenesisSpec: %v", err) - } - } else { - err = source.FromJSONFile(file, conf) - if err != nil { - fatalf("could not read GenesisSpec: %v", err) - } - } - return -} diff --git a/config/source/source.go b/config/source/source.go index b4a31d809bb6d150eaaaa7af1b2c17522fc39d45..16a1ad7830a6dfd0944a68a73e53f016f9990ce3 100644 --- a/config/source/source.go +++ b/config/source/source.go @@ -13,6 +13,7 @@ import ( "github.com/BurntSushi/toml" "github.com/cep21/xdgbasedir" "github.com/imdario/mergo" + "regexp" ) // If passed this identifier try to read config from STDIN @@ -31,6 +32,16 @@ type ConfigProvider interface { var _ ConfigProvider = &configSource{} +type Format string + +const ( + JSON Format = "JSON" + TOML Format = "TOML" + Unknown Format = "" +) + +var jsonRegex = regexp.MustCompile(`\s*{`) + type configSource struct { from string skip bool @@ -116,26 +127,14 @@ func EachOf(providers ...ConfigProvider) *configSource { return Cascade(os.Stderr, false, providers...) } -// Try to source config from provided JSON file, is skipNonExistent is true then the provider will fall-through (skip) -// when the file doesn't exist, rather than returning an error -func JSONFile(configFile string, skipNonExistent bool) *configSource { +// Try to source config from provided file detecting the file format, is skipNonExistent is true then the provider will +// fall-through (skip) when the file doesn't exist, rather than returning an error +func File(configFile string, skipNonExistent bool) *configSource { return &configSource{ skip: ShouldSkipFile(configFile, skipNonExistent), from: fmt.Sprintf("JSON config file at '%s'", configFile), apply: func(baseConfig interface{}) error { - return FromJSONFile(configFile, baseConfig) - }, - } -} - -// Try to source config from provided TOML file, is skipNonExistent is true then the provider will fall-through (skip) -// when the file doesn't exist, rather than returning an error -func TOMLFile(configFile string, skipNonExistent bool) *configSource { - return &configSource{ - skip: ShouldSkipFile(configFile, skipNonExistent), - from: fmt.Sprintf("TOML config file at '%s'", configFile), - apply: func(baseConfig interface{}) error { - return FromTOMLFile(configFile, baseConfig) + return FromFile(configFile, baseConfig) }, } } @@ -157,7 +156,7 @@ func XDGBaseDir(configFileName string) *configSource { if err != nil { return err } - return FromTOMLFile(configFile, baseConfig) + return FromFile(configFile, baseConfig) }, } } @@ -183,30 +182,39 @@ func Default(defaultConfig interface{}) *configSource { } } -func FromJSONFile(configFile string, conf interface{}) error { +func FromFile(configFile string, conf interface{}) error { bs, err := ReadFile(configFile) if err != nil { return err } - return FromJSONString(string(bs), conf) + return FromString(string(bs), conf) } -func FromTOMLFile(configFile string, conf interface{}) error { - bs, err := ReadFile(configFile) +func FromTOMLString(tomlString string, conf interface{}) error { + _, err := toml.Decode(tomlString, conf) if err != nil { return err } + return nil +} - return FromTOMLString(string(bs), conf) +func FromString(configString string, conf interface{}) error { + switch DetectFormat(configString) { + case JSON: + return FromJSONString(configString, conf) + case TOML: + return FromTOMLString(configString, conf) + default: + return fmt.Errorf("unknown configuration format:\n%s", configString) + } } -func FromTOMLString(tomlString string, conf interface{}) error { - _, err := toml.Decode(tomlString, conf) - if err != nil { - return err +func DetectFormat(configString string) Format { + if jsonRegex.MatchString(configString) { + return JSON } - return nil + return TOML } func FromJSONString(jsonString string, conf interface{}) error { diff --git a/config/source/source_test.go b/config/source/source_test.go index 7bf062b40dd6e1713725eb74da0177ce1d2ea861..08cc5424bbae5398ba8211df50a75e2c926247ae 100644 --- a/config/source/source_test.go +++ b/config/source/source_test.go @@ -31,7 +31,7 @@ func TestFile(t *testing.T) { file := writeConfigFile(t, newTestConfig()) defer os.Remove(file) conf := new(animalConfig) - err := TOMLFile(file, false).Apply(conf) + err := File(file, false).Apply(conf) assert.NoError(t, err) assert.Equal(t, tomlString, TOMLString(conf)) } @@ -42,7 +42,7 @@ func TestCascade(t *testing.T) { conf := newTestConfig() err := Cascade(os.Stderr, true, Environment(envVar), - TOMLFile("", false)).Apply(conf) + File("", false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, newTestConfig(), conf) @@ -53,7 +53,7 @@ func TestCascade(t *testing.T) { conf = new(animalConfig) err = Cascade(os.Stderr, true, Environment(envVar), - TOMLFile(file, false)).Apply(conf) + File(file, false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, TOMLString(fileConfig), TOMLString(conf)) @@ -66,11 +66,18 @@ func TestCascade(t *testing.T) { conf = newTestConfig() err = Cascade(os.Stderr, true, Environment(envVar), - TOMLFile(file, false)).Apply(conf) + File(file, false)).Apply(conf) assert.NoError(t, err) assert.Equal(t, TOMLString(envConfig), TOMLString(conf)) } +func TestDetectFormat(t *testing.T) { + assert.Equal(t, TOML, DetectFormat("")) + assert.Equal(t, JSON, DetectFormat("{")) + assert.Equal(t, JSON, DetectFormat("\n\n\t \n\n {")) + assert.Equal(t, TOML, DetectFormat("[Tendermint]\n Seeds =\"foobar@val0\"}")) +} + func writeConfigFile(t *testing.T, conf interface{}) string { tomlString := TOMLString(conf) f, err := ioutil.TempFile("", "source-test.toml") diff --git a/keys/mock/key_client_mock.go b/keys/mock/key_client_mock.go index 96a614cac83ebe8367a84e76fad91563a1198375..1faa65e269e5ed2fb4112f41c5adfd60e6ae8076 100644 --- a/keys/mock/key_client_mock.go +++ b/keys/mock/key_client_mock.go @@ -18,11 +18,18 @@ import ( "crypto/rand" "fmt" + "bytes" + "text/template" + + "encoding/base64" + acm "github.com/hyperledger/burrow/account" . "github.com/hyperledger/burrow/keys" "github.com/tendermint/ed25519" crypto "github.com/tendermint/go-crypto" + "github.com/tmthrgd/go-hex" "golang.org/x/crypto/ripemd160" + "github.com/pkg/errors" ) //--------------------------------------------------------------------- @@ -30,14 +37,35 @@ import ( // Simple ed25519 key structure for mock purposes with ripemd160 address type MockKey struct { + Name string Address acm.Address - PrivateKey [ed25519.PrivateKeySize]byte PublicKey []byte + PrivateKey []byte } -func newMockKey() (*MockKey, error) { +const DefaultDumpKeysFormat = `{ + "Keys": [<< range $index, $key := . >><< if $index>>,<< end >> + { + "Name": "<< $key.Name >>", + "Address": "<< $key.Address >>", + "PublicKey": "<< $key.PublicKeyBase64 >>", + "PrivateKey": "<< $key.PrivateKeyBase64 >>" + }<< end >> + ] +}` + +const LeftTemplateDelim = "<<" +const RightTemplateDelim = ">>" + +var DefaultDumpKeysTemplate = template.Must(template.New("MockKeyClient_DumpKeys"). + Delims(LeftTemplateDelim, RightTemplateDelim). + Parse(DefaultDumpKeysFormat)) + +func newMockKey(name string) (*MockKey, error) { key := &MockKey{ - PublicKey: make([]byte, ed25519.PublicKeySize), + Name: name, + PublicKey: make([]byte, ed25519.PublicKeySize), + PrivateKey: make([]byte, ed25519.PrivateKeySize), } // this is a mock key, so the entropy of the source is purely // for testing @@ -65,15 +93,34 @@ func mockKeyFromPrivateAccount(privateAccount acm.PrivateAccount) *MockKey { panic(fmt.Errorf("mock key client only supports ed25519 private keys at present")) } key := &MockKey{ - Address: privateAccount.Address(), - PublicKey: privateAccount.PublicKey().RawBytes(), + Name: privateAccount.Address().String(), + Address: privateAccount.Address(), + PublicKey: privateAccount.PublicKey().RawBytes(), + PrivateKey: privateAccount.PrivateKey().RawBytes(), } - copy(key.PrivateKey[:], privateAccount.PrivateKey().RawBytes()) return key } func (mockKey *MockKey) Sign(message []byte) (acm.Signature, error) { - return acm.SignatureFromBytes(ed25519.Sign(&mockKey.PrivateKey, message)[:]) + var privateKey [ed25519.PrivateKeySize]byte + copy(privateKey[:], mockKey.PrivateKey) + return acm.SignatureFromBytes(ed25519.Sign(&privateKey, message)[:]) +} + +func (mockKey *MockKey) PrivateKeyBase64() string { + return base64.StdEncoding.EncodeToString(mockKey.PrivateKey[:]) +} + +func (mockKey *MockKey) PrivateKeyHex() string { + return hex.EncodeUpperToString(mockKey.PrivateKey[:]) +} + +func (mockKey *MockKey) PublicKeyBase64() string { + return base64.StdEncoding.EncodeToString(mockKey.PublicKey) +} + +func (mockKey *MockKey) PublicKeyHex() string { + return hex.EncodeUpperToString(mockKey.PublicKey) } //--------------------------------------------------------------------- @@ -96,26 +143,26 @@ func NewMockKeyClient(privateAccounts ...acm.PrivateAccount) *MockKeyClient { return client } -func (mock *MockKeyClient) NewKey() acm.Address { +func (mkc *MockKeyClient) NewKey(name string) acm.Address { // Only tests ED25519 curve and ripemd160. - key, err := newMockKey() + key, err := newMockKey(name) if err != nil { panic(fmt.Sprintf("Mocked key client failed on key generation: %s", err)) } - mock.knownKeys[key.Address] = key + mkc.knownKeys[key.Address] = key return key.Address } -func (mock *MockKeyClient) Sign(signAddress acm.Address, message []byte) (acm.Signature, error) { - key := mock.knownKeys[signAddress] +func (mkc *MockKeyClient) Sign(signAddress acm.Address, message []byte) (acm.Signature, error) { + key := mkc.knownKeys[signAddress] if key == nil { return acm.Signature{}, fmt.Errorf("Unknown address (%s)", signAddress) } return key.Sign(message) } -func (mock *MockKeyClient) PublicKey(address acm.Address) (acm.PublicKey, error) { - key := mock.knownKeys[address] +func (mkc *MockKeyClient) PublicKey(address acm.Address) (acm.PublicKey, error) { + key := mkc.knownKeys[address] if key == nil { return acm.PublicKey{}, fmt.Errorf("Unknown address (%s)", address) } @@ -124,10 +171,27 @@ func (mock *MockKeyClient) PublicKey(address acm.Address) (acm.PublicKey, error) return acm.PublicKeyFromGoCryptoPubKey(pubKeyEd25519.Wrap()) } -func (mock *MockKeyClient) Generate(keyName string, keyType KeyType) (acm.Address, error) { - return mock.NewKey(), nil +func (mkc *MockKeyClient) Generate(keyName string, keyType KeyType) (acm.Address, error) { + return mkc.NewKey(keyName), nil } -func (mock *MockKeyClient) HealthCheck() error { +func (mkc *MockKeyClient) HealthCheck() error { return nil } + +func (mkc *MockKeyClient) DumpKeys(templateString string) (string, error) { + tmpl, err := template.New("DumpKeys").Delims(LeftTemplateDelim, RightTemplateDelim).Parse(templateString) + if err != nil { + errors.Wrap(err, "could not dump keys to template") + } + buf := new(bytes.Buffer) + keys := make([]*MockKey, 0, len(mkc.knownKeys)) + for _, k := range mkc.knownKeys { + keys = append(keys, k) + } + err = tmpl.Execute(buf, keys) + if err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/keys/mock/key_client_mock_test.go b/keys/mock/key_client_mock_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ffcec0019c000eecfa116ec1be3e2b275cb6c992 --- /dev/null +++ b/keys/mock/key_client_mock_test.go @@ -0,0 +1,29 @@ +package mock + +import ( + "testing" + + "encoding/json" + + "github.com/hyperledger/burrow/keys" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMockKeyClient_DumpKeys(t *testing.T) { + keyClient := NewMockKeyClient() + _, err := keyClient.Generate("foo", keys.KeyTypeEd25519Ripemd160) + require.NoError(t, err) + _, err = keyClient.Generate("foobar", keys.KeyTypeEd25519Ripemd160) + require.NoError(t, err) + dump, err := keyClient.DumpKeys(DefaultDumpKeysFormat) + require.NoError(t, err) + + // Check JSON equal + var keys struct{ Keys []*MockKey } + err = json.Unmarshal([]byte(dump), &keys) + require.NoError(t, err) + bs, err := json.MarshalIndent(keys, "", " ") + require.NoError(t, err) + assert.Equal(t, string(bs), dump) +}