From e32aefa18f724567b5a7d82c80ad2164e25d0f55 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Thu, 26 Oct 2023 16:35:20 +0200 Subject: [PATCH 1/5] Changed the default API port from 8080 to 14265 to match hornet --- components/restapi/params.go | 2 +- config_defaults.json | 2 +- .../templates/docker-compose-iota-core.yml.j2 | 4 ++-- documentation/docs/references/configuration.md | 4 ++-- tools/docker-network/docker-compose.yml | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/restapi/params.go b/components/restapi/params.go index 440fdc102..121d07ae8 100644 --- a/components/restapi/params.go +++ b/components/restapi/params.go @@ -9,7 +9,7 @@ type ParametersRestAPI struct { // Enabled defines whether the REST API plugin is enabled. Enabled bool `default:"true" usage:"whether the REST API plugin is enabled"` // the bind address on which the REST API listens on - BindAddress string `default:"0.0.0.0:8080" usage:"the bind address on which the REST API listens on"` + BindAddress string `default:"0.0.0.0:14265" usage:"the bind address on which the REST API listens on"` // the HTTP REST routes which can be called without authorization. Wildcards using * are allowed PublicRoutes []string `usage:"the HTTP REST routes which can be called without authorization. Wildcards using * are allowed"` // the HTTP REST routes which need to be called with authorization. Wildcards using * are allowed diff --git a/config_defaults.json b/config_defaults.json index 53114f287..f24471e14 100644 --- a/config_defaults.json +++ b/config_defaults.json @@ -44,7 +44,7 @@ }, "restAPI": { "enabled": true, - "bindAddress": "0.0.0.0:8080", + "bindAddress": "0.0.0.0:14265", "publicRoutes": [ "/health", "/api/routes", diff --git a/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 b/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 index a5284d45b..96da41c3e 100644 --- a/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 +++ b/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 @@ -26,7 +26,7 @@ services: ports: - "14666:14666/tcp" # P2P - "6061:6061/tcp" # pprof - - "8080:8080/tcp" # REST-API + - "8080:14265/tcp" # REST-API - "8081:8081/tcp" # Dashboard - "9311:9311/tcp" # Prometheus - "9029:9029/tcp" # INX @@ -46,7 +46,7 @@ services: --p2p.db.path=/app/data/peerdb --profiling.enabled=true --profiling.bindAddress=0.0.0.0:6061 - --restAPI.bindAddress=0.0.0.0:8080 + --restAPI.bindAddress=0.0.0.0:14265 --database.path=/app/data/database --protocol.snapshot.path=/app/data/snapshot.bin {% if 'node-01' in inventory_hostname or 'node-02' in inventory_hostname or 'node-03' in inventory_hostname %} diff --git a/documentation/docs/references/configuration.md b/documentation/docs/references/configuration.md index bbcd516e6..ba034fde1 100644 --- a/documentation/docs/references/configuration.md +++ b/documentation/docs/references/configuration.md @@ -175,7 +175,7 @@ Example: | Name | Description | Type | Default value | | ------------------------------ | ---------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | enabled | Whether the REST API plugin is enabled | boolean | true | -| bindAddress | The bind address on which the REST API listens on | string | "0.0.0.0:8080" | +| bindAddress | The bind address on which the REST API listens on | string | "0.0.0.0:14265" | | publicRoutes | The HTTP REST routes which can be called without authorization. Wildcards using \* are allowed | array | /health
/api/routes
/api/core/v3/info
/api/core/v3/blocks\*
/api/core/v3/transactions\*
/api/core/v3/commitments\*
/api/core/v3/outputs\*
/api/core/v3/accounts\*
/api/core/v3/validators\*
/api/core/v3/rewards\*
/api/core/v3/committee
/api/debug/v2/\*
/api/indexer/v2/\*
/api/mqtt/v2 | | protectedRoutes | The HTTP REST routes which need to be called with authorization. Wildcards using \* are allowed | array | /api/\* | | debugRequestLoggerEnabled | Whether the debug logging for requests should be enabled | boolean | false | @@ -204,7 +204,7 @@ Example: { "restAPI": { "enabled": true, - "bindAddress": "0.0.0.0:8080", + "bindAddress": "0.0.0.0:14265", "publicRoutes": [ "/health", "/api/routes", diff --git a/tools/docker-network/docker-compose.yml b/tools/docker-network/docker-compose.yml index bd63e2eab..e8823c6b6 100644 --- a/tools/docker-network/docker-compose.yml +++ b/tools/docker-network/docker-compose.yml @@ -20,7 +20,7 @@ services: networks: - iota-core ports: - - "8080:8080/tcp" # REST-API + - "8080:14265/tcp" # REST-API - "8081:8081/tcp" # Dashboard - "6081:6061/tcp" # pprof - "9089:9029/tcp" # INX @@ -49,7 +49,7 @@ services: networks: - iota-core ports: - - "8070:8080/tcp" # REST-API + - "8070:14265/tcp" # REST-API - "8071:8081/tcp" # Dashboard - "6071:6061/tcp" # pprof - "9029:9029/tcp" # INX @@ -77,7 +77,7 @@ services: networks: - iota-core ports: - - "8090:8080/tcp" # REST-API + - "8090:14265/tcp" # REST-API - "8091:8081/tcp" # Dashboard - "6091:6061/tcp" # pprof - "9099:9029/tcp" # INX @@ -105,7 +105,7 @@ services: networks: - iota-core ports: - - "8040:8080/tcp" # REST-API + - "8040:14265/tcp" # REST-API - "8041:8081/tcp" # Dashboard - "6041:6061/tcp" # pprof - "9049:9029/tcp" # INX @@ -130,7 +130,7 @@ services: networks: - iota-core ports: - - "8030:8080/tcp" # REST-API + - "8030:14265/tcp" # REST-API - "8031:8081/tcp" # Dashboard - "6031:6061/tcp" # pprof - "9039:9029/tcp" # INX From 4c722f0645f8c097ec7493c5af1f0662c7fcb854 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Thu, 26 Oct 2023 16:35:47 +0200 Subject: [PATCH 2/5] Introduce IdentityPrivateKeyFileName constant --- components/p2p/component.go | 2 +- components/p2p/params.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/p2p/component.go b/components/p2p/component.go index 9635df585..dff432bb7 100644 --- a/components/p2p/component.go +++ b/components/p2p/component.go @@ -230,7 +230,7 @@ func provide(c *dig.Container) error { if err := c.Provide(func(deps p2pDeps) p2pResult { res := p2pResult{} - privKeyFilePath := filepath.Join(deps.P2PDatabasePath, "identity.key") + privKeyFilePath := filepath.Join(deps.P2PDatabasePath, IdentityPrivateKeyFileName) // make sure nobody copies around the peer store since it contains the private key of the node Component.LogInfof(`WARNING: never share your "%s" folder as it contains your node's private key!`, deps.P2PDatabasePath) diff --git a/components/p2p/params.go b/components/p2p/params.go index 37f099d85..7b2cba4e7 100644 --- a/components/p2p/params.go +++ b/components/p2p/params.go @@ -6,7 +6,8 @@ import ( const ( // CfgPeers defines the static peers this node should retain a connection to (CLI). - CfgPeers = "peers" + CfgPeers = "peers" + IdentityPrivateKeyFileName = "identity.key" ) // ParametersP2P contains the definition of configuration parameters used by the p2p plugin. From 7c9f366314e1d4548087f888cbb4e797b2ce8993 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Thu, 26 Oct 2023 16:44:23 +0200 Subject: [PATCH 3/5] Ported a subset of the hornet toolset --- components/app/app.go | 20 ++++ pkg/toolset/ed25519.go | 172 ++++++++++++++++++++++++++++ pkg/toolset/jwt.go | 109 ++++++++++++++++++ pkg/toolset/node_info.go | 50 ++++++++ pkg/toolset/p2p_identity_extract.go | 66 +++++++++++ pkg/toolset/p2p_identity_gen.go | 136 ++++++++++++++++++++++ pkg/toolset/toolset.go | 157 +++++++++++++++++++++++++ tools/gendoc/go.mod | 1 + tools/gendoc/go.sum | 2 + 9 files changed, 713 insertions(+) create mode 100644 pkg/toolset/ed25519.go create mode 100644 pkg/toolset/jwt.go create mode 100644 pkg/toolset/node_info.go create mode 100644 pkg/toolset/p2p_identity_extract.go create mode 100644 pkg/toolset/p2p_identity_gen.go create mode 100644 pkg/toolset/toolset.go diff --git a/components/app/app.go b/components/app/app.go index 4cb029500..5c4b42957 100644 --- a/components/app/app.go +++ b/components/app/app.go @@ -1,6 +1,9 @@ package app import ( + "fmt" + "os" + "github.com/iotaledger/hive.go/app" "github.com/iotaledger/hive.go/app/components/profiling" "github.com/iotaledger/hive.go/app/components/shutdown" @@ -15,6 +18,7 @@ import ( "github.com/iotaledger/iota-core/components/restapi" coreapi "github.com/iotaledger/iota-core/components/restapi/core" "github.com/iotaledger/iota-core/components/validator" + "github.com/iotaledger/iota-core/pkg/toolset" ) var ( @@ -28,6 +32,12 @@ var ( func App() *app.App { return app.New(Name, Version, // app.WithVersionCheck("iotaledger", "iota-core"), + app.WithUsageText(fmt.Sprintf(`Usage of %s (%s %s): + +Run '%s tools' to list all available tools. + +Command line flags: +`, os.Args[0], Name, Version, os.Args[0])), app.WithInitComponent(InitComponent), app.WithComponents( shutdown.Component, @@ -63,5 +73,15 @@ func init() { AdditionalConfigs: []*app.ConfigurationSet{ app.NewConfigurationSet("peering", "peering", "peeringConfigFilePath", "peeringConfig", false, true, false, "peering.json", "n"), }, + Init: initialize, + } +} + +func initialize(_ *app.App) error { + if toolset.ShouldHandleTools() { + toolset.HandleTools() + // HandleTools will call os.Exit } + + return nil } diff --git a/pkg/toolset/ed25519.go b/pkg/toolset/ed25519.go new file mode 100644 index 000000000..bae379fbf --- /dev/null +++ b/pkg/toolset/ed25519.go @@ -0,0 +1,172 @@ +package toolset + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" + "os" + + flag "github.com/spf13/pflag" + "github.com/wollac/iota-crypto-demo/pkg/bip32path" + "github.com/wollac/iota-crypto-demo/pkg/bip39" + "github.com/wollac/iota-crypto-demo/pkg/slip10" + "github.com/wollac/iota-crypto-demo/pkg/slip10/eddsa" + + "github.com/iotaledger/hive.go/app/configuration" + "github.com/iotaledger/hive.go/crypto" + iotago "github.com/iotaledger/iota.go/v4" +) + +func printEd25519Info(mnemonic bip39.Mnemonic, path bip32path.Path, prvKey ed25519.PrivateKey, pubKey ed25519.PublicKey, hrp iotago.NetworkPrefix, outputJSON bool) error { + addr := iotago.Ed25519AddressFromPubKey(pubKey) + + type keys struct { + BIP39 string `json:"mnemonic,omitempty"` + BIP32 string `json:"path,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` + PublicKey string `json:"publicKey"` + Ed25519Address string `json:"ed25519"` + Bech32Address string `json:"bech32"` + } + + k := keys{ + PublicKey: hex.EncodeToString(pubKey), + Ed25519Address: hex.EncodeToString(addr[:]), + Bech32Address: addr.Bech32(hrp), + } + + if prvKey != nil { + k.PrivateKey = hex.EncodeToString(prvKey) + } + + if mnemonic != nil { + k.BIP39 = mnemonic.String() + k.BIP32 = path.String() + } + + if outputJSON { + return printJSON(k) + } + + if len(k.BIP39) > 0 { + fmt.Println("Your seed BIP39 mnemonic: ", k.BIP39) + fmt.Println() + fmt.Println("Your BIP32 path: ", k.BIP32) + } + + if k.PrivateKey != "" { + fmt.Println("Your ed25519 private key: ", k.PrivateKey) + } + + fmt.Println("Your ed25519 public key: ", k.PublicKey) + fmt.Println("Your ed25519 address: ", k.Ed25519Address) + fmt.Println("Your bech32 address: ", k.Bech32Address) + + return nil +} + +func generateEd25519Key(args []string) error { + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + hrpFlag := fs.String(FlagToolHRP, string(iotago.PrefixTestnet), "the HRP which should be used for the Bech32 address") + bip32Path := fs.String(FlagToolBIP32Path, "m/44'/4218'/0'/0'/0'", "the BIP32 path that should be used to derive keys from seed") + mnemonicFlag := fs.String(FlagToolMnemonic, "", "the BIP-39 mnemonic sentence that should be used to derive the seed from (optional)") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolEd25519Key) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s", + ToolEd25519Key, + FlagToolHRP, + string(iotago.PrefixTestnet))) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + if len(*hrpFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolHRP) + } + + if len(*bip32Path) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolBIP32Path) + } + + var mnemonicSentence bip39.Mnemonic + if len(*mnemonicFlag) == 0 { + // Generate random entropy by using ed25519 key generation and using the private key seed (32 bytes) + _, random, err := ed25519.GenerateKey(nil) + if err != nil { + return err + } + entropy := random.Seed() + + mnemonicSentence, err = bip39.EntropyToMnemonic(entropy) + if err != nil { + return err + } + } else { + mnemonicSentence = bip39.ParseMnemonic(*mnemonicFlag) + if len(mnemonicSentence) != 24 { + return fmt.Errorf("'%s' contains an invalid sentence length. Mnemonic should be 24 words", FlagToolMnemonic) + } + } + + path, err := bip32path.ParsePath(*bip32Path) + if err != nil { + return err + } + + seed, err := bip39.MnemonicToSeed(mnemonicSentence, "") + if err != nil { + return err + } + + key, err := slip10.DeriveKeyFromPath(seed, eddsa.Ed25519(), path) + if err != nil { + return err + } + pubKey, prvKey := key.Key.(eddsa.Seed).Ed25519Key() + + return printEd25519Info(mnemonicSentence, path, ed25519.PrivateKey(prvKey), ed25519.PublicKey(pubKey), iotago.NetworkPrefix(*hrpFlag), *outputJSONFlag) +} + +func generateEd25519Address(args []string) error { + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + hrpFlag := fs.String(FlagToolHRP, string(iotago.PrefixTestnet), "the HRP which should be used for the Bech32 address") + publicKeyFlag := fs.String(FlagToolPublicKey, "", "an ed25519 public key") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolEd25519Addr) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s --%s %s", + ToolEd25519Addr, + FlagToolHRP, + string(iotago.PrefixTestnet), + FlagToolPublicKey, + "[PUB_KEY]", + )) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + if len(*hrpFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolHRP) + } + + if len(*publicKeyFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolPublicKey) + } + + // parse pubkey + pubKey, err := crypto.ParseEd25519PublicKeyFromString(*publicKeyFlag) + if err != nil { + return fmt.Errorf("can't decode '%s': %w", FlagToolPublicKey, err) + } + + return printEd25519Info(nil, nil, nil, pubKey, iotago.NetworkPrefix(*hrpFlag), *outputJSONFlag) +} diff --git a/pkg/toolset/jwt.go b/pkg/toolset/jwt.go new file mode 100644 index 000000000..9585eee8c --- /dev/null +++ b/pkg/toolset/jwt.go @@ -0,0 +1,109 @@ +package toolset + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/libp2p/go-libp2p/core/peer" + flag "github.com/spf13/pflag" + + "github.com/iotaledger/hive.go/app/configuration" + hivep2p "github.com/iotaledger/hive.go/crypto/p2p" + "github.com/iotaledger/hive.go/crypto/pem" + "github.com/iotaledger/iota-core/components/p2p" + "github.com/iotaledger/iota-core/pkg/jwt" +) + +func generateJWTApiToken(args []string) error { + + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + databasePathFlag := fs.String(FlagToolDatabasePath, DefaultValueP2PDatabasePath, "the path to the p2p database folder") + apiJWTSaltFlag := fs.String(FlagToolSalt, DefaultValueAPIJWTTokenSalt, "salt used inside the JWT tokens for the REST API") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolJWTApi) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s --%s %s", + ToolJWTApi, + FlagToolDatabasePath, + DefaultValueP2PDatabasePath, + FlagToolSalt, + DefaultValueAPIJWTTokenSalt)) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + if len(*databasePathFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolDatabasePath) + } + if len(*apiJWTSaltFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolSalt) + } + + databasePath := *databasePathFlag + privKeyFilePath := filepath.Join(databasePath, p2p.IdentityPrivateKeyFileName) + + salt := *apiJWTSaltFlag + + _, err := os.Stat(privKeyFilePath) + switch { + case os.IsNotExist(err): + // private key does not exist + return fmt.Errorf("private key file (%s) does not exist", privKeyFilePath) + + case err == nil || os.IsExist(err): + // private key file exists + + default: + return fmt.Errorf("unable to check private key file (%s): %w", privKeyFilePath, err) + } + + privKey, err := pem.ReadEd25519PrivateKeyFromPEMFile(privKeyFilePath) + if err != nil { + return fmt.Errorf("reading private key file for peer identity failed: %w", err) + } + + libp2pPrivKey, err := hivep2p.Ed25519PrivateKeyToLibp2pPrivateKey(privKey) + if err != nil { + return fmt.Errorf("reading private key file for peer identity failed: %w", err) + } + + peerID, err := peer.IDFromPublicKey(libp2pPrivKey.GetPublic()) + if err != nil { + return fmt.Errorf("unable to get peer identity from public key: %w", err) + } + + // API tokens do not expire. + jwtAuth, err := jwt.NewAuth(salt, + 0, + peerID.String(), + libp2pPrivKey, + ) + if err != nil { + return fmt.Errorf("JWT auth initialization failed: %w", err) + } + + jwtToken, err := jwtAuth.IssueJWT() + if err != nil { + return fmt.Errorf("issuing JWT token failed: %w", err) + } + + if *outputJSONFlag { + + result := struct { + JWT string `json:"jwt"` + }{ + JWT: jwtToken, + } + + return printJSON(result) + } + + fmt.Println("Your API JWT token: ", jwtToken) + + return nil +} diff --git a/pkg/toolset/node_info.go b/pkg/toolset/node_info.go new file mode 100644 index 000000000..058a3e6d6 --- /dev/null +++ b/pkg/toolset/node_info.go @@ -0,0 +1,50 @@ +package toolset + +import ( + "context" + "fmt" + "os" + + flag "github.com/spf13/pflag" + + "github.com/iotaledger/hive.go/app/configuration" + "github.com/iotaledger/iota.go/v4/nodeclient" +) + +func nodeInfo(args []string) error { + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + nodeURLFlag := fs.String(FlagToolNodeURL, "http://localhost:14265", "URL of the node (optional)") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolNodeInfo) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s", + ToolNodeInfo, + FlagToolNodeURL, + "http://192.168.1.221:14265", + )) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + client, err := nodeclient.New(*nodeURLFlag) + if err != nil { + return err + } + + info, err := client.Info(context.Background()) + if err != nil { + return err + } + + if *outputJSONFlag { + return printJSON(info) + } + + fmt.Printf("Name: %s\nVersion: %s\nLatestAcceptedBlockSlot: %d\nLatestConfirmedBlockSlot: %d\nIsHealthy: %s\n", info.Name, info.Version, info.Status.LatestAcceptedBlockSlot, info.Status.LatestConfirmedBlockSlot, yesOrNo(info.Status.IsHealthy)) + + return nil +} diff --git a/pkg/toolset/p2p_identity_extract.go b/pkg/toolset/p2p_identity_extract.go new file mode 100644 index 000000000..45a364d10 --- /dev/null +++ b/pkg/toolset/p2p_identity_extract.go @@ -0,0 +1,66 @@ +package toolset + +import ( + "fmt" + "os" + "path/filepath" + + flag "github.com/spf13/pflag" + + "github.com/iotaledger/hive.go/app/configuration" + hivep2p "github.com/iotaledger/hive.go/crypto/p2p" + "github.com/iotaledger/hive.go/crypto/pem" + "github.com/iotaledger/iota-core/components/p2p" +) + +func extractP2PIdentity(args []string) error { + + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + databasePathFlag := fs.String(FlagToolDatabasePath, DefaultValueP2PDatabasePath, "the path to the p2p database folder") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolP2PExtractIdentity) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s", + ToolP2PExtractIdentity, + FlagToolDatabasePath, + DefaultValueP2PDatabasePath)) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + if len(*databasePathFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolDatabasePath) + } + + databasePath := *databasePathFlag + privKeyFilePath := filepath.Join(databasePath, p2p.IdentityPrivateKeyFileName) + + _, err := os.Stat(privKeyFilePath) + switch { + case os.IsNotExist(err): + // private key does not exist + return fmt.Errorf("private key file (%s) does not exist", privKeyFilePath) + + case err == nil || os.IsExist(err): + // private key file exists + + default: + return fmt.Errorf("unable to check private key file (%s): %w", privKeyFilePath, err) + } + + privKey, err := pem.ReadEd25519PrivateKeyFromPEMFile(privKeyFilePath) + if err != nil { + return fmt.Errorf("reading private key file for peer identity failed: %w", err) + } + + libp2pPrivKey, err := hivep2p.Ed25519PrivateKeyToLibp2pPrivateKey(privKey) + if err != nil { + return err + } + + return printP2PIdentity(libp2pPrivKey, libp2pPrivKey.GetPublic(), *outputJSONFlag) +} diff --git a/pkg/toolset/p2p_identity_gen.go b/pkg/toolset/p2p_identity_gen.go new file mode 100644 index 000000000..b376fd5e8 --- /dev/null +++ b/pkg/toolset/p2p_identity_gen.go @@ -0,0 +1,136 @@ +package toolset + +import ( + "crypto/ed25519" + "encoding/hex" + "fmt" + "os" + "path/filepath" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/mr-tron/base58" + flag "github.com/spf13/pflag" + + "github.com/iotaledger/hive.go/app/configuration" + hivecrypto "github.com/iotaledger/hive.go/crypto" + "github.com/iotaledger/hive.go/crypto/pem" + "github.com/iotaledger/iota-core/components/p2p" + "github.com/iotaledger/iota.go/v4/hexutil" +) + +func generateP2PIdentity(args []string) error { + + fs := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + databasePathFlag := fs.String(FlagToolOutputPath, DefaultValueP2PDatabasePath, "the path to the output folder") + privateKeyFlag := fs.String(FlagToolPrivateKey, "", "the p2p private key") + outputJSONFlag := fs.Bool(FlagToolOutputJSON, false, FlagToolDescriptionOutputJSON) + + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", ToolP2PIdentityGen) + fs.PrintDefaults() + println(fmt.Sprintf("\nexample: %s --%s %s --%s %s", + ToolP2PIdentityGen, + FlagToolDatabasePath, + DefaultValueP2PDatabasePath, + FlagToolPrivateKey, + "[PRIVATE_KEY]", + )) + } + + if err := parseFlagSet(fs, args); err != nil { + return err + } + + if len(*databasePathFlag) == 0 { + return fmt.Errorf("'%s' not specified", FlagToolDatabasePath) + } + + databasePath := *databasePathFlag + privKeyFilePath := filepath.Join(databasePath, p2p.IdentityPrivateKeyFileName) + + if err := os.MkdirAll(databasePath, 0700); err != nil { + return fmt.Errorf("could not create peer store database dir '%s': %w", databasePath, err) + } + + _, err := os.Stat(privKeyFilePath) + switch { + case err == nil || os.IsExist(err): + // private key file already exists + return fmt.Errorf("private key file (%s) already exists", privKeyFilePath) + + case os.IsNotExist(err): + // private key file does not exist, create a new one + + default: + return fmt.Errorf("unable to check private key file (%s): %w", privKeyFilePath, err) + } + + var privKey ed25519.PrivateKey + if privateKeyFlag != nil && len(*privateKeyFlag) > 0 { + privKey, err = hivecrypto.ParseEd25519PrivateKeyFromString(*privateKeyFlag) + if err != nil { + return fmt.Errorf("invalid private key given '%s': %w", *privateKeyFlag, err) + } + } else { + // create identity + _, privKey, err = ed25519.GenerateKey(nil) + if err != nil { + return fmt.Errorf("unable to generate Ed25519 private key for peer identity: %w", err) + } + } + + libp2pPrivKey, libp2pPubKey, err := crypto.KeyPairFromStdKey(&privKey) + if err != nil { + return fmt.Errorf("unable to convert given private key '%s': %w", hexutil.EncodeHex(privKey), err) + } + + if err := pem.WriteEd25519PrivateKeyToPEMFile(privKeyFilePath, privKey); err != nil { + return fmt.Errorf("writing private key file for peer identity failed: %w", err) + } + + return printP2PIdentity(libp2pPrivKey, libp2pPubKey, *outputJSONFlag) +} + +func printP2PIdentity(libp2pPrivKey crypto.PrivKey, libp2pPubKey crypto.PubKey, outputJSON bool) error { + + type P2PIdentity struct { + PrivateKey string `json:"privateKey"` + PublicKey string `json:"publicKey"` + PublicKeyBase58 string `json:"publicKeyBase58"` + PeerID string `json:"peerId"` + } + + privKeyBytes, err := libp2pPrivKey.Raw() + if err != nil { + return fmt.Errorf("unable to get raw private key bytes: %w", err) + } + + pubKeyBytes, err := libp2pPubKey.Raw() + if err != nil { + return fmt.Errorf("unable to get raw public key bytes: %w", err) + } + + peerID, err := peer.IDFromPublicKey(libp2pPubKey) + if err != nil { + return fmt.Errorf("unable to get peer identity from public key: %w", err) + } + + identity := P2PIdentity{ + PrivateKey: hex.EncodeToString(privKeyBytes), + PublicKey: hex.EncodeToString(pubKeyBytes), + PublicKeyBase58: base58.Encode(pubKeyBytes), + PeerID: peerID.String(), + } + + if outputJSON { + return printJSON(identity) + } + + fmt.Println("Your p2p private key (hex): ", identity.PrivateKey) + fmt.Println("Your p2p public key (hex): ", identity.PublicKey) + fmt.Println("Your p2p public key (base58): ", identity.PublicKeyBase58) + fmt.Println("Your p2p PeerID: ", identity.PeerID) + + return nil +} diff --git a/pkg/toolset/toolset.go b/pkg/toolset/toolset.go new file mode 100644 index 000000000..cad18b14e --- /dev/null +++ b/pkg/toolset/toolset.go @@ -0,0 +1,157 @@ +package toolset + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + flag "github.com/spf13/pflag" + + "github.com/iotaledger/hive.go/app/configuration" + "github.com/iotaledger/hive.go/ierrors" +) + +const ( + FlagToolDatabasePath = "databasePath" + + FlagToolOutputPath = "outputPath" + + FlagToolPrivateKey = "privateKey" + FlagToolPublicKey = "publicKey" + + FlagToolHRP = "hrp" + FlagToolBIP32Path = "bip32Path" + FlagToolMnemonic = "mnemonic" + FlagToolSalt = "salt" + + FlagToolNodeURL = "nodeURL" + + FlagToolOutputJSON = "json" + FlagToolDescriptionOutputJSON = "format output as JSON" +) + +const ( + ToolP2PIdentityGen = "p2pidentity-gen" + ToolP2PExtractIdentity = "p2pidentity-extract" + ToolEd25519Key = "ed25519-key" + ToolEd25519Addr = "ed25519-addr" + ToolJWTApi = "jwt-api" + ToolNodeInfo = "node-info" +) + +const ( + DefaultValueAPIJWTTokenSalt = "IOTA" + DefaultValueP2PDatabasePath = "testnet/p2pstore" +) + +// ShouldHandleTools checks if tools were requested. +func ShouldHandleTools() bool { + args := os.Args[1:] + + for _, arg := range args { + if strings.ToLower(arg) == "tool" || strings.ToLower(arg) == "tools" { + return true + } + } + + return false +} + +// HandleTools handles available tools. +func HandleTools() { + + args := os.Args[1:] + if len(args) == 1 { + listTools() + os.Exit(1) + } + + tools := map[string]func([]string) error{ + ToolP2PIdentityGen: generateP2PIdentity, + ToolP2PExtractIdentity: extractP2PIdentity, + ToolEd25519Key: generateEd25519Key, + ToolEd25519Addr: generateEd25519Address, + ToolJWTApi: generateJWTApiToken, + ToolNodeInfo: nodeInfo, + } + + tool, exists := tools[strings.ToLower(args[1])] + if !exists { + fmt.Print("tool not found.\n\n") + listTools() + os.Exit(1) + } + + if err := tool(args[2:]); err != nil { + if ierrors.Is(err, flag.ErrHelp) { + // help text was requested + os.Exit(0) + } + + fmt.Printf("\nerror: %s\n", err) + os.Exit(1) + } + + os.Exit(0) +} + +func listTools() { + fmt.Printf("%-20s generates a p2p identity private key file\n", fmt.Sprintf("%s:", ToolP2PIdentityGen)) + fmt.Printf("%-20s extracts the p2p identity from the private key file\n", fmt.Sprintf("%s:", ToolP2PExtractIdentity)) + fmt.Printf("%-20s generates an ed25519 key pair\n", fmt.Sprintf("%s:", ToolEd25519Key)) + fmt.Printf("%-20s generates an ed25519 address from a public key\n", fmt.Sprintf("%s:", ToolEd25519Addr)) + fmt.Printf("%-20s generates a JWT token for REST-API access\n", fmt.Sprintf("%s:", ToolJWTApi)) + fmt.Printf("%-20s queries the info endpoint of a node\n", fmt.Sprintf("%s:", ToolNodeInfo)) +} + +func yesOrNo(value bool) string { + if value { + return "YES" + } + + return "NO" +} + +func parseFlagSet(fs *flag.FlagSet, args []string) error { + + if err := fs.Parse(args); err != nil { + return err + } + + // Check if all parameters were parsed + if fs.NArg() != 0 { + return ierrors.New("too much arguments") + } + + return nil +} + +func printJSON(obj interface{}) error { + output, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + + fmt.Println(string(output)) + + return nil +} + +//nolint:unused // we will need it at a later point in time +func loadConfigFile(filePath string, parameters map[string]any) error { + config := configuration.New() + flagset := configuration.NewUnsortedFlagSet("", flag.ContinueOnError) + + for namespace, pointerToStruct := range parameters { + config.BindParameters(flagset, namespace, pointerToStruct) + } + + if err := config.LoadFile(filePath); err != nil { + return fmt.Errorf("loading config file failed: %w", err) + } + + config.UpdateBoundParameters() + + return nil +} diff --git a/tools/gendoc/go.mod b/tools/gendoc/go.mod index f024b6b9d..5fdd31230 100644 --- a/tools/gendoc/go.mod +++ b/tools/gendoc/go.mod @@ -156,6 +156,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + github.com/wollac/iota-crypto-demo v0.0.0-20221117162917-b10619eccb98 // indirect github.com/zyedidia/generic v1.2.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.19.0 // indirect diff --git a/tools/gendoc/go.sum b/tools/gendoc/go.sum index 2a2b0dd6e..79c766b0b 100644 --- a/tools/gendoc/go.sum +++ b/tools/gendoc/go.sum @@ -668,6 +668,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= +github.com/wollac/iota-crypto-demo v0.0.0-20221117162917-b10619eccb98 h1:i7k63xHOX2ntuHrhHewfKro67c834jug2DIk599fqAA= +github.com/wollac/iota-crypto-demo v0.0.0-20221117162917-b10619eccb98/go.mod h1:Knu2XMRWe8SkwTlHc/+ghP+O9DEaZRQQEyTjvLJ5Cck= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= From 38d44b528ff28d45e0dfcd69912b4956eef5104a Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Thu, 26 Oct 2023 16:45:14 +0200 Subject: [PATCH 4/5] Added a healthcheck to the Dockerfile --- Dockerfile | 2 ++ Dockerfile.dev | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 44a17fda1..ec10c0f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,8 @@ RUN cp ./peering.json /app/peering.json # using distroless cc "nonroot" image, which includes everything in the base image (glibc, libssl and openssl) FROM gcr.io/distroless/cc-debian12:nonroot +HEALTHCHECK --interval=10s --timeout=5s --retries=30 CMD ["/app/iota-core", "tools", "node-info"] + # Copy the app dir into distroless image COPY --chown=nonroot:nonroot --from=build /app /app diff --git a/Dockerfile.dev b/Dockerfile.dev index e9073b384..f91006d26 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -61,6 +61,8 @@ RUN mkdir -p /app/data/peerdb # using distroless cc "nonroot" image, which includes everything in the base image (glibc, libssl and openssl) FROM gcr.io/distroless/cc-debian12:nonroot +HEALTHCHECK --interval=10s --timeout=5s --retries=30 CMD ["/app/iota-core", "tools", "node-info"] + # Copy the app dir into distroless image COPY --chown=nonroot:nonroot --from=build /app /app From 4578d1114b59979eed63301ddc20e793a992f5e2 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Thu, 26 Oct 2023 16:47:59 +0200 Subject: [PATCH 5/5] Use the new healthcheck to make inx services wait before starting --- .../templates/docker-compose-iota-core.yml.j2 | 6 +++--- tools/docker-network/docker-compose.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 b/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 index 96da41c3e..d3bc9d82b 100644 --- a/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 +++ b/deploy/ansible/roles/iota-core-node/templates/docker-compose-iota-core.yml.j2 @@ -73,7 +73,7 @@ services: restart: unless-stopped depends_on: iota-core: - condition: service_started + condition: service_healthy ulimits: nofile: soft: 16384 @@ -93,7 +93,7 @@ services: restart: unless-stopped depends_on: iota-core: - condition: service_started + condition: service_healthy inx-indexer: condition: service_started environment: @@ -111,7 +111,7 @@ services: restart: unless-stopped depends_on: iota-core: - condition: service_started + condition: service_healthy inx-indexer: condition: service_started inx-blockissuer: diff --git a/tools/docker-network/docker-compose.yml b/tools/docker-network/docker-compose.yml index e8823c6b6..22d21b2dc 100644 --- a/tools/docker-network/docker-compose.yml +++ b/tools/docker-network/docker-compose.yml @@ -193,7 +193,7 @@ services: restart: unless-stopped depends_on: node-1-validator: - condition: service_started + condition: service_healthy ulimits: nofile: soft: 16384 @@ -210,7 +210,7 @@ services: restart: unless-stopped depends_on: node-1-validator: - condition: service_started + condition: service_healthy inx-indexer: condition: service_started networks: @@ -229,7 +229,7 @@ services: restart: unless-stopped depends_on: node-1-validator: - condition: service_started + condition: service_healthy inx-indexer: condition: service_started inx-blockissuer: