initial auto-update

This commit is contained in:
Tyler Sturos 2025-03-28 21:40:08 -08:00
parent 0966faee9f
commit 947f3f565f
24 changed files with 4019 additions and 60 deletions

142
client/cmd/link.go Normal file
View File

@ -0,0 +1,142 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var symlinkPath string
var linkCmd = &cobra.Command{
Use: "link",
Short: "Create a symlink to qclient in PATH",
Long: fmt.Sprintf(`Create a symlink to the qclient binary in a suitable directory in your PATH.
This allows you to run qclient from anywhere without specifying the full path.
By default it will create the symlink in the current directory /usr/local/bin.
You can also specify a custom directory using the --path flag.
Example: qclient link --path /usr/local/bin`),
RunE: func(cmd *cobra.Command, args []string) error {
// Get the path to the current executable
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Determine the target directory and path for the symlink
targetDir, targetPath, err := determineSymlinkLocation()
if err != nil {
return err
}
// If operation was cancelled by the user
if targetDir == "" && targetPath == "" {
return nil
}
// Create the symlink (handles existing symlinks)
if err := utils.CreateSymlink(execPath, targetPath); err != nil {
return err
}
// Save the symlink path to ClientConfig
config := utils.ClientConfig{
SymlinkPath: targetPath,
}
// Use the UpdateClientConfig function to save the config
if err := utils.UpdateClientConfig(&config); err != nil {
fmt.Printf("Warning: Failed to save symlink path to config: %v\n", err)
} else {
fmt.Printf("Saved symlink path %s to client configuration\n", targetPath)
}
fmt.Printf("Symlink created at %s\n", targetPath)
return nil
},
}
// determineSymlinkLocation finds the appropriate location for the symlink
// Returns the target directory, the full path for the symlink, and any error
func determineSymlinkLocation() (string, string, error) {
// If user provided a custom path
if symlinkPath != "" {
return validateUserProvidedPath(symlinkPath)
}
// Otherwise, find a suitable directory in PATH
return utils.NodeDefaultSymlinkDir, utils.DefaultQClientSymlinkPath, nil
}
// isDirectoryInPath checks if a directory is in the PATH environment variable
func isDirectoryInPath(dir string) bool {
pathEnv := os.Getenv("PATH")
pathDirs := strings.Split(pathEnv, string(os.PathListSeparator))
// Normalize paths for comparison
absDir, err := filepath.Abs(dir)
if err != nil {
return false
}
for _, pathDir := range pathDirs {
absPathDir, err := filepath.Abs(pathDir)
if err != nil {
continue
}
if absDir == absPathDir {
return true
}
}
return false
}
// validateUserProvidedPath checks if the provided path is a valid directory
func validateUserProvidedPath(path string) (string, string, error) {
// Check if the provided path is a directory
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", "", fmt.Errorf("directory does not exist: %s", path)
}
return "", "", fmt.Errorf("error checking directory: %w", err)
}
if !info.IsDir() {
return "", "", fmt.Errorf("the specified path is not a directory: %s", path)
}
// Check if the directory is in PATH
if !isDirectoryInPath(path) {
// Ask user for confirmation to proceed with a directory not in PATH
fmt.Printf("Warning: The directory '%s' is not in your PATH environment variable.\n", path)
fmt.Println("The symlink will be created, but you may not be able to run 'qclient' from anywhere.")
fmt.Print("Do you want to continue? [y/N]: ")
var response string
fmt.Scanln(&response)
if strings.ToLower(response) != "y" {
fmt.Println("Operation cancelled.")
return "", "", nil
}
}
// Use the provided directory
targetDir := path
targetPath := filepath.Join(targetDir, "qclient")
return targetDir, targetPath, nil
}
func init() {
rootCmd.AddCommand(linkCmd)
linkCmd.Flags().StringVar(&symlinkPath, "path", "", "Specify a custom directory for the symlink")
}

46
client/cmd/node/info.go Normal file
View File

@ -0,0 +1,46 @@
package node
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// infoCmd represents the info command
var infoCmd = &cobra.Command{
Use: "info",
Short: "Get information about the Quilibrium node",
Long: `Get information about the Quilibrium node.
Available subcommands:
latest-version Get the latest available version of Quilibrium node
Examples:
# Get the latest version
qclient node info latest-version`,
}
// latestVersionCmd represents the latest-version command
var latestVersionCmd = &cobra.Command{
Use: "latest-version",
Short: "Get the latest available version of Quilibrium node",
Long: `Get the latest available version of Quilibrium node by querying the releases API.
This command fetches the version information from https://releases.quilibrium.com/release.`,
Run: func(cmd *cobra.Command, args []string) {
version, err := utils.GetLatestVersion(utils.ReleaseTypeNode)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Latest available version: %s\n", version)
},
}
func init() {
// Add the latest-version subcommand to the info command
infoCmd.AddCommand(latestVersionCmd)
// Add the info command to the node command
NodeCmd.AddCommand(infoCmd)
}

115
client/cmd/node/install.go Normal file
View File

@ -0,0 +1,115 @@
package node
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// installCmd represents the command to install the Quilibrium node
var installCmd = &cobra.Command{
Use: "install [version]",
Short: "Install Quilibrium node",
Long: `Install Quilibrium node binary.
If no version is specified, the latest version will be installed.
Examples:
# Install the latest version
qclient node install
# Install a specific version
qclient node install 2.1.0
# Install without creating a dedicated user
qclient node install --no-create-user`,
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
// Get system information and validate
osType, arch, err := utils.GetSystemInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
// Determine version to install
version := determineVersion(args)
fmt.Fprintf(os.Stdout, "Installing Quilibrium node for %s-%s, version: %s\n", osType, arch, version)
// Check if we need to create a dedicated user (opt-out)
if err := createNodeUser(); err != nil {
fmt.Fprintf(os.Stderr, "Error creating user: %v\n", err)
fmt.Fprintf(os.Stdout, "Continuing with installation as current user...\n")
}
// Install the node
installNode(version)
},
}
func init() {
// Add the install command to the node command
NodeCmd.AddCommand(installCmd)
}
// installNode installs the Quilibrium node
func installNode(version string) {
// Create installation directory
if err := utils.ValidateAndCreateDir(installPath); err != nil {
fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err)
return
}
// Create data directory
if err := utils.ValidateAndCreateDir(dataPath); err != nil {
fmt.Fprintf(os.Stderr, "Error creating data directory: %v\n", err)
return
}
// Download and install the node
if version == "latest" {
latestVersion, err := utils.GetLatestVersion(utils.ReleaseTypeNode)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting latest version: %v\n", err)
return
}
version = latestVersion
fmt.Fprintf(os.Stdout, "Installing latest version: %s\n", version)
}
if err := installByVersion(version); err != nil {
fmt.Fprintf(os.Stderr, "Error installing specific version: %v\n", err)
return
}
// Finish installation
nodeBinaryPath := filepath.Join(installPath, version, "node")
finishInstallation(nodeBinaryPath, version)
}
// installByVersion installs a specific version of the Quilibrium node
func installByVersion(version string) error {
// Create version-specific directory
versionDir := filepath.Join(installPath, version)
if err := utils.ValidateAndCreateDir(versionDir); err != nil {
return fmt.Errorf("failed to create version directory: %w", err)
}
// Download the release
if err := utils.DownloadRelease(utils.ReleaseTypeNode, version); err != nil {
return fmt.Errorf("failed to download release: %w", err)
}
// Download signature files
if err := utils.DownloadReleaseSignatures(utils.ReleaseTypeNode, version); err != nil {
return fmt.Errorf("failed to download signature files: %w", err)
}
return nil
}

59
client/cmd/node/node.go Normal file
View File

@ -0,0 +1,59 @@
package node
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var (
// Base URL for Quilibrium releases
baseReleaseURL = "https://releases.quilibrium.com"
// Default symlink path for the node binary
defaultSymlinkPath = "/usr/local/bin/quilibrium-node"
// Default installation directory base paths in order of preference
installPath = "/opt/quilibrium"
// Default data directory paths in order of preference
dataPath = "/var/lib/quilibrium"
logPath = "/var/log/quilibrium"
// Default user to run the node
nodeUser = "quilibrium"
serviceName = "quilibrium-node"
// Default config file name
defaultConfigFileName = "node.yaml"
osType string
arch string
)
// NodeCmd represents the node command
var NodeCmd = &cobra.Command{
Use: "node",
Short: "Quilibrium node commands",
Long: `Run Quilibrium node commands.`,
}
func init() {
// Add subcommands
NodeCmd.AddCommand(installCmd)
NodeCmd.AddCommand(updateNodeCmd)
NodeCmd.AddCommand(nodeServiceCmd)
localOsType, localArch, err := utils.GetSystemInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
osType = localOsType
arch = localArch
}

284
client/cmd/node/service.go Normal file
View File

@ -0,0 +1,284 @@
package node
import (
"fmt"
"os"
"os/exec"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// nodeServiceCmd represents the command to manage the Quilibrium node service
var nodeServiceCmd = &cobra.Command{
Use: "service [command]",
Short: "Manage the Quilibrium node service",
Long: `Manage the Quilibrium node service.
Available commands:
start Start the node service
stop Stop the node service
restart Restart the node service
status Check the status of the node service
enable Enable the node service to start on boot
disable Disable the node service from starting on boot
install Install the service for the current OS
Examples:
# Start the node service
qclient node service start
# Check service status
qclient node service status
# Enable service to start on boot
qclient node service enable`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
command := args[0]
switch command {
case "start":
startService()
case "stop":
stopService()
case "restart":
restartService()
case "status":
checkServiceStatus()
case "enable":
enableService()
case "disable":
disableService()
case "reload":
reloadService()
case "install":
installService()
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
fmt.Fprintf(os.Stderr, "Available commands: start, stop, restart, status, enable, disable, reload, install\n")
os.Exit(1)
}
},
}
// installService installs the appropriate service configuration for the current OS
func installService() {
if err := utils.CheckAndRequestSudo("Installing service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Installing Quilibrium node service for %s...\n", osType)
if osType == "darwin" {
installMacOSService()
} else if osType == "linux" {
if err := createSystemdServiceFile(); err != nil {
fmt.Fprintf(os.Stderr, "Error creating systemd service file: %v\n", err)
return
}
} else {
fmt.Fprintf(os.Stderr, "Error: Unsupported operating system: %s\n", osType)
return
}
fmt.Fprintf(os.Stdout, "Quilibrium node service installed successfully\n")
}
// startService starts the Quilibrium node service
func startService() {
if err := utils.CheckAndRequestSudo("Starting service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command
cmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", serviceName))
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err)
return
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "start", serviceName)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err)
return
}
}
fmt.Fprintf(os.Stdout, "Started Quilibrium node service\n")
}
// stopService stops the Quilibrium node service
func stopService() {
if err := utils.CheckAndRequestSudo("Stopping service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command
cmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", serviceName))
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err)
return
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "stop", serviceName)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err)
return
}
}
fmt.Fprintf(os.Stdout, "Stopped Quilibrium node service\n")
}
// restartService restarts the Quilibrium node service
func restartService() {
if err := utils.CheckAndRequestSudo("Restarting service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command - stop then start
stopCmd := exec.Command("sudo", "launchctl", "stop", fmt.Sprintf("com.quilibrium.%s", serviceName))
if err := stopCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error stopping service: %v\n", err)
return
}
startCmd := exec.Command("sudo", "launchctl", "start", fmt.Sprintf("com.quilibrium.%s", serviceName))
if err := startCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error starting service: %v\n", err)
return
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "restart", serviceName)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error restarting service: %v\n", err)
return
}
}
fmt.Fprintf(os.Stdout, "Restarted Quilibrium node service\n")
}
// reloadService reloads the Quilibrium node service
func reloadService() {
if err := utils.CheckAndRequestSudo("Reloading service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command - unload then load
plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName)
unloadCmd := exec.Command("sudo", "launchctl", "unload", plistPath)
if err := unloadCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error unloading service: %v\n", err)
return
}
loadCmd := exec.Command("sudo", "launchctl", "load", plistPath)
if err := loadCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error loading service: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Reloaded launchd service\n")
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "daemon-reload")
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error reloading service: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Reloaded systemd service\n")
}
}
// checkServiceStatus checks the status of the Quilibrium node service
func checkServiceStatus() {
if err := utils.CheckAndRequestSudo("Checking service status requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command
cmd := exec.Command("sudo", "launchctl", "list", fmt.Sprintf("com.quilibrium.%s", serviceName))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error checking service status: %v\n", err)
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "status", serviceName)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error checking service status: %v\n", err)
}
}
}
// enableService enables the Quilibrium node service to start on boot
func enableService() {
if err := utils.CheckAndRequestSudo("Enabling service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command - load with -w flag to enable at boot
plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName)
cmd := exec.Command("sudo", "launchctl", "load", "-w", plistPath)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error enabling service: %v\n", err)
return
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "enable", serviceName)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error enabling service: %v\n", err)
return
}
}
fmt.Fprintf(os.Stdout, "Enabled Quilibrium node service to start on boot\n")
}
// disableService disables the Quilibrium node service from starting on boot
func disableService() {
if err := utils.CheckAndRequestSudo("Disabling service requires root privileges"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
if osType == "darwin" {
// MacOS launchd command - unload with -w flag to disable at boot
plistPath := fmt.Sprintf("/Library/LaunchDaemons/com.quilibrium.%s.plist", serviceName)
cmd := exec.Command("sudo", "launchctl", "unload", "-w", plistPath)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error disabling service: %v\n", err)
return
}
} else {
// Linux systemd command
cmd := exec.Command("sudo", "systemctl", "disable", serviceName)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error disabling service: %v\n", err)
return
}
}
fmt.Fprintf(os.Stdout, "Disabled Quilibrium node service from starting on boot\n")
}

411
client/cmd/node/shared.go Normal file
View File

@ -0,0 +1,411 @@
package node
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// determineVersion gets the version to install from args or defaults to "latest"
func determineVersion(args []string) string {
if len(args) > 0 {
return args[0]
}
return "latest"
}
// confirmPaths asks the user to confirm the installation and data paths
func confirmPaths(installPath, dataPath string) bool {
fmt.Print("Do you want to continue with these paths? [Y/n]: ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
return response == "" || response == "y" || response == "yes"
}
// createNodeUser creates a dedicated user for running the node
func createNodeUser() error {
fmt.Fprintf(os.Stdout, "Creating dedicated user '%s' for running the node...\n", nodeUser)
// Check for sudo privileges
if err := utils.CheckAndRequestSudo("Creating system user requires root privileges"); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
var cmd *exec.Cmd
if osType == "linux" {
// Check if user already exists
checkCmd := exec.Command("id", nodeUser)
if checkCmd.Run() == nil {
fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser)
return nil
}
// Create user on Linux
cmd = exec.Command("useradd", "-r", "-s", "/bin/false", "-m", "-c", "Quilibrium Node User", nodeUser)
} else if osType == "darwin" {
// Check if user already exists on macOS
checkCmd := exec.Command("dscl", ".", "-read", "/Users/"+nodeUser)
if checkCmd.Run() == nil {
fmt.Fprintf(os.Stdout, "User '%s' already exists\n", nodeUser)
return nil
}
// Create user on macOS
// Get next available user ID
uidCmd := exec.Command("dscl", ".", "-list", "/Users", "UniqueID")
uidOutput, err := uidCmd.Output()
if err != nil {
return fmt.Errorf("failed to get user IDs: %v", err)
}
// Find the highest UID and add 1
var maxUID int = 500 // Start with a reasonable system UID
for _, line := range strings.Split(string(uidOutput), "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 {
var uid int
fmt.Sscanf(fields[len(fields)-1], "%d", &uid)
if uid > maxUID && uid < 65000 { // Avoid system UIDs
maxUID = uid
}
}
}
nextUID := maxUID + 1
// Create the user
cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser)
if err := cmd.Run(); err != nil {
return fmt.Errorf("Failed to create user: %v", err)
}
// Set the user's properties
commands := [][]string{
{"-create", "/Users/" + nodeUser, "UniqueID", fmt.Sprintf("%d", nextUID)},
{"-create", "/Users/" + nodeUser, "PrimaryGroupID", "20"}, // staff group
{"-create", "/Users/" + nodeUser, "UserShell", "/bin/false"},
{"-create", "/Users/" + nodeUser, "NFSHomeDirectory", "/var/empty"},
{"-create", "/Users/" + nodeUser, "RealName", "Quilibrium Node User"},
}
for _, args := range commands {
cmd = exec.Command("dscl", append([]string{"."}, args...)...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to set user property %s: %v", args[1], err)
}
}
// Disable the user account
cmd = exec.Command("dscl", ".", "-create", "/Users/"+nodeUser, "Password", "*")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to disable user account: %v", err)
}
return nil
} else {
return fmt.Errorf("user creation not supported on %s", osType)
}
// Run the command
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
fmt.Fprintf(os.Stdout, "User '%s' created successfully\n", nodeUser)
return nil
}
// setOwnership sets the ownership of directories to the node user
func setOwnership() {
fmt.Fprintf(os.Stdout, "Setting ownership of %s and %s to %s...\n", installPath, dataPath, nodeUser)
// Change ownership of installation directory
chownCmd := exec.Command("chown", "-R", nodeUser+":"+nodeUser, installPath)
if err := chownCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", installPath, err)
}
// Change ownership of data directory
chownCmd = exec.Command("chown", "-R", nodeUser+":"+nodeUser, dataPath)
if err := chownCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", dataPath, err)
}
}
// setupLogRotation creates a logrotate configuration file for the Quilibrium node
func setupLogRotation() error {
// Check if we need sudo privileges for creating logrotate config
if err := utils.CheckAndRequestSudo("Creating logrotate configuration requires root privileges"); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
// Create logrotate configuration
configContent := fmt.Sprintf(`%s/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 %s %s
postrotate
systemctl reload quilibrium-node >/dev/null 2>&1 || true
endscript
}`, logPath, nodeUser, nodeUser)
// Write the configuration file
configPath := "/etc/logrotate.d/quilibrium-node"
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
return fmt.Errorf("failed to create logrotate configuration: %w", err)
}
// Create log directory with proper permissions
if err := os.MkdirAll(logPath, 0750); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
// Set ownership of log directory
chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, logPath)
if err := chownCmd.Run(); err != nil {
return fmt.Errorf("failed to set log directory ownership: %w", err)
}
fmt.Fprintf(os.Stdout, "Created log rotation configuration at %s\n", configPath)
return nil
}
// finishInstallation completes the installation process by making the binary executable and creating a symlink
func finishInstallation(nodeBinaryPath string, version string) {
setOwnership()
// Make the binary executable
if err := os.Chmod(nodeBinaryPath, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error making binary executable: %v\n", err)
return
}
// Check if we need sudo privileges for creating symlink in system directory
if strings.HasPrefix(defaultSymlinkPath, "/usr/") || strings.HasPrefix(defaultSymlinkPath, "/bin/") || strings.HasPrefix(defaultSymlinkPath, "/sbin/") {
if err := utils.CheckAndRequestSudo(fmt.Sprintf("Creating symlink at %s requires root privileges", defaultSymlinkPath)); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to get sudo privileges: %v\n", err)
return
}
}
// Create symlink using the utils package
if err := utils.CreateSymlink(nodeBinaryPath, defaultSymlinkPath); err != nil {
fmt.Fprintf(os.Stderr, "Error creating symlink: %v\n", err)
}
// Set up log rotation
if err := setupLogRotation(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to set up log rotation: %v\n", err)
}
// Create systemd service file
if osType == "linux" {
if err := createSystemdServiceFile(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create systemd service file: %v\n", err)
}
} else if osType == "darwin" {
installMacOSService()
} else {
fmt.Fprintf(os.Stderr, "Warning: Background service file creation not supported on %s\n", osType)
return
}
// Print success message
printSuccessMessage(version)
}
// printSuccessMessage prints a success message after installation
func printSuccessMessage(version string) {
fmt.Fprintf(os.Stdout, "\nSuccessfully installed Quilibrium node %s\n", version)
fmt.Fprintf(os.Stdout, "Installation directory: %s\n", installPath)
fmt.Fprintf(os.Stdout, "Data directory: %s\n", dataPath)
fmt.Fprintf(os.Stdout, "Binary symlinked to %s\n", defaultSymlinkPath)
fmt.Fprintf(os.Stdout, "Log directory: %s\n", logPath)
fmt.Fprintf(os.Stdout, "Environment file: /etc/default/quilibrium-node\n")
fmt.Fprintf(os.Stdout, "Service file: /etc/systemd/system/quilibrium-node.service\n")
fmt.Fprintf(os.Stdout, "\nTo start the node, you can run:\n")
fmt.Fprintf(os.Stdout, " %s --config %s/config/config.yaml\n",
defaultSymlinkPath, dataPath)
fmt.Fprintf(os.Stdout, " # Or use systemd service:\n")
fmt.Fprintf(os.Stdout, " sudo systemctl start quilibrium-node\n")
fmt.Fprintf(os.Stdout, "\nFor more options, run:\n")
fmt.Fprintf(os.Stdout, " quilibrium-node --help\n")
}
// createSystemdServiceFile creates the systemd service file with environment file support
func createSystemdServiceFile() error {
// Check if we need sudo privileges
if err := utils.CheckAndRequestSudo("Creating systemd service file requires root privileges"); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
// Create environment file content
envContent := fmt.Sprintf(`# Quilibrium Node Environment`, dataPath)
// Write environment file
envPath := filepath.Join(dataPath, "config", "quilibrium.env")
if err := os.WriteFile(envPath, []byte(envContent), 0640); err != nil {
return fmt.Errorf("failed to create environment file: %w", err)
}
// Set ownership of environment file
chownCmd := exec.Command("chown", nodeUser+":"+nodeUser, envPath)
if err := chownCmd.Run(); err != nil {
return fmt.Errorf("failed to set environment file ownership: %w", err)
}
// Create systemd service file content
serviceContent := fmt.Sprintf(`[Unit]
Description=Quilibrium Node Service
After=network.target
[Service]
Type=simple
User=quilibrium
EnvironmentFile=/opt/quilibrium/config/quilibrium.env
ExecStart=/usr/local/bin/quilibrium-node --config /opt/quilibrium/config
Restart=on-failure
RestartSec=10
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
`, nodeUser, defaultSymlinkPath, dataPath)
// Write service file
servicePath := "/etc/systemd/system/quilibrium-node.service"
if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
return fmt.Errorf("failed to create service file: %w", err)
}
// Reload systemd daemon
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd daemon: %w", err)
}
fmt.Fprintf(os.Stdout, "Created systemd service file at %s\n", servicePath)
fmt.Fprintf(os.Stdout, "Created environment file at %s\n", envPath)
return nil
}
// installMacOSService installs a launchd service on macOS
func installMacOSService() {
fmt.Println("Installing launchd service for Quilibrium node...")
// Create plist file content
plistTemplate := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{{.Label}}</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/quilibrium-node</string>
<string>--config</string>
<string>/opt/quilibrium/config/</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>QUILIBRIUM_DATA_DIR</key>
<string>{{.DataPath}}</string>
<key>QUILIBRIUM_LOG_LEVEL</key>
<string>info</string>
<key>QUILIBRIUM_LISTEN_GRPC_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8337</string>
<key>QUILIBRIUM_LISTEN_REST_MULTIADDR</key>
<string>/ip4/127.0.0.1/tcp/8338</string>
<key>QUILIBRIUM_STATS_MULTIADDR</key>
<string>/dns/stats.quilibrium.com/tcp/443</string>
<key>QUILIBRIUM_NETWORK_ID</key>
<string>0</string>
<key>QUILIBRIUM_DEBUG</key>
<string>false</string>
<key>QUILIBRIUM_SIGNATURE_CHECK</key>
<string>true</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>{{.LogPath}}/node.err</string>
<key>StandardOutPath</key>
<string>{{.LogPath}}/node.log</string>
</dict>
</plist>`
// Prepare template data
data := struct {
Label string
DataPath string
ServiceName string
LogPath string
}{
Label: fmt.Sprintf("com.quilibrium.node"),
DataPath: dataPath,
ServiceName: "node",
LogPath: logPath,
}
// Parse and execute template
tmpl, err := template.New("plist").Parse(plistTemplate)
if err != nil {
fmt.Printf("Error creating plist template: %v\n", err)
return
}
// Determine plist file path
var plistPath = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", data.Label)
// Write plist file
file, err := os.Create(plistPath)
if err != nil {
fmt.Printf("Error creating plist file: %v\n", err)
return
}
defer file.Close()
if err := tmpl.Execute(file, data); err != nil {
fmt.Printf("Error writing plist file: %v\n", err)
return
}
// Set correct permissions
chownCmd := exec.Command("chown", "root:wheel", plistPath)
if err := chownCmd.Run(); err != nil {
fmt.Printf("Warning: Failed to change ownership of plist file: %v\n", err)
}
// Load the service
var loadCmd = exec.Command("launchctl", "load", "-w", plistPath)
if err := loadCmd.Run(); err != nil {
fmt.Printf("Error loading service: %v\n", err)
fmt.Println("You may need to load the service manually.")
}
fmt.Printf("Launchd service installed successfully as %s\n", plistPath)
fmt.Println("\nTo start the service:")
fmt.Printf(" sudo launchctl start %s\n", data.Label)
fmt.Println("\nTo stop the service:")
fmt.Printf(" sudo launchctl stop %s\n", data.Label)
fmt.Println("\nTo view service logs:")
fmt.Printf(" cat %s/%s.log\n", data.LogPath, data.ServiceName)
}

96
client/cmd/node/update.go Normal file
View File

@ -0,0 +1,96 @@
package node
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// updateNodeCmd represents the command to update the Quilibrium node
var updateNodeCmd = &cobra.Command{
Use: "update [version]",
Short: "Update Quilibrium node",
Long: `Update Quilibrium node to a specified version or the latest version.
If no version is specified, the latest version will be installed.
Examples:
# Update to the latest version
qclient node update
# Update to a specific version
qclient node update 2.1.0`,
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
// Get system information and validate
// Determine version to install
version := determineVersion(args)
fmt.Fprintf(os.Stdout, "Updating Quilibrium node for %s-%s, version: %s\n", osType, arch, version)
// Update the node
updateNode(version)
},
}
func init() {
}
// updateNode handles the node update process
func updateNode(version string) {
// Check if we need sudo privileges
if err := utils.CheckAndRequestSudo(fmt.Sprintf("Updating node at %s requires root privileges", installPath)); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
// Create version-specific installation directory
versionDir := filepath.Join(installPath, "node", version)
if err := os.MkdirAll(versionDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err)
return
}
// Create data directory
versionDataDir := filepath.Join(dataPath, "node", version)
if err := os.MkdirAll(versionDataDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating data directory: %v\n", err)
return
}
// Construct the expected filename for the specified version
// Remove 'v' prefix if present for filename construction
versionWithoutV := strings.TrimPrefix(version, "v")
fileName := fmt.Sprintf("node-%s-%s-%s", versionWithoutV, osType, arch)
// Download the release directly
nodeBinaryPath := filepath.Join(dataPath, version, fileName)
err := utils.DownloadRelease(utils.ReleaseTypeNode, versionWithoutV)
if err != nil {
fmt.Fprintf(os.Stderr, "Error downloading version %s: %v\n", version, err)
fmt.Fprintf(os.Stderr, "The specified version %s does not exist for %s-%s\n", version, osType, arch)
// Clean up the created directories since installation failed
os.RemoveAll(versionDir)
os.RemoveAll(versionDataDir)
return
}
// Download signature files
if err := utils.DownloadReleaseSignatures(utils.ReleaseTypeNode, versionWithoutV); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to download signature files: %v\n", err)
fmt.Fprintf(os.Stdout, "Continuing with installation...\n")
}
// Ensure log rotation is set up
if err := setupLogRotation(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to set up log rotation: %v\n", err)
}
// Successfully downloaded the specific version
finishInstallation(nodeBinaryPath, version)
}

1
client/cmd/node/utils.go Normal file
View File

@ -0,0 +1 @@
package node

View File

@ -1,6 +1,7 @@
package cmd
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/hex"
@ -17,6 +18,8 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"source.quilibrium.com/quilibrium/monorepo/client/cmd/node"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
@ -27,10 +30,11 @@ var simulateFail bool
var LightNode bool = false
var DryRun bool = false
var publicRPC bool = false
var rootCmd = &cobra.Command{
Use: "qclient",
Short: "Quilibrium RPC Client",
Short: "Quilibrium client",
Long: `Quilibrium client is a command-line tool for managing Quilibrium nodes.
It provides commands for installing, updating, and managing Quilibrium nodes.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if signatureCheck {
ex, err := os.Executable()
@ -50,80 +54,97 @@ var rootCmd = &cobra.Command{
checksum := sha3.Sum256(b)
digest, err := os.ReadFile(ex + ".dgst")
if err != nil {
fmt.Println("Digest file not found")
os.Exit(1)
}
fmt.Println("The digest file was not found. Do you want to continue without signature verification? (y/n)")
fmt.Println("You can also use --signature-check=false in your command to skip this prompt")
parts := strings.Split(string(digest), " ")
if len(parts) != 2 {
fmt.Println("Invalid digest file format")
os.Exit(1)
}
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
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(config.Signatories); i++ {
signatureFile := fmt.Sprintf(ex+".dgst.sig.%d", i)
sig, err := os.ReadFile(signatureFile)
if err != nil {
continue
}
pubkey, _ := hex.DecodeString(config.Signatories[i-1])
if !ed448.Verify(pubkey, digest, sig, "") {
fmt.Printf("Failed signature check for signatory #%d\n", i)
if response != "y" && response != "yes" {
fmt.Println("Exiting due to missing digest file")
os.Exit(1)
}
count++
}
if count < ((len(config.Signatories)-4)/2)+((len(config.Signatories)-4)%2) {
fmt.Printf("Quorum on signatures not met")
os.Exit(1)
}
fmt.Println("Continuing without signature verification")
signatureCheck = false
} else {
parts := strings.Split(string(digest), " ")
if len(parts) != 2 {
fmt.Println("Invalid digest file format")
os.Exit(1)
}
fmt.Println("Signature check passed")
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(config.Signatories); i++ {
signatureFile := fmt.Sprintf(ex+".dgst.sig.%d", i)
sig, err := os.ReadFile(signatureFile)
if err != nil {
continue
}
pubkey, _ := hex.DecodeString(config.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(config.Signatories)-4)/2)+((len(config.Signatories)-4)%2) {
fmt.Printf("Quorum on signatures not met")
os.Exit(1)
}
fmt.Println("Signature check passed")
}
} else {
fmt.Println("Signature check bypassed, be sure you know what you're doing")
}
_, err := os.Stat(configDirectory)
if os.IsNotExist(err) {
fmt.Printf("config directory doesn't exist: %s\n", configDirectory)
os.Exit(1)
}
// Skip config checks for node and link commands
if len(os.Args) > 1 && (os.Args[1] != "node" && os.Args[1] != "link") {
// These commands handle their own configuration
_, err := os.Stat(configDirectory)
if os.IsNotExist(err) {
fmt.Printf("config directory doesn't exist: %s\n", configDirectory)
os.Exit(1)
}
NodeConfig, err = config.LoadConfig(configDirectory, "", false)
if err != nil {
fmt.Printf("invalid config directory: %s\n", configDirectory)
os.Exit(1)
}
NodeConfig, err = config.LoadConfig(configDirectory, "", false)
if err != nil {
fmt.Printf("invalid config directory: %s\n", configDirectory)
os.Exit(1)
}
if publicRPC {
fmt.Println("Public RPC enabled, using light node")
LightNode = true
}
if !LightNode && NodeConfig.ListenGRPCMultiaddr == "" {
fmt.Println("No ListenGRPCMultiaddr found in config, using light node")
LightNode = true
if publicRPC {
fmt.Println("Public RPC enabled, using light node")
LightNode = true
}
if !LightNode && NodeConfig.ListenGRPCMultiaddr == "" {
fmt.Println("No ListenGRPCMultiaddr found in config, using light node")
LightNode = true
}
}
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
@ -193,6 +214,46 @@ func init() {
&publicRPC,
"public-rpc",
false,
"uses the public RPC",
"uses the public RPCd",
)
// Create config directory if it doesn't exist
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// Skip for help command
if cmd.Name() == "help" {
return nil
}
// Create config directory if it doesn't exist
if err := os.MkdirAll(utils.ClientConfigDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
// Check for signature files
ex, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %v", err)
}
digestPath := ex + ".dgst"
if signatureCheck && !utils.FileExists(digestPath) {
fmt.Println("Signature file not found. Would you like to download it? (y/n)")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
if response == "y" || response == "yes" {
fmt.Println("Downloading signature file...")
// TODO: Implement signature download logic
fmt.Println("Signature download not implemented yet. Please download manually.")
} else {
fmt.Println("Continuing without signature verification")
signatureCheck = false
}
}
return nil
}
// Add the node command
rootCmd.AddCommand(node.NodeCmd)
}

172
client/cmd/update.go Normal file
View File

@ -0,0 +1,172 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var (
osType = runtime.GOOS
arch = runtime.GOARCH
)
// updateCmd represents the command to update the Quilibrium client
var updateCmd = &cobra.Command{
Use: "update [version]",
Short: "Update Quilibrium client",
Long: `Update Quilibrium client to a specified version or the latest version.
If no version is specified, the latest version will be installed.
Examples:
# Update to the latest version
qclient update
# Update to a specific version
qclient update 2.1.0`,
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
// Get system information and validate
localOsType, localArch, err := utils.GetSystemInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
osType = localOsType
arch = localArch
// Determine version to install
version := determineVersion(args)
fmt.Fprintf(os.Stdout, "Updating Quilibrium client for %s-%s, version: %s\n", osType, arch, version)
// Update the client
updateClient(version)
},
}
func init() {
rootCmd.AddCommand(updateCmd)
}
// determineVersion gets the version to install from args or defaults to "latest"
func determineVersion(args []string) string {
if len(args) > 0 {
return args[0]
}
return "latest"
}
// updateClient handles the client update process
func updateClient(version string) {
// Check if we need sudo privileges
if err := utils.CheckAndRequestSudo(fmt.Sprintf("Updating client at %s requires root privileges", utils.ClientInstallPath)); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
// Create version-specific installation directory
versionDir := filepath.Join(utils.ClientInstallPath, version)
if err := os.MkdirAll(versionDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err)
return
}
// Create data directory
versionDataDir := filepath.Join(utils.ClientDataPath, version)
if err := os.MkdirAll(versionDataDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating data directory: %v\n", err)
return
}
// If version is "latest", get the latest version
if version == "latest" {
latestVersion, err := utils.GetLatestVersion(utils.ReleaseTypeQClient)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting latest version: %v\n", err)
return
}
version = latestVersion
fmt.Fprintf(os.Stdout, "Latest version: %s\n", version)
}
// Construct the expected filename for the specified version
// Remove 'v' prefix if present for filename construction
versionWithoutV := strings.TrimPrefix(version, "v")
// Download the release directly
err := utils.DownloadRelease(utils.ReleaseTypeQClient, versionWithoutV)
if err != nil {
fmt.Fprintf(os.Stderr, "Error downloading version %s: %v\n", version, err)
fmt.Fprintf(os.Stderr, "The specified version %s does not exist for %s-%s\n", version, osType, arch)
// Clean up the created directories since installation failed
os.RemoveAll(versionDir)
os.RemoveAll(versionDataDir)
return
}
// Download signature files
if err := utils.DownloadReleaseSignatures(utils.ReleaseTypeQClient, versionWithoutV); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to download signature files: %v\n", err)
fmt.Fprintf(os.Stdout, "Continuing with installation...\n")
}
// Successfully downloaded the specific version
finishInstallation(version)
}
// finishInstallation completes the update process
func finishInstallation(version string) {
// Read current config
config, err := utils.ReadClientConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading config: %v\n", err)
return
}
// Update config with new version
config.Version = version
if err := utils.UpdateClientConfig(config); err != nil {
fmt.Fprintf(os.Stderr, "Error updating config: %v\n", err)
return
}
// Construct executable path
execPath := filepath.Join(utils.ClientInstallPath, version, "qclient")
// Make the binary executable
if err := os.Chmod(execPath, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error making binary executable: %v\n", err)
return
}
// Create symlink to the new version
symlinkPath := utils.DefaultQClientSymlinkPath
if config.SymlinkPath != "" {
symlinkPath = config.SymlinkPath
}
// Check if we need sudo privileges for creating symlink in system directory
if strings.HasPrefix(symlinkPath, "/usr/") || strings.HasPrefix(symlinkPath, "/bin/") || strings.HasPrefix(symlinkPath, "/sbin/") {
if err := utils.CheckAndRequestSudo(fmt.Sprintf("Creating symlink at %s requires root privileges", symlinkPath)); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to get sudo privileges: %v\n", err)
return
}
}
// Create symlink
if err := utils.CreateSymlink(execPath, symlinkPath); err != nil {
fmt.Fprintf(os.Stderr, "Error creating symlink: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Successfully updated qclient to version %s\n", version)
fmt.Fprintf(os.Stdout, "Executable: %s\n", execPath)
fmt.Fprintf(os.Stdout, "Symlink: %s\n", symlinkPath)
}

86
client/cmd/version.go Normal file
View File

@ -0,0 +1,86 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
// Version information - fallback if executable name doesn't contain version
const (
DefaultVersion = "1.0.0"
)
// VersionInfo holds version and hash information
type VersionInfo struct {
Version string
SHA256 string
MD5 string
}
// GetVersionInfo extracts version from executable and optionally calculates hashes
func GetVersionInfo(calcChecksum bool) (VersionInfo, error) {
executable, err := os.Executable()
if err != nil {
return VersionInfo{Version: DefaultVersion}, fmt.Errorf("error getting executable path: %v", err)
}
// Extract version from executable name (e.g. qclient-2.0.3-linux-amd)
baseName := filepath.Base(executable)
versionPattern := regexp.MustCompile(`qclient-([0-9]+\.[0-9]+\.[0-9]+)`)
matches := versionPattern.FindStringSubmatch(baseName)
version := DefaultVersion
if len(matches) > 1 {
version = matches[1]
}
// If version not found or checksum requested, calculate hash
if len(matches) <= 1 || calcChecksum {
sha256Hash, md5Hash, err := utils.CalculateFileHashes(executable)
if err != nil {
return VersionInfo{Version: version}, fmt.Errorf("error calculating file hashes: %v", err)
}
return VersionInfo{
Version: version,
SHA256: sha256Hash,
MD5: md5Hash,
}, nil
}
return VersionInfo{
Version: version,
}, nil
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Display the qclient version",
Long: `Display the qclient version and optionally calculate SHA256 and MD5 hashes of the executable.`,
Run: func(cmd *cobra.Command, args []string) {
showChecksum, _ := cmd.Flags().GetBool("checksum")
info, err := GetVersionInfo(showChecksum)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("qclient %s\n", info.Version)
if info.SHA256 != "" && info.MD5 != "" {
fmt.Printf("SHA256: %s\n", info.SHA256)
fmt.Printf("MD5: %s\n", info.MD5)
}
},
}
func init() {
versionCmd.Flags().Bool("checksum", false, "Show SHA256 and MD5 checksums of the executable")
rootCmd.AddCommand(versionCmd)
}

39
client/test/Dockerfile Normal file
View File

@ -0,0 +1,39 @@
# Use build argument to specify the base image
ARG DISTRO=ubuntu
ARG VERSION=24.04
# Use the specified distribution as the base image
FROM --platform=$BUILDPLATFORM ${DISTRO}:${VERSION}
ARG TARGETARCH
ARG TARGETOS
RUN echo "TARGETARCH: $TARGETARCH"
RUN echo "TARGETOS: $TARGETOS"
# Install required packages
RUN apt-get update && apt-get install -y \
curl \
sudo \
lsb-release \
&& rm -rf /var/lib/apt/lists/*
# Create a non-root user for testing
RUN useradd -m -s /bin/bash testuser && \
echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Set working directory
WORKDIR /app
# Copy the client binary and test script
COPY build/qclient/${TARGETARCH}_${TARGETOS}/qclient /opt/quilibrium/bin/qclient
COPY test_install.sh /app/
# Set permissions
RUN chmod +x /opt/quilibrium/bin/qclient /app/test_install.sh
# Switch to test user
USER testuser
# Run the test script
CMD ["/app/test_install.sh"]

View File

@ -0,0 +1,87 @@
FROM ubuntu:24.04 AS build-base
ENV PATH="${PATH}:/root/.cargo/bin/"
# Install GMP 6.2 (6.3 which MacOS is using only available on Debian unstable)
RUN apt-get update && apt-get install -y \
build-essential \
curl \
git \
cmake \
libgmp-dev \
libmpfr-dev \
libmpfr6 \
wget \
m4 \
pkg-config \
gcc \
g++ \
make \
autoconf \
automake \
libtool \
&& rm -rf /var/lib/apt/lists/*
# Install Go 1.22.5 for amd64
RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz && \
rm -rf /usr/local/go && \
tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz && \
rm go1.22.5.linux-amd64.tar.gz
ENV PATH=$PATH:/usr/local/go/bin
# Install FLINT library
RUN git clone https://github.com/flintlib/flint.git && \
cd flint && \
git checkout flint-3.0 && \
./bootstrap.sh && \
./configure \
--prefix=/usr/local \
--with-gmp=/usr/local \
--with-mpfr=/usr/local \
--enable-static \
--disable-shared \
CFLAGS="-O3" && \
make && \
make install && \
cd .. && \
rm -rf flint
# Install Rust toolchain
COPY docker/rustup-init.sh /opt/rustup-init.sh
RUN /opt/rustup-init.sh -y --profile minimal
# Install uniffi-bindgen-go
RUN cargo install uniffi-bindgen-go --git https://github.com/NordSecurity/uniffi-bindgen-go --tag v0.2.1+v0.25.0
FROM build-base AS build
ENV GOEXPERIMENT=arenas
ENV QUILIBRIUM_SIGNATURE_CHECK=false
WORKDIR /opt/ceremonyclient
COPY . .
## Generate Rust bindings for VDF
WORKDIR /opt/ceremonyclient/vdf
RUN ./generate.sh
## Generate Rust bindings for BLS48581
WORKDIR /opt/ceremonyclient/bls48581
RUN ./generate.sh
## Generate Rust bindings for VerEnc
WORKDIR /opt/ceremonyclient/verenc
RUN ./generate.sh
# Build and install qclient
WORKDIR /opt/ceremonyclient/client
RUN ./build.sh -o qclient
# Create final stage to copy the binary
FROM scratch AS export-stage
COPY --from=build /opt/ceremonyclient/client/qclient ./client/test/

155
client/test/README.md Normal file
View File

@ -0,0 +1,155 @@
# Quil Test Runner Documentation
This document describes the usage and functionality of the Quil test runner script (`run_tests.sh`), which is designed to run tests across different Linux distributions using Docker containers.
## Overview
The test runner allows you to:
- Run tests on multiple Linux distributions simultaneously
- Test on a specific distribution and version
- Customize container tags for test runs
- Build the client binary using a standardized Docker build process
## Prerequisites
- Docker installed and running on your system
- Bash shell
- Access to the Quil client source code
- Sufficient disk space for building the client (approximately 2GB recommended)
## Build Environment
The test runner uses a multi-stage build process based on `Dockerfile.qclient` which includes:
### Base Build Environment
- Ubuntu 24.04 as the base image
- Essential build tools (gcc, g++, make, etc.)
- GMP 6.2 and MPFR libraries
- Go 1.22.0 (amd64)
- Rust toolchain (via rustup)
- FLINT library (version 3.0)
- uniffi-bindgen-go (v0.2.1+v0.25.0)
### Build Process
1. Generates Rust bindings for:
- VDF (Verifiable Delay Function)
- BLS48581 (Boneh-Lynn-Shacham signature scheme)
- VerEnc (Verifiable Encryption)
2. Builds and installs:
- qclient binary
## Usage
### Basic Usage
To run tests on all supported distributions (Ubuntu 22.04, Ubuntu 24.04, and Debian 12):
```bash
./run_tests.sh
```
### Custom Test Configuration
To run tests on a specific distribution and version:
```bash
./run_tests.sh -d DISTRO -v VERSION [-t TAG]
```
#### Parameters:
- `-d, --distro`: The Linux distribution to test (e.g., ubuntu, debian)
- `-v, --version`: The version of the distribution (e.g., 22.04, 12)
- `-t, --tag`: (Optional) Custom tag for the test container. If not provided, a tag will be automatically generated
#### Examples:
```bash
# Test Ubuntu 22.04 with auto-generated tag
./run_tests.sh -d ubuntu -v 22.04
# Test Debian 12 with custom tag
./run_tests.sh -d debian -v 12 -t my-custom-test
# Show help message
./run_tests.sh --help
```
## Supported Distributions
By default, the script tests the following distributions:
- Ubuntu 22.04
- Ubuntu 24.04
- Debian 12
## How It Works
1. The script first builds the client binary using `Dockerfile.qclient`:
- Creates a build container with all required dependencies
- Generates necessary Rust bindings
- Builds the qclient binary
- Extracts the binary to the test directory
- Cleans up the build container
2. For each test run:
- Creates a Docker container using the specified distribution and version
- Copies the built client binary into the test container
- Builds the test environment
- Runs the tests
- Cleans up the container after completion
## Error Handling
- The script uses `set -e` to exit on any error
- If any test fails, the script will exit with status code 1
- Docker containers are automatically removed after test completion using the `--rm` flag
- Build errors in `Dockerfile.qclient` will be clearly displayed in the output
## Notes
- When running all tests simultaneously, the script uses background processes to parallelize the test runs
- The script automatically generates container tags if not specified, using the format `distroversion` (e.g., `ubuntu2204`)
- Make sure you have sufficient system resources when running multiple tests simultaneously
- The build process requires significant disk space due to the multi-stage build and dependencies
- The client binary is built specifically for amd64 architecture
## Troubleshooting
If you encounter issues:
1. Ensure Docker is running:
```bash
systemctl status docker
```
2. Check Docker logs for container-specific issues:
```bash
docker logs quil-test-[tag]
```
3. Verify you have sufficient disk space and memory for running multiple containers
4. For build-related issues:
- Check if all required dependencies are available in the target distribution
- Verify the build environment has sufficient resources
- Check the build logs for specific error messages
- Ensure you're running on an amd64 system or using appropriate Docker platform settings
## Direct Docker Build
If you just want to build the qclient in a Docker container without running the tests (useful if you can't build for the target testing environment):
```bash
# in the project root directory
## Will take awhile to build flint on initial build
docker build -f client/test/Dockerfile.qclient -t qclient .
```
This command builds the Docker image with the qclient binary according to the specifications in `Dockerfile.qclient`. The resulting image will be tagged as `qclient`.
## Contributing
When adding new distributions or versions:
1. Update the default test configurations in the script
2. Ensure the corresponding Dockerfile supports the new distribution/version
3. Test the changes thoroughly before committing
4. Verify that all dependencies in `Dockerfile.qclient` are available in the target distribution

97
client/test/run_tests.sh Executable file
View File

@ -0,0 +1,97 @@
#!/bin/bash
set -e
# Help function
show_help() {
echo "Usage: $0 [OPTIONS]"
echo "Run tests on specified Linux distributions"
echo ""
echo "Options:"
echo " -d, --distro DISTRO Specify the distribution (e.g., ubuntu, debian)"
echo " -v, --version VERSION Specify the version (e.g., 22.04, 12)"
echo " -t, --tag TAG Specify a custom tag for the test container"
echo " -h, --help Show this help message"
echo ""
echo "If no arguments are provided, runs tests on all supported distributions"
exit 0
}
# Parse command line arguments
DISTRO=""
VERSION=""
TAG=""
while [[ $# -gt 0 ]]; do
case $1 in
-d|--distro)
DISTRO="$2"
shift 2
;;
-v|--version)
VERSION="$2"
shift 2
;;
-t|--tag)
TAG="$2"
shift 2
;;
-h|--help)
show_help
;;
*)
echo "Unknown option: $1"
show_help
;;
esac
done
# Build the client binary using Dockerfile.qclient
echo "Building client binary using Dockerfile.qclient..."
docker build -t quil-qclient-builder -f Dockerfile.qclient ..
docker create --name quil-qclient-temp quil-qclient-builder
docker cp quil-qclient-temp:/usr/local/bin/qclient ./qclient
docker rm quil-qclient-temp
# Function to run tests for a specific distribution
run_distro_test() {
local distro=$1
local version=$2
local tag=$3
echo "Testing on $distro $version..."
docker build \
--build-arg DISTRO=$distro \
--build-arg VERSION=$version \
-t quil-test-$tag \
-f Dockerfile .
docker run --rm quil-test-$tag
}
# If custom distro/version/tag is provided, run single test
if [ ! -z "$DISTRO" ] && [ ! -z "$VERSION" ]; then
if [ -z "$TAG" ]; then
TAG="${DISTRO}${VERSION//./}"
fi
echo "Running custom test configuration..."
run_distro_test "$DISTRO" "$VERSION" "$TAG"
else
# Run tests on all distributions simultaneously
echo "Running tests on all distributions simultaneously..."
run_distro_test "ubuntu" "22.04" "ubuntu22" &
UBUNTU22_PID=$!
run_distro_test "ubuntu" "24.04" "ubuntu24" &
UBUNTU24_PID=$!
run_distro_test "debian" "12" "debian12" &
DEBIAN12_PID=$!
# Wait for all tests to complete
wait $UBUNTU22_PID $UBUNTU24_PID $DEBIAN12_PID
# Check exit status of each test
if [ $? -ne 0 ]; then
echo "One or more tests failed!"
exit 1
fi
fi
echo "All distribution tests completed!"

115
client/test/test_install.sh Normal file
View File

@ -0,0 +1,115 @@
#!/bin/bash
set -e
# Get distribution information
DISTRO=$(lsb_release -si 2>/dev/null || echo "Unknown")
VERSION=$(lsb_release -sr 2>/dev/null || echo "Unknown")
echo "Starting Quilibrium node installation test on $DISTRO $VERSION..."
# Test 1: Install latest version
echo "Test 1: Installing latest version..."
qclient node install
get_latest_version() {
# Fetch the latest version from the releases API
local latest_version=$(curl -s https://releases.quilibrium.com/release | head -n 1 | cut -d'-' -f2)
echo "$latest_version"
}
LATEST_VERSION=$(get_latest_version)
# Verify installation
echo "Verifying installation..."
if [ ! -f "/opt/quilibrium/$LATEST_VERSION/node-$LATEST_VERSION-linux-amd64" ]; then
echo "Error: Latest version binary not found"
exit 1
fi
# Verify latest version matches
echo "Verifying latest version matches..."
get_latest_version
# Test 2: Install specific version
echo "Test 2: Installing specific version..."
qclient node install "2.0.6.2"
# Verify specific version installation
echo "Verifying specific version installation..."
if [ ! -f "/opt/quilibrium/2.0.6.2/node-2.0.6.2-linux-amd64" ]; then
echo "Error: Specific version binary not found"
exit 1
fi
# Test 3: Verify service file creation
echo "Test 3: Verifying service file creation..."
if [ ! -f "/etc/systemd/system/quilibrium-node.service" ]; then
echo "Error: Service file not found"
exit 1
fi
# Verify service file content
echo "Verifying service file content..."
if ! grep -q "EnvironmentFile=/etc/default/quilibrium-node" /etc/systemd/system/quilibrium-node.service; then
echo "Error: Service file missing EnvironmentFile directive"
exit 1
fi
# Test 4: Verify environment file
echo "Test 4: Verifying environment file..."
if [ ! -f "/etc/default/quilibrium-node" ]; then
echo "Error: Environment file not found"
exit 1
fi
# Verify environment file permissions
echo "Verifying environment file permissions..."
if [ "$(stat -c %a /etc/default/quilibrium-node)" != "640" ]; then
echo "Error: Environment file has incorrect permissions"
exit 1
fi
# Test 5: Verify data directory
echo "Test 5: Verifying data directory..."
if [ ! -d "/var/lib/quilibrium" ]; then
echo "Error: Data directory not found"
exit 1
fi
# Verify data directory permissions
echo "Verifying data directory permissions..."
if [ "$(stat -c %a /var/lib/quilibrium)" != "755" ]; then
echo "Error: Data directory has incorrect permissions"
exit 1
fi
# Test 6: Verify config file
echo "Test 6: Verifying config file..."
if [ ! -f "/var/lib/quilibrium/config/node.yaml" ]; then
echo "Error: Config file not found"
exit 1
fi
# Verify config file permissions
echo "Verifying config file permissions..."
if [ "$(stat -c %a /var/lib/quilibrium/config/node.yaml)" != "644" ]; then
echo "Error: Config file has incorrect permissions"
exit 1
fi
# Test 7: Verify binary symlink
echo "Test 7: Verifying binary symlink..."
if [ ! -L "/usr/local/bin/quilibrium-node" ]; then
echo "Error: Binary symlink not found"
exit 1
fi
# Test 8: Verify binary execution
echo "Test 8: Verifying binary execution..."
if ! quilibrium-node --version > /dev/null 2>&1; then
echo "Error: Binary execution failed"
exit 1
fi
echo "All tests passed successfully on $DISTRO $VERSION!"

168
client/utils/download.go Normal file
View File

@ -0,0 +1,168 @@
package utils
import (
"bufio"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
var BaseReleaseURL = "https://releases.quilibrium.com"
// DownloadRelease downloads a specific release file
func DownloadRelease(releaseType ReleaseType, version string) error {
fileName := fmt.Sprintf("%s-%s-%s-%s", releaseType, version, osType, arch)
return DownloadReleaseFile(releaseType, fileName, version, true)
}
// GetLatestVersion fetches the latest version from the releases API
func GetLatestVersion(releaseType ReleaseType) (string, error) {
// Determine the appropriate URL based on the release type
releaseURL := fmt.Sprintf("%s/release", BaseReleaseURL)
if releaseType == ReleaseTypeQClient {
releaseURL = fmt.Sprintf("%s/qclient-release", BaseReleaseURL)
}
resp, err := http.Get(releaseURL)
if err != nil {
return "", fmt.Errorf("failed to fetch latest version: %w", err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
if !scanner.Scan() {
return "", fmt.Errorf("no response data found")
}
// Get the first line which contains the filename
filename := scanner.Text()
// Split the filename by "-" and get the version part
parts := strings.Split(filename, "-")
if len(parts) < 2 {
return "", fmt.Errorf("invalid filename format: %s", filename)
}
// The version is the second part (index 1)
version := parts[1]
return version, nil
}
// DownloadReleaseFile downloads a release file from the Quilibrium releases server
func DownloadReleaseFile(releaseType ReleaseType, fileName string, version string, showError bool) error {
url := fmt.Sprintf("%s/%s", BaseReleaseURL, fileName)
destPath := filepath.Join(DataPath, string(releaseType), version, fileName)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if showError {
return fmt.Errorf("failed to download file: %s", resp.Status)
} else {
return nil
}
}
out, err := os.Create(destPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "Downloaded %s to %s\n", fileName, destPath)
return nil
}
// DownloadReleaseSignatures downloads signature files for a release
func DownloadReleaseSignatures(releaseType ReleaseType, version string) error {
var files []string
baseName := fmt.Sprintf("%s-%s-%s-%s", releaseType, version, osType, arch)
// Add digest file URL
files = append(files, baseName+".dgst")
// Add signature file URLs
signerNums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}
for _, num := range signerNums {
files = append(files, fmt.Sprintf("%s.dgst.sig.%d", baseName, num))
}
for _, file := range files {
err := DownloadReleaseFile(releaseType, file, version, false)
if err != nil {
return err
}
}
return nil
}
// GetLatestReleaseFiles fetches the list of available release files
func GetLatestReleaseFiles(releaseType ReleaseType) ([]string, error) {
releaseURL := fmt.Sprintf("%s/release", BaseReleaseURL)
if releaseType == ReleaseTypeQClient {
releaseURL = fmt.Sprintf("%s/qclient-release", BaseReleaseURL)
}
resp, err := http.Get(releaseURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch releases: %s", resp.Status)
}
// Read the response body and parse it
var releases []string
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
releases = append(releases, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading response: %w", err)
}
return releases, nil
}
// FilterReleasesByOSArch filters releases by OS and architecture
func FilterReleasesByOSArch(releases []string, osType, arch string) []string {
var filtered []string
for _, release := range releases {
if strings.Contains(release, osType) && strings.Contains(release, arch) {
filtered = append(filtered, release)
}
}
return filtered
}
// ExtractVersionFromFileName extracts the version from a release filename
func ExtractVersionFromFileName(releaseType ReleaseType, fileName, osType, arch string) string {
version := strings.TrimPrefix(fileName, string(releaseType)+"-")
version = strings.TrimSuffix(version, "-"+osType+"-"+arch)
return version
}
// DownloadAllReleaseFiles downloads all release files
func DownloadAllReleaseFiles(releaseType ReleaseType, fileNames []string, installDir string) bool {
for _, fileName := range fileNames {
filePath := filepath.Join(installDir, fileName)
if err := DownloadReleaseFile(releaseType, fileName, filePath, true); err != nil {
fmt.Fprintf(os.Stderr, "Error downloading release file %s: %v\n", fileName, err)
return false
}
}
return true
}

238
client/utils/fileUtils.go Normal file
View File

@ -0,0 +1,238 @@
package utils
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
)
// DefaultNodeUser is the default user name for node operations
var DefaultNodeUser = "quilibrium"
var ClientConfigDir = filepath.Join("/etc/quilibrium/", "config")
var ClientConfigPath = filepath.Join(ClientConfigDir, "client.yaml")
var ClientInstallPath = filepath.Join("/opt/quilibrium/", "client")
var DataPath = filepath.Join("/var/quilibrium/", "data")
var ClientDataPath = filepath.Join(DataPath, "client")
var NodeDataPath = filepath.Join(DataPath, "node")
var NodeDefaultSymlinkDir = "/usr/local/bin"
var DefaultNodeSymlinkPath = filepath.Join(NodeDefaultSymlinkDir, "quilibrium-node")
var DefaultQClientSymlinkPath = filepath.Join(NodeDefaultSymlinkDir, "qclient")
var osType = runtime.GOOS
var arch = runtime.GOARCH
// CalculateFileHashes calculates SHA256 and MD5 hashes for a file
func CalculateFileHashes(filePath string) (string, string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", "", fmt.Errorf("error opening file: %w", err)
}
defer file.Close()
// Calculate SHA256
sha256Hash := sha256.New()
if _, err := io.Copy(sha256Hash, file); err != nil {
return "", "", fmt.Errorf("error calculating SHA256: %w", err)
}
// Reset file position to beginning for MD5 calculation
if _, err := file.Seek(0, 0); err != nil {
return "", "", fmt.Errorf("error seeking file: %w", err)
}
// Calculate MD5
md5Hash := md5.New()
if _, err := io.Copy(md5Hash, file); err != nil {
return "", "", fmt.Errorf("error calculating MD5: %w", err)
}
return hex.EncodeToString(sha256Hash.Sum(nil)), hex.EncodeToString(md5Hash.Sum(nil)), nil
}
// CreateSymlink creates a symlink, handling the case where it already exists
func CreateSymlink(execPath, targetPath string) error {
// Check if the symlink already exists
if _, err := os.Lstat(targetPath); err == nil {
// Symlink exists, ask if user wants to overwrite
if !ConfirmSymlinkOverwrite(targetPath) {
fmt.Println("Operation cancelled.")
return nil
}
// Remove existing symlink
if err := os.Remove(targetPath); err != nil {
return fmt.Errorf("failed to remove existing symlink: %w", err)
}
}
// Create the symlink
if err := os.Symlink(execPath, targetPath); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
return nil
}
// ReadClientConfig reads the client configuration from the specified config directory
// If the config file doesn't exist, it returns an empty config
func ReadClientConfig() (*ClientConfig, error) {
// Check if config file exists
if !FileExists(ClientConfigPath) {
// Return empty config if file doesn't exist
return &ClientConfig{}, nil
}
// Read the config file
data, err := os.ReadFile(ClientConfigPath)
if err != nil {
return nil, fmt.Errorf("error reading config file: %w", err)
}
// Parse YAML
var config ClientConfig
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("error parsing config file: %w", err)
}
return &config, nil
}
// UpdateClientConfig updates the client configuration in the specified config directory
// If the config file doesn't exist, it creates a new one
func UpdateClientConfig(config *ClientConfig) error {
configDir := ClientConfigDir
// Check if we need sudo privileges (if config directory is in a system directory)
if err := CheckAndRequestSudo(fmt.Sprintf("Updating config directory at %s requires root privileges", configDir)); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
// Create config directory if it doesn't exist
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
configPath := GetConfigPath(configDir)
// Marshal config to YAML
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("error serializing config: %w", err)
}
// Write config to file
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("error writing config file: %w", err)
}
// Set ownership if a dedicated user was created
if DefaultNodeUser != "" {
// Check for sudo privileges for changing ownership
if err := CheckAndRequestSudo(fmt.Sprintf("Changing ownership of %s requires root privileges", configPath)); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
chownCmd := exec.Command("chown", DefaultNodeUser+":"+DefaultNodeUser, configPath)
if err := chownCmd.Run(); err != nil {
return fmt.Errorf("failed to change ownership of config file: %w", err)
}
}
return nil
}
// CreateConfigFile creates a basic configuration file for the node
func CreateConfigFile(configDir, dataDir, version string) {
// Create a ClientConfig struct
config := ClientConfig{
Version: version,
DataDir: ClientDataPath,
SymlinkPath: DefaultQClientSymlinkPath,
}
// Use UpdateClientConfig to save the configuration
if err := UpdateClientConfig(&config); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to create config file: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Created configuration file at %s/config.yaml\n", configDir)
}
// ValidateAndCreateDir validates a directory path and creates it if it doesn't exist
func ValidateAndCreateDir(path string) error {
// Check if the directory exists
info, err := os.Stat(path)
if err == nil {
// Path exists, check if it's a directory
if !info.IsDir() {
return fmt.Errorf("%s exists but is not a directory", path)
}
return nil
}
// Directory doesn't exist, try to create it
if os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", path, err)
}
return nil
}
// Some other error occurred
return fmt.Errorf("error checking directory %s: %v", path, err)
}
// IsWritable checks if a directory is writable
func IsWritable(dir string) bool {
// Check if directory exists
info, err := os.Stat(dir)
if err != nil || !info.IsDir() {
return false
}
// Check if directory is writable by creating a temporary file
tempFile := filepath.Join(dir, ".quilibrium_write_test")
file, err := os.Create(tempFile)
if err != nil {
return false
}
file.Close()
os.Remove(tempFile)
return true
}
// CanCreateAndWrite checks if we can create and write to a directory
func CanCreateAndWrite(dir string) bool {
// Try to create the directory
if err := os.MkdirAll(dir, 0755); err != nil {
return false
}
// Check if we can write to it
return IsWritable(dir)
}
// FileExists checks if a file exists
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// GetConfigPath returns the path to the client configuration file
func GetConfigPath(configDir string) string {
return filepath.Join(configDir, "config.yaml")
}
// IsClientConfigured checks if the client is configured
func IsClientConfigured(configDir string) bool {
configPath := GetConfigPath(configDir)
return FileExists(configPath)
}

28
client/utils/system.go Normal file
View File

@ -0,0 +1,28 @@
package utils
import (
"fmt"
"runtime"
)
// GetSystemInfo determines and validates the OS and architecture
func GetSystemInfo() (string, string, error) {
osType := runtime.GOOS
arch := runtime.GOARCH
// Check if OS type is supported
if osType != "darwin" && osType != "linux" {
return "", "", fmt.Errorf("unsupported operating system: %s", osType)
}
// Map Go architecture names to Quilibrium architecture names
if arch == "amd64" {
arch = "amd64"
} else if arch == "arm64" {
arch = "arm64"
} else {
return "", "", fmt.Errorf("unsupported architecture: %s", arch)
}
return osType, arch, nil
}

20
client/utils/types.go Normal file
View File

@ -0,0 +1,20 @@
package utils
type ClientConfig struct {
Version string
DataDir string
SymlinkPath string
}
type NodeConfig struct {
ClientConfig
DataDir string
User string
}
type ReleaseType string
const (
ReleaseTypeQClient ReleaseType = "qclient"
ReleaseTypeNode ReleaseType = "node"
)

View File

@ -0,0 +1,37 @@
package utils
import (
"fmt"
"os"
"os/exec"
"strings"
)
// ConfirmSymlinkOverwrite asks the user to confirm overwriting an existing symlink
func ConfirmSymlinkOverwrite(path string) bool {
fmt.Printf("Symlink already exists at %s. Overwrite? [y/N]: ", path)
var response string
fmt.Scanln(&response)
return strings.ToLower(response) == "y"
}
// CheckAndRequestSudo checks if we have sudo privileges and requests them if needed
func CheckAndRequestSudo(reason string) error {
// Check if we're already root
if os.Geteuid() == 0 {
return nil
}
// Check if sudo is available
if _, err := exec.LookPath("sudo"); err != nil {
return fmt.Errorf("sudo is not available: %w", err)
}
// Request sudo privileges
cmd := exec.Command("sudo", "-v")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to get sudo privileges: %w", err)
}
return nil
}

View File

@ -0,0 +1,8 @@
#include <verenc.h>
// This file exists beacause of
// https://github.com/golang/go/issues/11263
void cgo_rust_task_callback_bridge_verenc(RustTaskCallback cb, const void * taskData, int8_t status) {
cb(taskData, status);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,439 @@
// This file was autogenerated by some hot garbage in the `uniffi` crate.
// Trust me, you don't want to mess with it!
#include <stdbool.h>
#include <stdint.h>
// The following structs are used to implement the lowest level
// of the FFI, and thus useful to multiple uniffied crates.
// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H.
#ifdef UNIFFI_SHARED_H
// We also try to prevent mixing versions of shared uniffi header structs.
// If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V6
#ifndef UNIFFI_SHARED_HEADER_V6
#error Combining helper code from multiple versions of uniffi is not supported
#endif // ndef UNIFFI_SHARED_HEADER_V6
#else
#define UNIFFI_SHARED_H
#define UNIFFI_SHARED_HEADER_V6
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V6 in this file. ⚠️
typedef struct RustBuffer {
int32_t capacity;
int32_t len;
uint8_t *data;
} RustBuffer;
typedef int32_t (*ForeignCallback)(uint64_t, int32_t, uint8_t *, int32_t, RustBuffer *);
// Task defined in Rust that Go executes
typedef void (*RustTaskCallback)(const void *, int8_t);
// Callback to execute Rust tasks using a Go routine
//
// Args:
// executor: ForeignExecutor lowered into a uint64_t value
// delay: Delay in MS
// task: RustTaskCallback to call
// task_data: data to pass the task callback
typedef int8_t (*ForeignExecutorCallback)(uint64_t, uint32_t, RustTaskCallback, void *);
typedef struct ForeignBytes {
int32_t len;
const uint8_t *data;
} ForeignBytes;
// Error definitions
typedef struct RustCallStatus {
int8_t code;
RustBuffer errorBuf;
} RustCallStatus;
// Continuation callback for UniFFI Futures
typedef void (*RustFutureContinuation)(void * , int8_t);
// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️
// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V6 in this file. ⚠️
#endif // def UNIFFI_SHARED_H
// Needed because we can't execute the callback directly from go.
void cgo_rust_task_callback_bridge_verenc(RustTaskCallback, const void *, int8_t);
int8_t uniffiForeignExecutorCallbackverenc(uint64_t, uint32_t, RustTaskCallback, void*);
void uniffiFutureContinuationCallbackverenc(void*, int8_t);
RustBuffer uniffi_verenc_fn_func_chunk_data_for_verenc(
RustBuffer data,
RustCallStatus* out_status
);
RustBuffer uniffi_verenc_fn_func_combine_chunked_data(
RustBuffer chunks,
RustCallStatus* out_status
);
RustBuffer uniffi_verenc_fn_func_new_verenc_proof(
RustBuffer data,
RustCallStatus* out_status
);
RustBuffer uniffi_verenc_fn_func_new_verenc_proof_encrypt_only(
RustBuffer data,
RustBuffer encryption_key_bytes,
RustCallStatus* out_status
);
RustBuffer uniffi_verenc_fn_func_verenc_compress(
RustBuffer proof,
RustCallStatus* out_status
);
RustBuffer uniffi_verenc_fn_func_verenc_recover(
RustBuffer recovery,
RustCallStatus* out_status
);
int8_t uniffi_verenc_fn_func_verenc_verify(
RustBuffer proof,
RustCallStatus* out_status
);
RustBuffer ffi_verenc_rustbuffer_alloc(
int32_t size,
RustCallStatus* out_status
);
RustBuffer ffi_verenc_rustbuffer_from_bytes(
ForeignBytes bytes,
RustCallStatus* out_status
);
void ffi_verenc_rustbuffer_free(
RustBuffer buf,
RustCallStatus* out_status
);
RustBuffer ffi_verenc_rustbuffer_reserve(
RustBuffer buf,
int32_t additional,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_continuation_callback_set(
RustFutureContinuation callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_u8(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_u8(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_u8(
void* handle,
RustCallStatus* out_status
);
uint8_t ffi_verenc_rust_future_complete_u8(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_i8(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_i8(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_i8(
void* handle,
RustCallStatus* out_status
);
int8_t ffi_verenc_rust_future_complete_i8(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_u16(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_u16(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_u16(
void* handle,
RustCallStatus* out_status
);
uint16_t ffi_verenc_rust_future_complete_u16(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_i16(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_i16(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_i16(
void* handle,
RustCallStatus* out_status
);
int16_t ffi_verenc_rust_future_complete_i16(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_u32(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_u32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_u32(
void* handle,
RustCallStatus* out_status
);
uint32_t ffi_verenc_rust_future_complete_u32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_i32(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_i32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_i32(
void* handle,
RustCallStatus* out_status
);
int32_t ffi_verenc_rust_future_complete_i32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_u64(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_u64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_u64(
void* handle,
RustCallStatus* out_status
);
uint64_t ffi_verenc_rust_future_complete_u64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_i64(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_i64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_i64(
void* handle,
RustCallStatus* out_status
);
int64_t ffi_verenc_rust_future_complete_i64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_f32(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_f32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_f32(
void* handle,
RustCallStatus* out_status
);
float ffi_verenc_rust_future_complete_f32(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_f64(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_f64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_f64(
void* handle,
RustCallStatus* out_status
);
double ffi_verenc_rust_future_complete_f64(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_pointer(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_pointer(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_pointer(
void* handle,
RustCallStatus* out_status
);
void* ffi_verenc_rust_future_complete_pointer(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_rust_buffer(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_rust_buffer(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_rust_buffer(
void* handle,
RustCallStatus* out_status
);
RustBuffer ffi_verenc_rust_future_complete_rust_buffer(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_poll_void(
void* handle,
void* uniffi_callback,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_cancel_void(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_free_void(
void* handle,
RustCallStatus* out_status
);
void ffi_verenc_rust_future_complete_void(
void* handle,
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_chunk_data_for_verenc(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_combine_chunked_data(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_new_verenc_proof(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_new_verenc_proof_encrypt_only(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_verenc_compress(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_verenc_recover(
RustCallStatus* out_status
);
uint16_t uniffi_verenc_checksum_func_verenc_verify(
RustCallStatus* out_status
);
uint32_t ffi_verenc_uniffi_contract_version(
RustCallStatus* out_status
);