Feat/2.1 qclient refactor and node install (#429)

* initial auto-update

* working link, update, and testing docker container and scripts

* refactor packages/folders

* move files to proper folders

* fix typos

Closes #421

* optimize rpm imports

* optimize channel imports

* Refactor split command to allow testing of split operations

Closes #338

* modify split and test for folder changes

* remove alias

* fix docker warning about FROM and AS being in different letter case

Closes #422

* QClient Account Command

* Display transaction details and confirmation prompts for transfer and merge commands

* build qclient docker improvements

* update build args for mpfr.so.6

* update install and node commands

* remove NodeConfig check for qclient node commands

* udpate

* working node commands

* update commands

* move utils and rename package

---------

Co-authored-by: Vasyl Tretiakov <vasyl.tretiakov@gmail.com>
Co-authored-by: littleblackcloud <163544315+littleblackcloud@users.noreply.github.com>
Co-authored-by: 0xOzgur <29779769+0xOzgur@users.noreply.github.com>
Co-authored-by: Cassandra Heart <7929478+CassOnMars@users.noreply.github.com>
This commit is contained in:
Tyler Sturos 2025-04-11 18:43:20 -08:00 committed by GitHub
parent 7952964d4e
commit 9cfbdef12c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 6345 additions and 500 deletions

View File

@ -1,4 +1,4 @@
FROM golang:1.20.14-alpine3.19 as build
FROM golang:1.20.14-alpine3.19 AS build
ARG NODE_VERSION
ARG MAX_KEY_ID

View File

@ -1,9 +1,16 @@
FROM ubuntu:24.04 as build-base
# syntax=docker.io/docker/dockerfile:1.7-labs
FROM --platform=${BUILDPLATFORM} ubuntu:24.04 AS base
ENV PATH="${PATH}:/root/.cargo/bin/"
ARG TARGETOS
ARG TARGETARCH
# Install GMP 6.2 (6.3 which MacOS is using only available on Debian unstable)
RUN apt-get update && apt-get install -y \
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
build-essential \
curl \
git \
@ -29,24 +36,30 @@ RUN wget https://raw.githubusercontent.com/emp-toolkit/emp-readme/master/scripts
RUN python install.py --install --tool --ot
RUN cd emp-tool && sed -i 's/add_library(${NAME} SHARED ${sources})/add_library(${NAME} STATIC ${sources})/g' CMakeLists.txt && mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local && cd .. && make && make install && cd ..
RUN cd emp-ot && mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local && cd .. && make && make install && cd ..
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
cd emp-tool && sed -i 's/add_library(${NAME} SHARED ${sources})/add_library(${NAME} STATIC ${sources})/g' CMakeLists.txt && mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local && cd .. && make && make install && cd ..
RUN apt update && apt install -y wget && \
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
cd emp-ot && mkdir build && cd build && cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local && cd .. && make && make install && cd ..
ARG GO_VERSION=1.23.5
RUN --mount=type=cache,target=/usr/local,id=usr-local-${TARGETOS}-${TARGETARCH} \
apt update && apt install -y wget && \
ARCH=$(dpkg --print-architecture) && \
case ${ARCH} in \
amd64) GOARCH=amd64 ;; \
arm64) GOARCH=arm64 ;; \
*) echo "Unsupported architecture: ${ARCH}" && exit 1 ;; \
esac && \
wget https://go.dev/dl/go1.23.5.linux-${GOARCH}.tar.gz && \
wget https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz && \
rm -rf /usr/local/go && \
tar -C /usr/local -xzf go1.23.5.linux-${GOARCH}.tar.gz && \
rm go1.23.5.linux-${GOARCH}.tar.gz
tar -C /usr/local -xzf go${GO_VERSION}.linux-${GOARCH}.tar.gz && \
rm go${GO_VERSION}.linux-${GOARCH}.tar.gz
ENV PATH=$PATH:/usr/local/go/bin
RUN git clone https://github.com/flintlib/flint.git && \
RUN --mount=type=cache,target=/usr/local,id=usr-local-${TARGETOS}-${TARGETARCH} \
git clone https://github.com/flintlib/flint.git && \
cd flint && \
git checkout flint-3.0 && \
./bootstrap.sh && \
@ -64,65 +77,197 @@ RUN git clone https://github.com/flintlib/flint.git && \
COPY docker/rustup-init.sh /opt/rustup-init.sh
RUN /opt/rustup-init.sh -y --profile minimal
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
/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
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
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
FROM base AS build
ENV GOEXPERIMENT=arenas
ENV QUILIBRIUM_SIGNATURE_CHECK=false
# Install grpcurl before building the node and client
# as to avoid needing to redo it on rebuilds
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
WORKDIR /opt/ceremonyclient
COPY . .
# Copy everything except node and client so as to avoid
# invalidating the cache at this point on client or node rebuilds
WORKDIR /opt/ceremonyclient/ferret
RUN ./generate.sh
COPY --exclude=node \
--exclude=client \
--exclude=sidecar . .
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
## Generate Rust bindings for VDF
WORKDIR /opt/ceremonyclient/vdf
RUN ./generate.sh
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
./generate.sh
## Generate Rust bindings for Ferret
WORKDIR /opt/ceremonyclient/ferret
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/bin,id=go-bin-${TARGETOS}-${TARGETARCH} \
go mod download
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/bin,id=go-bin-${TARGETOS}-${TARGETARCH} \
./generate.sh
## Generate Rust bindings for BLS48581
WORKDIR /opt/ceremonyclient/bls48581
RUN ./generate.sh
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
go mod download
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
./generate.sh
## Generate Rust bindings for VerEnc
WORKDIR /opt/ceremonyclient/verenc
RUN ./generate.sh
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
RUN --mount=type=cache,target=/root/.cargo,id=cargo-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target/,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
./generate.sh
FROM build AS build-node
# Build and install the node
COPY ./node /opt/ceremonyclient/node
WORKDIR /opt/ceremonyclient/node
RUN ./build.sh && cp node /usr/bin
RUN go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
./build.sh && cp node /usr/bin
FROM build AS build-qclient
ARG TARGETOS
ARG TARGETARCH
# Build and install qclient
COPY ./node /opt/ceremonyclient/node
WORKDIR /opt/ceremonyclient/node
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
COPY ./client /opt/ceremonyclient/client
WORKDIR /opt/ceremonyclient/client
RUN ./build.sh -o qclient && cp qclient /usr/bin
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
ARG BINARIES_DIR=/opt/ceremonyclient/target/release
ENV CGO_ENABLED=1
ENV CGO_LDFLAGS="-L/usr/local/lib -lflint -lgmp -lmpfr -ldl -lm -L$BINARIES_DIR -lvdf -lverenc -lbls48581 -static"
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target,id=target-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags "-linkmode 'external'" -o qclient
RUN cp qclient /usr/bin
FROM build AS build-sidecar
# Build and install sidecar
COPY ./node /opt/ceremonyclient/node
WORKDIR /opt/ceremonyclient/node
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
COPY ./sidecar /opt/ceremonyclient/sidecar
WORKDIR /opt/ceremonyclient/sidecar
RUN ./build.sh -o sidecar && cp sidecar /usr/bin
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/root/.cache/go-build,id=go-build-${TARGETOS}-${TARGETARCH} \
go mod download
RUN --mount=type=cache,target=/usr/local/,id=usr-local-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/go/pkg/mod,id=go-mod-${TARGETOS}-${TARGETARCH} \
--mount=type=cache,target=/opt/ceremonyclient/target,id=target-${TARGETOS}-${TARGETARCH} \
./build.sh -o sidecar && cp sidecar /usr/bin
# Allows exporting single binary
FROM scratch as qclient
COPY --from=build /usr/bin/qclient /qclient
ENTRYPOINT [ "/qclient" ]
# Allows exporting single binary
FROM scratch as sidecar
COPY --from=build /usr/bin/sidecar /sidecar
FROM scratch AS sidecar
COPY --from=build-sidecar /usr/bin/sidecar /sidecar
ENTRYPOINT [ "/sidecar" ]
# Allows exporting single binary
FROM scratch AS node
COPY --from=build /usr/bin/node /node
COPY --from=build-node /usr/bin/node /node
ENTRYPOINT [ "/node" ]
# Allows exporting single binary
FROM scratch AS qclient-unix
COPY --from=build-qclient /usr/bin/qclient /qclient
ENTRYPOINT [ "/qclient" ]
FROM qclient-unix AS qclient-linux
FROM qclient-unix AS qclient-darwin
FROM qclient-${TARGETOS} AS qclient
FROM ubuntu:24.04
RUN apt-get update && apt-get install libflint-dev -y
@ -146,8 +291,8 @@ LABEL org.opencontainers.image.revision=$GIT_COMMIT
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=build /usr/bin/node /usr/local/bin
COPY --from=build /opt/ceremonyclient/client/qclient /usr/local/bin
COPY --from=build-node /usr/bin/node /usr/local/bin
COPY --from=build-qclient /opt/ceremonyclient/client/qclient /usr/local/bin
WORKDIR /root

View File

@ -0,0 +1,12 @@
client/build/
target/
node/build/
node/.vscode/
.git
.gitignore
vdf/generated/
bls48581/generated/
ferret/generated/
sidecar/generated/
verenc/generated/
Dockerfile*

View File

@ -1,4 +1,4 @@
FROM ubuntu:24.04 as build-base
FROM ubuntu:24.04 AS build-base
ENV PATH="${PATH}:/root/.cargo/bin/"
@ -61,7 +61,7 @@ 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
FROM build-base AS build
ENV GOEXPERIMENT=arenas
ENV QUILIBRIUM_SIGNATURE_CHECK=false
@ -90,7 +90,7 @@ WORKDIR /opt/ceremonyclient/client
RUN ./build.sh -o qclient && cp qclient /usr/bin
# Allows exporting single binary
FROM scratch as qclient
FROM scratch AS qclient
COPY --from=build /usr/bin/qclient /qclient
ENTRYPOINT [ "/qclient" ]

View File

@ -85,7 +85,7 @@ tasks:
- docker build --platform linux/amd64 -f Dockerfile.sourceavx512 --output node/build/amd64_avx512_linux --target=node .
build_qclient_amd64_linux:
desc: Build the QClient node binary for AMD64 Linux. Outputs to node/build.
desc: Build the QClient node binary for AMD64 Linux. Outputs to client/build.
cmds:
- docker build --platform linux/amd64 -f Dockerfile.source --output client/build/amd64_linux --target=qclient .
@ -142,3 +142,8 @@ tasks:
cmds:
- docker push ${QUILIBRIUM_IMAGE_NAME:-quilibrium}:{{.VERSION}}
- docker push ${QUILIBRIUM_IMAGE_NAME:-quilibrium}:latest
test:qclient:
desc: Test the Quilibrium docker image.
cmds:
- client/test/run_tests.sh -d 'ubuntu' -v '24.04'

View File

@ -26,7 +26,8 @@ case "$os_type" in
fi
;;
"Linux")
go build -ldflags "-linkmode 'external' -extldflags '-L$BINARIES_DIR -Wl,-Bstatic -lvdf -lbls48581 -lverenc -Wl,-Bdynamic -lstdc++ -ldl -lm -lflint -lgmp -lmpfr'" "$@"
export CGO_LDFLAGS="-L/usr/local/lib -lflint -lgmp -lmpfr -ldl -lm -L$BINARIES_DIR -lvdf -lverenc -lbls48581 -static"
go build -ldflags "-linkmode 'external'" "$@"
;;
*)
echo "Unsupported platform"

View File

@ -1,12 +0,0 @@
package cmd
import "github.com/spf13/cobra"
var configCmd = &cobra.Command{
Use: "config",
Short: "Performs a configuration operation",
}
func init() {
rootCmd.AddCommand(configCmd)
}

View File

@ -0,0 +1,44 @@
package config
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var ConfigCmd = &cobra.Command{
Use: "config",
Short: "Performs a configuration operation",
}
var printConfigCmd = &cobra.Command{
Use: "print",
Short: "Print the current configuration",
Run: func(cmd *cobra.Command, args []string) {
config, err := utils.LoadClientConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading config: %v\n", err)
os.Exit(1)
}
// Print the config in a readable format
fmt.Printf("Data Directory: %s\n", config.DataDir)
fmt.Printf("Symlink Path: %s\n", config.SymlinkPath)
fmt.Printf("Signature Check: %v\n", config.SignatureCheck)
},
}
var createDefaultConfigCmd = &cobra.Command{
Use: "create-default",
Short: "Create a default configuration file",
Run: func(cmd *cobra.Command, args []string) {
utils.CreateDefaultConfig()
},
}
func init() {
ConfigCmd.AddCommand(printConfigCmd)
ConfigCmd.AddCommand(createDefaultConfigCmd)
}

View File

@ -0,0 +1,58 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var signatureCheckCmd = &cobra.Command{
Use: "signature-check [true|false]",
Short: "Set signature check setting",
Long: `Set the signature check setting in the client configuration.
When disabled, signature verification will be bypassed (not recommended for production use).
Use 'true' to enable or 'false' to disable. If no argument is provided, it toggles the current setting.`,
Run: func(cmd *cobra.Command, args []string) {
config, err := utils.LoadClientConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
if len(args) > 0 {
// Set the signature check based on the provided argument
value := strings.ToLower(args[0])
if value == "true" {
config.SignatureCheck = true
} else if value == "false" {
config.SignatureCheck = false
} else {
// If the value is not true or false, print error message and exit
fmt.Printf("Error: Invalid value '%s'. Please use 'true' or 'false'.\n", value)
os.Exit(1)
}
} else {
// Toggle the signature check setting if no arguments are provided
config.SignatureCheck = !config.SignatureCheck
}
// Save the updated config
if err := utils.SaveClientConfig(config); err != nil {
fmt.Printf("Error saving config: %v\n", err)
os.Exit(1)
}
status := "enabled"
if !config.SignatureCheck {
status = "disabled"
}
fmt.Printf("Signature check has been set to %s and will be persisted across future commands unless reset.\n", status)
},
}
func init() {
ConfigCmd.AddCommand(signatureCheckCmd)
}

View File

@ -0,0 +1 @@
package config

View File

@ -0,0 +1,51 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
var versionFlag string
var downloadSignaturesCmd = &cobra.Command{
Use: "download-signatures",
Short: "Download signature files for the current binary",
Long: `Download signature files for the current binary. This command will download
the digest file and all signature files needed for verification. If --version is specified,
it will download signatures for that version. Otherwise, it will download signatures for
the latest version.`,
Run: func(cmd *cobra.Command, args []string) {
var version string
if versionFlag != "" {
// Use specified version
version = versionFlag
} else {
// Get the current version
version = config.GetVersionString()
}
// Download signature files
if err := utils.DownloadReleaseSignatures(utils.ReleaseTypeQClient, version); err != nil {
fmt.Fprintf(os.Stderr, "Error downloading signature files: %v\n", err)
os.Exit(1)
}
fmt.Printf("Successfully downloaded signature files for version %s\n", version)
},
}
func init() {
downloadSignaturesCmd.Flags().StringVarP(
&versionFlag,
"version",
"v",
"",
"Version to download signatures for (defaults to latest version)",
)
rootCmd.AddCommand(downloadSignaturesCmd)
}

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

@ -0,0 +1,100 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var symlinkPath = "/usr/local/bin/qclient"
var linkCmd = &cobra.Command{
Use: "link",
Short: "Create a symlink to qclient in PATH",
Long: `Create a symlink to the qclient binary in the directory /usr/local/bin/.
Example: qclient link`,
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)
}
IsSudo := utils.IsSudo()
if IsSudo {
fmt.Println("Running as sudo, creating symlink at /usr/local/bin/qclient")
} else {
fmt.Println("Cannot create symlink at /usr/local/bin/qclient, please run this command with sudo")
os.Exit(1)
}
// Check if the current executable is in the expected location
expectedPrefix := utils.ClientDataPath
// Check if the current executable is in the expected location
if !strings.HasPrefix(execPath, expectedPrefix) {
fmt.Printf("Current executable is not in the expected location: %s\n", execPath)
fmt.Printf("Expected location should start with: %s\n", expectedPrefix)
// Ask user if they want to move it
fmt.Print("Would you like to move the executable to the standard location? (y/n): ")
var response string
fmt.Scanln(&response)
if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" {
if err := moveExecutableToStandardLocation(execPath); err != nil {
return fmt.Errorf("failed to move executable: %w", err)
}
// Update execPath to the new location
execPath, err = os.Executable()
if err != nil {
return fmt.Errorf("failed to get new executable path: %w", err)
}
} else {
fmt.Println("Continuing with current location...")
}
}
// Create the symlink (handles existing symlinks)
if err := utils.CreateSymlink(execPath, symlinkPath); err != nil {
return err
}
fmt.Printf("Symlink created at %s\n", symlinkPath)
return nil
},
}
func moveExecutableToStandardLocation(execPath string) error {
// Get the directory of the current executable
version, err := GetVersionInfo(false)
if err != nil {
return fmt.Errorf("failed to get version info: %w", err)
}
destDir := filepath.Join(utils.ClientDataPath, "bin", version.Version)
// Create the standard location directory if it doesn't exist
currentUser, err := utils.GetCurrentSudoUser()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
if err := utils.ValidateAndCreateDir(destDir, currentUser); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// Move the executable to the standard location
if err := os.Rename(execPath, filepath.Join(destDir, StandardizedQClientFileName)); err != nil {
return fmt.Errorf("failed to move executable: %w", err)
}
return nil
}
func init() {
rootCmd.AddCommand(linkCmd)
}

View File

@ -0,0 +1,296 @@
package node
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/cobra"
)
// autoUpdateCmd represents the command to setup automatic updates
var autoUpdateCmd = &cobra.Command{
Use: "auto-update [enable|disable|status]",
Short: "Setup automatic update checks",
Long: `Setup, remove, or check status of a cron job to automatically check for Quilibrium node updates every 10 minutes.
This command will create, remove, or check a cron entry that runs 'qclient node update' every 10 minutes
to check for and apply any available updates.
Example:
# Setup automatic update checks
qclient node auto-update enable
# Remove automatic update checks
qclient node auto-update disable
# Check if automatic update is enabled
qclient node auto-update status`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 || (args[0] != "enable" && args[0] != "disable" && args[0] != "status") {
fmt.Fprintf(os.Stderr, "Error: must specify 'enable', 'disable', or 'status'\n")
cmd.Help()
return
}
if args[0] == "enable" {
setupCronJob()
} else if args[0] == "disable" {
removeCronJob()
} else if args[0] == "status" {
checkAutoUpdateStatus()
}
},
}
func init() {
NodeCmd.AddCommand(autoUpdateCmd)
}
func setupCronJob() {
// Get full path to qclient executable
qclientPath, err := exec.LookPath("qclient")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: qclient executable not found in PATH: %v\n", err)
fmt.Fprintf(os.Stderr, "Please ensure qclient is properly installed and in your PATH (use 'sudo qclient link')\n")
return
}
// Absolute path for qclient
qclientAbsPath, err := filepath.Abs(qclientPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting absolute path for qclient: %v\n", err)
return
}
// OS-specific setup
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
setupUnixCron(qclientAbsPath)
} else {
fmt.Fprintf(os.Stderr, "Error: auto-update is only supported on macOS and Linux\n")
return
}
}
func removeCronJob() {
// OS-specific removal
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
removeUnixCron()
} else {
fmt.Fprintf(os.Stderr, "Error: auto-update is only supported on macOS and Linux\n")
return
}
}
func isCrontabInstalled() bool {
// Check if crontab is installed
_, err := exec.LookPath("crontab")
return err == nil
}
func installCrontab() {
fmt.Fprintf(os.Stdout, "Installing cron package...\n")
// Install crontab
updateCmd := exec.Command("sudo", "apt-get", "update")
updateCmd.Stdout = nil
updateCmd.Stderr = nil
if err := updateCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error updating package lists: %v\n", err)
return
}
installCmd := exec.Command("sudo", "apt-get", "install", "-y", "cron")
installCmd.Stdout = nil
installCmd.Stderr = nil
if err := installCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error installing cron package: %v\n", err)
return
}
// verify crontab is installed
if isCrontabInstalled() {
fmt.Fprintf(os.Stdout, "Cron package installed\n")
} else {
fmt.Fprintf(os.Stderr, "Error: cron package not installed\n")
os.Exit(1)
}
}
func setupUnixCron(qclientPath string) {
if !isCrontabInstalled() {
fmt.Fprintf(os.Stdout, "Crontab command not found, attempting to install cron package...\n")
installCrontab()
}
fmt.Fprintf(os.Stdout, "Setting up cron job...\n")
// Create cron expression: run every 10 minutes
cronExpression := fmt.Sprintf("*/10 * * * * %s node update > /dev/null 2>&1", qclientPath)
// Check existing crontab
checkCmd := exec.Command("crontab", "-l")
checkOutput, err := checkCmd.CombinedOutput()
var currentCrontab string
if err != nil {
// If there's no crontab yet, this is fine, start with empty crontab
currentCrontab = ""
} else {
currentCrontab = string(checkOutput)
}
// Check if our update command is already in crontab
if strings.Contains(currentCrontab, "### qclient-auto-update") {
fmt.Fprintf(os.Stdout, "Automatic update check is already configured in crontab\n")
return
}
// Add new cron entry with indicators
newCrontab := currentCrontab
if strings.TrimSpace(newCrontab) != "" && !strings.HasSuffix(newCrontab, "\n") {
newCrontab += "\n"
}
newCrontab += "### qclient-auto-update\n" +
cronExpression + "\n" +
"### end-qclient-auto-update\n"
// Write to temporary file
tempFile, err := os.CreateTemp("", "qclient-crontab")
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating temporary file: %v\n", err)
return
}
defer os.Remove(tempFile.Name())
if _, err := tempFile.WriteString(newCrontab); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to temporary file: %v\n", err)
return
}
tempFile.Close()
// Install new crontab
installCmd := exec.Command("crontab", tempFile.Name())
if err := installCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error installing crontab: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Successfully configured cron job to check for updates every 10 minutes\n")
fmt.Fprintf(os.Stdout, "Added: %s\n", cronExpression)
}
func removeUnixCron() {
if !isCrontabInstalled() {
fmt.Fprintf(os.Stderr, "Error: crontab command not found\n")
return
}
fmt.Fprintf(os.Stdout, "Removing auto-update cron job...\n")
// Check existing crontab
checkCmd := exec.Command("crontab", "-l")
checkOutput, err := checkCmd.CombinedOutput()
if err != nil {
fmt.Fprintf(os.Stderr, "Error checking existing crontab: %v\n", err)
return
}
currentCrontab := string(checkOutput)
// No crontab or doesn't contain our section
if currentCrontab == "" || !strings.Contains(currentCrontab, "### qclient-auto-update") {
fmt.Fprintf(os.Stdout, "No auto-update job found in crontab\n")
return
}
// Remove our section
startMarker := "### qclient-auto-update"
endMarker := "### end-qclient-auto-update"
startIdx := strings.Index(currentCrontab, startMarker)
endIdx := strings.Index(currentCrontab, endMarker)
var newCrontab string
if startIdx >= 0 && endIdx >= 0 {
endIdx += len(endMarker)
// Remove the section including markers
newCrontab = currentCrontab[:startIdx] + currentCrontab[endIdx:]
} else {
newCrontab = currentCrontab
}
// Clean up any leftover double newlines
newCrontab = strings.ReplaceAll(newCrontab, "\n\n\n", "\n\n")
if strings.TrimSpace(newCrontab) != "" && !strings.HasSuffix(newCrontab, "\n") {
newCrontab += "\n"
}
// Write to temporary file
tempFile, err := os.CreateTemp("", "qclient-crontab")
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating temporary file: %v\n", err)
return
}
defer os.Remove(tempFile.Name())
if _, err := tempFile.WriteString(newCrontab); err != nil {
fmt.Fprintf(os.Stderr, "Error writing to temporary file: %v\n", err)
return
}
tempFile.Close()
// Install new crontab
installCmd := exec.Command("crontab", tempFile.Name())
if err := installCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error updating crontab: %v\n", err)
return
}
fmt.Fprintf(os.Stdout, "Successfully removed auto-update cron job\n")
}
func checkAutoUpdateStatus() {
if !isCrontabInstalled() {
fmt.Fprintf(os.Stderr, "Error: crontab command not found\n")
fmt.Fprintf(os.Stdout, "Auto-update is not enabled (crontab not installed)\n")
return
}
// Check existing crontab
checkCmd := exec.Command("crontab", "-l")
checkOutput, err := checkCmd.CombinedOutput()
if err != nil {
fmt.Fprintf(os.Stdout, "Auto-update is not enabled (no crontab found)\n")
return
}
currentCrontab := string(checkOutput)
if strings.Contains(currentCrontab, "### qclient-auto-update") {
// Extract the cron expression
startMarker := "### qclient-auto-update"
endMarker := "### end-qclient-auto-update"
startIdx := strings.Index(currentCrontab, startMarker) + len(startMarker)
endIdx := strings.Index(currentCrontab, endMarker)
if startIdx >= 0 && endIdx >= 0 {
cronSection := currentCrontab[startIdx:endIdx]
cronLines := strings.Split(strings.TrimSpace(cronSection), "\n")
if len(cronLines) > 0 {
fmt.Fprintf(os.Stdout, "Auto-update is enabled.")
fmt.Fprintf(os.Stdout, "The installed schedule is: %s\n", strings.TrimSpace(cronLines[0]))
} else {
fmt.Fprintf(os.Stdout, "Auto-update is enabled\n")
}
} else {
fmt.Fprintf(os.Stdout, "Auto-update is enabled\n")
}
} else {
fmt.Fprintf(os.Stdout, "Auto-update is not enabled\n")
}
}

10
client/cmd/node/clean.go Normal file
View File

@ -0,0 +1,10 @@
package node
// TODO: Implement a clean command that will remove old versions of the node,
// signatures, and logs
// qlient node clean
// qlient node clean --all (all logs, old node binaries and signatures)
// qlient node clean --logs (remove all logs)
// qlient node clean --node (remove all old node binary versions, including signatures)
// to remove even current versions, they must run 'qclient node uninstall'

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)
}

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

@ -0,0 +1,185 @@
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 and create a service to run it.
## Service Management
You can start, stop, and restart the service with:
qclient node service start
qclient node service stop
qclient node service restart
qclient node service status
qclient node service enable
qclient node service disable
### Mac OS Notes
On Mac OS, the service is managed by launchd (installed by default)
### Linux Notes
On Linux, the service is managed by systemd (will be installed by this command).
## Config Management
A config directory will be created in the user's home directory at .quilibrium/configs/.
To create a default config, run the following command:
qclient node config create-default [name-for-config]
or, you can import existing configs one at a timefrom a directory:
qclient node config import [name-for-config] /path/to/src/config/dir/
You can then select the config to use when running the node with:
qclient node set-default [name-for-config]
## Binary Management
Binaries and signatures are installed to /var/quilibrium/bin/node/[version]/
You can update the node binary with:
qclient node update [version]
### Auto-update
You can enable auto-update with:
qclient node auto-update enable
You can disable auto-update with:
qclient node auto-update disable
You can check the auto-update status with:
qclient node auto-update status
## Log Management
Logging uses system logging with logrotate installed by default.
Logs are installed to /var/log/quilibrium
The logrotate config is installed to /etc/logrotate.d/quilibrium
You can view the logs with:
qclient node logs [version]
When installing with this command, if no version is specified, the latest version will be installed.
Sudo is required to install the node to install the node binary, logging,systemd (on Linux), and create the config directory.
Examples:
# Install the latest version
qclient node install
# Install a specific version
qclient node install 2.1.0
`,
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
}
if !utils.IsSudo() {
fmt.Println("This command must be run with sudo: sudo qclient node install")
fmt.Println("Sudo is required to install the node binary, logging, systemd (on Linux) service, and create the config directory.")
os.Exit(1)
}
// Determine version to install
version := determineVersion(args)
// 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, "Found latest version: %s\n", version)
}
// do a pre-flight check to ensure the release file exists
fileName := fmt.Sprintf("%s-%s-%s-%s", utils.ReleaseTypeNode, version, osType, arch)
url := fmt.Sprintf("%s/%s", utils.BaseReleaseURL, fileName)
if !utils.DoesRemoteFileExist(url) {
fmt.Printf("The release file %s does not exist on the release server\n", fileName)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, "Installing Quilibrium node for %s-%s, version: %s\n", osType, arch, version)
// Install the node
InstallNode(version)
},
}
// installNode installs the Quilibrium node
func InstallNode(version string) {
// Create installation directory
if err := utils.ValidateAndCreateDir(utils.NodeDataPath, NodeUser); err != nil {
fmt.Fprintf(os.Stderr, "Error creating installation directory: %v\n", err)
return
}
if utils.IsExistingNodeVersion(version) {
fmt.Fprintf(os.Stderr, "Error: Node version %s already exists\n", version)
os.Exit(1)
}
if err := InstallByVersion(version); err != nil {
fmt.Fprintf(os.Stderr, "Error installing specific version: %v\n", err)
os.Exit(1)
}
createService()
finishInstallation(version)
}
// installByVersion installs a specific version of the Quilibrium node
func InstallByVersion(version string) error {
versionDir := filepath.Join(utils.NodeDataPath, version)
if err := utils.ValidateAndCreateDir(versionDir, NodeUser); 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
}

View File

@ -0,0 +1,3 @@
package log
// TODO: Implement a log subcommand to clean the logs

View File

@ -0,0 +1,3 @@
package log
// TODO: Implement a command to view and manage the logs

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

@ -0,0 +1,83 @@
package node
import (
"fmt"
"os"
"os/user"
"path/filepath"
"github.com/spf13/cobra"
configCmd "source.quilibrium.com/quilibrium/monorepo/client/cmd/node/nodeconfig"
proverCmd "source.quilibrium.com/quilibrium/monorepo/client/cmd/node/prover"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
var (
// Base URL for Quilibrium releases
baseReleaseURL = "https://releases.quilibrium.com"
// Default symlink path for the node binary
defaultSymlinkPath = "/usr/local/bin/quilibrium-node"
logPath = "/var/log/quilibrium"
ServiceName = "quilibrium-node"
OsType string
Arch string
configDirectory string
NodeConfig *config.Config
NodeUser *user.User
ConfigDirs string
NodeConfigToRun string
)
// NodeCmd represents the node command
var NodeCmd = &cobra.Command{
Use: "node",
Short: "Quilibrium node commands",
Long: `Run Quilibrium node commands.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Store reference to parent's PersistentPreRun to call it first
parent := cmd.Parent()
if parent != nil && parent.PersistentPreRun != nil {
parent.PersistentPreRun(parent, args)
}
// Then run the node-specific initialization
var userLookup *user.User
var err error
userLookup, err = utils.GetCurrentSudoUser()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current user: %v\n", err)
os.Exit(1)
}
NodeUser = userLookup
ConfigDirs = filepath.Join(userLookup.HomeDir, ".quilibrium", "configs")
NodeConfigToRun = filepath.Join(userLookup.HomeDir, ".quilibrium", "configs", "default")
},
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
func init() {
NodeCmd.PersistentFlags().StringVar(&configDirectory, "config", ".config", "config directory (default is .config/)")
// Add subcommands
NodeCmd.AddCommand(InstallCmd)
NodeCmd.AddCommand(configCmd.ConfigCmd)
NodeCmd.AddCommand(updateNodeCmd)
NodeCmd.AddCommand(nodeServiceCmd)
NodeCmd.AddCommand(proverCmd.ProverCmd)
localOsType, localArch, err := utils.GetSystemInfo()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
OsType = localOsType
Arch = localArch
}

View File

@ -0,0 +1,43 @@
package nodeconfig
// TODO: Implement a command to assign a config's rewards to a config's rewards address
// Should be able to assign to a specific address or by name of a config directory
// e.g. qclient node config assign-rewards my-config my-throwaway-config
// this finds the address from my-throwaway-config and assigns it to my-config
// qlient node config assign-rewards my-config --reset will reset the rewards address to the default address
// or
// qclient node config assign-rewards my-config --address 0x1234567890abcdef1234567890abcdef1234567890abcdef
//
// If no address is provided, the command will prompt for an address
// the prompt should prompt clearly the user for each part, asking if
// the user wants to use one of the config files as the source of the address
// or if they want to enter a new address manually
// if no configs are found locally, it should prompt the user to create a new config
// or import one
// i.e. Which config do you want to re-assign rewards?
// (1) my-config
// (2) my-other-config
// (3) my-throwaway-config
//
// Enter the number of the config you want to re-assign rewards: 1
// Finding address from my-config...
// Successfully found address 0x1234567890abcdef1234567890abcdef1234567890abcdef
// Which reward address do you want to assign to my-config?
// (1) my-other-config
// (2) my-throwaway-config
// (3) Enter a new address manually
// (4) Reset to default address
//
// Enter the number of the reward address you want to assign: 2
//
// Finding address from my-throwaway-config...
// Successfully found address 0x1234567890abcdef1234567890abcdef1234567890abcdef
// Assigning rewards from my-config to my-throwaway-config
// Successfully assigned rewards.
//
// Summary:
// Node address: 0x1234567890abcdef1234567890abcdef1234567890abcdef
// Rewards address: 0x1234567890abcdef1234567890abcdef1234567890abcdef
//
// Successfully updated my-config

View File

@ -0,0 +1,60 @@
package nodeconfig
import (
"fmt"
"os"
"os/user"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var (
NodeUser *user.User
ConfigDirs string
NodeConfigToRun string
SetDefault bool
)
// ConfigCmd represents the node config command
var ConfigCmd = &cobra.Command{
Use: "config",
Short: "Manage node configuration",
Long: `Manage Quilibrium node configuration.
This command provides utilities for configuring your Quilibrium node, such as:
- Setting configuration values
- Setting default configuration
- Creating default configuration
- Importing configuration
`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Check if the config directory exists
user, err := utils.GetCurrentSudoUser()
if err != nil {
fmt.Println("Error getting current user:", err)
os.Exit(1)
}
ConfigDirs = filepath.Join(user.HomeDir, ".quilibrium", "configs")
NodeConfigToRun = filepath.Join(user.HomeDir, ".quilibrium", "configs", "default")
if utils.FileExists(ConfigDirs) {
utils.ValidateAndCreateDir(ConfigDirs, user)
}
},
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
func init() {
importCmd.Flags().BoolVarP(&SetDefault, "default", "d", false, "Select this config as the default")
ConfigCmd.AddCommand(importCmd)
ConfigCmd.AddCommand(SwitchConfigCmd)
createCmd.Flags().BoolVarP(&SetDefault, "default", "d", false, "Select this config as the default")
ConfigCmd.AddCommand(createCmd)
ConfigCmd.AddCommand(setCmd)
}

View File

@ -0,0 +1,101 @@
package nodeconfig
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var createCmd = &cobra.Command{
Use: "create [name]",
Short: "Create a default configuration file set for a node",
Long: fmt.Sprintf(`Create a default configuration by running quilibrium-node with --peer-id and --config flags, then symlink it to the default configuration.
Example:
qclient node config create
qclient node config create myconfig
qclient node config create myconfig --default
The first example will create a new configuration at %s/default-config.
The second example will create a new configuration at %s/myconfig.
The third example will create a new configuration at %s/myconfig and symlink it so the node will use it.`,
ConfigDirs, ConfigDirs, ConfigDirs),
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Determine the config name (default-config or user-provided)
var configName string
if len(args) > 0 {
configName = args[0]
} else {
// Prompt for a name if none provided
fmt.Print("Enter a name for the configuration (cannot be 'default'): ")
fmt.Scanln(&configName)
if configName == "" {
configName = "default-config"
}
}
// Check if trying to use "default" which is reserved for the symlink
if configName == "default" {
fmt.Println("Error: 'default' is reserved for the symlink. Please use a different name.")
os.Exit(1)
}
// Construct the configuration directory path
configDir := filepath.Join(ConfigDirs, configName)
// Create directory if it doesn't exist
if err := utils.ValidateAndCreateDir(configDir, NodeUser); err != nil {
fmt.Printf("Failed to create config directory: %s\n", err)
os.Exit(1)
}
// Run quilibrium-node command to generate config
// this is a hack to get the config files to be created
// TODO: fix this
// to fix this, we need to extrapolate the Node's config and keystore construction
// and reuse it for this command
nodeCmd := exec.Command("quilibrium-node", "--peer-id", "--config", configDir)
nodeCmd.Stdout = os.Stdout
nodeCmd.Stderr = os.Stderr
fmt.Printf("Running quilibrium-node to generate configuration in %s...\n", configName)
if err := nodeCmd.Run(); err != nil {
// Check if the error is due to the command not being found
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Printf("Error running quilibrium-node: %s\n", exitErr)
} else if _, ok := err.(*exec.Error); ok {
fmt.Printf("Error: quilibrium-node command not found. Please ensure it is installed and in your PATH.\n")
} else {
fmt.Printf("Error: %s\n", err)
}
os.RemoveAll(configDir)
os.Exit(1)
}
// Check if the configuration was created successfully
if !HasConfigFiles(configDir) {
fmt.Printf("Failed to generate configuration files in: %s\n", configDir)
os.Exit(1)
}
if SetDefault {
// Create the symlink
if err := utils.CreateSymlink(configDir, NodeConfigToRun); err != nil {
fmt.Printf("Failed to create symlink: %s\n", err)
os.Exit(1)
}
fmt.Printf("Successfully created %s configuration and symlinked to default\n", configName)
} else {
fmt.Printf("Successfully created %s configuration\n", configName)
}
fmt.Println("The keys.yml file will only contain 'null:' until the node is started.")
},
}

View File

@ -0,0 +1,78 @@
package nodeconfig
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var importCmd = &cobra.Command{
Use: "import [name] [source_directory]",
Short: "Import config.yml and keys.yml from a source directory",
Long: `Import config.yml and keys.yml from a source directory to the QuilibriumRoot config folder.
Example:
qclient node config import mynode /path/to/source
This will copy config.yml and keys.yml from /path/to/source to /home/quilibrium/configs/mynode/`,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
sourceDir := args[1]
// Check if source directory exists
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
fmt.Printf("Source directory does not exist: %s\n", sourceDir)
os.Exit(1)
}
if !HasConfigFiles(sourceDir) {
fmt.Printf("Source directory does not contain both config.yml and keys.yml files: %s\n", sourceDir)
os.Exit(1)
}
// Create target directory in the standard location
targetDir := filepath.Join(ConfigDirs, name)
if err := utils.ValidateAndCreateDir(targetDir, NodeUser); err != nil {
fmt.Printf("Failed to create target directory: %s\n", err)
os.Exit(1)
}
// Define source file paths
sourceConfigPath := filepath.Join(sourceDir, "config.yml")
sourceKeysPath := filepath.Join(sourceDir, "keys.yml")
// Copy config.yml
targetConfigPath := filepath.Join(targetDir, "config.yml")
if err := utils.CopyFile(sourceConfigPath, targetConfigPath); err != nil {
fmt.Printf("Failed to copy config.yml: %s\n", err)
os.Exit(1)
}
// Copy keys.yml
targetKeysPath := filepath.Join(targetDir, "keys.yml")
if err := utils.CopyFile(sourceKeysPath, targetKeysPath); err != nil {
fmt.Printf("Failed to copy keys.yml: %s\n", err)
os.Exit(1)
}
if SetDefault {
// Create the symlink
if err := utils.CreateSymlink(targetDir, NodeConfigToRun); err != nil {
fmt.Printf("Failed to create symlink: %s\n", err)
os.Exit(1)
}
fmt.Printf("Successfully imported config files to %s and symlinked to default\n", name)
} else {
fmt.Printf("Successfully imported config files to %s\n", targetDir)
}
},
}
func init() {
}

View File

@ -0,0 +1,82 @@
package nodeconfig
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
var setCmd = &cobra.Command{
Use: "set [name] [key] [value]",
Short: "Set a configuration value",
Long: `Set a configuration value in the node config.yml file.
Example:
qclient node config set mynode engine.statsMultiaddr /dns/stats.quilibrium.com/tcp/443
`,
Args: cobra.ExactArgs(3),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
key := args[1]
value := args[2]
// Construct the config directory path
configDir := filepath.Join(ConfigDirs, name)
configFile := filepath.Join(configDir, "config.yml")
// Check if config directory exists
if _, err := os.Stat(configDir); os.IsNotExist(err) {
fmt.Printf("Config directory does not exist: %s\n", configDir)
os.Exit(1)
}
// Check if config file exists
if _, err := os.Stat(configFile); os.IsNotExist(err) {
fmt.Printf("Config file does not exist: %s\n", configFile)
os.Exit(1)
}
// Load the config
cfg, err := config.LoadConfig(configDir, "", false)
if err != nil {
fmt.Printf("Failed to load config: %s\n", err)
os.Exit(1)
}
// Update the config based on the key
switch key {
case "engine.statsMultiaddr":
cfg.Engine.StatsMultiaddr = value
case "p2p.listenMultiaddr":
cfg.P2P.ListenMultiaddr = value
case "listenGrpcMultiaddr":
cfg.ListenGRPCMultiaddr = value
case "listenRestMultiaddr":
cfg.ListenRestMultiaddr = value
case "engine.autoMergeCoins":
if value == "true" {
cfg.Engine.AutoMergeCoins = true
} else if value == "false" {
cfg.Engine.AutoMergeCoins = false
} else {
fmt.Printf("Invalid value for %s: must be 'true' or 'false'\n", key)
os.Exit(1)
}
default:
fmt.Printf("Unsupported configuration key: %s\n", key)
fmt.Println("Supported keys: engine.statsMultiaddr, p2p.listenMultiaddr, listenGrpcMultiaddr, listenRestMultiaddr, engine.autoMergeCoins")
os.Exit(1)
}
// Save the updated config
if err := config.SaveConfig(configDir, cfg); err != nil {
fmt.Printf("Failed to save config: %s\n", err)
os.Exit(1)
}
fmt.Printf("Successfully updated %s to %s in %s\n", key, value, configFile)
},
}

View File

@ -0,0 +1,82 @@
package nodeconfig
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var SwitchConfigCmd = &cobra.Command{
Use: "switch [name]",
Short: "Switch the config to be run by the node",
Long: fmt.Sprintf(`Switch the configuration to be run by the node by creating a symlink.
Example:
qclient node config switch mynode
This will symlink %s/mynode to %s`, ConfigDirs, NodeConfigToRun),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var name string
if len(args) > 0 {
name = args[0]
} else {
// List available configurations
configs, err := ListConfigurations()
if err != nil {
fmt.Printf("Error listing configurations: %s\n", err)
os.Exit(1)
}
if len(configs) == 0 {
fmt.Println("No configurations found. Create one with 'qclient node config create'")
os.Exit(1)
}
fmt.Println("Available configurations:")
for i, config := range configs {
fmt.Printf("%d. %s\n", i+1, config)
}
// Prompt for choice
var choice int
fmt.Print("Enter the number of the configuration to set as default: ")
_, err = fmt.Scanf("%d", &choice)
if err != nil || choice < 1 || choice > len(configs) {
fmt.Println("Invalid choice. Please enter a valid number.")
os.Exit(1)
}
name = configs[choice-1]
}
// Construct the source directory path
sourceDir := filepath.Join(ConfigDirs, name)
// Check if source directory exists
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
fmt.Printf("Config directory does not exist: %s\n", sourceDir)
os.Exit(1)
}
// Check if the source directory has both config.yml and keys.yml files
if !HasConfigFiles(sourceDir) {
fmt.Printf("Source directory does not contain both config.yml and keys.yml files: %s\n", sourceDir)
os.Exit(1)
}
// Construct the default directory path
defaultDir := filepath.Join(ConfigDirs, "default")
// Create the symlink
if err := utils.CreateSymlink(sourceDir, defaultDir); err != nil {
fmt.Printf("Failed to create symlink: %s\n", err)
os.Exit(1)
}
fmt.Printf("Successfully set %s as the default configuration\n", name)
},
}

View File

@ -0,0 +1,42 @@
package nodeconfig
import (
"os"
"path/filepath"
)
// HasConfigFiles checks if a directory contains both config.yml and keys.yml files
func HasConfigFiles(dirPath string) bool {
configPath := filepath.Join(dirPath, "config.yml")
keysPath := filepath.Join(dirPath, "keys.yml")
// Check if both files exist
configExists := false
keysExists := false
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
configExists = true
}
if _, err := os.Stat(keysPath); !os.IsNotExist(err) {
keysExists = true
}
return configExists && keysExists
}
func ListConfigurations() ([]string, error) {
files, err := os.ReadDir(ConfigDirs)
if err != nil {
return nil, err
}
configs := make([]string, 0)
for _, file := range files {
if file.IsDir() {
configs = append(configs, file.Name())
}
}
return configs, nil
}

View File

@ -0,0 +1,19 @@
package prover
import (
"github.com/spf13/cobra"
)
var ConfigDirectory string
var ProverCmd = &cobra.Command{
Use: "prover",
Short: "Performs a configuration operation for given prover info",
Run: func(cmd *cobra.Command, args []string) {},
}
func init() {
ProverCmd.PersistentFlags().StringVar(&ConfigDirectory, "config", ".config", "config directory (default is .config/)")
ProverCmd.AddCommand(proverPauseCmd)
ProverCmd.AddCommand(proverConfigMergeCmd)
}

View File

@ -1,22 +1,21 @@
package cmd
package prover
import (
_ "embed"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path"
"strconv"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/pkg/errors"
"github.com/shopspring/decimal"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/node/config"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
nodeConfig "source.quilibrium.com/quilibrium/monorepo/node/config"
)
var DryRun bool
var proverConfigMergeCmd = &cobra.Command{
Use: "merge",
Short: "Merges config data for prover seniority",
@ -32,35 +31,35 @@ var proverConfigMergeCmd = &cobra.Command{
os.Exit(1)
}
primaryConfig, err := config.LoadConfig(args[0], "", true)
primaryConfig, err := nodeConfig.LoadConfig(args[0], "", true)
if err != nil {
fmt.Printf("invalid config directory: %s\n", args[0])
os.Exit(1)
}
otherPaths := []string{}
peerIds := []string{GetPeerIDFromConfig(primaryConfig).String()}
peerIds := []string{utils.GetPeerIDFromConfig(primaryConfig).String()}
for _, p := range args[1:] {
if !path.IsAbs(p) {
fmt.Printf("%s is not an absolute path\n", p)
}
cfg, err := config.LoadConfig(p, "", true)
cfg, err := nodeConfig.LoadConfig(p, "", true)
if err != nil {
fmt.Printf("invalid config directory: %s\n", p)
os.Exit(1)
}
peerId := GetPeerIDFromConfig(cfg).String()
peerId := utils.GetPeerIDFromConfig(cfg).String()
peerIds = append(peerIds, peerId)
otherPaths = append(otherPaths, p)
}
if DryRun {
bridged := []*BridgedPeerJson{}
firstRetro := []*FirstRetroJson{}
secondRetro := []*SecondRetroJson{}
thirdRetro := []*ThirdRetroJson{}
fourthRetro := []*FourthRetroJson{}
bridged := []*utils.BridgedPeerJson{}
firstRetro := []*utils.FirstRetroJson{}
secondRetro := []*utils.SecondRetroJson{}
thirdRetro := []*utils.ThirdRetroJson{}
fourthRetro := []*utils.FourthRetroJson{}
err = json.Unmarshal(bridgedPeersJsonBinary, &bridged)
if err != nil {
@ -210,7 +209,7 @@ var proverConfigMergeCmd = &cobra.Command{
p,
)
}
err := config.SaveConfig(args[0], primaryConfig)
err := nodeConfig.SaveConfig(args[0], primaryConfig)
if err != nil {
panic(err)
}
@ -218,82 +217,22 @@ var proverConfigMergeCmd = &cobra.Command{
},
}
func GetPrivKeyFromConfig(cfg *config.Config) (crypto.PrivKey, error) {
peerPrivKey, err := hex.DecodeString(cfg.P2P.PeerPrivKey)
if err != nil {
panic(errors.Wrap(err, "error unmarshaling peerkey"))
}
privKey, err := crypto.UnmarshalEd448PrivateKey(peerPrivKey)
return privKey, err
}
func GetPeerIDFromConfig(cfg *config.Config) peer.ID {
peerPrivKey, err := hex.DecodeString(cfg.P2P.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"))
}
return id
}
type BridgedPeerJson struct {
Amount string `json:"amount"`
Identifier string `json:"identifier"`
Variant string `json:"variant"`
}
type FirstRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}
type SecondRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
JanPresence bool `json:"janPresence"`
FebPresence bool `json:"febPresence"`
MarPresence bool `json:"marPresence"`
AprPresence bool `json:"aprPresence"`
MayPresence bool `json:"mayPresence"`
}
type ThirdRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}
type FourthRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}
//go:embed bridged.json
//go:embed premainnet-data/bridged.json
var bridgedPeersJsonBinary []byte
//go:embed first_retro.json
//go:embed premainnet-data/first_retro.json
var firstRetroJsonBinary []byte
//go:embed second_retro.json
//go:embed premainnet-data/second_retro.json
var secondRetroJsonBinary []byte
//go:embed third_retro.json
//go:embed premainnet-data/third_retro.json
var thirdRetroJsonBinary []byte
//go:embed fourth_retro.json
//go:embed premainnet-data/fourth_retro.json
var fourthRetroJsonBinary []byte
func init() {
proverCmd.AddCommand(proverConfigMergeCmd)
proverConfigMergeCmd.Flags().BoolVar(&DryRun, "dry-run", false, "Evaluate seniority score without merging configs")
ProverCmd.AddCommand(proverConfigMergeCmd)
}

View File

@ -1,8 +1,9 @@
package cmd
package prover
import (
"bytes"
"fmt"
"os"
"strings"
"time"
@ -13,12 +14,16 @@ import (
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/go-libp2p-blossomsub/pb"
nodeConfig "source.quilibrium.com/quilibrium/monorepo/node/config"
"source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/token/application"
"source.quilibrium.com/quilibrium/monorepo/node/p2p"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
var NodeConfig *nodeConfig.Config
var proverPauseCmd = &cobra.Command{
Use: "pause",
Short: "Pauses a prover",
@ -28,13 +33,23 @@ var proverPauseCmd = &cobra.Command{
`,
Run: func(cmd *cobra.Command, args []string) {
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
NodeConfig, err = utils.LoadNodeConfig(ConfigDirectory)
if err != nil {
fmt.Printf("invalid config directory: %s\n", ConfigDirectory)
os.Exit(1)
}
pubsub := p2p.NewBlossomSub(NodeConfig.P2P, logger)
intrinsicFilter := p2p.GetBloomFilter(application.TOKEN_ADDRESS, 256, 3)
pubsub.Subscribe(
append([]byte{0x00}, intrinsicFilter...),
func(message *pb.Message) error { return nil },
)
key, err := GetPrivKeyFromConfig(NodeConfig)
key, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
@ -145,5 +160,5 @@ func publishMessage(
}
func init() {
proverCmd.AddCommand(proverPauseCmd)
ProverCmd.AddCommand(proverPauseCmd)
}

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

@ -0,0 +1,493 @@
package node
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"text/template"
"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" {
// launchctl is already installed on macOS by default, so no need to check for it
installMacOSService()
} else if OsType == "linux" {
// systemd is not installed on linux by default, so we need to check for it
if !utils.CheckForSystemd() {
// install systemd if not found
installSystemd()
}
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")
}
func installSystemd() {
fmt.Fprintf(os.Stdout, "Installing systemd...\n")
updateCmd := exec.Command("sudo", "apt-get", "update")
updateCmd.Stdout = nil
updateCmd.Stderr = nil
if err := updateCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error updating package lists: %v\n", err)
return
}
installCmd := exec.Command("sudo", "apt-get", "install", "-y", "systemd")
installCmd.Stdout = nil
installCmd.Stderr = nil
if err := installCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error installing systemd: %v\n", err)
return
}
}
// 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")
}
func createService() {
// 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
}
}
// createSystemdServiceFile creates the systemd service file with environment file support
func createSystemdServiceFile() error {
if !utils.CheckForSystemd() {
installSystemd()
}
// 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 := `# Quilibrium Node Environment`
// Write environment file
envPath := filepath.Join(utils.RootQuilibriumPath, "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 := utils.ChownPath(envPath, NodeUser, false)
if chownCmd != nil {
return fmt.Errorf("failed to set environment file ownership: %w", chownCmd)
}
// 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 %s
Restart=on-failure
RestartSec=10
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
`, ConfigDirs+"/default")
// Write service file
servicePath := "/etc/systemd/system/quilibrium-node.service"
if err := utils.WriteFileAuto(servicePath, serviceContent); err != nil {
return fmt.Errorf("failed to create service file: %w", err)
}
// Reload systemd daemon
reloadCmd := exec.Command("sudo", "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: utils.NodeDataPath,
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)
}

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

@ -0,0 +1,152 @@
package node
import (
"fmt"
"os"
"path/filepath"
"strings"
"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"
// }
// setOwnership sets the ownership of directories to the node user
func setOwnership() {
// Change ownership of installation directory
err := utils.ChownPath(utils.NodeDataPath, NodeUser, true)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to change ownership of %s: %v\n", utils.NodeDataPath, 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.Username, NodeUser.Username)
// Write the configuration file
configPath := "/etc/logrotate.d/quilibrium-node"
if err := utils.WriteFile(configPath, configContent); err != nil {
return fmt.Errorf("failed to create logrotate configuration: %w", err)
}
// Create log directory with proper permissions
if err := utils.ValidateAndCreateDir(logPath, NodeUser); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
// Set ownership of log directory
err := utils.ChownPath(logPath, NodeUser, true)
if 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(version string) {
setOwnership()
normalizedBinaryName := "node-" + version + "-" + OsType + "-" + Arch
// Finish installation
nodeBinaryPath := filepath.Join(utils.NodeDataPath, version, normalizedBinaryName)
fmt.Printf("Making binary executable: %s\n", nodeBinaryPath)
// Make the binary executable
if err := utils.ChmodPath(nodeBinaryPath, 0755, "executable"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to make binary executable: %v\n", err)
}
// 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)
}
// 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, "Binary download directory: %s\n", filepath.Join(utils.NodeDataPath, version))
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, "\nConfiguration:\n")
fmt.Fprintf(os.Stdout, " To create a new configuration:\n")
fmt.Fprintf(os.Stdout, " qclient node config create [name] --default\n")
fmt.Fprintf(os.Stdout, " quilibrium-node --peer-id %s/default-config\n", ConfigDirs)
fmt.Fprintf(os.Stdout, "\n To use an existing configuration:\n")
fmt.Fprintf(os.Stdout, " cp -r /path/to/your/existing/config %s/default-config\n", ConfigDirs)
fmt.Fprintf(os.Stdout, " # Or modify the service file to point to your existing config:\n")
fmt.Fprintf(os.Stdout, " sudo nano /etc/systemd/system/quilibrium-node.service\n")
fmt.Fprintf(os.Stdout, " # Then reload systemd:\n")
fmt.Fprintf(os.Stdout, " sudo systemctl daemon-reload\n")
fmt.Fprintf(os.Stdout, "\nTo select a configuration:\n")
fmt.Fprintf(os.Stdout, " qclient node config switch <config-name>\n")
fmt.Fprintf(os.Stdout, " # Or use the --default flag when creating a config to automatically select it:\n")
fmt.Fprintf(os.Stdout, " qclient node config create --default\n")
fmt.Fprintf(os.Stdout, "\nTo manually start the node (must create a config first), you can run:\n")
fmt.Fprintf(os.Stdout, " %s --config %s/myconfig/\n",
ServiceName, ConfigDirs)
fmt.Fprintf(os.Stdout, " # Or use systemd service using the default config:\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")
}

View File

@ -0,0 +1,10 @@
package node
// TODO: Implement a command to uninstall the current and previous versions
// of the node and all files, not including user data
// this should NEVER delete the user data, only the node files and logs
// prompt the user for confirmation or a --force flag to skip the confirmation
// qlient node uninstall
// qlient node uninstall --force (skip the confirmation)

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

@ -0,0 +1,68 @@
package node
import (
"fmt"
"os"
"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)
// 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, "Found latest version: %s\n", version)
}
if utils.IsExistingNodeVersion(version) {
fmt.Fprintf(os.Stderr, "Error: Node version %s already exists\n", version)
os.Exit(1)
}
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", utils.NodeDataPath)); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return
}
InstallNode(version)
}

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

@ -0,0 +1 @@
package node

View File

@ -1,12 +0,0 @@
package cmd
import "github.com/spf13/cobra"
var proverCmd = &cobra.Command{
Use: "prover",
Short: "Performs a configuration operation for given prover info",
}
func init() {
configCmd.AddCommand(proverCmd)
}

View File

@ -1,37 +1,62 @@
package cmd
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/cloudflare/circl/sign/ed448"
"github.com/multiformats/go-multiaddr"
mn "github.com/multiformats/go-multiaddr/net"
"github.com/spf13/cobra"
"golang.org/x/crypto/sha3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
clientConfig "source.quilibrium.com/quilibrium/monorepo/client/cmd/config"
"source.quilibrium.com/quilibrium/monorepo/client/cmd/node"
"source.quilibrium.com/quilibrium/monorepo/client/cmd/token"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
var configDirectory string
var signatureCheck bool = true
var byPassSignatureCheck bool = false
var NodeConfig *config.Config
var simulateFail bool
var LightNode bool = false
var DryRun bool = false
var publicRPC bool = false
var ClientConfig *utils.ClientConfig
var StandardizedQClientFileName string = "qclient-" + config.GetVersionString() + "-" + osType + "-" + arch
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 cmd.Name() == "help" || cmd.Name() == "download-signatures" {
return
}
if !utils.FileExists(utils.GetConfigPath()) {
fmt.Println("Client config not found, creating default config")
utils.CreateDefaultConfig()
}
clientConfig, err := utils.LoadClientConfig()
if err != nil {
fmt.Printf("Error loading client config: %v\n", err)
os.Exit(1)
}
if !clientConfig.SignatureCheck || byPassSignatureCheck {
signatureCheck = false
}
if signatureCheck {
ex, err := os.Executable()
if err != nil {
@ -48,114 +73,111 @@ var rootCmd = &cobra.Command{
}
checksum := sha3.Sum256(b)
digest, err := os.ReadFile(ex + ".dgst")
// First check var data path for signatures
varDataPath := filepath.Join(utils.ClientDataPath, config.GetVersionString())
digestPath := filepath.Join(varDataPath, StandardizedQClientFileName+".dgst")
fmt.Printf("Checking signature for %s\n", digestPath)
// Try to read digest from var data path first
digest, err := os.ReadFile(digestPath)
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(config.Signatories); i++ {
signatureFile := fmt.Sprintf(ex+".dgst.sig.%d", i)
sig, err := os.ReadFile(signatureFile)
// Fall back to checking next to executable
digest, err = os.ReadFile(ex + ".dgst")
if err != nil {
continue
}
fmt.Println("")
fmt.Println("The digest file was not found. Do you want to continue without signature verification? (y/n)")
pubkey, _ := hex.DecodeString(config.Signatories[i-1])
if !ed448.Verify(pubkey, digest, sig, "") {
fmt.Printf("Failed signature check for signatory #%d\n", i)
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
if response != "y" && response != "yes" {
fmt.Println("Exiting due to missing digest file")
fmt.Println("The signature files (if they exist) can be downloaded with the 'qclient download-signatures' command")
fmt.Println("You can also skip this prompt manually by using the --signature-check=false flag or to permanently disable signature checks running 'qclient config signature-check false'")
os.Exit(1)
}
// Check if the user is trying to run the config command to disable/enable signature checks
if len(os.Args) >= 3 && os.Args[1] == "config" && os.Args[2] != "signature-check" {
fmt.Println("The signature files (if they exist) can be downloaded with the 'qclient download-signatures' command")
fmt.Println("You can also skip this prompt manually by using the --signature-check=false flag or to permanently disable signature checks running 'qclient config signature-check false'")
}
fmt.Println("Continuing without signature verification")
signatureCheck = false
}
}
if signatureCheck {
parts := strings.Split(string(digest), " ")
if len(parts) != 2 {
fmt.Println("Invalid digest file format")
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)
}
digestBytes, err := hex.DecodeString(parts[1][:64])
if err != nil {
fmt.Println("Invalid digest file format")
os.Exit(1)
}
fmt.Println("Signature check passed")
if !bytes.Equal(checksum[:], digestBytes) {
fmt.Println("Invalid digest for node")
os.Exit(1)
}
count := 0
for i := 1; i <= len(config.Signatories); i++ {
// Try var data path first for signature files
signatureFile := filepath.Join(varDataPath, fmt.Sprintf("%s.dgst.sig.%d", filepath.Base(ex), i))
sig, err := os.ReadFile(signatureFile)
if err != nil {
// Fall back to checking next to executable
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")
fmt.Println("----------------------------------------------------------")
fmt.Println("")
}
_, 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)
}
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
}
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("")
},
}
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)
}
}
func GetGRPCClient() (*grpc.ClientConn, error) {
addr := "rpc.quilibrium.com:8337"
credentials := credentials.NewTLS(&tls.Config{InsecureSkipVerify: false})
if !LightNode {
ma, err := multiaddr.NewMultiaddr(NodeConfig.ListenGRPCMultiaddr)
if err != nil {
panic(err)
}
_, addr, err = mn.DialArgs(ma)
if err != nil {
panic(err)
}
credentials = insecure.NewCredentials()
}
return grpc.Dial(
addr,
grpc.WithTransportCredentials(
credentials,
),
grpc.WithDefaultCallOptions(
grpc.MaxCallSendMsgSize(600*1024*1024),
grpc.MaxCallRecvMsgSize(600*1024*1024),
),
)
}
func signatureCheckDefault() bool {
envVarValue, envVarExists := os.LookupEnv("QUILIBRIUM_SIGNATURE_CHECK")
if envVarExists {
@ -171,28 +193,23 @@ func signatureCheckDefault() bool {
}
func init() {
rootCmd.PersistentFlags().StringVar(
&configDirectory,
"config",
".config/",
"config directory (default is .config/)",
)
rootCmd.PersistentFlags().BoolVar(
&DryRun,
"dry-run",
false,
"runs the command (if applicable) without actually mutating state (printing effect output)",
)
rootCmd.PersistentFlags().BoolVar(
&signatureCheck,
"signature-check",
signatureCheckDefault(),
"bypass signature check (not recommended for binaries) (default true or value of QUILIBRIUM_SIGNATURE_CHECK env var)",
)
rootCmd.PersistentFlags().BoolVar(
&publicRPC,
"public-rpc",
rootCmd.PersistentFlags().BoolVarP(
&byPassSignatureCheck,
"yes",
"y",
false,
"uses the public RPC",
"auto-approve and bypass signature check (sets signature-check=false)",
)
// Add the node command
rootCmd.AddCommand(node.NodeCmd)
rootCmd.AddCommand(clientConfig.ConfigCmd)
rootCmd.AddCommand(token.TokenCmd)
}

View File

@ -1,93 +0,0 @@
package cmd
import (
"context"
"encoding/hex"
"fmt"
"math/big"
"os"
"strings"
"github.com/shopspring/decimal"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
var splitCmd = &cobra.Command{
Use: "split",
Short: "Splits a coin into multiple coins",
Long: `Splits a coin into multiple coins:
split <OfCoin> <Amounts>...
OfCoin - the address of the coin to split
Amounts - the sets of amounts to split
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 3 {
fmt.Println("invalid command")
os.Exit(1)
}
coinaddrHex, _ := strings.CutPrefix(args[0], "0x")
coinaddr, err := hex.DecodeString(coinaddrHex)
if err != nil {
panic(err)
}
coin := &protobufs.CoinRef{
Address: coinaddr,
}
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
amounts := [][]byte{}
for _, amt := range args[1:] {
amount, err := decimal.NewFromString(amt)
if err != nil {
fmt.Println("invalid amount")
os.Exit(1)
}
amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
amountBytes := amount.BigInt().FillBytes(make([]byte, 32))
amounts = append(amounts, amountBytes)
}
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
pubKeyBytes, err := privKey.GetPublic().Raw()
if err != nil {
panic(err)
}
split := &protobufs.SplitCoinRequest{
OfCoin: coin,
Amounts: amounts,
}
if err := split.SignED448(pubKeyBytes, privKey.Sign); err != nil {
panic(err)
}
if err := split.Validate(); err != nil {
panic(err)
}
_, err = client.SendMessage(
context.Background(),
split.TokenRequest(),
)
if err != nil {
panic(err)
}
},
}
func init() {
tokenCmd.AddCommand(splitCmd)
}

View File

@ -1,14 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
)
var tokenCmd = &cobra.Command{
Use: "token",
Short: "Performs a token operation",
}
func init() {
rootCmd.AddCommand(tokenCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"fmt"
@ -21,5 +21,5 @@ var acceptCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(acceptCmd)
TokenCmd.AddCommand(acceptCmd)
}

View File

@ -0,0 +1,28 @@
package token
import (
"fmt"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
)
var accountCmd = &cobra.Command{
Use: "account",
Short: "Shows the account address of the managing account",
Run: func(cmd *cobra.Command, args []string) {
peerId := utils.GetPeerIDFromConfig(NodeConfig)
addr, err := poseidon.HashBytes([]byte(peerId))
if err != nil {
panic(err)
}
addrBytes := addr.FillBytes(make([]byte, 32))
fmt.Printf("Account: 0x%x\n", addrBytes)
},
}
func init() {
TokenCmd.AddCommand(accountCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"bytes"
@ -16,6 +16,7 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap"
"google.golang.org/grpc"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
"source.quilibrium.com/quilibrium/monorepo/node/execution/intrinsics/token/application"
"source.quilibrium.com/quilibrium/monorepo/node/p2p"
@ -44,8 +45,8 @@ var allCmd = &cobra.Command{
db := store.NewPebbleDB(NodeConfig.DB)
logger, _ := zap.NewProduction()
dataProofStore := store.NewPebbleDataProofStore(db, logger)
peerId := GetPeerIDFromConfig(NodeConfig)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"context"
@ -7,6 +7,7 @@ import (
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
@ -21,8 +22,8 @@ var balanceCmd = &cobra.Command{
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
peerId := GetPeerIDFromConfig(NodeConfig)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
@ -88,5 +89,5 @@ var balanceCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(balanceCmd)
TokenCmd.AddCommand(balanceCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"context"
@ -8,6 +8,7 @@ import (
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
@ -35,8 +36,8 @@ var coinsCmd = &cobra.Command{
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
peerId := GetPeerIDFromConfig(NodeConfig)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
@ -136,5 +137,5 @@ var coinsCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(coinsCmd)
TokenCmd.AddCommand(coinsCmd)
}

View File

@ -1,12 +1,14 @@
package cmd
package token
import (
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
@ -31,8 +33,8 @@ var mergeCmd = &cobra.Command{
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
peerId := GetPeerIDFromConfig(NodeConfig)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
@ -112,6 +114,29 @@ var mergeCmd = &cobra.Command{
merge := &protobufs.MergeCoinRequest{
Coins: coinaddrs,
}
// Display merge details and confirmation prompt
fmt.Printf("\nMerge Transaction Details:\n")
fmt.Printf("Number of coins to merge: %d\n", len(coinaddrs))
fmt.Println("Coins to be merged:")
for i, coin := range coinaddrs {
fmt.Printf("%d. 0x%x\n", i+1, coin.Address)
}
fmt.Print("\nDo you want to proceed with this merge? (yes/no): ")
var response string
fmt.Scanln(&response)
if strings.ToLower(response) != "yes" {
fmt.Println("Merge transaction cancelled by user.")
return
}
// Create payload for merge operation
payload := []byte("merge")
for _, coinRef := range coinaddrs {
payload = append(payload, coinRef.Address...)
}
if err := merge.SignED448(pubKeyBytes, privKey.Sign); err != nil {
panic(err)
}
@ -128,10 +153,10 @@ var mergeCmd = &cobra.Command{
panic(err)
}
println("Merge request sent successfully")
fmt.Println("Merge transaction sent successfully.")
},
}
func init() {
tokenCmd.AddCommand(mergeCmd)
TokenCmd.AddCommand(mergeCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"github.com/spf13/cobra"
@ -10,5 +10,5 @@ var mintCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(mintCmd)
TokenCmd.AddCommand(mintCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"fmt"
@ -21,5 +21,5 @@ var mutualReceiveCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(mutualReceiveCmd)
TokenCmd.AddCommand(mutualReceiveCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"fmt"
@ -25,5 +25,5 @@ var mutualTransferCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(mutualTransferCmd)
TokenCmd.AddCommand(mutualTransferCmd)
}

View File

@ -1,4 +1,4 @@
package cmd
package token
import (
"fmt"
@ -21,5 +21,5 @@ var rejectCmd = &cobra.Command{
}
func init() {
tokenCmd.AddCommand(rejectCmd)
TokenCmd.AddCommand(rejectCmd)
}

318
client/cmd/token/split.go Normal file
View File

@ -0,0 +1,318 @@
package token
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"math/big"
"os"
"strings"
"github.com/iden3/go-iden3-crypto/poseidon"
"github.com/shopspring/decimal"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
var parts int
var partAmount string
var splitCmd = &cobra.Command{
Use: "split",
Short: "Splits a coin into multiple coins",
Long: `Splits a coin into multiple coins:
split <OfCoin> <Amounts>...
split <--parts PARTS> [--part-amount AMOUNT] <OfCoin>
OfCoin - the address of the coin to split
Amounts - the sets of amounts to split
Example - Split a coin into the specified amounts:
$ qclient token coins
1.000000000000 QUIL (Coin 0x1234)
$ qclient token split 0x1234 0.5 0.25 0.25
$ qclient token coins
0.250000000000 QUIL (Coin 0x1111)
0.250000000000 QUIL (Coin 0x2222)
0.500000000000 QUIL (Coin 0x3333)
Example - Split a coin into three parts:
$ qclient token coins
1.000000000000 QUIL (Coin 0x1234)
$ qclient token split 0x1234 --parts 3
$ qclient token coins
0.000000000250 QUIL (Coin 0x1111)
0.333333333250 QUIL (Coin 0x2222)
0.333333333250 QUIL (Coin 0x3333)
0.333333333250 QUIL (Coin 0x4444)
**Note:** Coin 0x1111 is the remainder.
Example - Split a coin into two parts using the specified amounts:
$ qclient token coins
1.000000000000 QUIL (Coin 0x1234)
$ qclient token split 0x1234 --parts 2 --part-amount 0.35
$ qclient token coins
0.300000000000 QUIL (Coin 0x1111)
0.350000000000 QUIL (Coin 0x2222)
0.350000000000 QUIL (Coin 0x3333)
**Note:** Coin 0x1111 is the remainder.
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 3 && parts == 1 {
fmt.Println("did you forget to specify <OfCoin> and <Amounts>?")
os.Exit(1)
}
if len(args) < 1 && parts > 1 {
fmt.Println("did you forget to specify <OfCoin>?")
os.Exit(1)
}
if len(args) > 1 && parts > 1 {
fmt.Println("-p/--parts can't be combined with <Amounts>")
os.Exit(1)
}
if len(args) > 1 && partAmount != "" {
fmt.Println("-a/--part-amount can't be combined with <Amounts>")
os.Exit(1)
}
if parts > 100 {
fmt.Println("too many parts, maximum is 100")
os.Exit(1)
}
payload := []byte("split")
coinaddrHex, _ := strings.CutPrefix(args[0], "0x")
coinaddr, err := hex.DecodeString(coinaddrHex)
if err != nil {
panic(err)
}
coin := &protobufs.CoinRef{
Address: coinaddr,
}
payload = append(payload, coinaddr...)
// Get the amount of the coin to be split
totalAmount := getCoinAmount(coinaddr)
amounts := [][]byte{}
// Split the coin into the user specified amounts
if parts == 1 {
amounts, payload, err = Split(args[1:], amounts, payload, totalAmount)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// Split the coin into parts
if parts > 1 && partAmount == "" {
amounts, payload = SplitIntoParts(amounts, payload, totalAmount, parts)
}
// Split the coin into parts of the user specified amount
if parts > 1 && partAmount != "" {
amounts, payload, err = SplitIntoPartsAmount(amounts, payload, totalAmount, parts, partAmount)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
key, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
sig, err := key.Sign(payload)
if err != nil {
panic(err)
}
pub, err := key.GetPublic().Raw()
if err != nil {
panic(err)
}
_, err = client.SendMessage(
context.Background(),
&protobufs.TokenRequest{
Request: &protobufs.TokenRequest_Split{
Split: &protobufs.SplitCoinRequest{
OfCoin: coin,
Amounts: amounts,
Signature: &protobufs.Ed448Signature{
Signature: sig,
PublicKey: &protobufs.Ed448PublicKey{
KeyValue: pub,
},
},
},
},
},
)
if err != nil {
panic(err)
}
},
}
func init() {
splitCmd.Flags().IntVarP(&parts, "parts", "p", 1, "number of parts to split the coin into")
splitCmd.Flags().StringVarP(&partAmount, "part-amount", "a", "", "amount of each part")
TokenCmd.AddCommand(splitCmd)
}
func Split(args []string, amounts [][]byte, payload []byte, totalAmount *big.Int) ([][]byte, []byte, error) {
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
inputAmount := new(big.Int)
for _, amt := range args {
amount, err := decimal.NewFromString(amt)
if err != nil {
return nil, nil, fmt.Errorf("invalid amount, must be a decimal number like 0.02 or 2")
}
amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
inputAmount = inputAmount.Add(inputAmount, amount.BigInt())
amountBytes := amount.BigInt().FillBytes(make([]byte, 32))
amounts = append(amounts, amountBytes)
payload = append(payload, amountBytes...)
}
// Check if the user specified amounts sum to the total amount of the coin
if inputAmount.Cmp(totalAmount) != 0 {
return nil, nil, fmt.Errorf("the specified amounts must sum to the total amount of the coin")
}
return amounts, payload, nil
}
func SplitIntoParts(amounts [][]byte, payload []byte, totalAmount *big.Int, parts int) ([][]byte, []byte) {
amount := new(big.Int).Div(totalAmount, big.NewInt(int64(parts)))
amountBytes := amount.FillBytes(make([]byte, 32))
for i := int64(0); i < int64(parts); i++ {
amounts = append(amounts, amountBytes)
payload = append(payload, amountBytes...)
}
// If there is a remainder, we need to add it as a separate amount
// because the amounts must sum to the original coin amount.
remainder := new(big.Int).Mod(totalAmount, big.NewInt(int64(parts)))
if remainder.Cmp(big.NewInt(0)) != 0 {
remainderBytes := remainder.FillBytes(make([]byte, 32))
amounts = append(amounts, remainderBytes)
payload = append(payload, remainderBytes...)
}
return amounts, payload
}
func SplitIntoPartsAmount(amounts [][]byte, payload []byte, totalAmount *big.Int, parts int, partAmount string) ([][]byte, []byte, error) {
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
amount, err := decimal.NewFromString(partAmount)
if err != nil {
return nil, nil, fmt.Errorf("invalid amount, must be a decimal number like 0.02 or 2")
}
amount = amount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
inputAmount := new(big.Int).Mul(amount.BigInt(), big.NewInt(int64(parts)))
amountBytes := amount.BigInt().FillBytes(make([]byte, 32))
for i := int64(0); i < int64(parts); i++ {
amounts = append(amounts, amountBytes)
payload = append(payload, amountBytes...)
}
// If there is a remainder, we need to add it as a separate amount
// because the amounts must sum to the original coin amount.
remainder := new(big.Int).Sub(totalAmount, inputAmount)
if remainder.Cmp(big.NewInt(0)) != 0 {
remainderBytes := remainder.FillBytes(make([]byte, 32))
amounts = append(amounts, remainderBytes)
payload = append(payload, remainderBytes...)
}
// Check if the user specified amounts sum to the total amount of the coin
if new(big.Int).Add(inputAmount, new(big.Int).Abs(remainder)).Cmp(totalAmount) != 0 {
return nil, nil, fmt.Errorf("the specified amounts must sum to the total amount of the coin")
}
return amounts, payload, nil
}
func getCoinAmount(coinaddr []byte) *big.Int {
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
peerId := utils.GetPeerIDFromConfig(NodeConfig)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
pub, err := privKey.GetPublic().Raw()
if err != nil {
panic(err)
}
addr, err := poseidon.HashBytes([]byte(peerId))
if err != nil {
panic(err)
}
addrBytes := addr.FillBytes(make([]byte, 32))
resp, err := client.GetTokensByAccount(
context.Background(),
&protobufs.GetTokensByAccountRequest{
Address: addrBytes,
},
)
if err != nil {
panic(err)
}
if len(resp.Coins) != len(resp.FrameNumbers) {
panic("invalid response from RPC")
}
altAddr, err := poseidon.HashBytes([]byte(pub))
if err != nil {
panic(err)
}
altAddrBytes := altAddr.FillBytes(make([]byte, 32))
resp2, err := client.GetTokensByAccount(
context.Background(),
&protobufs.GetTokensByAccountRequest{
Address: altAddrBytes,
},
)
if err != nil {
panic(err)
}
if len(resp.Coins) != len(resp.FrameNumbers) {
panic("invalid response from RPC")
}
var amount *big.Int
for i, coin := range resp.Coins {
if bytes.Equal(resp.Addresses[i], coinaddr) {
amount = new(big.Int).SetBytes(coin.Amount)
}
}
for i, coin := range resp2.Coins {
if bytes.Equal(resp.Addresses[i], coinaddr) {
amount = new(big.Int).SetBytes(coin.Amount)
}
}
return amount
}

View File

@ -0,0 +1,232 @@
package token_test
import (
"encoding/hex"
"math/big"
"reflect"
"strings"
"testing"
"github.com/shopspring/decimal"
"source.quilibrium.com/quilibrium/monorepo/client/cmd/token"
)
func TestSplit(t *testing.T) {
tests := []struct {
name string
args []string
totalAmount string
amounts [][]byte
payload []byte
expectError bool
}{
{
name: "Valid split - specified amounts",
args: []string{"0x1234", "0.5", "0.25", "0.25"},
totalAmount: "1.0",
amounts: [][]byte{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 40, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0},
},
payload: []byte{
115, 112, 108, 105, 116,
18, 52,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238, 107, 40, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 119, 53, 148, 0,
},
expectError: false,
},
{
name: "Invalid split - amounts do not sum to the total amount of the coin",
args: []string{"0x1234", "0.5", "0.25"},
totalAmount: "1.0",
amounts: [][]byte{},
payload: []byte{},
expectError: true,
},
{
name: "Invalid split - amounts exceed total amount of the coin",
args: []string{"0x1234", "0.5", "1"},
totalAmount: "1.0",
amounts: [][]byte{},
payload: []byte{},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload := []byte("split")
coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x")
coinaddr, err := hex.DecodeString(coinaddrHex)
if err != nil {
panic(err)
}
payload = append(payload, coinaddr...)
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
totalAmount, _ := decimal.NewFromString(tc.totalAmount)
totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
amounts := [][]byte{}
if tc.expectError {
_, _, err = token.Split(tc.args[1:], amounts, payload, totalAmount.BigInt())
if err == nil {
t.Errorf("want error for invalid split, got nil")
}
} else {
amounts, payload, err = token.Split(tc.args[1:], amounts, payload, totalAmount.BigInt())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(tc.amounts, amounts) {
t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts)
}
if !reflect.DeepEqual(tc.payload, payload) {
t.Errorf("expected payloads: %v, got: %v", tc.payload, payload)
}
}
})
}
}
func TestSplitParts(t *testing.T) {
tests := []struct {
name string
args []string
parts int
totalAmount string
amounts [][]byte
payload []byte
}{
{
name: "Valid split - into parts",
args: []string{"0x1234"},
parts: 3,
totalAmount: "1.0",
amounts: [][]byte{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2},
},
payload: []byte{
115, 112, 108, 105, 116,
18, 52,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 158, 242, 26, 170,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload := []byte("split")
coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x")
coinaddr, err := hex.DecodeString(coinaddrHex)
if err != nil {
panic(err)
}
payload = append(payload, coinaddr...)
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
totalAmount, _ := decimal.NewFromString(tc.totalAmount)
totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
amounts := [][]byte{}
amounts, payload = token.SplitIntoParts(amounts, payload, totalAmount.BigInt(), tc.parts)
if !reflect.DeepEqual(tc.amounts, amounts) {
t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts)
}
if !reflect.DeepEqual(tc.payload, payload) {
t.Errorf("expected payloads: %v, got: %v", tc.payload, payload)
}
})
}
}
func TestSplitIntoPartsAmount(t *testing.T) {
tests := []struct {
name string
args []string
parts int
partAmount string
totalAmount string
amounts [][]byte
payload []byte
expectError bool
}{
{
name: "Valid split - into parts of specified amount",
args: []string{"0x1234"},
parts: 2,
partAmount: "0.35",
totalAmount: "1.0",
amounts: [][]byte{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 13, 24, 0},
},
payload: []byte{
115, 112, 108, 105, 116,
18, 52,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 166, 228, 156, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 143, 13, 24, 0,
},
expectError: false,
},
{
name: "Invalid split - amounts exceed total amount of the coin",
args: []string{"0x1234"},
parts: 3,
partAmount: "0.5",
totalAmount: "1.0",
amounts: [][]byte{},
payload: []byte{},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
payload := []byte("split")
coinaddrHex, _ := strings.CutPrefix(tc.args[0], "0x")
coinaddr, err := hex.DecodeString(coinaddrHex)
if err != nil {
panic(err)
}
payload = append(payload, coinaddr...)
conversionFactor, _ := new(big.Int).SetString("1DCD65000", 16)
totalAmount, _ := decimal.NewFromString(tc.totalAmount)
totalAmount = totalAmount.Mul(decimal.NewFromBigInt(conversionFactor, 0))
amounts := [][]byte{}
if tc.expectError {
_, _, err = token.SplitIntoPartsAmount(amounts, payload, totalAmount.BigInt(), tc.parts, tc.partAmount)
if err == nil {
t.Errorf("want error for invalid split, got nil")
}
} else {
amounts, payload, err = token.SplitIntoPartsAmount(amounts, payload, totalAmount.BigInt(), tc.parts, tc.partAmount)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(tc.amounts, amounts) {
t.Errorf("expected amounts: %v, got: %v", tc.amounts, amounts)
}
if !reflect.DeepEqual(tc.payload, payload) {
t.Errorf("expected payloads: %v, got: %v", tc.payload, payload)
}
}
})
}
}

49
client/cmd/token/token.go Normal file
View File

@ -0,0 +1,49 @@
package token
import (
"fmt"
"os"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
var LightNode bool = false
var publicRPC bool = false
var NodeConfig *config.Config
var configDirectory string
var TokenCmd = &cobra.Command{
Use: "token",
Short: "Performs a token operation",
Run: func(cmd *cobra.Command, args []string) {
// 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)
}
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 init() {
TokenCmd.PersistentFlags().BoolVar(&publicRPC, "public-rpc", false, "Use public RPC for token operations")
TokenCmd.PersistentFlags().StringVar(&configDirectory, "config", ".config", "config directory (default is .config/)")
}

View File

@ -0,0 +1,116 @@
package token
import (
"context"
"encoding/hex"
"fmt"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
var transferCmd = &cobra.Command{
Use: "transfer",
Short: "Creates a pending transfer of coin",
Long: `Creates a pending transfer of coin:
transfer <ToAccount> <OfCoin>
ToAccount account address, must be specified
OfCoin the address of the coin to send in whole`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
panic("invalid arguments")
}
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
privKey, err := utils.GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
var coinaddr *protobufs.CoinRef
payload := []byte("transfer")
toaddr := []byte{}
for i, arg := range args {
addrHex, _ := strings.CutPrefix(arg, "0x")
addr, err := hex.DecodeString(addrHex)
if err != nil {
panic(err)
}
if i == 0 {
toaddr = addr
continue
}
coinaddr = &protobufs.CoinRef{
Address: addr,
}
payload = append(payload, addr...)
}
payload = append(payload, coinaddr.Address...)
// Display transaction details and confirmation prompt
fmt.Printf("\nTransaction Details:\n")
fmt.Printf("To Address: 0x%x\n", toaddr)
fmt.Printf("Coin Address: 0x%x\n", coinaddr.Address)
fmt.Print("\nDo you want to proceed with this transaction? (yes/no): ")
var response string
fmt.Scanln(&response)
if strings.ToLower(response) != "yes" {
fmt.Println("Transaction cancelled by user.")
return
}
sig, err := privKey.Sign(payload)
if err != nil {
panic(err)
}
pub, err := privKey.GetPublic().Raw()
if err != nil {
panic(err)
}
_, err = client.SendMessage(
context.Background(),
&protobufs.TokenRequest{
Request: &protobufs.TokenRequest_Transfer{
Transfer: &protobufs.TransferCoinRequest{
OfCoin: coinaddr,
ToAccount: &protobufs.AccountRef{
Account: &protobufs.AccountRef_ImplicitAccount{
ImplicitAccount: &protobufs.ImplicitAccount{
Address: toaddr,
},
},
},
Signature: &protobufs.Ed448Signature{
Signature: sig,
PublicKey: &protobufs.Ed448PublicKey{
KeyValue: pub,
},
},
},
},
},
)
if err != nil {
panic(err)
}
fmt.Println("Transaction sent successfully.")
},
}
func init() {
TokenCmd.AddCommand(transferCmd)
}

40
client/cmd/token/utils.go Normal file
View File

@ -0,0 +1,40 @@
package token
import (
"crypto/tls"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"github.com/multiformats/go-multiaddr"
mn "github.com/multiformats/go-multiaddr/net"
)
func GetGRPCClient() (*grpc.ClientConn, error) {
addr := "rpc.quilibrium.com:8337"
credentials := credentials.NewTLS(&tls.Config{InsecureSkipVerify: false})
if !LightNode {
ma, err := multiaddr.NewMultiaddr(NodeConfig.ListenGRPCMultiaddr)
if err != nil {
panic(err)
}
_, addr, err = mn.DialArgs(ma)
if err != nil {
panic(err)
}
credentials = insecure.NewCredentials()
}
return grpc.Dial(
addr,
grpc.WithTransportCredentials(
credentials,
),
grpc.WithDefaultCallOptions(
grpc.MaxCallSendMsgSize(600*1024*1024),
grpc.MaxCallRecvMsgSize(600*1024*1024),
),
)
}

View File

@ -1,90 +0,0 @@
package cmd
import (
"context"
"encoding/hex"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
)
var transferCmd = &cobra.Command{
Use: "transfer",
Short: "Creates a pending transfer of coin",
Long: `Creates a pending transfer of coin:
transfer <ToAccount> <OfCoin>
ToAccount account address, must be specified
OfCoin the address of the coin to send in whole
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
panic("invalid arguments")
}
conn, err := GetGRPCClient()
if err != nil {
panic(err)
}
defer conn.Close()
client := protobufs.NewNodeServiceClient(conn)
privKey, err := GetPrivKeyFromConfig(NodeConfig)
if err != nil {
panic(err)
}
pubKeyBytes, err := privKey.GetPublic().Raw()
if err != nil {
panic(err)
}
var coinaddr *protobufs.CoinRef
toaddr := []byte{}
for i, arg := range args {
addrHex, _ := strings.CutPrefix(arg, "0x")
addr, err := hex.DecodeString(addrHex)
if err != nil {
panic(err)
}
if i == 0 {
toaddr = addr
continue
}
coinaddr = &protobufs.CoinRef{
Address: addr,
}
}
transfer := &protobufs.TransferCoinRequest{
OfCoin: coinaddr,
ToAccount: &protobufs.AccountRef{
Account: &protobufs.AccountRef_ImplicitAccount{
ImplicitAccount: &protobufs.ImplicitAccount{
Address: toaddr,
},
},
},
}
if err := transfer.SignED448(pubKeyBytes, privKey.Sign); err != nil {
panic(err)
}
if err := transfer.Validate(); err != nil {
panic(err)
}
_, err = client.SendMessage(
context.Background(),
transfer.TokenRequest(),
)
if err != nil {
panic(err)
}
},
}
func init() {
tokenCmd.AddCommand(transferCmd)
}

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

@ -0,0 +1,167 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
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.
If the current version is already the latest version, the command will exit with a message.
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) {
currentVersion := config.GetVersionString()
// 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)
}
if version == currentVersion {
fmt.Fprintf(os.Stdout, "Already on version %s\n", currentVersion)
return
}
// 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
}
// 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) {
// Construct executable path
execPath := filepath.Join(utils.ClientDataPath, version, "qclient-"+version+"-"+osType+"-"+arch)
// 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
// 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)
}

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

@ -0,0 +1,89 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/spf13/cobra"
"source.quilibrium.com/quilibrium/monorepo/client/utils"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
// Version information - fallback if executable name doesn't contain version
var (
DefaultVersion = config.GetVersionString()
)
// 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("%s\n", info.Version)
if showChecksum {
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)
}

31
client/test/Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Use build argument to specify the base image
ARG DISTRO=ubuntu
ARG VERSION=24.04
# Base stage with common setup
FROM --platform=$BUILDPLATFORM ${DISTRO}:${VERSION} AS base
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 \
bash-completion \
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
# Final test stage
FROM base AS qclient-test
WORKDIR /app
CMD ["/app/test_install.sh"]

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

@ -0,0 +1,165 @@
# 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
sudo task build_qclient_amd64_linux
sudo task build_qclient_arm64_linux
# for mac, you will need to build on a mac
```
## Run a test container
```
sudo docker run -it \
-v "/home/user/ceremonyclient/client/test/:/app" \
-v "/home/user/ceremenoyclient/client/build/amd64_linux/qclient:/opt/quilibrium/bin/qclient" \
quil-test /bin/bash
This command builds the Docker image with the qclient binary according to the specifications in `Dockerfile.source`. 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

View File

@ -0,0 +1,3 @@
dataDir: /var/quilibrium/data/qclient
symlinkPath: /usr/local/bin/qclient
signatureCheck: true

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

@ -0,0 +1,124 @@
#!/bin/bash
set -e
CLIENT_DIR="${CLIENT_DIR:-$( cd "$(dirname "$(realpath "$( dirname "${BASH_SOURCE[0]}" )")")" >/dev/null 2>&1 && pwd )}"
echo "CLIENT_DIR: $CLIENT_DIR"
# 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 " --no-cache Disable all Docker build cache"
echo ""
echo "If no arguments are provided, runs tests on all supported distributions"
exit 0
}
# Parse command line arguments
DISTRO=""
VERSION=""
TAG=""
NO_CACHE=""
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
;;
--no-cache)
NO_CACHE="--no-cache"
shift
;;
-h|--help)
show_help
;;
*)
echo "Unknown option: $1"
show_help
;;
esac
done
# 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..."
# Build the base stage first (this can be cached)
docker build \
$NO_CACHE \
--build-arg DISTRO=$distro \
--build-arg VERSION=$version \
-t quil-test-$tag-base \
--target base \
-f client/test/Dockerfile .
# Build the final test stage
docker build \
--build-arg DISTRO=$distro \
--build-arg VERSION=$version \
-t quil-test-$tag \
--target qclient-test \
-f client/test/Dockerfile .
# Ensure test files are executable
chmod +x "$CLIENT_DIR/test/test_install.sh"
chmod +x "$CLIENT_DIR/test/test_utils.sh"
chmod +x "$CLIENT_DIR/build/amd64_linux/qclient"
# Set ownership to match testuser (uid:gid 1000:1000)
chown 1000:1000 "$CLIENT_DIR/build/amd64_linux/qclient"
# Run the container with mounted test directory and binary
docker run --rm \
-v "$CLIENT_DIR/test:/app" \
-v "$CLIENT_DIR/build/amd64_linux/qclient:/opt/quilibrium/bin/qclient" \
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!"

127
client/test/test_install.sh Executable file
View File

@ -0,0 +1,127 @@
#!/bin/bash
set -e
# Source the test utilities
source "$(dirname "$0")/test_utils.sh"
# 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: Link the qclient binary to ensure it's in the PATH
echo "Linking qclient binary for testing..."
if [ -f "/opt/quilibrium/bin/qclient" ]; then
echo "qclient binary already exists at /opt/quilibrium/bin/qclient"
else
echo "qclient binary not found at /opt/quilibrium/bin/qclient"
exit 1
fi
# Test: Link the qclient binary to the system PATH
echo "Testing qclient link command..."
run_test_with_format "sudo /opt/quilibrium/bin/qclient link"
# Verify qclient is now in PATH
echo "Verifying qclient is in PATH after link command..."
run_test_with_format "which qclient" | grep -q "/usr/local/bin/qclient" && echo "SUCCESS: qclient found in PATH" || echo "FAILURE: qclient not found in PATH"
# Test qclient can be executed directly
echo "Testing qclient can be executed directly..."
run_test_with_format "qclient --help" | grep -q "Usage:" && echo "SUCCESS: qclient executable works" || echo "FAILURE: qclient executable not working properly"
# Test: Ensure no config file exists initially
echo "Testing no config file exists initially..."
run_test_with_format "test ! -f /etc/quilibrium/config/qclient.yaml"
# Test: Create default config
echo "Testing default config creation..."
run_test_with_format "qclient config create-default --signature-check=false"
# Test: Verify config file was created
echo "Verifying config file was created..."
run_test_with_format "test -f /etc/quilibrium/config/qclient.yaml"
# Test: Excec arbitrary qclient command and verify signature check
echo "Testing config print command..."
run_test_with_format "qclient config print" | grep -v "Checking signature for"
# Test: Toggle signature check
echo "Testing toggle-signature-check command..."
run_test_with_format "qclient config toggle-signature-check --signature-check=false"
run_test_with_format "qclient config print" | grep -v "Checking signature for"
# Test: Ensure qclient is in the PATH
echo "Testing qclient in PATH..."
run_test_with_format "sudo /opt/quilibrium/bin/qclient link"
run_test_with_format "which qclient"
run_test_with_format "qclient version"
run_test_with_format "qclient config print"
# Test 0: Install latest version
# Check if download-signatures command exists in qclient help
run_test_with_format "qclient help | grep -q 'download-signatures'"
# Test downloading signatures
run_test_with_format "sudo qclient download-signatures"
# Test 1: Install latest version
run_test_with_format "sudo 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
run_test_with_format "test -f /opt/quilibrium/$LATEST_VERSION/node-$LATEST_VERSION-linux-amd64"
# Verify latest version matches
run_test_with_format "get_latest_version"
# Test 2: Install specific version
run_test_with_format "qclient node install '2.0.6.2' --signature-check=false"
# Verify specific version installation
run_test_with_format "test -f /opt/quilibrium/2.0.6.2/node-2.0.6.2-linux-amd64"
# Test 3: Verify service file creation
run_test_with_format "test -f /etc/systemd/system/quilibrium-node.service"
# Verify service file content
run_test_with_format "grep -q 'EnvironmentFile=/etc/default/quilibrium-node' /etc/systemd/system/quilibrium-node.service"
# Test 4: Verify environment file
run_test_with_format "test -f /etc/default/quilibrium-node"
# Verify environment file permissions
run_test_with_format "test '$(stat -c %a /etc/default/quilibrium-node)' = '640'"
# Test 5: Verify data directory
run_test_with_format "test -d /var/lib/quilibrium"
# Verify data directory permissions
run_test_with_format "test '$(stat -c %a /var/lib/quilibrium)' = '755'"
# Test 6: Verify config file
run_test_with_format "test -f /var/lib/quilibrium/config/node.yaml"
# Verify config file permissions
run_test_with_format "test '$(stat -c %a /var/lib/quilibrium/config/node.yaml)' = '644'"
# Test 7: Verify binary symlink
run_test_with_format "test -L /usr/local/bin/quilibrium-node"
# Test 8: Verify binary execution
run_test_with_format "quilibrium-node --version"
echo "All tests passed successfully on $DISTRO $VERSION!"

49
client/test/test_utils.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# ANSI color codes
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Function to run a test command and format its output
run_test_with_format() {
local test_cmd="$1"
local indent=" " # 4 spaces for indentation
echo -e "${BLUE}Running test: $test_cmd${NC}"
echo "----------------------------------------"
# Run the command and capture stdout and stderr separately
local stdout
local stderr
stdout=$(eval "$test_cmd" 2> >(tee /dev/stderr))
exit_code=$?
stderr=$(cat)
# Format and print the stdout with indentation
if [ -n "$stdout" ]; then
echo "$stdout" | while IFS= read -r line; do
echo -e "${GREEN}$indent$line${NC}"
done
fi
# Check for stderr output and exit code
if [ -n "$stderr" ] || [ $exit_code -ne 0 ]; then
echo -e "${RED}${indent}Test failed:${NC}"
if [ -n "$stderr" ]; then
echo "$stderr" | while IFS= read -r line; do
echo -e "${RED}${indent}$line${NC}"
done
fi
if [ $exit_code -ne 0 ]; then
echo -e "${RED}${indent}Exit code: $exit_code${NC}"
fi
echo "----------------------------------------"
return 1
fi
echo -e "${GREEN}${indent}Test completed successfully${NC}"
echo "----------------------------------------"
return 0
}

91
client/utils/config.go Normal file
View File

@ -0,0 +1,91 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
var ClientConfigDir = filepath.Join(os.Getenv("HOME"), ".quilibrium")
var ClientConfigFile = string(ReleaseTypeQClient) + "-config.yaml"
var ClientConfigPath = filepath.Join(ClientConfigDir, ClientConfigFile)
func CreateDefaultConfig() {
configPath := GetConfigPath()
fmt.Printf("Creating default config: %s\n", configPath)
SaveClientConfig(&ClientConfig{
DataDir: ClientDataPath,
SymlinkPath: DefaultQClientSymlinkPath,
SignatureCheck: true,
})
sudoUser, err := GetCurrentSudoUser()
if err != nil {
fmt.Println("Error getting current sudo user")
os.Exit(1)
}
ChownPath(GetUserQuilibriumDir(), sudoUser, true)
}
// LoadClientConfig loads the client configuration from the config file
func LoadClientConfig() (*ClientConfig, error) {
configPath := GetConfigPath()
// Create default config if it doesn't exist
if _, err := os.Stat(configPath); os.IsNotExist(err) {
config := &ClientConfig{
DataDir: ClientDataPath,
SymlinkPath: filepath.Join(ClientDataPath, "current"),
SignatureCheck: true,
}
if err := SaveClientConfig(config); err != nil {
return nil, err
}
return config, nil
}
// Read existing config
data, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
config := &ClientConfig{}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, err
}
return config, nil
}
// SaveClientConfig saves the client configuration to the config file
func SaveClientConfig(config *ClientConfig) error {
data, err := yaml.Marshal(config)
if err != nil {
return err
}
// Ensure the config directory exists
if err := os.MkdirAll(GetConfigDir(), 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
return os.WriteFile(GetConfigPath(), data, 0644)
}
// GetConfigPath returns the path to the client configuration file
func GetConfigPath() string {
return filepath.Join(GetConfigDir(), ClientConfigFile)
}
func GetConfigDir() string {
return filepath.Join(GetUserQuilibriumDir())
}
// IsClientConfigured checks if the client is configured
func IsClientConfigured() bool {
return FileExists(ClientConfigPath)
}

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

@ -0,0 +1,208 @@
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)
fmt.Printf("Getting binary %s...\n", fileName)
fmt.Println("Will save to", filepath.Join(BinaryPath, string(releaseType), version))
url := fmt.Sprintf("%s/%s", BaseReleaseURL, fileName)
if !DoesRemoteFileExist(url) {
fmt.Printf("the release file %s does not exist on the release server\n", fileName)
os.Exit(1)
}
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)
destDir := filepath.Join(BinaryPath, string(releaseType), version)
os.MkdirAll(destDir, 0755)
destPath := filepath.Join(destDir, fileName)
fmt.Printf("Downloading %s...", 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.Print(" done\n")
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)
fmt.Printf("Searching for signatures for %s from %s\n", baseName, BaseReleaseURL)
fmt.Println("Will save to", filepath.Join(BinaryPath, string(releaseType), version))
// 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 {
// Check if the remote signature file exists
sigFile := fmt.Sprintf("%s.dgst.sig.%d", baseName, num)
remoteURL := fmt.Sprintf("%s/%s", BaseReleaseURL, sigFile)
if !DoesRemoteFileExist(remoteURL) {
continue
}
fmt.Printf("Found signature file %s\n", sigFile)
files = append(files, fmt.Sprintf("%s.dgst.sig.%d", baseName, num))
}
if len(files) == 0 {
fmt.Printf("No signature files found for %s\n", baseName)
return nil
}
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
}
func DoesRemoteFileExist(url string) bool {
resp, err := http.Head(url)
if err != nil || resp.StatusCode != http.StatusOK {
return false
}
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
return true
}

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

@ -0,0 +1,237 @@
package utils
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
)
var ClientInstallPath = filepath.Join("/opt/quilibrium/", string(ReleaseTypeQClient))
var RootQuilibriumPath = filepath.Join("/var/quilibrium/")
var BinaryPath = filepath.Join(RootQuilibriumPath, "bin")
var ClientDataPath = filepath.Join(BinaryPath, string(ReleaseTypeQClient))
var NodeDataPath = filepath.Join(BinaryPath, string(ReleaseTypeNode))
var DefaultSymlinkDir = "/usr/local/bin"
var DefaultNodeSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeNode))
var DefaultQClientSymlinkPath = filepath.Join(DefaultSymlinkDir, string(ReleaseTypeQClient))
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)
}
}
fmt.Printf("Creating symlink %s -> %s\n", targetPath, execPath)
// Create the symlink
if err := os.Symlink(execPath, targetPath); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
return nil
}
// ValidateAndCreateDir validates a directory path and creates it if it doesn't exist
func ValidateAndCreateDir(path string, user *user.User) 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) {
fmt.Printf("Creating directory %s\n", path)
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", path, err)
}
if user != nil {
ChownPath(path, user, false)
}
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)
}
func IsSudo() bool {
user, err := user.Current()
if err != nil {
return false
}
return user.Username == "root"
}
// ChownPath changes the owner of a file or directory to the specified user
func ChownPath(path string, user *user.User, isRecursive bool) error {
// Change ownership of the path
if isRecursive {
fmt.Printf("Changing ownership of %s (recursive) to %s\n", path, user.Username)
if err := exec.Command("chown", "-R", user.Uid+":"+user.Gid, path).Run(); err != nil {
return fmt.Errorf("failed to change ownership of %s to %s (requires sudo): %v", path, user.Uid, err)
}
} else {
fmt.Printf("Changing ownership of %s to %s\n", path, user.Username)
if err := exec.Command("chown", user.Uid+":"+user.Gid, path).Run(); err != nil {
return fmt.Errorf("failed to change ownership of %s to %s (requires sudo): %v", path, user.Uid, err)
}
}
return nil
}
func ChmodPath(path string, mode os.FileMode, description string) error {
fmt.Printf("Changing path: %s to %s (%s)\n", path, mode, description)
return os.Chmod(path, mode)
}
func WriteFile(path string, content string) error {
return os.WriteFile(path, []byte(content), 0644)
}
// WriteFileAuto writes content to a file, automatically using sudo only if necessary
func WriteFileAuto(path string, content string) error {
// First check if file exists and is writable
if FileExists(path) {
// Try to open the file for writing to check permissions
file, err := os.OpenFile(path, os.O_WRONLY, 0)
if err == nil {
// File is writable, close it and write normally
file.Close()
fmt.Printf("Writing to file %s using normal permissions\n", path)
return os.WriteFile(path, []byte(content), 0644)
}
} else {
// Check if parent directory is writable
dir := filepath.Dir(path)
if IsWritable(dir) {
fmt.Printf("Writing to file %s using normal permissions\n", path)
return os.WriteFile(path, []byte(content), 0644)
}
}
// If we reach here, sudo is needed
fmt.Printf("Writing to file %s using sudo\n", path)
cmd := exec.Command("sudo", "tee", path)
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to get stdin pipe: %w", err)
}
// Start the command
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start sudo command: %w", err)
}
// Write content to stdin
if _, err := io.WriteString(stdin, content); err != nil {
return fmt.Errorf("failed to write to stdin: %w", err)
}
stdin.Close()
// Wait for the command to finish
if err := cmd.Wait(); err != nil {
return fmt.Errorf("sudo tee command failed: %w", err)
}
return nil
}
// CopyFile copies a file from src to dst
func CopyFile(src, dst string) error {
fmt.Printf("Copying file from %s to %s\n", src, dst)
sourceData, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, sourceData, 0600)
}

64
client/utils/node.go Normal file
View File

@ -0,0 +1,64 @@
package utils
import (
"encoding/hex"
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/pkg/errors"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"source.quilibrium.com/quilibrium/monorepo/node/config"
)
func GetPeerIDFromConfig(cfg *config.Config) peer.ID {
peerPrivKey, err := hex.DecodeString(cfg.P2P.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"))
}
return id
}
func GetPrivKeyFromConfig(cfg *config.Config) (crypto.PrivKey, error) {
peerPrivKey, err := hex.DecodeString(cfg.P2P.PeerPrivKey)
if err != nil {
panic(errors.Wrap(err, "error unmarshaling peerkey"))
}
privKey, err := crypto.UnmarshalEd448PrivateKey(peerPrivKey)
return privKey, err
}
func IsExistingNodeVersion(version string) bool {
return FileExists(filepath.Join(NodeDataPath, version))
}
func CheckForSystemd() bool {
// Check if systemctl command exists
_, err := exec.LookPath("systemctl")
return err == nil
}
func LoadNodeConfig(configDirectory string) (*config.Config, error) {
NodeConfig, err := config.LoadConfig(configDirectory, "", false)
if err != nil {
fmt.Printf("invalid config directory: %s\n", configDirectory)
os.Exit(1)
}
return NodeConfig, nil
}

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

@ -0,0 +1,66 @@
package utils
import (
"bytes"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
)
// 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
}
func GetCurrentSudoUser() (*user.User, error) {
if os.Geteuid() != 0 {
return user.Current()
}
cmd := exec.Command("sh", "-c", "env | grep SUDO_USER | cut -d= -f2 | cut -d\\n -f1")
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("failed to get current sudo user: %v", err)
}
userLookup, err := user.Lookup(strings.TrimSpace(out.String()))
if err != nil {
return nil, fmt.Errorf("failed to get current sudo user: %v", err)
}
return userLookup, nil
}
func GetUserQuilibriumDir() string {
sudoUser, err := GetCurrentSudoUser()
if err != nil {
fmt.Println("Error getting current sudo user")
os.Exit(1)
}
return filepath.Join(sudoUser.HomeDir, ".quilibrium")
}

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

@ -0,0 +1,55 @@
package utils
type ClientConfig struct {
DataDir string `yaml:"dataDir"`
SymlinkPath string `yaml:"symlinkPath"`
SignatureCheck bool `yaml:"signatureCheck"`
}
type NodeConfig struct {
ClientConfig
RewardsAddress string `yaml:"rewardsAddress"`
AutoUpdateInterval string `yaml:"autoUpdateInterval"`
}
const (
DefaultAutoUpdateInterval = "*/10 * * * *"
)
type ReleaseType string
const (
ReleaseTypeQClient ReleaseType = "qclient"
ReleaseTypeNode ReleaseType = "node"
)
type BridgedPeerJson struct {
Amount string `json:"amount"`
Identifier string `json:"identifier"`
Variant string `json:"variant"`
}
type FirstRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}
type SecondRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
JanPresence bool `json:"janPresence"`
FebPresence bool `json:"febPresence"`
MarPresence bool `json:"marPresence"`
AprPresence bool `json:"aprPresence"`
MayPresence bool `json:"mayPresence"`
}
type ThirdRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}
type FourthRetroJson struct {
PeerId string `json:"peerId"`
Reward string `json:"reward"`
}

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

