diff --git a/Dockerfile.release b/Dockerfile.release index f6f3b2e..fec9d52 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -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 diff --git a/Dockerfile.source b/Dockerfile.source index 3ebfe58..89f9475 100644 --- a/Dockerfile.source +++ b/Dockerfile.source @@ -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 diff --git a/Dockerfile.source.dockerignore b/Dockerfile.source.dockerignore new file mode 100644 index 0000000..3f1c209 --- /dev/null +++ b/Dockerfile.source.dockerignore @@ -0,0 +1,12 @@ +client/build/ +target/ +node/build/ +node/.vscode/ +.git +.gitignore +vdf/generated/ +bls48581/generated/ +ferret/generated/ +sidecar/generated/ +verenc/generated/ +Dockerfile* \ No newline at end of file diff --git a/Dockerfile.sourceavx512 b/Dockerfile.sourceavx512 index c5b1dc8..f94cb04 100644 --- a/Dockerfile.sourceavx512 +++ b/Dockerfile.sourceavx512 @@ -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" ] diff --git a/Taskfile.yaml b/Taskfile.yaml index 575384e..03cc072 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -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' diff --git a/client/build.sh b/client/build.sh index 1d2757f..5d889b7 100755 --- a/client/build.sh +++ b/client/build.sh @@ -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" diff --git a/client/cmd/config.go b/client/cmd/config.go deleted file mode 100644 index a0c4b7e..0000000 --- a/client/cmd/config.go +++ /dev/null @@ -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) -} diff --git a/client/cmd/config/config.go b/client/cmd/config/config.go new file mode 100644 index 0000000..9895045 --- /dev/null +++ b/client/cmd/config/config.go @@ -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) +} diff --git a/client/cmd/config/signatureCheck.go b/client/cmd/config/signatureCheck.go new file mode 100644 index 0000000..794a159 --- /dev/null +++ b/client/cmd/config/signatureCheck.go @@ -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) +} diff --git a/client/cmd/config/utils.go b/client/cmd/config/utils.go new file mode 100644 index 0000000..d912156 --- /dev/null +++ b/client/cmd/config/utils.go @@ -0,0 +1 @@ +package config diff --git a/client/cmd/download-signatures.go b/client/cmd/download-signatures.go new file mode 100644 index 0000000..9d1e0a5 --- /dev/null +++ b/client/cmd/download-signatures.go @@ -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) +} diff --git a/client/cmd/link.go b/client/cmd/link.go new file mode 100644 index 0000000..21aa579 --- /dev/null +++ b/client/cmd/link.go @@ -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) +} diff --git a/client/cmd/node/autoUpdate.go b/client/cmd/node/autoUpdate.go new file mode 100644 index 0000000..06816fa --- /dev/null +++ b/client/cmd/node/autoUpdate.go @@ -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") + } +} diff --git a/client/cmd/node/clean.go b/client/cmd/node/clean.go new file mode 100644 index 0000000..6fe3735 --- /dev/null +++ b/client/cmd/node/clean.go @@ -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' diff --git a/client/cmd/node/info.go b/client/cmd/node/info.go new file mode 100644 index 0000000..c905c6e --- /dev/null +++ b/client/cmd/node/info.go @@ -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) +} diff --git a/client/cmd/node/install.go b/client/cmd/node/install.go new file mode 100644 index 0000000..fe56147 --- /dev/null +++ b/client/cmd/node/install.go @@ -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 +} diff --git a/client/cmd/node/log/clean.go b/client/cmd/node/log/clean.go new file mode 100644 index 0000000..101e149 --- /dev/null +++ b/client/cmd/node/log/clean.go @@ -0,0 +1,3 @@ +package log + +// TODO: Implement a log subcommand to clean the logs diff --git a/client/cmd/node/log/log.go b/client/cmd/node/log/log.go new file mode 100644 index 0000000..9310e83 --- /dev/null +++ b/client/cmd/node/log/log.go @@ -0,0 +1,3 @@ +package log + +// TODO: Implement a command to view and manage the logs diff --git a/client/cmd/node/node.go b/client/cmd/node/node.go new file mode 100644 index 0000000..7ee5e06 --- /dev/null +++ b/client/cmd/node/node.go @@ -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 +} diff --git a/client/cmd/node/nodeconfig/assign-rewards.go b/client/cmd/node/nodeconfig/assign-rewards.go new file mode 100644 index 0000000..7d1a917 --- /dev/null +++ b/client/cmd/node/nodeconfig/assign-rewards.go @@ -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 diff --git a/client/cmd/node/nodeconfig/config.go b/client/cmd/node/nodeconfig/config.go new file mode 100644 index 0000000..895646c --- /dev/null +++ b/client/cmd/node/nodeconfig/config.go @@ -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) + +} diff --git a/client/cmd/node/nodeconfig/create.go b/client/cmd/node/nodeconfig/create.go new file mode 100644 index 0000000..140e55c --- /dev/null +++ b/client/cmd/node/nodeconfig/create.go @@ -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.") + }, +} diff --git a/client/cmd/node/nodeconfig/import.go b/client/cmd/node/nodeconfig/import.go new file mode 100644 index 0000000..6b10529 --- /dev/null +++ b/client/cmd/node/nodeconfig/import.go @@ -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() { + +} diff --git a/client/cmd/node/nodeconfig/set.go b/client/cmd/node/nodeconfig/set.go new file mode 100644 index 0000000..fac7435 --- /dev/null +++ b/client/cmd/node/nodeconfig/set.go @@ -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) + }, +} diff --git a/client/cmd/node/nodeconfig/switch.go b/client/cmd/node/nodeconfig/switch.go new file mode 100644 index 0000000..4545bd0 --- /dev/null +++ b/client/cmd/node/nodeconfig/switch.go @@ -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) + }, +} diff --git a/client/cmd/node/nodeconfig/utils.go b/client/cmd/node/nodeconfig/utils.go new file mode 100644 index 0000000..a255c89 --- /dev/null +++ b/client/cmd/node/nodeconfig/utils.go @@ -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 +} diff --git a/client/cmd/bridged.json b/client/cmd/node/prover/premainnet-data/bridged.json similarity index 100% rename from client/cmd/bridged.json rename to client/cmd/node/prover/premainnet-data/bridged.json diff --git a/client/cmd/first_retro.json b/client/cmd/node/prover/premainnet-data/first_retro.json similarity index 100% rename from client/cmd/first_retro.json rename to client/cmd/node/prover/premainnet-data/first_retro.json diff --git a/client/cmd/fourth_retro.json b/client/cmd/node/prover/premainnet-data/fourth_retro.json similarity index 100% rename from client/cmd/fourth_retro.json rename to client/cmd/node/prover/premainnet-data/fourth_retro.json diff --git a/client/cmd/second_retro.json b/client/cmd/node/prover/premainnet-data/second_retro.json similarity index 100% rename from client/cmd/second_retro.json rename to client/cmd/node/prover/premainnet-data/second_retro.json diff --git a/client/cmd/third_retro.json b/client/cmd/node/prover/premainnet-data/third_retro.json similarity index 100% rename from client/cmd/third_retro.json rename to client/cmd/node/prover/premainnet-data/third_retro.json diff --git a/client/cmd/node/prover/prover.go b/client/cmd/node/prover/prover.go new file mode 100644 index 0000000..4d19f2c --- /dev/null +++ b/client/cmd/node/prover/prover.go @@ -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) +} diff --git a/client/cmd/proverMerge.go b/client/cmd/node/prover/proverMerge.go similarity index 63% rename from client/cmd/proverMerge.go rename to client/cmd/node/prover/proverMerge.go index d527554..968f210 100644 --- a/client/cmd/proverMerge.go +++ b/client/cmd/node/prover/proverMerge.go @@ -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) } diff --git a/client/cmd/proverPause.go b/client/cmd/node/prover/proverPause.go similarity index 87% rename from client/cmd/proverPause.go rename to client/cmd/node/prover/proverPause.go index 91b0a4f..30c780b 100644 --- a/client/cmd/proverPause.go +++ b/client/cmd/node/prover/proverPause.go @@ -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) } diff --git a/client/cmd/node/service.go b/client/cmd/node/service.go new file mode 100644 index 0000000..1984acb --- /dev/null +++ b/client/cmd/node/service.go @@ -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 := ` + + + + Label + {{.Label}} + ProgramArguments + + /usr/local/bin/quilibrium-node + --config + /opt/quilibrium/config/ + + EnvironmentVariables + + QUILIBRIUM_DATA_DIR + {{.DataPath}} + QUILIBRIUM_LOG_LEVEL + info + QUILIBRIUM_LISTEN_GRPC_MULTIADDR + /ip4/127.0.0.1/tcp/8337 + QUILIBRIUM_LISTEN_REST_MULTIADDR + /ip4/127.0.0.1/tcp/8338 + QUILIBRIUM_STATS_MULTIADDR + /dns/stats.quilibrium.com/tcp/443 + QUILIBRIUM_NETWORK_ID + 0 + QUILIBRIUM_DEBUG + false + QUILIBRIUM_SIGNATURE_CHECK + true + + RunAtLoad + + KeepAlive + + StandardErrorPath + {{.LogPath}}/node.err + StandardOutPath + {{.LogPath}}/node.log + +` + + // 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) +} diff --git a/client/cmd/node/shared.go b/client/cmd/node/shared.go new file mode 100644 index 0000000..4fe6a89 --- /dev/null +++ b/client/cmd/node/shared.go @@ -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 \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") +} diff --git a/client/cmd/node/uninstall.go b/client/cmd/node/uninstall.go new file mode 100644 index 0000000..9c93775 --- /dev/null +++ b/client/cmd/node/uninstall.go @@ -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) diff --git a/client/cmd/node/update.go b/client/cmd/node/update.go new file mode 100644 index 0000000..718becc --- /dev/null +++ b/client/cmd/node/update.go @@ -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) +} diff --git a/client/cmd/node/utils.go b/client/cmd/node/utils.go new file mode 100644 index 0000000..2b4023a --- /dev/null +++ b/client/cmd/node/utils.go @@ -0,0 +1 @@ +package node diff --git a/client/cmd/prover.go b/client/cmd/prover.go deleted file mode 100644 index fdf91cd..0000000 --- a/client/cmd/prover.go +++ /dev/null @@ -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) -} diff --git a/client/cmd/root.go b/client/cmd/root.go index 292a816..6548bef 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -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) } diff --git a/client/cmd/split.go b/client/cmd/split.go deleted file mode 100644 index 1274ce5..0000000 --- a/client/cmd/split.go +++ /dev/null @@ -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 - 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) -} diff --git a/client/cmd/token.go b/client/cmd/token.go deleted file mode 100644 index 496256c..0000000 --- a/client/cmd/token.go +++ /dev/null @@ -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) -} diff --git a/client/cmd/accept.go b/client/cmd/token/accept.go similarity index 89% rename from client/cmd/accept.go rename to client/cmd/token/accept.go index b79119d..eafe564 100644 --- a/client/cmd/accept.go +++ b/client/cmd/token/accept.go @@ -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) } diff --git a/client/cmd/token/account.go b/client/cmd/token/account.go new file mode 100644 index 0000000..1d911cb --- /dev/null +++ b/client/cmd/token/account.go @@ -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) +} diff --git a/client/cmd/all.go b/client/cmd/token/all.go similarity index 97% rename from client/cmd/all.go rename to client/cmd/token/all.go index 4f623c8..32986d1 100644 --- a/client/cmd/all.go +++ b/client/cmd/token/all.go @@ -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) } diff --git a/client/cmd/balance.go b/client/cmd/token/balance.go similarity index 89% rename from client/cmd/balance.go rename to client/cmd/token/balance.go index ab1fc57..a1b97d0 100644 --- a/client/cmd/balance.go +++ b/client/cmd/token/balance.go @@ -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) } diff --git a/client/cmd/coins.go b/client/cmd/token/coins.go similarity index 93% rename from client/cmd/coins.go rename to client/cmd/token/coins.go index 854a888..0f2e7d2 100644 --- a/client/cmd/coins.go +++ b/client/cmd/token/coins.go @@ -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) } diff --git a/client/cmd/merge.go b/client/cmd/token/merge.go similarity index 74% rename from client/cmd/merge.go rename to client/cmd/token/merge.go index aa8a7da..2500a6d 100644 --- a/client/cmd/merge.go +++ b/client/cmd/token/merge.go @@ -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) } diff --git a/client/cmd/mint.go b/client/cmd/token/mint.go similarity index 76% rename from client/cmd/mint.go rename to client/cmd/token/mint.go index 0ad82c7..1149e5c 100644 --- a/client/cmd/mint.go +++ b/client/cmd/token/mint.go @@ -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) } diff --git a/client/cmd/mutualReceive.go b/client/cmd/token/mutualReceive.go similarity index 88% rename from client/cmd/mutualReceive.go rename to client/cmd/token/mutualReceive.go index 0971589..6695760 100644 --- a/client/cmd/mutualReceive.go +++ b/client/cmd/token/mutualReceive.go @@ -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) } diff --git a/client/cmd/mutualTransfer.go b/client/cmd/token/mutualTransfer.go similarity index 91% rename from client/cmd/mutualTransfer.go rename to client/cmd/token/mutualTransfer.go index c65b1bb..310abca 100644 --- a/client/cmd/mutualTransfer.go +++ b/client/cmd/token/mutualTransfer.go @@ -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) } diff --git a/client/cmd/reject.go b/client/cmd/token/reject.go similarity index 89% rename from client/cmd/reject.go rename to client/cmd/token/reject.go index c120668..ab972f9 100644 --- a/client/cmd/reject.go +++ b/client/cmd/token/reject.go @@ -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) } diff --git a/client/cmd/token/split.go b/client/cmd/token/split.go new file mode 100644 index 0000000..12ed100 --- /dev/null +++ b/client/cmd/token/split.go @@ -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 ... + split <--parts PARTS> [--part-amount AMOUNT] + + 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 and ?") + os.Exit(1) + } + if len(args) < 1 && parts > 1 { + fmt.Println("did you forget to specify ?") + os.Exit(1) + } + if len(args) > 1 && parts > 1 { + fmt.Println("-p/--parts can't be combined with ") + os.Exit(1) + } + if len(args) > 1 && partAmount != "" { + fmt.Println("-a/--part-amount can't be combined with ") + 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 +} diff --git a/client/cmd/token/tests/split_test.go b/client/cmd/token/tests/split_test.go new file mode 100644 index 0000000..a5412dc --- /dev/null +++ b/client/cmd/token/tests/split_test.go @@ -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) + } + } + }) + } +} diff --git a/client/cmd/token/token.go b/client/cmd/token/token.go new file mode 100644 index 0000000..b3caff4 --- /dev/null +++ b/client/cmd/token/token.go @@ -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/)") +} diff --git a/client/cmd/token/transfer.go b/client/cmd/token/transfer.go new file mode 100644 index 0000000..5594084 --- /dev/null +++ b/client/cmd/token/transfer.go @@ -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 – 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) +} diff --git a/client/cmd/token/utils.go b/client/cmd/token/utils.go new file mode 100644 index 0000000..c711652 --- /dev/null +++ b/client/cmd/token/utils.go @@ -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), + ), + ) +} diff --git a/client/cmd/transfer.go b/client/cmd/transfer.go deleted file mode 100644 index f87dbb0..0000000 --- a/client/cmd/transfer.go +++ /dev/null @@ -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 – 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) -} diff --git a/client/cmd/update.go b/client/cmd/update.go new file mode 100644 index 0000000..a4e2ec1 --- /dev/null +++ b/client/cmd/update.go @@ -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) +} diff --git a/client/cmd/version.go b/client/cmd/version.go new file mode 100644 index 0000000..8d03beb --- /dev/null +++ b/client/cmd/version.go @@ -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) +} diff --git a/client/test/Dockerfile b/client/test/Dockerfile new file mode 100644 index 0000000..a3e0ffd --- /dev/null +++ b/client/test/Dockerfile @@ -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"] \ No newline at end of file diff --git a/client/test/README.md b/client/test/README.md new file mode 100644 index 0000000..18aaac4 --- /dev/null +++ b/client/test/README.md @@ -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 diff --git "a/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" "b/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" new file mode 100644 index 0000000..2beed71 --- /dev/null +++ "b/client/test/home/testuser\n/.quilibrium/config/qclient.yaml" @@ -0,0 +1,3 @@ +dataDir: /var/quilibrium/data/qclient +symlinkPath: /usr/local/bin/qclient +signatureCheck: true diff --git a/client/test/run_tests.sh b/client/test/run_tests.sh new file mode 100755 index 0000000..d8a4ef5 --- /dev/null +++ b/client/test/run_tests.sh @@ -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!" diff --git a/client/test/test_install.sh b/client/test/test_install.sh new file mode 100755 index 0000000..955694a --- /dev/null +++ b/client/test/test_install.sh @@ -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!" \ No newline at end of file diff --git a/client/test/test_utils.sh b/client/test/test_utils.sh new file mode 100755 index 0000000..37f97b4 --- /dev/null +++ b/client/test/test_utils.sh @@ -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 +} \ No newline at end of file diff --git a/client/utils/config.go b/client/utils/config.go new file mode 100644 index 0000000..93be350 --- /dev/null +++ b/client/utils/config.go @@ -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) +} diff --git a/client/utils/download.go b/client/utils/download.go new file mode 100644 index 0000000..b4cd51d --- /dev/null +++ b/client/utils/download.go @@ -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 +} diff --git a/client/utils/fileUtils.go b/client/utils/fileUtils.go new file mode 100644 index 0000000..62887d3 --- /dev/null +++ b/client/utils/fileUtils.go @@ -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) +} diff --git a/client/utils/node.go b/client/utils/node.go new file mode 100644 index 0000000..67e4440 --- /dev/null +++ b/client/utils/node.go @@ -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 +} diff --git a/client/utils/system.go b/client/utils/system.go new file mode 100644 index 0000000..a87ee6b --- /dev/null +++ b/client/utils/system.go @@ -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") +} diff --git a/client/utils/types.go b/client/utils/types.go new file mode 100644 index 0000000..dec318d --- /dev/null +++ b/client/utils/types.go @@ -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"` +} diff --git a/client/utils/userInputUtils.go b/client/utils/userInputUtils.go new file mode 100644 index 0000000..8bb7ac3 --- /dev/null +++ b/client/utils/userInputUtils.go @@ -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 +} diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 9d42125..745e060 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -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" diff --git a/crates/classgroup/src/gmp_classgroup/ffi.rs b/crates/classgroup/src/gmp_classgroup/ffi.rs index 76d94d2..b5492fb 100644 --- a/crates/classgroup/src/gmp_classgroup/ffi.rs +++ b/crates/classgroup/src/gmp_classgroup/ffi.rs @@ -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); diff --git a/crates/rpm/Cargo.toml b/crates/rpm/Cargo.toml index 848ad00..78004a5 100644 --- a/crates/rpm/Cargo.toml +++ b/crates/rpm/Cargo.toml @@ -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"] } diff --git a/crates/vdf/src/lib.rs b/crates/vdf/src/lib.rs index 4156829..8b84688 100644 --- a/crates/vdf/src/lib.rs +++ b/crates/vdf/src/lib.rs @@ -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>; diff --git a/crates/vdf/src/proof_pietrzak.rs b/crates/vdf/src/proof_pietrzak.rs index 786cecb..bea6b1d 100644 --- a/crates/vdf/src/proof_pietrzak.rs +++ b/crates/vdf/src/proof_pietrzak.rs @@ -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]); diff --git a/crates/vdf/src/proof_wesolowski.rs b/crates/vdf/src/proof_wesolowski.rs index bbc72b5..2d306e2 100644 --- a/crates/vdf/src/proof_wesolowski.rs +++ b/crates/vdf/src/proof_wesolowski.rs @@ -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 { [ diff --git a/verenc/generated/verenc/verenc.c b/verenc/generated/verenc/verenc.c new file mode 100644 index 0000000..b46e410 --- /dev/null +++ b/verenc/generated/verenc/verenc.c @@ -0,0 +1,8 @@ +#include + +// 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); +} \ No newline at end of file diff --git a/verenc/generated/verenc/verenc.go b/verenc/generated/verenc/verenc.go new file mode 100644 index 0000000..c36da21 --- /dev/null +++ b/verenc/generated/verenc/verenc.go @@ -0,0 +1,1055 @@ +package verenc + +// #include +import "C" + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "unsafe" +) + +type RustBuffer = C.RustBuffer + +type RustBufferI interface { + AsReader() *bytes.Reader + Free() + ToGoBytes() []byte + Data() unsafe.Pointer + Len() int + Capacity() int +} + +func RustBufferFromExternal(b RustBufferI) RustBuffer { + return RustBuffer{ + capacity: C.int(b.Capacity()), + len: C.int(b.Len()), + data: (*C.uchar)(b.Data()), + } +} + +func (cb RustBuffer) Capacity() int { + return int(cb.capacity) +} + +func (cb RustBuffer) Len() int { + return int(cb.len) +} + +func (cb RustBuffer) Data() unsafe.Pointer { + return unsafe.Pointer(cb.data) +} + +func (cb RustBuffer) AsReader() *bytes.Reader { + b := unsafe.Slice((*byte)(cb.data), C.int(cb.len)) + return bytes.NewReader(b) +} + +func (cb RustBuffer) Free() { + rustCall(func(status *C.RustCallStatus) bool { + C.ffi_verenc_rustbuffer_free(cb, status) + return false + }) +} + +func (cb RustBuffer) ToGoBytes() []byte { + return C.GoBytes(unsafe.Pointer(cb.data), C.int(cb.len)) +} + +func stringToRustBuffer(str string) RustBuffer { + return bytesToRustBuffer([]byte(str)) +} + +func bytesToRustBuffer(b []byte) RustBuffer { + if len(b) == 0 { + return RustBuffer{} + } + // We can pass the pointer along here, as it is pinned + // for the duration of this call + foreign := C.ForeignBytes{ + len: C.int(len(b)), + data: (*C.uchar)(unsafe.Pointer(&b[0])), + } + + return rustCall(func(status *C.RustCallStatus) RustBuffer { + return C.ffi_verenc_rustbuffer_from_bytes(foreign, status) + }) +} + +type BufLifter[GoType any] interface { + Lift(value RustBufferI) GoType +} + +type BufLowerer[GoType any] interface { + Lower(value GoType) RustBuffer +} + +type FfiConverter[GoType any, FfiType any] interface { + Lift(value FfiType) GoType + Lower(value GoType) FfiType +} + +type BufReader[GoType any] interface { + Read(reader io.Reader) GoType +} + +type BufWriter[GoType any] interface { + Write(writer io.Writer, value GoType) +} + +type FfiRustBufConverter[GoType any, FfiType any] interface { + FfiConverter[GoType, FfiType] + BufReader[GoType] +} + +func LowerIntoRustBuffer[GoType any](bufWriter BufWriter[GoType], value GoType) RustBuffer { + // This might be not the most efficient way but it does not require knowing allocation size + // beforehand + var buffer bytes.Buffer + bufWriter.Write(&buffer, value) + + bytes, err := io.ReadAll(&buffer) + if err != nil { + panic(fmt.Errorf("reading written data: %w", err)) + } + return bytesToRustBuffer(bytes) +} + +func LiftFromRustBuffer[GoType any](bufReader BufReader[GoType], rbuf RustBufferI) GoType { + defer rbuf.Free() + reader := rbuf.AsReader() + item := bufReader.Read(reader) + if reader.Len() > 0 { + // TODO: Remove this + leftover, _ := io.ReadAll(reader) + panic(fmt.Errorf("Junk remaining in buffer after lifting: %s", string(leftover))) + } + return item +} + +func rustCallWithError[U any](converter BufLifter[error], callback func(*C.RustCallStatus) U) (U, error) { + var status C.RustCallStatus + returnValue := callback(&status) + err := checkCallStatus(converter, status) + + return returnValue, err +} + +func checkCallStatus(converter BufLifter[error], status C.RustCallStatus) error { + switch status.code { + case 0: + return nil + case 1: + return converter.Lift(status.errorBuf) + case 2: + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if status.errorBuf.len > 0 { + panic(fmt.Errorf("%s", FfiConverterStringINSTANCE.Lift(status.errorBuf))) + } else { + panic(fmt.Errorf("Rust panicked while handling Rust panic")) + } + default: + return fmt.Errorf("unknown status code: %d", status.code) + } +} + +func checkCallStatusUnknown(status C.RustCallStatus) error { + switch status.code { + case 0: + return nil + case 1: + panic(fmt.Errorf("function not returning an error returned an error")) + case 2: + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if status.errorBuf.len > 0 { + panic(fmt.Errorf("%s", FfiConverterStringINSTANCE.Lift(status.errorBuf))) + } else { + panic(fmt.Errorf("Rust panicked while handling Rust panic")) + } + default: + return fmt.Errorf("unknown status code: %d", status.code) + } +} + +func rustCall[U any](callback func(*C.RustCallStatus) U) U { + returnValue, err := rustCallWithError(nil, callback) + if err != nil { + panic(err) + } + return returnValue +} + +func writeInt8(writer io.Writer, value int8) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint8(writer io.Writer, value uint8) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt16(writer io.Writer, value int16) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint16(writer io.Writer, value uint16) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt32(writer io.Writer, value int32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint32(writer io.Writer, value uint32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeInt64(writer io.Writer, value int64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeUint64(writer io.Writer, value uint64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeFloat32(writer io.Writer, value float32) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func writeFloat64(writer io.Writer, value float64) { + if err := binary.Write(writer, binary.BigEndian, value); err != nil { + panic(err) + } +} + +func readInt8(reader io.Reader) int8 { + var result int8 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint8(reader io.Reader) uint8 { + var result uint8 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt16(reader io.Reader) int16 { + var result int16 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint16(reader io.Reader) uint16 { + var result uint16 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt32(reader io.Reader) int32 { + var result int32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint32(reader io.Reader) uint32 { + var result uint32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readInt64(reader io.Reader) int64 { + var result int64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readUint64(reader io.Reader) uint64 { + var result uint64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readFloat32(reader io.Reader) float32 { + var result float32 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func readFloat64(reader io.Reader) float64 { + var result float64 + if err := binary.Read(reader, binary.BigEndian, &result); err != nil { + panic(err) + } + return result +} + +func init() { + + uniffiCheckChecksums() +} + +func uniffiCheckChecksums() { + // Get the bindings contract version from our ComponentInterface + bindingsContractVersion := 24 + // Get the scaffolding contract version by calling the into the dylib + scaffoldingContractVersion := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint32_t { + return C.ffi_verenc_uniffi_contract_version(uniffiStatus) + }) + if bindingsContractVersion != int(scaffoldingContractVersion) { + // If this happens try cleaning and rebuilding your project + panic("verenc: UniFFI contract version mismatch") + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_chunk_data_for_verenc(uniffiStatus) + }) + if checksum != 16794 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_chunk_data_for_verenc: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_combine_chunked_data(uniffiStatus) + }) + if checksum != 28541 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_combine_chunked_data: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_new_verenc_proof(uniffiStatus) + }) + if checksum != 7394 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_new_verenc_proof: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_new_verenc_proof_encrypt_only(uniffiStatus) + }) + if checksum != 17751 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_new_verenc_proof_encrypt_only: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_verenc_compress(uniffiStatus) + }) + if checksum != 11234 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_verenc_compress: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_verenc_recover(uniffiStatus) + }) + if checksum != 38626 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_verenc_recover: UniFFI API checksum mismatch") + } + } + { + checksum := rustCall(func(uniffiStatus *C.RustCallStatus) C.uint16_t { + return C.uniffi_verenc_checksum_func_verenc_verify(uniffiStatus) + }) + if checksum != 51440 { + // If this happens try cleaning and rebuilding your project + panic("verenc: uniffi_verenc_checksum_func_verenc_verify: UniFFI API checksum mismatch") + } + } +} + +type FfiConverterUint8 struct{} + +var FfiConverterUint8INSTANCE = FfiConverterUint8{} + +func (FfiConverterUint8) Lower(value uint8) C.uint8_t { + return C.uint8_t(value) +} + +func (FfiConverterUint8) Write(writer io.Writer, value uint8) { + writeUint8(writer, value) +} + +func (FfiConverterUint8) Lift(value C.uint8_t) uint8 { + return uint8(value) +} + +func (FfiConverterUint8) Read(reader io.Reader) uint8 { + return readUint8(reader) +} + +type FfiDestroyerUint8 struct{} + +func (FfiDestroyerUint8) Destroy(_ uint8) {} + +type FfiConverterUint64 struct{} + +var FfiConverterUint64INSTANCE = FfiConverterUint64{} + +func (FfiConverterUint64) Lower(value uint64) C.uint64_t { + return C.uint64_t(value) +} + +func (FfiConverterUint64) Write(writer io.Writer, value uint64) { + writeUint64(writer, value) +} + +func (FfiConverterUint64) Lift(value C.uint64_t) uint64 { + return uint64(value) +} + +func (FfiConverterUint64) Read(reader io.Reader) uint64 { + return readUint64(reader) +} + +type FfiDestroyerUint64 struct{} + +func (FfiDestroyerUint64) Destroy(_ uint64) {} + +type FfiConverterBool struct{} + +var FfiConverterBoolINSTANCE = FfiConverterBool{} + +func (FfiConverterBool) Lower(value bool) C.int8_t { + if value { + return C.int8_t(1) + } + return C.int8_t(0) +} + +func (FfiConverterBool) Write(writer io.Writer, value bool) { + if value { + writeInt8(writer, 1) + } else { + writeInt8(writer, 0) + } +} + +func (FfiConverterBool) Lift(value C.int8_t) bool { + return value != 0 +} + +func (FfiConverterBool) Read(reader io.Reader) bool { + return readInt8(reader) != 0 +} + +type FfiDestroyerBool struct{} + +func (FfiDestroyerBool) Destroy(_ bool) {} + +type FfiConverterString struct{} + +var FfiConverterStringINSTANCE = FfiConverterString{} + +func (FfiConverterString) Lift(rb RustBufferI) string { + defer rb.Free() + reader := rb.AsReader() + b, err := io.ReadAll(reader) + if err != nil { + panic(fmt.Errorf("reading reader: %w", err)) + } + return string(b) +} + +func (FfiConverterString) Read(reader io.Reader) string { + length := readInt32(reader) + buffer := make([]byte, length) + read_length, err := reader.Read(buffer) + if err != nil { + panic(err) + } + if read_length != int(length) { + panic(fmt.Errorf("bad read length when reading string, expected %d, read %d", length, read_length)) + } + return string(buffer) +} + +func (FfiConverterString) Lower(value string) RustBuffer { + return stringToRustBuffer(value) +} + +func (FfiConverterString) Write(writer io.Writer, value string) { + if len(value) > math.MaxInt32 { + panic("String is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + write_length, err := io.WriteString(writer, value) + if err != nil { + panic(err) + } + if write_length != len(value) { + panic(fmt.Errorf("bad write length when writing string, expected %d, written %d", len(value), write_length)) + } +} + +type FfiDestroyerString struct{} + +func (FfiDestroyerString) Destroy(_ string) {} + +type CompressedCiphertext struct { + Ctexts []VerencCiphertext + Aux [][]uint8 +} + +func (r *CompressedCiphertext) Destroy() { + FfiDestroyerSequenceTypeVerencCiphertext{}.Destroy(r.Ctexts) + FfiDestroyerSequenceSequenceUint8{}.Destroy(r.Aux) +} + +type FfiConverterTypeCompressedCiphertext struct{} + +var FfiConverterTypeCompressedCiphertextINSTANCE = FfiConverterTypeCompressedCiphertext{} + +func (c FfiConverterTypeCompressedCiphertext) Lift(rb RustBufferI) CompressedCiphertext { + return LiftFromRustBuffer[CompressedCiphertext](c, rb) +} + +func (c FfiConverterTypeCompressedCiphertext) Read(reader io.Reader) CompressedCiphertext { + return CompressedCiphertext{ + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Read(reader), + FfiConverterSequenceSequenceUint8INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeCompressedCiphertext) Lower(value CompressedCiphertext) RustBuffer { + return LowerIntoRustBuffer[CompressedCiphertext](c, value) +} + +func (c FfiConverterTypeCompressedCiphertext) Write(writer io.Writer, value CompressedCiphertext) { + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Write(writer, value.Ctexts) + FfiConverterSequenceSequenceUint8INSTANCE.Write(writer, value.Aux) +} + +type FfiDestroyerTypeCompressedCiphertext struct{} + +func (_ FfiDestroyerTypeCompressedCiphertext) Destroy(value CompressedCiphertext) { + value.Destroy() +} + +type VerencCiphertext struct { + C1 []uint8 + C2 []uint8 + I uint64 +} + +func (r *VerencCiphertext) Destroy() { + FfiDestroyerSequenceUint8{}.Destroy(r.C1) + FfiDestroyerSequenceUint8{}.Destroy(r.C2) + FfiDestroyerUint64{}.Destroy(r.I) +} + +type FfiConverterTypeVerencCiphertext struct{} + +var FfiConverterTypeVerencCiphertextINSTANCE = FfiConverterTypeVerencCiphertext{} + +func (c FfiConverterTypeVerencCiphertext) Lift(rb RustBufferI) VerencCiphertext { + return LiftFromRustBuffer[VerencCiphertext](c, rb) +} + +func (c FfiConverterTypeVerencCiphertext) Read(reader io.Reader) VerencCiphertext { + return VerencCiphertext{ + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeVerencCiphertext) Lower(value VerencCiphertext) RustBuffer { + return LowerIntoRustBuffer[VerencCiphertext](c, value) +} + +func (c FfiConverterTypeVerencCiphertext) Write(writer io.Writer, value VerencCiphertext) { + FfiConverterSequenceUint8INSTANCE.Write(writer, value.C1) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.C2) + FfiConverterUint64INSTANCE.Write(writer, value.I) +} + +type FfiDestroyerTypeVerencCiphertext struct{} + +func (_ FfiDestroyerTypeVerencCiphertext) Destroy(value VerencCiphertext) { + value.Destroy() +} + +type VerencDecrypt struct { + BlindingPubkey []uint8 + DecryptionKey []uint8 + Statement []uint8 + Ciphertexts CompressedCiphertext +} + +func (r *VerencDecrypt) Destroy() { + FfiDestroyerSequenceUint8{}.Destroy(r.BlindingPubkey) + FfiDestroyerSequenceUint8{}.Destroy(r.DecryptionKey) + FfiDestroyerSequenceUint8{}.Destroy(r.Statement) + FfiDestroyerTypeCompressedCiphertext{}.Destroy(r.Ciphertexts) +} + +type FfiConverterTypeVerencDecrypt struct{} + +var FfiConverterTypeVerencDecryptINSTANCE = FfiConverterTypeVerencDecrypt{} + +func (c FfiConverterTypeVerencDecrypt) Lift(rb RustBufferI) VerencDecrypt { + return LiftFromRustBuffer[VerencDecrypt](c, rb) +} + +func (c FfiConverterTypeVerencDecrypt) Read(reader io.Reader) VerencDecrypt { + return VerencDecrypt{ + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterTypeCompressedCiphertextINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeVerencDecrypt) Lower(value VerencDecrypt) RustBuffer { + return LowerIntoRustBuffer[VerencDecrypt](c, value) +} + +func (c FfiConverterTypeVerencDecrypt) Write(writer io.Writer, value VerencDecrypt) { + FfiConverterSequenceUint8INSTANCE.Write(writer, value.BlindingPubkey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.DecryptionKey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.Statement) + FfiConverterTypeCompressedCiphertextINSTANCE.Write(writer, value.Ciphertexts) +} + +type FfiDestroyerTypeVerencDecrypt struct{} + +func (_ FfiDestroyerTypeVerencDecrypt) Destroy(value VerencDecrypt) { + value.Destroy() +} + +type VerencProof struct { + BlindingPubkey []uint8 + EncryptionKey []uint8 + Statement []uint8 + Challenge []uint8 + Polycom [][]uint8 + Ctexts []VerencCiphertext + SharesRands []VerencShare +} + +func (r *VerencProof) Destroy() { + FfiDestroyerSequenceUint8{}.Destroy(r.BlindingPubkey) + FfiDestroyerSequenceUint8{}.Destroy(r.EncryptionKey) + FfiDestroyerSequenceUint8{}.Destroy(r.Statement) + FfiDestroyerSequenceUint8{}.Destroy(r.Challenge) + FfiDestroyerSequenceSequenceUint8{}.Destroy(r.Polycom) + FfiDestroyerSequenceTypeVerencCiphertext{}.Destroy(r.Ctexts) + FfiDestroyerSequenceTypeVerencShare{}.Destroy(r.SharesRands) +} + +type FfiConverterTypeVerencProof struct{} + +var FfiConverterTypeVerencProofINSTANCE = FfiConverterTypeVerencProof{} + +func (c FfiConverterTypeVerencProof) Lift(rb RustBufferI) VerencProof { + return LiftFromRustBuffer[VerencProof](c, rb) +} + +func (c FfiConverterTypeVerencProof) Read(reader io.Reader) VerencProof { + return VerencProof{ + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Read(reader), + FfiConverterSequenceTypeVerencShareINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeVerencProof) Lower(value VerencProof) RustBuffer { + return LowerIntoRustBuffer[VerencProof](c, value) +} + +func (c FfiConverterTypeVerencProof) Write(writer io.Writer, value VerencProof) { + FfiConverterSequenceUint8INSTANCE.Write(writer, value.BlindingPubkey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.EncryptionKey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.Statement) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.Challenge) + FfiConverterSequenceSequenceUint8INSTANCE.Write(writer, value.Polycom) + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Write(writer, value.Ctexts) + FfiConverterSequenceTypeVerencShareINSTANCE.Write(writer, value.SharesRands) +} + +type FfiDestroyerTypeVerencProof struct{} + +func (_ FfiDestroyerTypeVerencProof) Destroy(value VerencProof) { + value.Destroy() +} + +type VerencProofAndBlindingKey struct { + BlindingKey []uint8 + BlindingPubkey []uint8 + DecryptionKey []uint8 + EncryptionKey []uint8 + Statement []uint8 + Challenge []uint8 + Polycom [][]uint8 + Ctexts []VerencCiphertext + SharesRands []VerencShare +} + +func (r *VerencProofAndBlindingKey) Destroy() { + FfiDestroyerSequenceUint8{}.Destroy(r.BlindingKey) + FfiDestroyerSequenceUint8{}.Destroy(r.BlindingPubkey) + FfiDestroyerSequenceUint8{}.Destroy(r.DecryptionKey) + FfiDestroyerSequenceUint8{}.Destroy(r.EncryptionKey) + FfiDestroyerSequenceUint8{}.Destroy(r.Statement) + FfiDestroyerSequenceUint8{}.Destroy(r.Challenge) + FfiDestroyerSequenceSequenceUint8{}.Destroy(r.Polycom) + FfiDestroyerSequenceTypeVerencCiphertext{}.Destroy(r.Ctexts) + FfiDestroyerSequenceTypeVerencShare{}.Destroy(r.SharesRands) +} + +type FfiConverterTypeVerencProofAndBlindingKey struct{} + +var FfiConverterTypeVerencProofAndBlindingKeyINSTANCE = FfiConverterTypeVerencProofAndBlindingKey{} + +func (c FfiConverterTypeVerencProofAndBlindingKey) Lift(rb RustBufferI) VerencProofAndBlindingKey { + return LiftFromRustBuffer[VerencProofAndBlindingKey](c, rb) +} + +func (c FfiConverterTypeVerencProofAndBlindingKey) Read(reader io.Reader) VerencProofAndBlindingKey { + return VerencProofAndBlindingKey{ + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Read(reader), + FfiConverterSequenceTypeVerencShareINSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeVerencProofAndBlindingKey) Lower(value VerencProofAndBlindingKey) RustBuffer { + return LowerIntoRustBuffer[VerencProofAndBlindingKey](c, value) +} + +func (c FfiConverterTypeVerencProofAndBlindingKey) Write(writer io.Writer, value VerencProofAndBlindingKey) { + FfiConverterSequenceUint8INSTANCE.Write(writer, value.BlindingKey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.BlindingPubkey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.DecryptionKey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.EncryptionKey) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.Statement) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.Challenge) + FfiConverterSequenceSequenceUint8INSTANCE.Write(writer, value.Polycom) + FfiConverterSequenceTypeVerencCiphertextINSTANCE.Write(writer, value.Ctexts) + FfiConverterSequenceTypeVerencShareINSTANCE.Write(writer, value.SharesRands) +} + +type FfiDestroyerTypeVerencProofAndBlindingKey struct{} + +func (_ FfiDestroyerTypeVerencProofAndBlindingKey) Destroy(value VerencProofAndBlindingKey) { + value.Destroy() +} + +type VerencShare struct { + S1 []uint8 + S2 []uint8 + I uint64 +} + +func (r *VerencShare) Destroy() { + FfiDestroyerSequenceUint8{}.Destroy(r.S1) + FfiDestroyerSequenceUint8{}.Destroy(r.S2) + FfiDestroyerUint64{}.Destroy(r.I) +} + +type FfiConverterTypeVerencShare struct{} + +var FfiConverterTypeVerencShareINSTANCE = FfiConverterTypeVerencShare{} + +func (c FfiConverterTypeVerencShare) Lift(rb RustBufferI) VerencShare { + return LiftFromRustBuffer[VerencShare](c, rb) +} + +func (c FfiConverterTypeVerencShare) Read(reader io.Reader) VerencShare { + return VerencShare{ + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterSequenceUint8INSTANCE.Read(reader), + FfiConverterUint64INSTANCE.Read(reader), + } +} + +func (c FfiConverterTypeVerencShare) Lower(value VerencShare) RustBuffer { + return LowerIntoRustBuffer[VerencShare](c, value) +} + +func (c FfiConverterTypeVerencShare) Write(writer io.Writer, value VerencShare) { + FfiConverterSequenceUint8INSTANCE.Write(writer, value.S1) + FfiConverterSequenceUint8INSTANCE.Write(writer, value.S2) + FfiConverterUint64INSTANCE.Write(writer, value.I) +} + +type FfiDestroyerTypeVerencShare struct{} + +func (_ FfiDestroyerTypeVerencShare) Destroy(value VerencShare) { + value.Destroy() +} + +type FfiConverterSequenceUint8 struct{} + +var FfiConverterSequenceUint8INSTANCE = FfiConverterSequenceUint8{} + +func (c FfiConverterSequenceUint8) Lift(rb RustBufferI) []uint8 { + return LiftFromRustBuffer[[]uint8](c, rb) +} + +func (c FfiConverterSequenceUint8) Read(reader io.Reader) []uint8 { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]uint8, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterUint8INSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceUint8) Lower(value []uint8) RustBuffer { + return LowerIntoRustBuffer[[]uint8](c, value) +} + +func (c FfiConverterSequenceUint8) Write(writer io.Writer, value []uint8) { + if len(value) > math.MaxInt32 { + panic("[]uint8 is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterUint8INSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceUint8 struct{} + +func (FfiDestroyerSequenceUint8) Destroy(sequence []uint8) { + for _, value := range sequence { + FfiDestroyerUint8{}.Destroy(value) + } +} + +type FfiConverterSequenceTypeVerencCiphertext struct{} + +var FfiConverterSequenceTypeVerencCiphertextINSTANCE = FfiConverterSequenceTypeVerencCiphertext{} + +func (c FfiConverterSequenceTypeVerencCiphertext) Lift(rb RustBufferI) []VerencCiphertext { + return LiftFromRustBuffer[[]VerencCiphertext](c, rb) +} + +func (c FfiConverterSequenceTypeVerencCiphertext) Read(reader io.Reader) []VerencCiphertext { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]VerencCiphertext, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeVerencCiphertextINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeVerencCiphertext) Lower(value []VerencCiphertext) RustBuffer { + return LowerIntoRustBuffer[[]VerencCiphertext](c, value) +} + +func (c FfiConverterSequenceTypeVerencCiphertext) Write(writer io.Writer, value []VerencCiphertext) { + if len(value) > math.MaxInt32 { + panic("[]VerencCiphertext is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeVerencCiphertextINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeVerencCiphertext struct{} + +func (FfiDestroyerSequenceTypeVerencCiphertext) Destroy(sequence []VerencCiphertext) { + for _, value := range sequence { + FfiDestroyerTypeVerencCiphertext{}.Destroy(value) + } +} + +type FfiConverterSequenceTypeVerencShare struct{} + +var FfiConverterSequenceTypeVerencShareINSTANCE = FfiConverterSequenceTypeVerencShare{} + +func (c FfiConverterSequenceTypeVerencShare) Lift(rb RustBufferI) []VerencShare { + return LiftFromRustBuffer[[]VerencShare](c, rb) +} + +func (c FfiConverterSequenceTypeVerencShare) Read(reader io.Reader) []VerencShare { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([]VerencShare, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterTypeVerencShareINSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceTypeVerencShare) Lower(value []VerencShare) RustBuffer { + return LowerIntoRustBuffer[[]VerencShare](c, value) +} + +func (c FfiConverterSequenceTypeVerencShare) Write(writer io.Writer, value []VerencShare) { + if len(value) > math.MaxInt32 { + panic("[]VerencShare is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterTypeVerencShareINSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceTypeVerencShare struct{} + +func (FfiDestroyerSequenceTypeVerencShare) Destroy(sequence []VerencShare) { + for _, value := range sequence { + FfiDestroyerTypeVerencShare{}.Destroy(value) + } +} + +type FfiConverterSequenceSequenceUint8 struct{} + +var FfiConverterSequenceSequenceUint8INSTANCE = FfiConverterSequenceSequenceUint8{} + +func (c FfiConverterSequenceSequenceUint8) Lift(rb RustBufferI) [][]uint8 { + return LiftFromRustBuffer[[][]uint8](c, rb) +} + +func (c FfiConverterSequenceSequenceUint8) Read(reader io.Reader) [][]uint8 { + length := readInt32(reader) + if length == 0 { + return nil + } + result := make([][]uint8, 0, length) + for i := int32(0); i < length; i++ { + result = append(result, FfiConverterSequenceUint8INSTANCE.Read(reader)) + } + return result +} + +func (c FfiConverterSequenceSequenceUint8) Lower(value [][]uint8) RustBuffer { + return LowerIntoRustBuffer[[][]uint8](c, value) +} + +func (c FfiConverterSequenceSequenceUint8) Write(writer io.Writer, value [][]uint8) { + if len(value) > math.MaxInt32 { + panic("[][]uint8 is too large to fit into Int32") + } + + writeInt32(writer, int32(len(value))) + for _, item := range value { + FfiConverterSequenceUint8INSTANCE.Write(writer, item) + } +} + +type FfiDestroyerSequenceSequenceUint8 struct{} + +func (FfiDestroyerSequenceSequenceUint8) Destroy(sequence [][]uint8) { + for _, value := range sequence { + FfiDestroyerSequenceUint8{}.Destroy(value) + } +} + +func ChunkDataForVerenc(data []uint8) [][]uint8 { + return FfiConverterSequenceSequenceUint8INSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_chunk_data_for_verenc(FfiConverterSequenceUint8INSTANCE.Lower(data), _uniffiStatus) + })) +} + +func CombineChunkedData(chunks [][]uint8) []uint8 { + return FfiConverterSequenceUint8INSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_combine_chunked_data(FfiConverterSequenceSequenceUint8INSTANCE.Lower(chunks), _uniffiStatus) + })) +} + +func NewVerencProof(data []uint8) VerencProofAndBlindingKey { + return FfiConverterTypeVerencProofAndBlindingKeyINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_new_verenc_proof(FfiConverterSequenceUint8INSTANCE.Lower(data), _uniffiStatus) + })) +} + +func NewVerencProofEncryptOnly(data []uint8, encryptionKeyBytes []uint8) VerencProofAndBlindingKey { + return FfiConverterTypeVerencProofAndBlindingKeyINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_new_verenc_proof_encrypt_only(FfiConverterSequenceUint8INSTANCE.Lower(data), FfiConverterSequenceUint8INSTANCE.Lower(encryptionKeyBytes), _uniffiStatus) + })) +} + +func VerencCompress(proof VerencProof) CompressedCiphertext { + return FfiConverterTypeCompressedCiphertextINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_verenc_compress(FfiConverterTypeVerencProofINSTANCE.Lower(proof), _uniffiStatus) + })) +} + +func VerencRecover(recovery VerencDecrypt) []uint8 { + return FfiConverterSequenceUint8INSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) RustBufferI { + return C.uniffi_verenc_fn_func_verenc_recover(FfiConverterTypeVerencDecryptINSTANCE.Lower(recovery), _uniffiStatus) + })) +} + +func VerencVerify(proof VerencProof) bool { + return FfiConverterBoolINSTANCE.Lift(rustCall(func(_uniffiStatus *C.RustCallStatus) C.int8_t { + return C.uniffi_verenc_fn_func_verenc_verify(FfiConverterTypeVerencProofINSTANCE.Lower(proof), _uniffiStatus) + })) +} diff --git a/verenc/generated/verenc/verenc.h b/verenc/generated/verenc/verenc.h new file mode 100644 index 0000000..f240c59 --- /dev/null +++ b/verenc/generated/verenc/verenc.h @@ -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 +#include + +// 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 +); + + +