diff --git a/cmd/burrow/commands/configure.go b/cmd/burrow/commands/configure.go index b06f9a9a0d11882f54df922d581e2e0ef514b8f0..937b7bc1a6c5e4a185ce43638a5c5dcb290a785c 100644 --- a/cmd/burrow/commands/configure.go +++ b/cmd/burrow/commands/configure.go @@ -1,12 +1,14 @@ package commands import ( + "encoding/json" "fmt" "io/ioutil" "strings" "github.com/hyperledger/burrow/config" "github.com/hyperledger/burrow/config/source" + "github.com/hyperledger/burrow/crypto" "github.com/hyperledger/burrow/deployment" "github.com/hyperledger/burrow/execution" "github.com/hyperledger/burrow/genesis" @@ -16,8 +18,24 @@ import ( logging_config "github.com/hyperledger/burrow/logging/config" "github.com/hyperledger/burrow/logging/config/presets" "github.com/jawher/mow.cli" + "github.com/tendermint/go-amino" + tm_crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tendermint/p2p" ) +func processTemplate(config *genesis.GenesisDoc, templateIn, templateOut string) error { + data, err := ioutil.ReadFile(templateIn) + if err != nil { + return err + } + pkg := deployment.Config{Config: *config} + output, err := pkg.Dump(string(data)) + if err != nil { + return err + } + return ioutil.WriteFile(templateOut, []byte(output), 0644) +} + func Configure(output Output) func(cmd *cli.Cmd) { return func(cmd *cli.Cmd) { genesisSpecOpt := cmd.StringOpt("s genesis-spec", "", @@ -33,14 +51,23 @@ func Configure(output Output) func(cmd *cli.Cmd) { genesisDocOpt := cmd.StringOpt("g genesis-doc", "", "GenesisDoc in JSON or TOML to embed in config") + generateNodeKeys := cmd.BoolOpt("generate-node-keys", false, "Generate node keys for validators") + 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") + "File to output keys in a kubernetes secret definition (see --kubernetes-template)") - keysTemplateOpt := cmd.StringOpt("z keys-template", deployment.DefaultDumpKeysFormat, + keysTemplateOpt := cmd.StringOpt("z keys-template", deployment.KubernetesKeyDumpFormat, fmt.Sprintf("Go text/template template (left delim: %s right delim: %s) to generate secret keys "+ "file specified with --generate-keys.", deployment.LeftTemplateDelim, deployment.RightTemplateDelim)) + configTemplateIn := cmd.StringsOpt("t config-template-in", nil, + fmt.Sprintf("Go text/template template input filename (left delim: %s right delim: %s) to output generate config "+ + "file specified with --config-template-out", deployment.LeftTemplateDelim, deployment.RightTemplateDelim)) + + configTemplateOut := cmd.StringsOpt("t config-template-out", nil, + "Go text/template template output file. Template filename specified with --config-template-in "+ + "file specified with --config-template-out") + separateGenesisDoc := cmd.StringOpt("w separate-genesis-doc", "", "Emit a separate genesis doc as JSON or TOML") loggingOpt := cmd.StringOpt("l logging", "", @@ -56,7 +83,8 @@ func Configure(output Output) func(cmd *cli.Cmd) { chainNameOpt := cmd.StringOpt("n chain-name", "", "Default chain name") - cmd.Spec = "[--keys-url=<keys URL> | (--generate-keys=<secret keys files> [--keys-template=<text template for each key>])] " + + cmd.Spec = "[--keys-url=<keys URL> | (--generate-keys=<secret keys files> [--keys-template=<text template for key>])] " + + "[--config-template-in=<text template> --config-template-out=<output file>] " + "[--genesis-spec=<GenesisSpec file> | --genesis-doc=<GenesisDoc file>] " + "[--separate-genesis-doc=<genesis JSON file>] [--chain-name] [--json] " + "[--logging=<logging program>] [--describe-logging] [--debug]" @@ -87,6 +115,10 @@ func Configure(output Output) func(cmd *cli.Cmd) { conf.Keys.RemoteAddress = *keysUrlOpt } + if len(*configTemplateIn) != len(*configTemplateOut) { + output.Fatalf("--config-template-in and --config-template-out must be specified the same number of times") + } + // Genesis Spec if *genesisSpecOpt != "" { genesisSpec := new(spec.GenesisSpec) @@ -95,21 +127,64 @@ func Configure(output Output) func(cmd *cli.Cmd) { output.Fatalf("Could not read GenesisSpec: %v", err) } keyStore := keys.NewKeyStore(conf.Keys.KeysDirectory, conf.Keys.AllowBadFilePermissions, logging.NewNoopLogger()) + if *generateKeysOpt != "" { keyClient := keys.NewLocalKeyClient(keyStore, logging.NewNoopLogger()) - conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient) + conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient, *generateNodeKeys) if err != nil { output.Fatalf("Could not generate GenesisDoc from GenesisSpec using MockKeyClient: %v", err) } - allKeys, err := keyStore.AllKeys() + allNames, err := keyStore.GetAllNames() if err != nil { output.Fatalf("could get all keys: %v", err) } - pkg := deployment.Package{Keys: allKeys} - if err != nil { - output.Fatalf("Could not dump keys: %v", err) + + cdc := amino.NewCodec() + tm_crypto.RegisterAmino(cdc) + + pkg := deployment.KeysSecret{ChainName: conf.GenesisDoc.ChainName} + + for k := range allNames { + addr, err := crypto.AddressFromHexString(allNames[k]) + if err != nil { + output.Fatalf("Address %s not valid: %v", k, err) + } + key, err := keyStore.GetKey("", addr[:]) + if err != nil { + output.Fatalf("Failed to get key: %s: %v", k, err) + } + + // Is this is a validator node key? + nodeKey := false + for _, a := range conf.GenesisDoc.Validators { + if a.NodeAddress != nil && addr == *a.NodeAddress { + nodeKey = true + break + } + } + + if nodeKey { + privKey := tm_crypto.GenPrivKeyEd25519() + copy(privKey[:], key.PrivateKey.PrivateKey) + nodeKey := &p2p.NodeKey{ + PrivKey: privKey, + } + + json, err := cdc.MarshalJSON(nodeKey) + if err != nil { + output.Fatalf("go-amino failed to json marshall private key: %v", err) + } + pkg.NodeKeys = append(pkg.NodeKeys, deployment.Key{Name: k, Address: addr, KeyJSON: json}) + } else { + json, err := json.Marshal(key) + if err != nil { + output.Fatalf("Failed to json marshal key: %s: %v", k, err) + } + pkg.Keys = append(pkg.Keys, deployment.Key{Name: k, Address: addr, KeyJSON: json}) + } } + secretKeysString, err := pkg.Dump(*keysTemplateOpt) if err != nil { output.Fatalf("Could not dump keys: %v", err) @@ -128,7 +203,7 @@ func Configure(output Output) func(cmd *cli.Cmd) { } else { keyClient = keys.NewLocalKeyClient(keyStore, logging.NewNoopLogger()) } - conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient) + conf.GenesisDoc, err = genesisSpec.GenesisDoc(keyClient, *generateNodeKeys) } if err != nil { @@ -143,6 +218,13 @@ func Configure(output Output) func(cmd *cli.Cmd) { conf.GenesisDoc = genesisDoc } + for ind := range *configTemplateIn { + err := processTemplate(conf.GenesisDoc, (*configTemplateIn)[ind], (*configTemplateOut)[ind]) + if err != nil { + output.Fatalf("coult not template from %s to %s: %v", (*configTemplateIn)[ind], (*configTemplateOut)[ind], err) + } + } + // Logging if *loggingOpt != "" { ops := strings.Split(*loggingOpt, ",") @@ -183,6 +265,7 @@ func Configure(output Output) func(cmd *cli.Cmd) { } conf.GenesisDoc = nil } + if *jsonOutOpt { output.Printf(conf.JSONString()) } else { diff --git a/deployment/config.go b/deployment/config.go index b5ead42643ef360ee97f81b0b55f3c7b7ec96756..decf18c140a66073cfa0a633d93dad5d6ddd0928 100644 --- a/deployment/config.go +++ b/deployment/config.go @@ -3,19 +3,31 @@ package deployment import ( "bytes" "encoding/base64" + "encoding/json" "fmt" "reflect" "text/template" - "github.com/hyperledger/burrow/config" - "github.com/hyperledger/burrow/keys" + "github.com/hyperledger/burrow/crypto" + "github.com/hyperledger/burrow/genesis" "github.com/pkg/errors" "github.com/tmthrgd/go-hex" ) -type Package struct { - Keys []*keys.Key - BurrowConfig *config.BurrowConfig +type Config struct { + Config genesis.GenesisDoc +} + +type Key struct { + Name string + Address crypto.Address + KeyJSON json.RawMessage +} + +type KeysSecret struct { + Keys []Key + NodeKeys []Key + ChainName string } const DefaultDumpKeysFormat = `{ @@ -37,12 +49,18 @@ const HelmDumpKeysFormat = `privateKeys:<< range $key := . >> privateKey: << base64 $key.PrivateKey >><< end >> ` -const KubernetesKeyDumpFormat = `keysFiles:<< range $index, $key := . >> - key-<< printf "%03d" $index >>: << base64 $key.MonaxKeysJSON >><< end >> -keysAddresses:<< range $index, $key := . >> - key-<< printf "%03d" $index >>: << $key.Address >><< end >> -validatorAddresses:<< range $index, $key := . >> - - << $key.Address >><< end >> +const KubernetesKeyDumpFormat = `apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: << .ChainName >>-keys +data: +<<- range .Keys >> + << .Address >>.json: << base64 .KeyJSON >> +<<- end >> +<<- range .NodeKeys >> + << .Name >>: << base64 .KeyJSON >> +<<- end >> ` const LeftTemplateDelim = "<<" @@ -59,16 +77,30 @@ var templateFuncs template.FuncMap = map[string]interface{}{ var DefaultDumpKeysTemplate = template.Must(template.New("MockKeyClient_DumpKeys").Funcs(templateFuncs). Delims(LeftTemplateDelim, RightTemplateDelim). - Parse(DefaultDumpKeysFormat)) + Parse(KubernetesKeyDumpFormat)) -func (pkg *Package) Dump(templateString string) (string, error) { +func (pkg *KeysSecret) Dump(templateString string) (string, error) { tmpl, err := template.New("DumpKeys").Delims(LeftTemplateDelim, RightTemplateDelim).Funcs(templateFuncs). Parse(templateString) if err != nil { return "", errors.Wrap(err, "could not dump keys to template") } buf := new(bytes.Buffer) - err = tmpl.Execute(buf, pkg.Keys) + err = tmpl.Execute(buf, pkg) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func (pkg *Config) Dump(templateString string) (string, error) { + tmpl, err := template.New("ConfigTemplate").Delims(LeftTemplateDelim, RightTemplateDelim).Funcs(templateFuncs). + Parse(templateString) + if err != nil { + return "", errors.Wrap(err, "could not dump config to template") + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, pkg) if err != nil { return "", err } diff --git a/execution/execution_test.go b/execution/execution_test.go index 93fb03b443a9602ba5118f9312e080ad2f8a5bde..4a0116d42b18802f96f702c4ebcdd61ea6ea8862 100644 --- a/execution/execution_test.go +++ b/execution/execution_test.go @@ -1525,9 +1525,9 @@ proof-of-work chain as proof of what happened while they were gone ` t.Errorf("Unexpected bond height. Expected %v, got %v", blockchain.LastBlockHeight(), acc0Val.BondHeight) } - if acc0Val.VotingPower != 1 { + if acc0Val.Power != 1 { t.Errorf("Unexpected voting power. Expected %v, got %v", - acc0Val.VotingPower, acc0.Balance()) + acc0Val.Power, acc0.Balance()) } if acc0Val.Accum != 0 { t.Errorf("Unexpected accum. Expected 0, got %v", diff --git a/genesis/genesis.go b/genesis/genesis.go index 16edbe3303664304af5148d25cdffed1554c7810..5a6e076031699163b7e7bb3f469185b0eac635de 100644 --- a/genesis/genesis.go +++ b/genesis/genesis.go @@ -50,8 +50,9 @@ type Account struct { type Validator struct { BasicAccount - Name string - UnbondTo []BasicAccount + NodeAddress *crypto.Address `json:",omitempty" toml:",omitempty"` + Name string + UnbondTo []BasicAccount } //------------------------------------------------------------ @@ -60,7 +61,7 @@ type Validator struct { type GenesisDoc struct { GenesisTime time.Time ChainName string - Salt []byte `json:",omitempty"` + Salt []byte `json:",omitempty" toml:",omitempty"` GlobalPermissions ptypes.AccountPermissions Accounts []Account Validators []Validator @@ -153,8 +154,9 @@ func (gv *Validator) Clone() Validator { PublicKey: gv.PublicKey, Amount: gv.Amount, }, - Name: gv.Name, - UnbondTo: unbondToClone, + Name: gv.Name, + UnbondTo: unbondToClone, + NodeAddress: gv.NodeAddress, } } diff --git a/genesis/spec/genesis_spec.go b/genesis/spec/genesis_spec.go index b1f6e92020727099ce5843b6035ab6ad75290689..3f3aba5f387e92a26a00221ea3f9412cf9c8d42c 100644 --- a/genesis/spec/genesis_spec.go +++ b/genesis/spec/genesis_spec.go @@ -14,7 +14,7 @@ import ( ) const DefaultAmount uint64 = 1000000 -const DefaultAmountBonded uint64 = 10000 +const DefaultPower uint64 = 10000 // A GenesisSpec is schematic representation of a genesis state, that is it is a template // for a GenesisDoc excluding that which needs to be instantiated at the point of genesis @@ -41,7 +41,7 @@ func (gs *GenesisSpec) RealiseKeys(keyClient keys.KeyClient) error { } // Produce a fully realised GenesisDoc from a template GenesisDoc that may omit values -func (gs *GenesisSpec) GenesisDoc(keyClient keys.KeyClient) (*genesis.GenesisDoc, error) { +func (gs *GenesisSpec) GenesisDoc(keyClient keys.KeyClient, generateNodeKeys bool) (*genesis.GenesisDoc, error) { genesisDoc := new(genesis.GenesisDoc) if gs.GenesisTime == nil { genesisDoc.GenesisTime = time.Now() @@ -69,9 +69,9 @@ func (gs *GenesisSpec) GenesisDoc(keyClient keys.KeyClient) (*genesis.GenesisDoc templateAccounts := gs.Accounts if len(gs.Accounts) == 0 { - amountBonded := DefaultAmountBonded + Power := DefaultPower templateAccounts = append(templateAccounts, TemplateAccount{ - AmountBonded: &amountBonded, + Power: &Power, }) } @@ -82,10 +82,10 @@ func (gs *GenesisSpec) GenesisDoc(keyClient keys.KeyClient) (*genesis.GenesisDoc } genesisDoc.Accounts = append(genesisDoc.Accounts, *account) // Create a corresponding validator - if templateAccount.AmountBonded != nil { + if templateAccount.Power != nil { // Note this does not modify the input template templateAccount.Address = &account.Address - validator, err := templateAccount.Validator(keyClient, i) + validator, err := templateAccount.Validator(keyClient, i, generateNodeKeys) if err != nil { return nil, fmt.Errorf("could not create Validator from template: %v", err) } diff --git a/genesis/spec/genesis_spec_test.go b/genesis/spec/genesis_spec_test.go index 6ce214dc434fda5299990847385feb9cb110a513..0a9aeb7af802c71f93f3fa755c9740ddd5d6a01a 100644 --- a/genesis/spec/genesis_spec_test.go +++ b/genesis/spec/genesis_spec_test.go @@ -17,11 +17,11 @@ func TestGenesisSpec_GenesisDoc(t *testing.T) { amtBonded := uint64(100) genesisSpec := GenesisSpec{ Accounts: []TemplateAccount{{ - AmountBonded: &amtBonded, + Power: &amtBonded, }}, } - genesisDoc, err := genesisSpec.GenesisDoc(keyClient) + genesisDoc, err := genesisSpec.GenesisDoc(keyClient, false) require.NoError(t, err) require.Len(t, genesisDoc.Accounts, 1) // Should create validator @@ -51,7 +51,7 @@ func TestGenesisSpec_GenesisDoc(t *testing.T) { }}, } - genesisDoc, err = genesisSpec.GenesisDoc(keyClient) + genesisDoc, err = genesisSpec.GenesisDoc(keyClient, false) require.NoError(t, err) require.Len(t, genesisDoc.Accounts, 2) @@ -66,7 +66,7 @@ func TestGenesisSpec_GenesisDoc(t *testing.T) { // Try an empty spec genesisSpec = GenesisSpec{} - genesisDoc, err = genesisSpec.GenesisDoc(keyClient) + genesisDoc, err = genesisSpec.GenesisDoc(keyClient, false) require.NoError(t, err) // Similar assersions to first case - should generate our default single identity chain diff --git a/genesis/spec/presets.go b/genesis/spec/presets.go index 9454520c9f284666349d8b2334c59879e1c169d0..30e6c08a186cb14fb5c02e9797194568737051b0 100644 --- a/genesis/spec/presets.go +++ b/genesis/spec/presets.go @@ -12,13 +12,13 @@ import ( func FullAccount(name string) GenesisSpec { // Inheriting from the arbitrary figures used by monax tool for now amount := uint64(99999999999999) - amountBonded := uint64(9999999999) + Power := uint64(9999999999) return GenesisSpec{ Accounts: []TemplateAccount{{ - Name: name, - Amount: &amount, - AmountBonded: &amountBonded, - Permissions: []string{permission.AllString}, + Name: name, + Amount: &amount, + Power: &Power, + Permissions: []string{permission.AllString}, }, }, } @@ -67,13 +67,13 @@ func DeveloperAccount(name string) GenesisSpec { func ValidatorAccount(name string) GenesisSpec { // Inheriting from the arbitrary figures used by monax tool for now amount := uint64(9999999999) - amountBonded := amount - 1 + Power := amount - 1 return GenesisSpec{ Accounts: []TemplateAccount{{ - Name: name, - Amount: &amount, - AmountBonded: &amountBonded, - Permissions: []string{permission.BondString}, + Name: name, + Amount: &amount, + Power: &Power, + Permissions: []string{permission.BondString}, }}, } } @@ -147,7 +147,7 @@ func mergeAccount(base, override TemplateAccount) TemplateAccount { } base.Amount = addUint64Pointers(base.Amount, override.Amount) - base.AmountBonded = addUint64Pointers(base.AmountBonded, override.AmountBonded) + base.Power = addUint64Pointers(base.Power, override.Power) base.Permissions = mergeStrings(base.Permissions, override.Permissions) base.Roles = mergeStrings(base.Roles, override.Roles) diff --git a/genesis/spec/presets_test.go b/genesis/spec/presets_test.go index dde6f296c7103fa5ca33dcaf0416aa797c440265..df77f9bdba93339754ef237cf503f292e1cf13c9 100644 --- a/genesis/spec/presets_test.go +++ b/genesis/spec/presets_test.go @@ -12,7 +12,7 @@ import ( func TestMergeGenesisSpecAccounts(t *testing.T) { keyClient := mock.NewKeyClient() gs := MergeGenesisSpecs(FullAccount("0"), ParticipantAccount("1"), ParticipantAccount("2")) - gd, err := gs.GenesisDoc(keyClient) + gd, err := gs.GenesisDoc(keyClient, false) require.NoError(t, err) assert.Len(t, gd.Validators, 1) assert.Len(t, gd.Accounts, 3) diff --git a/genesis/spec/template_account.go b/genesis/spec/template_account.go index 51e43049837dd6915c363d8c40958ca21918f503..9d3c2acc4d60bbcc4465ef250b94335260bac715 100644 --- a/genesis/spec/template_account.go +++ b/genesis/spec/template_account.go @@ -14,25 +14,34 @@ type TemplateAccount struct { // Template accounts sharing a name will be merged when merging genesis specs Name string `json:",omitempty" toml:",omitempty"` // Address is convenient to have in file for reference, but otherwise ignored since derived from PublicKey - Address *crypto.Address `json:",omitempty" toml:",omitempty"` - PublicKey *crypto.PublicKey `json:",omitempty" toml:",omitempty"` - Amount *uint64 `json:",omitempty" toml:",omitempty"` - AmountBonded *uint64 `json:",omitempty" toml:",omitempty"` - Permissions []string `json:",omitempty" toml:",omitempty"` - Roles []string `json:",omitempty" toml:",omitempty"` + Address *crypto.Address `json:",omitempty" toml:",omitempty"` + NodeAddress *crypto.Address `json:",omitempty" toml:",omitempty"` + PublicKey *crypto.PublicKey `json:",omitempty" toml:",omitempty"` + Amount *uint64 `json:",omitempty" toml:",omitempty"` + Power *uint64 `json:",omitempty" toml:",omitempty"` + Permissions []string `json:",omitempty" toml:",omitempty"` + Roles []string `json:",omitempty" toml:",omitempty"` } -func (ta TemplateAccount) Validator(keyClient keys.KeyClient, index int) (*genesis.Validator, error) { +func (ta TemplateAccount) Validator(keyClient keys.KeyClient, index int, generateNodeKeys bool) (*genesis.Validator, error) { var err error gv := new(genesis.Validator) gv.PublicKey, gv.Address, err = ta.RealisePubKeyAndAddress(keyClient) if err != nil { return nil, err } - if ta.AmountBonded == nil { - gv.Amount = DefaultAmountBonded + if generateNodeKeys && ta.NodeAddress == nil { + // If neither PublicKey or Address set then generate a new one + address, err := keyClient.Generate("nodekey-"+ta.Name, crypto.CurveTypeEd25519) + if err != nil { + return nil, err + } + ta.NodeAddress = &address + } + if ta.Power == nil { + gv.Amount = DefaultPower } else { - gv.Amount = *ta.AmountBonded + gv.Amount = *ta.Power } if ta.Name == "" { gv.Name = accountNameFromIndex(index) @@ -45,6 +54,7 @@ func (ta TemplateAccount) Validator(keyClient keys.KeyClient, index int) (*genes PublicKey: gv.PublicKey, Amount: gv.Amount, }} + gv.NodeAddress = ta.NodeAddress return gv, nil } diff --git a/keys/key_store.go b/keys/key_store.go index 990c4233550ee93842a177fc4e729dd81f05e136..9da69b3fb2bb26aabcb89d37bbde2ea3066c4fee 100644 --- a/keys/key_store.go +++ b/keys/key_store.go @@ -340,6 +340,10 @@ func WriteKeyFile(addr []byte, dataDirPath string, content []byte) (err error) { return ioutil.WriteFile(keyFilePath, content, 0600) // read, write for user } +func (ks *KeyStore) GetAllNames() (map[string]string, error) { + return coreNameList(ks.keysDirPath) +} + func GetAllAddresses(dataDirPath string) (addresses [][]byte, err error) { fileInfos, err := ioutil.ReadDir(dataDirPath) if err != nil { @@ -347,7 +351,7 @@ func GetAllAddresses(dataDirPath string) (addresses [][]byte, err error) { } addresses = make([][]byte, len(fileInfos)) for i, fileInfo := range fileInfos { - addr := strings.TrimSuffix(fileInfo.Name(), "json") + addr := strings.TrimSuffix(fileInfo.Name(), ".json") address, err := hex.DecodeString(addr) if err != nil { continue