@ -9,7 +9,6 @@ name = "channel"
[dependencies]
base64 = "0.22.1"
hex = "0.4.3"
serde_json = "1.0.117"
ed448-goldilocks-plus = "0.11.2"
rand = "0.8.5"

View File

@ -23,11 +23,11 @@ use super::super::gmp::mpz::{mp_bitcnt_t, mp_limb_t};
use libc::{c_int, c_long, c_ulong, c_void, size_t};
// pub use c_ulong;
use std::{mem, usize};
// We use the unsafe versions to avoid unecessary allocations.
// We use the unsafe versions to avoid unnecessary allocations.
extern "C" {
fn adapted_nudupl(a: *mut Mpz, b: *mut Mpz, c: *mut Mpz, times: c_ulong);
}
// We use the unsafe versions to avoid unecessary allocations.
// We use the unsafe versions to avoid unnecessary allocations.
#[link(name = "gmp")]
extern "C" {
fn __gmpz_gcdext(gcd: *mut Mpz, s: *mut Mpz, t: *mut Mpz, a: *const Mpz, b: *const Mpz);

View File

@ -8,15 +8,11 @@ crate-type = ["lib", "staticlib"]
name = "rpm"
[dependencies]
hex = "0.4.3"
serde_json = "1.0.117"
uniffi = { version= "0.25", features = ["cli"]}
curve25519-dalek = "4.1.3"
rand = "0.8.5"
num = "0.4.3"
lazy_static = "1.5.0"
subtle = "2.6.1"
rayon = "1.10"
[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }

View File

@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#![deny(warnings)]
//! # Rust implementations of class groups and verifyable delay functions
//! # Rust implementations of class groups and verifiable delay functions
//!
//! This repo includes three crates
//!
//! * `classgroup`, which includes a class group implementation, as well as a
//! trait for class groups.
//! * `vdf`, which includes a Verifyable Delay Function (VDF) trait, as well as
//! * `vdf`, which includes a Verifiable Delay Function (VDF) trait, as well as
//! an implementation of that trait.
//! * `vdf-cli`, which includes a command-line interface to the `vdf` crate. It
//! also includes additional commands, which are deprecated and will later be
@ -155,7 +155,7 @@ pub trait VDFParams: Clone + Eq {
/// consumers of this trait **MUST NOT** expect this.
///
/// Instances of this trait are *not* expected to be `Sync`. This allows them
/// to reuse allocations (such as scratch memory) accross invocations without
/// to reuse allocations (such as scratch memory) across invocations without
/// the need for locking. However, they **MUST** be `Send` and `Clone`, so that
/// consumers can duplicate them and send them across threads.
pub trait VDF: Send + Debug {
@ -170,7 +170,7 @@ pub trait VDF: Send + Debug {
/// The challenge is an opaque byte string of arbitrary length.
/// Implementors **MUST NOT** make any assumptions about its contents,
/// and **MUST** produce distinct outputs for distinct challenges
/// (except with negiligible probability).
/// (except with negligible probability).
///
/// This can be most easily implemented by using the challenge as part of
/// the input of a cryptographic hash function. The VDFs provided in this
@ -209,7 +209,7 @@ pub trait VDF: Send + Debug {
///
/// # Rationale
///
/// It would be more ideomatic Rust to use the type system to enforce that a
/// It would be more idiomatic Rust to use the type system to enforce that a
/// difficulty has been validated before use. However, I (Demi) have not
/// yet figured out an object-safe way to do so.
fn check_difficulty(&self, difficulty: u64) -> Result<(), InvalidIterations>;

View File

@ -454,7 +454,7 @@ mod test {
assert_eq!(calculate_final_t(Iterations(100), 8), 100);
}
#[test]
fn check_assuptions_about_stdlib() {
fn check_assumptions_about_stdlib() {
assert_eq!(62 - u64::leading_zeros(1024u64), 9);
let mut q: Vec<_> = (1..4).step_by(2).collect();
assert_eq!(q[..], [1, 3]);

View File

@ -91,7 +91,7 @@ pub fn approximate_parameters(t: f64) -> (usize, u8, u64) {
fn u64_to_bytes(q: u64) -> [u8; 8] {
if false {
// This use of `std::mem::transumte` is correct, but still not justified.
// This use of `std::mem::transmute` is correct, but still not justified.
unsafe { std::mem::transmute(q.to_be()) }
} else {
[

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
);