diff --git a/cmd/cql-adapter/README.md b/cmd/cql-adapter/README.md index 3787a4ed2..976dd3bb3 100644 --- a/cmd/cql-adapter/README.md +++ b/cmd/cql-adapter/README.md @@ -1,4 +1,4 @@ -This doc introduce the usage of CovenantSQL adapter. This adapter lets you use CovenantSQL on any platform from any programming languages using http(s) protocol. The CovenantSQL Java/Python Driver currently is based on adapter to service. +This doc introduce the usage of CovenantSQL adapter. This adapter lets you use CovenantSQL on any platform from any programming languages using http(s) protocol. The CovenantSQL Java/Python/NodeJS Driver currently is based on adapter to service. ## Prerequisites @@ -8,80 +8,20 @@ Make sure the ```$GOPATH/bin``` is in your ```$PATH```, download build the adapt $ go get github.com/CovenantSQL/CovenantSQL/cmd/cql-adapter ``` -Adapter requires a simple ```config.yaml``` like we use in client bundled with exclusive adpater configuration. +## Adapter Usage + +Adapter can use the same ```config.yaml``` and key pair with `cql` ### Generating Default Config File First, generate the main configuration file. Same as [Generating Default Config File in Golang Client Doc](https://github.com/CovenantSQL/CovenantSQL/tree/develop/client#generating-default-config-file). An existing configuration file can also be used. -### Configure Adapter - -Adapter use tls certificate for client authorization, a public or self-signed ssl certificate is required for adapter server to start. The adapter config is placed as a ```Adapter``` section of the main config file including following configurable fields. - -| Name | Type | Description | Default | -| ----------------- | -------- | ------------------------------------------------------------ | ------- | -| ListenAddr | string | adapter server listen address | | -| CertificatePath | string | adapter server tls certificate file
** all following file path is related to working root | | -| PrivateKeyPath | string | adapter server tls private key file | | -| VerifyCertificate | bool | should adapter server verify client certificate or not
a client custom CA is required, all valid clients certificate should be issued by this CA | false | -| AdminCerts | []string | each item requires to be a certificate file path
client with configured certificate will be granted with ADMIN privilege
ADMIN privilege is able to CREATE/DROP database, send WRITE/READ request | | -| WriteCerts | []string | same format as ```AdminCerts ``` field
client with configured certificate will be granted with WRITE privilege
WRITE privilege is able to send WRITE/READ request only | | -| StorageDriver | string | two available storage driver: ```sqlite3``` and ```covenantsql```, use ```sqlite3``` driver for test purpose only | | -| StorageRoot | string | required by ```sqlite3``` storage driver, database files is placed under this root path, this path is treated as relative to working root | | - -[mkcert](https://github.com/FiloSottile/mkcert) is a handy command to generate tls certificates, run the following command to generate the server certificate. - -`````` -$ CAROOT=$(pwd) mkcert server -Using the local CA at "/demo" ✨ -Warning: the local CA is not installed in the system trust store! ⚠️ -Warning: the local CA is not installed in the Firefox trust store! ⚠️ -Run "mkcert -install" to avoid verification errors ‼️ - -Created a new certificate valid for the following names 📜 - - "server" - -The certificate is at "./server.pem" and the key at "./server-key.pem" ✅ - -And move them to ~/.cql/ dir. -`````` - -You can use following interactive command to generate adapter config. - -```shell -$ cql-utils -tool adapterconfgen -ListenAddr (default: 0.0.0.0:4661): ⏎ -CertificatePath (default: server.pem): ⏎ -PrivateKeyPath (default: server-key.pem): ⏎ -VerifyCertificate (default: true) (y/n): ⏎ -ClientCAPath (default:): ⏎ -AdminCerts (default:): ⏎ -WriteCerts (default:): ⏎ -StorageDriver (default: covenantsql): ⏎ -StorageRoot (default:): ⏎ - -$ tail -n 20 ~/.cql/config.yaml -... skipping irrelevant configuration -Adapter: - ListenAddr: 0.0.0.0:4661 - CertificatePath: server.pem - PrivateKeyPath: server.key - VerifyCertificate: false - ClientCAPath: - AdminCerts: [] - WriteCerts: [] - StorageDriver: covenantsql - StorageRoot: -``` - -## Adapter Usage - ### Start Start the adapter by following commands: ```shell -$ cql-adapter +$ cql-adapter -listen 127.0.0.1:4661 ``` ### API @@ -117,6 +57,41 @@ $ cql-adapter } ``` +### Configure HTTPS Adapter + +Adapter use tls certificate for client authorization, a public or self-signed ssl certificate is required for adapter server to start. The adapter config is placed as a ```Adapter``` section of the main config file including following configurable fields. + +| Name | Type | Description | Default | +| ----------------- | -------- | ------------------------------------------------------------ | ------- | +| ListenAddr | string | adapter server listen address | | +| CertificatePath | string | adapter server tls certificate file
** all following file path is related to working root | | +| PrivateKeyPath | string | adapter server tls private key file | | +| VerifyCertificate | bool | should adapter server verify client certificate or not
a client custom CA is required, all valid clients certificate should be issued by this CA | false | +| AdminCerts | []string | each item requires to be a certificate file path
client with configured certificate will be granted with ADMIN privilege
ADMIN privilege is able to CREATE/DROP database, send WRITE/READ request | | +| WriteCerts | []string | same format as ```AdminCerts ``` field
client with configured certificate will be granted with WRITE privilege
WRITE privilege is able to send WRITE/READ request only | | +| StorageDriver | string | two available storage driver: ```sqlite3``` and ```covenantsql```, use ```sqlite3``` driver for test purpose only | | +| StorageRoot | string | required by ```sqlite3``` storage driver, database files is placed under this root path, this path is treated as relative to working root | | + +[mkcert](https://github.com/FiloSottile/mkcert) is a handy command to generate tls certificates, run the following command to generate the server certificate. + +`````` +$ CAROOT=$(pwd) mkcert server +Using the local CA at "/demo" ✨ +Warning: the local CA is not installed in the system trust store! ⚠️ +Warning: the local CA is not installed in the Firefox trust store! ⚠️ +Run "mkcert -install" to avoid verification errors ‼️ + +Created a new certificate valid for the following names 📜 + - "server" + +The certificate is at "./server.pem" and the key at "./server-key.pem" ✅ + +And move them to ~/.cql/ dir. +`````` + +You can use following interactive command to generate adapter config. + + ###### Example ```bash diff --git a/cmd/cql-adapter/config/config.go b/cmd/cql-adapter/config/config.go index a6c82771a..64821cbfb 100644 --- a/cmd/cql-adapter/config/config.go +++ b/cmd/cql-adapter/config/config.go @@ -65,7 +65,7 @@ type Config struct { } type confWrapper struct { - Adapter *Config `yaml:"Adapter"` + Adapter Config `yaml:"Adapter"` } // LoadConfig load and verify config in config file and set to global config instance. @@ -81,14 +81,11 @@ func LoadConfig(configPath string, password string) (config *Config, err error) return } - if configWrapper.Adapter == nil { - err = ErrEmptyAdapterConfig - log.WithError(err).Error("could not read adapter config") - return - } - - config = configWrapper.Adapter + config = &configWrapper.Adapter + if len(config.StorageDriver) == 0 { + config.StorageDriver = "covenantsql" + } if config.StorageDriver == "covenantsql" { // init client if err = client.Init(configPath, []byte(password)); err != nil { @@ -103,7 +100,7 @@ func LoadConfig(configPath string, password string) (config *Config, err error) if config.CertificatePath == "" || config.PrivateKeyPath == "" { // http mode - log.Warningf("running in http mode") + log.Info("running in http mode") } else { // tls mode // init tls config diff --git a/cmd/cql-adapter/config/errors.go b/cmd/cql-adapter/config/errors.go index 312887eda..bdc626574 100644 --- a/cmd/cql-adapter/config/errors.go +++ b/cmd/cql-adapter/config/errors.go @@ -19,8 +19,6 @@ package config import "github.com/pkg/errors" var ( - // ErrEmptyAdapterConfig defines empty adapter config. - ErrEmptyAdapterConfig = errors.New("empty adapter config") // ErrInvalidStorageConfig defines error on incomplete storage config. ErrInvalidStorageConfig = errors.New("invalid storage config") // ErrInvalidCertificateFile defines invalid certificate file error. diff --git a/cmd/cql-adapter/main.go b/cmd/cql-adapter/main.go index 7866fefb6..03c067e9b 100644 --- a/cmd/cql-adapter/main.go +++ b/cmd/cql-adapter/main.go @@ -36,6 +36,7 @@ const name = "cql-adapeter" var ( version = "unknown" configFile string + listenAddr string password string showVersion bool ) @@ -43,6 +44,7 @@ var ( func init() { flag.StringVar(&configFile, "config", "~/.cql/config.yaml", "Config file for adapter") flag.StringVar(&password, "password", "", "Master key password") + flag.StringVar(&listenAddr, "listen", "", "Listen address for adapter api") flag.BoolVar(&asymmetric.BypassSignature, "bypass-signature", false, "Disable signature sign and verify, for testing") flag.BoolVar(&showVersion, "version", false, "Show version information and exit") @@ -62,7 +64,7 @@ func main() { log.Infof("args %#v : %s", f.Name, f.Value) }) - server, err := NewHTTPAdapter(configFile, password) + server, err := NewHTTPAdapter(listenAddr, configFile, password) if err != nil { log.WithError(err).Fatal("init adapter failed") return diff --git a/cmd/cql-adapter/server.go b/cmd/cql-adapter/server.go index a9b739eb1..72d4be5f7 100644 --- a/cmd/cql-adapter/server.go +++ b/cmd/cql-adapter/server.go @@ -33,7 +33,7 @@ type HTTPAdapter struct { } // NewHTTPAdapter creates adapter to service. -func NewHTTPAdapter(configFile string, password string) (adapter *HTTPAdapter, err error) { +func NewHTTPAdapter(listenAddr string, configFile string, password string) (adapter *HTTPAdapter, err error) { adapter = new(HTTPAdapter) // load config file @@ -42,6 +42,9 @@ func NewHTTPAdapter(configFile string, password string) (adapter *HTTPAdapter, e return } + if listenAddr != "" { + cfg.ListenAddr = listenAddr + } // init server handler := handlers.CORS()(api.GetRouter()) @@ -61,7 +64,7 @@ func (adapter *HTTPAdapter) Serve() (err error) { // bind port, start tls listener var listener net.Listener - if listener, err = net.Listen("tcp", cfg.ListenAddr); err != nil { + if listener, err = net.Listen("tcp", adapter.server.Addr); err != nil { return } diff --git a/cmd/cql-utils/adapterconfgen.go b/cmd/cql-utils/adapterconfgen.go deleted file mode 100644 index fd789c88c..000000000 --- a/cmd/cql-utils/adapterconfgen.go +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright 2018 The CovenantSQL Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "bufio" - "fmt" - "io/ioutil" - "os" - "strings" - - "github.com/CovenantSQL/CovenantSQL/utils/log" - yaml "gopkg.in/yaml.v2" -) - -type adapterConfig struct { - ListenAddr string `yaml:"ListenAddr"` - CertificatePath string `yaml:"CertificatePath"` - PrivateKeyPath string `yaml:"PrivateKeyPath"` - VerifyCertificate bool `yaml:"VerifyCertificate"` - ClientCAPath string `yaml:"ClientCAPath"` - AdminCerts []string `yaml:"AdminCerts"` - WriteCerts []string `yaml:"WriteCerts"` - StorageDriver string `yaml:"StorageDriver"` - StorageRoot string `yaml:"StorageRoot"` -} - -var ( - defaultAdapterConfig = &adapterConfig{ - ListenAddr: "0.0.0.0:4661", - CertificatePath: "server.pem", - PrivateKeyPath: "server-key.pem", - VerifyCertificate: false, - ClientCAPath: "", - AdminCerts: []string{}, - WriteCerts: []string{}, - StorageDriver: "covenantsql", - StorageRoot: "", - } -) - -func (c *adapterConfig) readListenAddr() { - newAddr := readDataFromStdin("ListenAddr (default: %v): ", c.ListenAddr) - if newAddr != "" { - c.ListenAddr = newAddr - } -} - -func (c *adapterConfig) readCertificatePath() { - newCertPath := readDataFromStdin("CertificatePath (default: %v): ", c.CertificatePath) - if newCertPath != "" { - c.CertificatePath = newCertPath - } -} - -func (c *adapterConfig) readPrivateKeyPath() { - newPrivateKeyPath := readDataFromStdin("PrivateKeyPath (default: %v): ", c.PrivateKeyPath) - if newPrivateKeyPath != "" { - c.PrivateKeyPath = newPrivateKeyPath - } -} - -func (c *adapterConfig) readVerifyCertificate() bool { - shouldVerifyCertificate := readDataFromStdin( - "VerifyCertificate (default: %v) (y/n): ", c.VerifyCertificate) - if shouldVerifyCertificate != "" { - switch shouldVerifyCertificate { - case "y": - fallthrough - case "Y": - c.VerifyCertificate = true - case "N": - fallthrough - case "n": - c.VerifyCertificate = false - } - } - - return c.VerifyCertificate -} - -func (c *adapterConfig) readClientCAPath() { - newClientCAPath := readDataFromStdin("ClientCAPath (default: %v)", c.ClientCAPath) - if newClientCAPath != "" { - c.ClientCAPath = newClientCAPath - } -} - -func (c *adapterConfig) readAdminCerts() { - var newCerts []string - - for { - record := readDataFromStdin("AdminCerts: ") - - if record == "" { - break - } - - newCerts = append(newCerts, record) - } - - c.AdminCerts = newCerts -} - -func (c *adapterConfig) readWriteCerts() { - var newCerts []string - - for { - record := readDataFromStdin("WriteCerts: ") - - if record == "" { - break - } - - newCerts = append(newCerts, record) - } - - c.WriteCerts = newCerts -} - -func (c *adapterConfig) readStorageDriver() { - newStorageDriver := readDataFromStdin("StorageDriver (default: %v)", c.StorageDriver) - if newStorageDriver != "" { - c.StorageDriver = newStorageDriver - } -} - -func (c *adapterConfig) readStorageRoot() { - newStorageRoot := readDataFromStdin("StorageRoot (default: %v)", c.StorageRoot) - if newStorageRoot != "" { - c.StorageRoot = newStorageRoot - } -} - -func (c *adapterConfig) loadFromExistingConfig(rawConfig yaml.MapSlice) { - if rawConfig == nil { - return - } - - // find adapter config in map slice - var originalConfig interface{} - - for _, item := range rawConfig { - if keyStr, ok := item.Key.(string); ok && keyStr == "Adapter" { - originalConfig = item.Value - } - } - - if originalConfig == nil { - return - } - - // fill values to config structure - var configBytes []byte - var err error - - if configBytes, err = yaml.Marshal(originalConfig); err != nil { - log.WithError(err).Warning("load previous adapter config failed") - return - } - - if err = yaml.Unmarshal(configBytes, c); err != nil { - log.WithError(err).Warning("load previous adapter config failed") - return - } - - return -} - -func (c *adapterConfig) writeToRawConfig(rawConfig yaml.MapSlice) yaml.MapSlice { - if rawConfig == nil { - return rawConfig - } - - // find adapter config in map slice - for i, item := range rawConfig { - if keyStr, ok := item.Key.(string); ok && keyStr == "Adapter" { - rawConfig[i].Value = c - return rawConfig - } - } - - // not found - rawConfig = append(rawConfig, yaml.MapItem{ - Key: "Adapter", - Value: c, - }) - - return rawConfig -} - -func (c *adapterConfig) readAllConfig() { - c.readListenAddr() - c.readCertificatePath() - c.readPrivateKeyPath() - - if c.readVerifyCertificate() { - c.readClientCAPath() - c.readAdminCerts() - c.readWriteCerts() - } - - c.readStorageDriver() - c.readStorageRoot() -} - -func readDataFromStdin(prompt string, values ...interface{}) (s string) { - reader := bufio.NewReader(os.Stdin) - fmt.Printf(prompt, values...) - s, _ = reader.ReadString('\n') - s = strings.TrimSpace(s) - return -} - -func runAdapterConfGen() { - if configFile == "" { - log.Error("config file path is required for adapterconfgen tool") - os.Exit(1) - } - - var err error - var configBytes []byte - if configBytes, err = ioutil.ReadFile(configFile); err != nil { - log.WithError(err).Error("an existing config file is required for adapterconfggen") - os.Exit(1) - } - - // load config - var rawConfig yaml.MapSlice - if err = yaml.Unmarshal(configBytes, &rawConfig); err != nil { - log.WithError(err).Error("a valid config file is required for adapterconfgen") - os.Exit(1) - } - - if rawConfig == nil { - log.WithError(err).Error("a confgen generated config is required for adapterconfgen") - os.Exit(1) - } - - // backup config - bakConfigFile := configFile + ".bak" - if err = ioutil.WriteFile(bakConfigFile, configBytes, 0600); err != nil { - log.WithError(err).Error("backup config file failed") - os.Exit(1) - } - - defaultAdapterConfig.loadFromExistingConfig(rawConfig) - defaultAdapterConfig.readAllConfig() - rawConfig = defaultAdapterConfig.writeToRawConfig(rawConfig) - - if configBytes, err = yaml.Marshal(rawConfig); err != nil { - log.WithError(err).Error("marshal config failed") - os.Exit(1) - } - - if err = ioutil.WriteFile(configFile, configBytes, 0600); err != nil { - log.WithError(err).Error("write config to file failed") - os.Exit(1) - } - - log.Info("adapter config generated") - - return -} diff --git a/cmd/cql-utils/main.go b/cmd/cql-utils/main.go index 9099daeff..b40e0ddfb 100644 --- a/cmd/cql-utils/main.go +++ b/cmd/cql-utils/main.go @@ -43,7 +43,7 @@ const name = "cql-utils" func init() { log.SetLevel(log.InfoLevel) - flag.StringVar(&tool, "tool", "", "Tool type, miner, keytool, rpc, nonce, confgen, addrgen, adapterconfgen") + flag.StringVar(&tool, "tool", "", "Tool type, miner, keytool, rpc, nonce, confgen, addrgen") flag.StringVar(&publicKeyHex, "public", "", "Public key hex string to mine node id/nonce") flag.StringVar(&privateKeyFile, "private", "~/.cql/private.key", "Private key file to generate/show") flag.StringVar(&configFile, "config", "~/.cql/config.yaml", "Config file to use") @@ -92,8 +92,6 @@ func main() { runNonce() case "confgen": runConfgen() - case "adapterconfgen": - runAdapterConfGen() case "addrgen": if privateKeyFile == "" && publicKeyHex == "" { log.Error("privateKey path or publicKey hex is required for addrgen") diff --git a/conf/testnet/config.yaml b/conf/testnet/config.yaml index 6716e0b7c..e2d0655dd 100644 --- a/conf/testnet/config.yaml +++ b/conf/testnet/config.yaml @@ -5,6 +5,16 @@ PrivateKeyFile: "private.key" DHTFileName: "dht.db" ListenAddr: "127.0.0.1:4661" ThisNodeID: "00000f3b43288fe99831eb533ab77ec455d13e11fc38ec35a42d4edd17aa320d" +QPS: 1000 +ChainBusPeriod: 0s +BillingBlockCount: 60 +BPPeriod: 10s +BPTick: 3s +SQLChainPeriod: 1m0s +SQLChainTick: 10s +SQLChainTTL: 10 +MinProviderDeposit: 1000000 + ValidDNSKeys: koPbw9wmYZ7ggcjnQ6ayHyhHaDNMYELKTqT+qRGrZpWSccr/lBcrm10Z1PuQHB3Azhii+sb0PYFkH1ruxLhe5g==: cloudflare.com mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==: cloudflare.com @@ -17,6 +27,9 @@ DNSSeed: - 202.46.34.74 - 202.46.34.75 - 202.46.34.76 +Adapter: + ListenAddr: "127.0.0.1:4661" + StorageDriver: covenantsql BlockProducer: PublicKey: "02c1db96f2ba7e1cb4e9822d12de0f63fb666feb828c7f509e81fab9bd7a34039c" @@ -27,29 +40,7 @@ BlockProducer: c: 0 d: 6148914694092305796 ChainFileName: chain.db - BPGenesisInfo: - Version: 1 - Producer: "0000000000000000000000000000000000000000000000000000000000000001" - MerkleRoot: "0000000000000000000000000000000000000000000000000000000000000001" - ParentHash: "0000000000000000000000000000000000000000000000000000000000000001" - Timestamp: 2019-01-02T13:33:00Z - BlockHash: f745ca6427237aac858dd3c7f2df8e6f3c18d0f1c164e07a1c6b8eebeba6b154 - BaseAccounts: - - Address: ba0ba731c7a76ccef2c1170f42038f7e228dfb474ef0190dfe35d9a37911ed37 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 1a7b0959bbd0d0ec529278a61c0056c277bffe75b2646e1699b46b10a90210be - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 9235bc4130a2ed4e6c35ea189dab35198ebb105640bedb97dd5269cc80863b16 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 9e1618775cceeb19f110e04fbc6c5bca6c8e4e9b116e193a42fe69bf602e7bcd - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 58aceaf4b730b54bf00c0fb3f7b14886de470767f313c2d108968cd8bf0794b7 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 + KnownNodes: - ID: 00000000000589366268c274fdc11ec8bdb17e668d2f619555a2e9c1a29c91d8 Nonce: @@ -114,12 +105,3 @@ KnownNodes: Addr: "127.0.0.1:4661" PublicKey: 02ec784ca599f21ef93fe7abdc68d78817ab6c9b31f2324d15ea174d9da498b4c4 Role: Client -QPS: 1000 -ChainBusPeriod: 0s -BillingBlockCount: 60 -BPPeriod: 10s -BPTick: 3s -SQLChainPeriod: 1m0s -SQLChainTick: 10s -SQLChainTTL: 10 -MinProviderDeposit: 1000000 diff --git a/conf/testnet/parameters.go b/conf/testnet/parameters.go index 1d490b2e3..7a9b5badc 100644 --- a/conf/testnet/parameters.go +++ b/conf/testnet/parameters.go @@ -53,6 +53,9 @@ DNSSeed: - 202.46.34.74 - 202.46.34.75 - 202.46.34.76 +Adapter: + ListenAddr: "127.0.0.1:4661" + StorageDriver: covenantsql BlockProducer: PublicKey: "02c1db96f2ba7e1cb4e9822d12de0f63fb666feb828c7f509e81fab9bd7a34039c" @@ -63,29 +66,7 @@ BlockProducer: c: 0 d: 6148914694092305796 ChainFileName: "chain.db" - BPGenesisInfo: - Version: 1 - BlockHash: f745ca6427237aac858dd3c7f2df8e6f3c18d0f1c164e07a1c6b8eebeba6b154 - Producer: 0000000000000000000000000000000000000000000000000000000000000001 - MerkleRoot: 0000000000000000000000000000000000000000000000000000000000000001 - ParentHash: 0000000000000000000000000000000000000000000000000000000000000001 - Timestamp: 2019-01-02T13:33:00.00Z - BaseAccounts: - - Address: ba0ba731c7a76ccef2c1170f42038f7e228dfb474ef0190dfe35d9a37911ed37 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 1a7b0959bbd0d0ec529278a61c0056c277bffe75b2646e1699b46b10a90210be - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 9235bc4130a2ed4e6c35ea189dab35198ebb105640bedb97dd5269cc80863b16 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 9e1618775cceeb19f110e04fbc6c5bca6c8e4e9b116e193a42fe69bf602e7bcd - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 - - Address: 58aceaf4b730b54bf00c0fb3f7b14886de470767f313c2d108968cd8bf0794b7 - StableCoinBalance: 10000000000000000000 - CovenantCoinBalance: 10000000000000000000 + KnownNodes: - ID: 00000000000589366268c274fdc11ec8bdb17e668d2f619555a2e9c1a29c91d8 Nonce: