//go:build !js && !wasm package main import ( "bytes" "crypto/rand" "encoding/binary" "encoding/hex" "flag" "fmt" "io/fs" "log" "math/big" "os" "os/exec" "os/signal" "path/filepath" "runtime" rdebug "runtime/debug" "runtime/pprof" "strconv" "strings" "syscall" "time" "go.uber.org/zap" "golang.org/x/crypto/sha3" "google.golang.org/protobuf/proto" "source.quilibrium.com/quilibrium/monorepo/node/protobufs" "source.quilibrium.com/quilibrium/monorepo/node/utils" "github.com/cloudflare/circl/sign/ed448" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/pbnjay/memory" "github.com/pkg/errors" "source.quilibrium.com/quilibrium/monorepo/node/app" "source.quilibrium.com/quilibrium/monorepo/node/config" qcrypto "source.quilibrium.com/quilibrium/monorepo/node/crypto" "source.quilibrium.com/quilibrium/monorepo/node/crypto/kzg" "source.quilibrium.com/quilibrium/monorepo/node/rpc" ) var ( configDirectory = flag.String( "config", filepath.Join(".", ".config"), "the configuration directory", ) balance = flag.Bool( "balance", false, "print the node's confirmed token balance to stdout and exit", ) dbConsole = flag.Bool( "db-console", false, "starts the node in database console mode", ) importPrivKey = flag.String( "import-priv-key", "", "creates a new config using a specific key from the phase one ceremony", ) peerId = flag.Bool( "peer-id", false, "print the peer id to stdout from the config and exit", ) cpuprofile = flag.String( "cpuprofile", "", "write cpu profile to file", ) memprofile = flag.String( "memprofile", "", "write memory profile after 20m to this file", ) nodeInfo = flag.Bool( "node-info", false, "print node related information", ) debug = flag.Bool( "debug", false, "sets log output to debug (verbose)", ) dhtOnly = flag.Bool( "dht-only", false, "sets a node to run strictly as a dht bootstrap peer (not full node)", ) network = flag.Uint( "network", 0, "sets the active network for the node (mainnet = 0, primary testnet = 1)", ) signatureCheck = flag.Bool( "signature-check", signatureCheckDefault(), "enables or disables signature validation (default true or value of QUILIBRIUM_SIGNATURE_CHECK env var)", ) core = flag.Int( "core", 0, "specifies the core of the process (defaults to zero, the initial launcher)", ) parentProcess = flag.Int( "parent-process", 0, "specifies the parent process pid for a data worker", ) integrityCheck = flag.Bool( "integrity-check", false, "runs an integrity check on the store, helpful for confirming backups are not corrupted (defaults to false)", ) ) var signatories = []string{ "b1214da7f355f5a9edb7bcc23d403bdf789f070cca10db2b4cadc22f2d837afb650944853e35d5f42ef3c4105b802b144b4077d5d3253e4100", "de4cfe7083104bfe32f0d4082fa0200464d8b10804a811653eedda376efcad64dd222f0f0ceb0b8ae58abe830d7a7e3f3b2d79d691318daa00", "540237a35e124882d6b64e7bb5718273fa338e553f772b77fe90570e45303762b34131bdcb6c0b9f2cf9e393d9c7e0f546eeab0bcbbd881680", "fbe4166e37f93f90d2ebf06305315ae11b37e501d09596f8bde11ba9d343034fbca80f252205aa2f582a512a72ad293df371baa582da072900", "4160572e493e1bf15c44e055b11bf75230c76c7d2c67b48066770ab03dfd5ed34c97b9a431ec18578c83a0df9250b8362c38068650e8b01400", "45170b626884b85d61ae109f2aa9b0e1ecc18b181508431ea6308f3869f2adae49da9799a0a594eaa4ef3ad492518fb1729decd44169d40d00", "92cd8ee5362f3ae274a75ab9471024dbc144bff441ed8af7d19750ac512ff51e40e7f7b01e4f96b6345dd58878565948c3eb52c53f250b5080", "001a4cbfce5d9aeb7e20665b0d236721b228a32f0baee62ffa77f45b82ecaf577e8a38b7ef91fcf7d2d2d2b504f085461398d30b24abb1d700", "65b835071731c6e785bb2d107c7d85d8a537d79c435c3f42bb2f87027f93f858d7b37c598cef267a5db46e345f7a6f81969b465686657d1e00", "4507626f7164e7d8c304c07ff8d2e23c113fe108b221d2e60672f4d07750345815e2b4b3cc3df4d3466bf2f669c35c3172e06511270612ab00", "4fb2537345e46be3d5f96340c1441007501702dd5bfaf6dbf6943bbefceca8fb2b94ec0a8a1a2f49850fbe1d10244889a4f40abfa9e0c9e000", "57be2861faf0fffcbfd122c85c77010dce8f213030905781b85b6f345d912c7b5ace17797d9810899dfb8d13e7c8369595740725ab3dd5bd00", "61628beef8f6964466fd078d6a2b90a397ab0777a14b9728227fd19f36752f9451b1a8d780740a0b9a8ce3df5f89ca7b9ff17de9274a270980", "5547afc71b02821e2f5bfdd30fbe1374c3853898deff20a1b5cc729b8e81670fbbb9d1e917f85d153ea4b26bbf6f9c546dc1b64b9916608d80", "81d63a45f068629f568de812f18be5807bfe828a830097f09cf02330d6acd35e3607401df3fda08b03b68ea6e68afd506b23506b11e87a0f80", "6e2872f73c4868c4286bef7bfe2f5479a41c42f4e07505efa4883c7950c740252e0eea78eef10c584b19b1dcda01f7767d3135d07c33244100", "a114b061f8d35e3f3497c8c43d83ba6b4af67aa7b39b743b1b0a35f2d66110b5051dd3d86f69b57122a35b64e624b8180bee63b6152fce4280", } func signatureCheckDefault() bool { envVarValue, envVarExists := os.LookupEnv("QUILIBRIUM_SIGNATURE_CHECK") if envVarExists { def, err := strconv.ParseBool(envVarValue) if err == nil { return def } else { fmt.Println("Invalid environment variable QUILIBRIUM_SIGNATURE_CHECK, must be 'true' or 'false'. Got: " + envVarValue) } } return true } func main() { flag.Parse() if *signatureCheck { if runtime.GOOS == "windows" { fmt.Println("Signature check not available for windows yet, skipping...") } else { ex, err := os.Executable() if err != nil { panic(err) } b, err := os.ReadFile(ex) if err != nil { fmt.Println( "Error encountered during signature check – are you running this " + "from source? (use --signature-check=false)", ) panic(err) } checksum := sha3.Sum256(b) digest, err := os.ReadFile(ex + ".dgst") if err != nil { fmt.Println("Digest file not found") os.Exit(1) } parts := strings.Split(string(digest), " ") if len(parts) != 2 { fmt.Println("Invalid digest file format") os.Exit(1) } digestBytes, err := hex.DecodeString(parts[1][:64]) if err != nil { fmt.Println("Invalid digest file format") os.Exit(1) } if !bytes.Equal(checksum[:], digestBytes) { fmt.Println("Invalid digest for node") os.Exit(1) } count := 0 for i := 1; i <= len(signatories); i++ { signatureFile := fmt.Sprintf(ex+".dgst.sig.%d", i) sig, err := os.ReadFile(signatureFile) if err != nil { continue } pubkey, _ := hex.DecodeString(signatories[i-1]) if !ed448.Verify(pubkey, digest, sig, "") { fmt.Printf("Failed signature check for signatory #%d\n", i) os.Exit(1) } count++ } if count < len(signatories)/2+len(signatories)%2 { fmt.Printf("Quorum on signatures not met") os.Exit(1) } fmt.Println("Signature check passed") } } else { fmt.Println("Signature check disabled, skipping...") } if *memprofile != "" && *core == 0 { go func() { for { time.Sleep(5 * time.Minute) f, err := os.Create(*memprofile) if err != nil { log.Fatal(err) } pprof.WriteHeapProfile(f) f.Close() } }() } if *cpuprofile != "" && *core == 0 { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } if *balance { config, err := config.LoadConfig(*configDirectory, "") if err != nil { panic(err) } printBalance(config) return } if *peerId { config, err := config.LoadConfig(*configDirectory, "") if err != nil { panic(err) } printPeerID(config.P2P) return } if *importPrivKey != "" { config, err := config.LoadConfig(*configDirectory, *importPrivKey) if err != nil { panic(err) } printPeerID(config.P2P) fmt.Println("Import completed, you are ready for the launch.") return } if *nodeInfo { config, err := config.LoadConfig(*configDirectory, "") if err != nil { panic(err) } printNodeInfo(config) return } if !*dbConsole && *core == 0 { printLogo() printVersion() fmt.Println(" ") } nodeConfig, err := config.LoadConfig(*configDirectory, "") if err != nil { panic(err) } if *network != 0 { if nodeConfig.P2P.BootstrapPeers[0] == config.BootstrapPeers[0] { fmt.Println( "Node has specified to run outside of mainnet but is still " + "using default bootstrap list. This will fail. Exiting.", ) os.Exit(1) } nodeConfig.Engine.GenesisSeed = fmt.Sprintf( "%02x%s", byte(*network), nodeConfig.Engine.GenesisSeed, ) nodeConfig.P2P.Network = uint8(*network) fmt.Println( "Node is operating outside of mainnet – be sure you intended to do this.", ) } clearIfTestData(*configDirectory, nodeConfig) if *dbConsole { console, err := app.NewDBConsole(nodeConfig) if err != nil { panic(err) } console.Run() return } if *dhtOnly { done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) dht, err := app.NewDHTNode(nodeConfig) if err != nil { panic(err) } go func() { dht.Start() }() <-done dht.Stop() return } if *core != 0 { runtime.GOMAXPROCS(1) rdebug.SetGCPercent(9999) if nodeConfig.Engine.DataWorkerMemoryLimit == 0 { nodeConfig.Engine.DataWorkerMemoryLimit = 1792 * 1024 * 1024 // 1.75GiB } rdebug.SetMemoryLimit(nodeConfig.Engine.DataWorkerMemoryLimit) if nodeConfig.Engine.DataWorkerBaseListenMultiaddr == "" { nodeConfig.Engine.DataWorkerBaseListenMultiaddr = "/ip4/127.0.0.1/tcp/%d" } if nodeConfig.Engine.DataWorkerBaseListenPort == 0 { nodeConfig.Engine.DataWorkerBaseListenPort = 40000 } if *parentProcess == 0 && len(nodeConfig.Engine.DataWorkerMultiaddrs) == 0 { panic("parent process pid not specified") } l, err := zap.NewProduction() if err != nil { panic(err) } rpcMultiaddr := fmt.Sprintf( nodeConfig.Engine.DataWorkerBaseListenMultiaddr, int(nodeConfig.Engine.DataWorkerBaseListenPort)+*core-1, ) if len(nodeConfig.Engine.DataWorkerMultiaddrs) != 0 { rpcMultiaddr = nodeConfig.Engine.DataWorkerMultiaddrs[*core-1] } srv, err := rpc.NewDataWorkerIPCServer( rpcMultiaddr, l, uint32(*core)-1, qcrypto.NewWesolowskiFrameProver(l), *parentProcess, ) if err != nil { panic(err) } err = srv.Start() if err != nil { panic(err) } return } fmt.Println("Loading ceremony state and starting node...") if !*integrityCheck { go spawnDataWorkers(nodeConfig) } kzg.Init() report := RunSelfTestIfNeeded(*configDirectory, nodeConfig) RunMigrationIfNeeded(*configDirectory, nodeConfig) done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) var node *app.Node if *debug { node, err = app.NewDebugNode(nodeConfig, report) } else { node, err = app.NewNode(nodeConfig, report) } if err != nil { panic(err) } if *integrityCheck { fmt.Println("Running integrity check...") node.VerifyProofIntegrity() fmt.Println("Integrity check passed!") return } repair(*configDirectory, node) runtime.GOMAXPROCS(1) if nodeConfig.ListenGRPCMultiaddr != "" { srv, err := rpc.NewRPCServer( nodeConfig.ListenGRPCMultiaddr, nodeConfig.ListenRestMultiaddr, node.GetLogger(), node.GetDataProofStore(), node.GetClockStore(), node.GetKeyManager(), node.GetPubSub(), node.GetMasterClock(), node.GetExecutionEngines(), ) if err != nil { panic(err) } go func() { err := srv.Start() if err != nil { panic(err) } }() } node.Start() <-done stopDataWorkers() node.Stop() } var dataWorkers []*exec.Cmd func spawnDataWorkers(nodeConfig *config.Config) { if len(nodeConfig.Engine.DataWorkerMultiaddrs) != 0 { fmt.Println( "Data workers configured by multiaddr, be sure these are running...", ) return } process, err := os.Executable() if err != nil { panic(err) } cores := runtime.GOMAXPROCS(0) dataWorkers = make([]*exec.Cmd, cores-1) fmt.Printf("Spawning %d data workers...\n", cores-1) for i := 1; i <= cores-1; i++ { i := i go func() { for { args := []string{ fmt.Sprintf("--core=%d", i), fmt.Sprintf("--parent-process=%d", os.Getpid()), } args = append(args, os.Args[1:]...) cmd := exec.Command(process, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout err := cmd.Start() if err != nil { panic(err) } dataWorkers[i-1] = cmd cmd.Wait() time.Sleep(25 * time.Millisecond) fmt.Printf("Data worker %d stopped, restarting...\n", i) } }() } } func stopDataWorkers() { for i := 0; i < len(dataWorkers); i++ { err := dataWorkers[i].Process.Signal(os.Kill) if err != nil { fmt.Printf( "fatal: unable to kill worker with pid %d, please kill this process!\n", dataWorkers[i].Process.Pid, ) } } } func RunMigrationIfNeeded( configDir string, nodeConfig *config.Config, ) { shouldMigrate := false migrationInfo := []byte{0x00, 0x00, 0x00} _, err := os.Stat(filepath.Join(configDir, "MIGRATIONS")) if err != nil && os.IsNotExist(err) { fmt.Println("Migrations file not found, will perform migration...") shouldMigrate = true } if !shouldMigrate { migrationInfo, err = os.ReadFile(filepath.Join(configDir, "MIGRATIONS")) if err != nil { panic(err) } if len(migrationInfo) < 3 || !bytes.Equal(migrationInfo, []byte{0x01, 0x04, 0x013}) { fmt.Println("Migrations file outdated, will perform migration...") shouldMigrate = true } } // If subsequent migrations arise, we will need to distinguish by version if shouldMigrate { fmt.Println("Running migration...") // Easiest migration in the world. err := os.RemoveAll(filepath.Join(configDir, "store")) if err != nil { fmt.Println("ERROR: Could not remove store, please be sure to do this before restarting the node.") panic(err) } err = os.WriteFile( filepath.Join(configDir, "MIGRATIONS"), []byte{0x01, 0x04, 0x13}, fs.FileMode(0600), ) if err != nil { fmt.Println("ERROR: Could not save migration file.") panic(err) } fmt.Println("Migration completed.") } } func RunSelfTestIfNeeded( configDir string, nodeConfig *config.Config, ) *protobufs.SelfTestReport { logger, _ := zap.NewProduction() cores := runtime.GOMAXPROCS(0) if len(nodeConfig.Engine.DataWorkerMultiaddrs) != 0 { cores = len(nodeConfig.Engine.DataWorkerMultiaddrs) + 1 } memory := memory.TotalMemory() d, err := os.Stat(filepath.Join(configDir, "store")) if d == nil { err := os.Mkdir(filepath.Join(configDir, "store"), 0755) if err != nil { panic(err) } } f, err := os.Stat(filepath.Join(configDir, "SELF_TEST")) if f != nil { if f.Size() != 0 { report := &protobufs.SelfTestReport{} selfTestBytes, err := os.ReadFile(filepath.Join(configDir, "SELF_TEST")) if err != nil { panic(err) } err = proto.Unmarshal(selfTestBytes, report) if err != nil { panic(err) } if report.Cores == uint32(cores) && binary.BigEndian.Uint64(report.Memory) == memory { return report } } logger.Info("no self-test report found, generating") } report := &protobufs.SelfTestReport{} difficulty := nodeConfig.Engine.Difficulty if difficulty == 0 || difficulty == 10000 { difficulty = 100000 } report.Difficulty = difficulty frameProver := qcrypto.NewWesolowskiFrameProver(logger) logger.Info("generating difficulty metric") start := time.Now().UnixMilli() _, err = frameProver.ProveMasterClockFrame( &protobufs.ClockFrame{ Filter: []byte{0x00}, FrameNumber: 0, Timestamp: 0, Difficulty: difficulty, ParentSelector: []byte{0x00}, Input: make([]byte, 516), Output: make([]byte, 516), }, 0, difficulty, ) if err != nil { panic(err) } end := time.Now().UnixMilli() report.DifficultyMetric = end - start logger.Info("generating entropy for commit/proof sizes") p16bytes := make([]byte, 1024) p128bytes := make([]byte, 8192) p1024bytes := make([]byte, 65536) p65536bytes := make([]byte, 4194304) rand.Read(p16bytes) rand.Read(p128bytes) rand.Read(p1024bytes) rand.Read(p65536bytes) kzgProver := qcrypto.NewKZGInclusionProver(logger) logger.Info("generating 16 degree commitment metric") start = time.Now().UnixMilli() _, err = kzgProver.CommitRaw(p16bytes, 16) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Commit_16Metric = end - start logger.Info("generating 128 degree commitment metric") start = time.Now().UnixMilli() _, err = kzgProver.CommitRaw(p128bytes, 128) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Commit_128Metric = end - start logger.Info("generating 1024 degree commitment metric") start = time.Now().UnixMilli() _, err = kzgProver.CommitRaw(p1024bytes, 1024) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Commit_1024Metric = end - start logger.Info("generating 65536 degree commitment metric") start = time.Now().UnixMilli() _, err = kzgProver.CommitRaw(p65536bytes, 65536) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Commit_65536Metric = end - start logger.Info("generating 16 degree proof metric") start = time.Now().UnixMilli() _, err = kzgProver.ProveRaw(p16bytes, 0, 16) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Proof_16Metric = end - start logger.Info("generating 128 degree proof metric") start = time.Now().UnixMilli() _, err = kzgProver.ProveRaw(p128bytes, 0, 128) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Proof_128Metric = end - start logger.Info("generating 1024 degree proof metric") start = time.Now().UnixMilli() _, err = kzgProver.ProveRaw(p1024bytes, 0, 1024) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Proof_1024Metric = end - start logger.Info("generating 65536 degree proof metric") start = time.Now().UnixMilli() _, err = kzgProver.ProveRaw(p65536bytes, 0, 65536) if err != nil { panic(err) } end = time.Now().UnixMilli() report.Proof_65536Metric = end - start report.Cores = uint32(cores) report.Memory = binary.BigEndian.AppendUint64([]byte{}, memory) disk := utils.GetDiskSpace(nodeConfig.DB.Path) report.Storage = binary.BigEndian.AppendUint64([]byte{}, disk) logger.Info("writing report") // tag: dusk – capabilities report in v1.5 reportBytes, err := proto.Marshal(report) if err != nil { panic(err) } err = os.WriteFile( filepath.Join(configDir, "SELF_TEST"), reportBytes, fs.FileMode(0600), ) if err != nil { panic(err) } return report } func clearIfTestData(configDir string, nodeConfig *config.Config) { _, err := os.Stat(filepath.Join(configDir, "RELEASE_VERSION")) if os.IsNotExist(err) { fmt.Println("Clearing test data...") err := os.RemoveAll(nodeConfig.DB.Path) if err != nil { panic(err) } versionFile, err := os.OpenFile( filepath.Join(configDir, "RELEASE_VERSION"), os.O_CREATE|os.O_RDWR, fs.FileMode(0600), ) if err != nil { panic(err) } _, err = versionFile.Write([]byte{0x01, 0x00, 0x00}) if err != nil { panic(err) } err = versionFile.Close() if err != nil { panic(err) } } } func repair(configDir string, node *app.Node) { _, err := os.Stat(filepath.Join(configDir, "REPAIR")) if os.IsNotExist(err) { node.RunRepair() repairFile, err := os.OpenFile( filepath.Join(configDir, "REPAIR"), os.O_CREATE|os.O_RDWR, fs.FileMode(0600), ) if err != nil { panic(err) } _, err = repairFile.Write([]byte{0x00, 0x00, 0x01}) if err != nil { panic(err) } err = repairFile.Close() if err != nil { panic(err) } } } func printBalance(config *config.Config) { if config.ListenGRPCMultiaddr == "" { _, _ = fmt.Fprintf(os.Stderr, "gRPC Not Enabled, Please Configure\n") os.Exit(1) } conn, err := app.ConnectToNode(config) if err != nil { panic(err) } defer conn.Close() client := protobufs.NewNodeServiceClient(conn) balance, err := app.FetchTokenBalance(client) if err != nil { panic(err) } // fmt.Println("Owned balance:", balance.Owned, "QUIL") conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16) r := new(big.Rat).SetFrac(balance.UnconfirmedOwned, conversionFactor) fmt.Println("Note: Balance is strictly rewards earned with 1.4.19+, check https://www.quilibrium.com/rewards for more info about previous rewards.") fmt.Println("Unclaimed balance:", r.FloatString(12), "QUIL") } func printPeerID(p2pConfig *config.P2PConfig) { peerPrivKey, err := hex.DecodeString(p2pConfig.PeerPrivKey) if err != nil { panic(errors.Wrap(err, "error unmarshaling peerkey")) } privKey, err := crypto.UnmarshalEd448PrivateKey(peerPrivKey) if err != nil { panic(errors.Wrap(err, "error unmarshaling peerkey")) } pub := privKey.GetPublic() id, err := peer.IDFromPublicKey(pub) if err != nil { panic(errors.Wrap(err, "error getting peer id")) } fmt.Println("Peer ID: " + id.String()) } func printNodeInfo(cfg *config.Config) { if cfg.ListenGRPCMultiaddr == "" { _, _ = fmt.Fprintf(os.Stderr, "gRPC Not Enabled, Please Configure\n") os.Exit(1) } printPeerID(cfg.P2P) conn, err := app.ConnectToNode(cfg) if err != nil { panic(err) } defer conn.Close() client := protobufs.NewNodeServiceClient(conn) nodeInfo, err := app.FetchNodeInfo(client) if err != nil { panic(err) } fmt.Println("Version: " + config.FormatVersion(nodeInfo.Version)) fmt.Println("Max Frame: " + strconv.FormatUint(nodeInfo.GetMaxFrame(), 10)) fmt.Println("Peer Score: " + strconv.FormatUint(nodeInfo.GetPeerScore(), 10)) printBalance(cfg) } func printLogo() { fmt.Println(" ..-------..") fmt.Println(" ..---'''' ''''---..") fmt.Println(" .---'' ''---.") fmt.Println(" .-' '-.") fmt.Println(" ..-' ..--''''''''''''''--.. '-..") fmt.Println(" .' .--'' ''--. ''.") fmt.Println(" .'' ..-' ''-. '.") fmt.Println(" ' ' ''. '.") fmt.Println(" '' .'' '. '") fmt.Println(" ' '' '. '") fmt.Println(" ' ' ########## . '") fmt.Println(" ' ' ############## ' '") fmt.Println(" ' ' ############## ' '") fmt.Println(" ' ' ############## ' '") fmt.Println("' ' ########## ' '") fmt.Println("' ' ' '") fmt.Println("' ' ' '") fmt.Println("' ' ####### ####### ' '") fmt.Println("' ' &######################### ' '") fmt.Println("' ' ##############% ############## ' '") fmt.Println(" ' ' &############## ############### ' '") fmt.Println(" ' ' ############### ##############% '. '") fmt.Println(" ' '. ########## ############### '-. '") fmt.Println(" '. . ##### ##############% '-.'") fmt.Println(" ' '. ###############") fmt.Println(" '. '.. ##############%") fmt.Println(" '. '-. ###############") fmt.Println(" '-. ''-.. .. ##############%") fmt.Println(" '-. ''---............----' '. ###############") fmt.Println(" '-.. '. ############") fmt.Println(" ''-.. ..' ########") fmt.Println(" ''---.. ...---'' ##") fmt.Println(" ''----------''") } func printVersion() { patch := config.GetPatchNumber() patchString := "" if patch != 0x00 { patchString = fmt.Sprintf("-p%d", patch) } fmt.Println(" ") fmt.Println(" Quilibrium Node - v" + config.GetVersionString() + patchString + " – Centauri") }