From 7e6e83b84cc8a5aa9237b1103cd9ed012a785764 Mon Sep 17 00:00:00 2001 From: Madhur Shrimal Date: Sat, 19 Oct 2024 01:24:32 -0400 Subject: [PATCH] initial commit --- .github/dependabot.yml | 11 + .github/workflows/check-fmt.yml | 33 ++ .github/workflows/golangci-lint.yml | 29 ++ .github/workflows/release.yml | 47 ++ .github/workflows/tests.yml | 19 + .gitignore | 3 + .goreleaser.yml | 61 +++ Dockerfile | 17 + LICENSE | 105 +++++ Makefile | 34 ++ README.md | 97 ++++ cmd/cerberus/main.go | 152 +++++++ go.mod | 43 ++ go.sum | 83 ++++ internal/common/common.go | 8 + internal/common/testutils/common.go | 14 + internal/configuration/configuration.go | 11 + internal/constants/constants.go | 5 + internal/crypto/bn254.go | 112 +++++ internal/crypto/utils.go | 157 +++++++ internal/metrics/README.md | 10 + internal/metrics/metrics.go | 82 ++++ internal/server/server.go | 76 ++++ internal/services/kms/kms.go | 149 +++++++ internal/services/kms/kms_test.go | 92 ++++ internal/services/signing/signing.go | 80 ++++ internal/services/signing/signing_test.go | 42 ++ ...c9a0d4ce1984a92b7ecb85bde8878fea5d1b0.json | 1 + internal/store/filesystem/filesystem.go | 101 +++++ internal/store/filesystem/filesystem_test.go | 69 +++ internal/store/store.go | 26 ++ monitoring/signer.json | 413 ++++++++++++++++++ 32 files changed, 2182 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/check-fmt.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/cerberus/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/common/common.go create mode 100644 internal/common/testutils/common.go create mode 100644 internal/configuration/configuration.go create mode 100644 internal/constants/constants.go create mode 100644 internal/crypto/bn254.go create mode 100644 internal/crypto/utils.go create mode 100644 internal/metrics/README.md create mode 100644 internal/metrics/metrics.go create mode 100644 internal/server/server.go create mode 100644 internal/services/kms/kms.go create mode 100644 internal/services/kms/kms_test.go create mode 100644 internal/services/signing/signing.go create mode 100644 internal/services/signing/signing_test.go create mode 100644 internal/services/signing/testdata/keystore/a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0.json create mode 100644 internal/store/filesystem/filesystem.go create mode 100644 internal/store/filesystem/filesystem_test.go create mode 100644 internal/store/store.go create mode 100644 monitoring/signer.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..81c33fa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/check-fmt.yml b/.github/workflows/check-fmt.yml new file mode 100644 index 0000000..7f91023 --- /dev/null +++ b/.github/workflows/check-fmt.yml @@ -0,0 +1,33 @@ +name: Check make fmt +on: + push: + branches: + - master + pull_request: + +permissions: + contents: read + +jobs: + check-make-fmt: + name: Check make fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: install go1.21 + uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Run make fmt + run: make fmt + + - name: Check if make fmt generated changes that should be committed + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Error: make fmt generated changes that should be committed. Please run 'make fmt' and commit the changes." + git diff + git status + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..805b26c --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,29 @@ +name: lint + +on: + push: + branches: + - master + pull_request: + +jobs: + Lint: + name: Lint + env: + GO_VERSION: '1.21' + GOPRIVATE: 'github.com/Layr-Labs/cerberus-api' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + - name: Configure Git for private modules + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global url."https://${{ github.token }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8698270 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +# This workflow uses gorelaser to both build a github release containing binaries for mac, windows, and linux, and to push the docker images to ghcr.io. +name: goreleaser + +on: + push: + # run only against tags + tags: + - "*" + +permissions: + contents: write + packages: write + # issues: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.21 + + - name: Login to ghcr + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + # 'latest', 'nightly', or a semver + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e98b940 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,19 @@ +name: tests + +on: + push: + branches: + - master + pull_request: + +jobs: + Test: + name: Unit Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Unit Test + run: make tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceb3176 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +data/ + +bin/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..315acb5 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,61 @@ +# ref. https://goreleaser.com/customization/build/ +version: 2 + +project_name: cerberus + +builds: + - id: cerberus + main: ./cmd/cerberus/main.go + binary: cerberus + flags: + - -v + ldflags: + - -X 'main.version={{ .Version }}' + # contains linux and darwin + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + +release: + # Repo in which the release will be created. + # Default is extracted from the origin remote URL or empty if its private hosted. + github: + owner: layr-labs + name: cerberus + + draft: true + +dockers: + - image_templates: + - ghcr.io/layr-labs/{{ .ProjectName }}:latest-amd64 + - ghcr.io/layr-labs/{{ .ProjectName }}:{{.Version}}-amd64 + use: buildx + dockerfile: Dockerfile + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=APP_VERSION={{ .Version }}" + goarch: amd64 + - image_templates: + - ghcr.io/layr-labs/{{ .ProjectName }}:latest-arm64 + - ghcr.io/layr-labs/{{ .ProjectName }}:{{.Version}}-arm64 + use: buildx + dockerfile: Dockerfile + build_flag_templates: + - "--platform=linux/arm64" + - "--build-arg=APP_VERSION={{ .Version }}" + goarch: arm64 + +docker_manifests: + - name_template: ghcr.io/layr-labs/{{ .ProjectName }}:{{ .Version }} + image_templates: + - ghcr.io/layr-labs/{{ .ProjectName }}:{{ .Version }}-amd64 + - ghcr.io/layr-labs/{{ .ProjectName }}:{{ .Version }}-arm64 + - name_template: ghcr.io/layr-labs/{{ .ProjectName }}:latest + image_templates: + - ghcr.io/layr-labs/{{ .ProjectName }}:latest-amd64 + - ghcr.io/layr-labs/{{ .ProjectName }}:latest-arm64 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f94509 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +#FROM golang:1.21 AS build +# +#WORKDIR /usr/src/app +# +#COPY go.mod go.sum ./ +# +#RUN go mod download && go mod tidy && go mod verify +# +#COPY . . +# +#ARG APP_VERSION +#RUN go build -ldflags "-X main.version=$APP_VERSION" -v -o bin/cerberus cmd/cerberus/main.go + +FROM debian:latest +COPY cerberus /cerberus + +ENTRYPOINT [ "/cerberus"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..99f2a43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,105 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Eigen Labs, Inc. + +Licensed Work: cerberus + The Licensed Work is (c) 2024 Eigen Labs, Inc. + +Additional Use Grant: + +You may additionally use any of the software included in the following repositories here, https://docs.google.com/spreadsheets/d/1PlJRow5C0GMqXZlIxRm5CEnkhH-gMV1wIdq1pCfbZco/edit?gid=0#gid=0, ("Additional Use Grant Software") for production commercial uses, but only if such uses are (i) built on or using the EigenLayer Protocol or EigenDA, and (ii) not Competing Uses. + +"Competing Use" means any use of the Additional Use Grant Software in any product, protocol, application or service that is made available to third parties and that (i) substitutes for use of EigenLayer Protocol or EigenDA, (i) offers the same or substantially similar functionality as the EigenLayer Protocol or EigenDA or (ili) is built on or using a protocol with substantially similar functionality as the EigenLayer Protocol. + +EigenLayer Protocol means the restaking protocol as further described in the documentation here, https://docs.eigenlayer.xyz, as updated from time to time. +EigenDA means the data availability protocol built on top of the EigenLayer Protocol as further described in the documentation here, https://docs.eigenlayer.xyz, as updated from time to time. + + +Change Date: 2026-10-20 + +Change License: MIT +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0e858d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +APP_NAME=cerberus +GO_LINES_IGNORED_DIRS= +GO_PACKAGES=./internal/... ./cmd/... +GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g") + +.PHONY: build +build: + @echo "Building..." + go build -o bin/$(APP_NAME) cmd/$(APP_NAME)/main.go + @echo "Done" + +.PHONY: start +start: + make build + ./bin/$(APP_NAME) --log-level=debug + +.PHONY: fmt +fmt: ## formats all go files + go fmt ./... + make format-lines + +.PHONY: format-lines +format-lines: ## formats all go files with golines + go install github.com/segmentio/golines@latest + golines -w -m 100 --ignore-generated --shorten-comments --ignored-dirs=${GO_LINES_IGNORED_DIRS} ${GO_FOLDERS} + +.PHONY: lint +lint: ## runs all linters + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + golangci-lint run ./... + +.PHONY: tests +tests: ## runs all tests + go test ./... -covermode=atomic \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f19a117 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Remote Signer Implementation of cerberus-api +This is a remote signer for BLS signatures on the BN254 curve. + + +### Installation +#### Quick start +```bash +$ git clone https://github.com/Layr-Labs/cerberus.git +$ cd cerberus +$ make start +``` + +#### Manual +```bash +git clone https://github.com/Layr-Labs/cerberus.git +cd cerberus +go build -o bin/cerberus cmd/cerberus/main.go +./bin/cerberus +``` + +### Usage options +| Options | Description | Default | +|--------------|---------------------------------------------|-----------------| +| keystore-dir | Directory to store encrypted keystore files | ./data/keystore | +| grpc-port | gRPC port for starting signer server | 50051 | +| log-format | format of the logs (text, json) | text | +| log-level | debug, info, warn, error | info | +| metrics-port | port to expose prometheus metrics | 9091 | +| help | show help | | +| version | show version | | + + +### Monitoring +The signer exposes prometheus metrics on the `/metrics` endpoint. You can scrape these metrics using a prometheus server. +There is a grafana dashboard available in the `monitoring` directory. You can import this dashboard into your grafana server to monitor the signer. + +### Configuring Server-side TLS (optional) + +Server-side TLS support is provided to encrypt traffic between the client and server. This can be enabled by starting the service with `tls-ca-cert` and `tls-server-key` parameters set: + +``` +cerberus -tls-ca-cert server.crt -tls-server-key server.key +``` + +The server can then be queried over a secure connection using a gRPC client that supports TLS. For example, using `grpcurl`: + +``` +grpcurl -cacert ../cerberus/server.crt -d '{"password": "test"}' -import-path . -proto proto/keymanager.proto localhost:50051 keymanager.v1.KeyManager/GenerateKeyPair +``` +#### Generating TLS certificates + +For local testing purposes, the following commands can be used to generate a server certificate and key. + +Create a file named `openssl.cnf` with the following content: + +``` +[ req ] +default_bits = 2048 +default_md = sha256 +default_keyfile = server.key +prompt = no +encrypt_key = no + +distinguished_name = req_distinguished_name +x509_extensions = v3_req + +[ req_distinguished_name ] +C = US +ST = California +L = San Francisco +O = My Company +OU = My Division +CN = localhost + +[ v3_req ] +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +``` + +```bash +# Generate the private key +openssl genpkey -algorithm RSA -out server.key + +# Generate the certificate signing request (CSR) +openssl req -new -key server.key -out server.csr -config openssl.cnf + +# Generate the self-signed certificate with SAN +openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt -extensions v3_req -extfile openssl.cnf + +``` + +server.crt and server.key files can then be used to start the server with TLS support. + +## Security Bugs +Please report security vulnerabilities to security@eigenlabs.org. Do NOT report security bugs via Github Issues. diff --git a/cmd/cerberus/main.go b/cmd/cerberus/main.go new file mode 100644 index 0000000..5ffb2ca --- /dev/null +++ b/cmd/cerberus/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/server" + + "github.com/urfave/cli/v2" +) + +var ( + version = "development" + + keystoreDirFlag = &cli.StringFlag{ + Name: "keystore-dir", + Usage: "Directory where the keystore files are stored", + Value: "./data/keystore", + EnvVars: []string{"KEYSTORE_DIR"}, + } + + grpcPortFlag = &cli.StringFlag{ + Name: "grpc-port", + Usage: "Port for the gRPC server", + Value: "50051", + EnvVars: []string{"GRPC_PORT"}, + } + + metricsPortFlag = &cli.StringFlag{ + Name: "metrics-port", + Usage: "Port for the metrics server", + Value: "9091", + EnvVars: []string{"METRICS_PORT"}, + } + + logLevelFlag = &cli.StringFlag{ + Name: "log-level", + Usage: "Log level - supported levels: debug, info, warn, error", + Value: "info", + EnvVars: []string{"LOG_LEVEL"}, + } + + logFormatFlag = &cli.StringFlag{ + Name: "log-format", + Usage: "Log format - supported formats: text, json", + Value: "text", + EnvVars: []string{"LOG_FORMAT"}, + } + + // TLS flags to set up secure gRPC server, optional + + tlsCaCert = &cli.StringFlag{ + Name: "tls-ca-cert", + Usage: "TLS CA certificate", + EnvVars: []string{"TLS_CA_CERT"}, + } + + tlsServerKey = &cli.StringFlag{ + Name: "tls-server-key", + Usage: "TLS server key", + EnvVars: []string{"TLS_SERVER_KEY"}, + } +) + +func main() { + cli.AppHelpTemplate = fmt.Sprintf(` + _ + | | + ___ ___ _ __ | |__ ___ _ __ _ _ ___ + / __| / _ \| '__|| '_ \ / _ \| '__|| | | |/ __| +| (__ | __/| | | |_) || __/| | | |_| |\__ \ + \___| \___||_| |_.__/ \___||_| \__,_||___/ + + +%s`, cli.AppHelpTemplate) + app := cli.NewApp() + + app.Name = "cerberus" + app.Usage = "Remote BLS Signer" + app.Version = version + app.Copyright = "(c) 2024 EigenLabs" + + app.Flags = []cli.Flag{ + keystoreDirFlag, + grpcPortFlag, + logFormatFlag, + logLevelFlag, + metricsPortFlag, + tlsCaCert, + tlsServerKey, + } + + app.Action = start + + if err := app.Run(os.Args); err != nil { + _, err := fmt.Fprintln(os.Stderr, err) + if err != nil { + return + } + os.Exit(1) + } +} + +func start(c *cli.Context) error { + keystoreDir := c.String(keystoreDirFlag.Name) + grpcPort := c.String(grpcPortFlag.Name) + metricsPort := c.String(metricsPortFlag.Name) + logLevel := c.String(logLevelFlag.Name) + logFormat := c.String(logFormatFlag.Name) + tlsCaCert := c.String(tlsCaCert.Name) + tlsServerKey := c.String(tlsServerKey.Name) + + cfg := &configuration.Configuration{ + KeystoreDir: keystoreDir, + GrpcPort: grpcPort, + MetricsPort: metricsPort, + TLSCACert: tlsCaCert, + TLSServerKey: tlsServerKey, + } + + sLogLevel := levelToLogLevel(logLevel) + slogOptions := slog.HandlerOptions{AddSource: true, Level: sLogLevel} + var logger *slog.Logger + if logFormat == "json" { + handler := slog.NewJSONHandler(os.Stdout, &slogOptions) + logger = slog.New(handler) + } else { + handler := slog.NewTextHandler(os.Stdout, &slogOptions) + logger = slog.New(handler) + } + + logger.Info(fmt.Sprintf("Starting cerberus server version: %s", version)) + server.Start(cfg, logger) + return nil +} + +func levelToLogLevel(level string) slog.Level { + switch level { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..339b29e --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module github.com/Layr-Labs/cerberus + +go 1.21 + +toolchain go1.21.11 + +require ( + github.com/Layr-Labs/bn254-keystore-go v0.0.0-20241007185542-1a1ca1c72eb4 + github.com/Layr-Labs/cerberus-api v0.0.0-20241016214048-d52f5ddc5559 + github.com/consensys/gnark-crypto v0.12.1 + github.com/prometheus/client_golang v1.20.3 + github.com/stretchr/testify v1.9.0 + github.com/urfave/cli/v2 v2.27.5 + google.golang.org/grpc v1.64.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.14.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aa37a8f --- /dev/null +++ b/go.sum @@ -0,0 +1,83 @@ +github.com/Layr-Labs/bn254-keystore-go v0.0.0-20241007185542-1a1ca1c72eb4 h1:0EigmyPWUGcKhbaRtuJV1XdAzlS/qwpgHf0oUhq+r7s= +github.com/Layr-Labs/bn254-keystore-go v0.0.0-20241007185542-1a1ca1c72eb4/go.mod h1:7J8hptSX8cFq7KmVb+rEO5aEifj7E44c3i0afIyr4WA= +github.com/Layr-Labs/cerberus-api v0.0.0-20241016214048-d52f5ddc5559 h1:qbkT/+txp6dbQmbN2ro37T9V4hzt+ZWyqxGcQZBKgwo= +github.com/Layr-Labs/cerberus-api v0.0.0-20241016214048-d52f5ddc5559/go.mod h1:Lm4fhzy0S3P7GjerzuseGaBFVczsIKmEhIjcT52Hluo= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.14.2 h1:YXVoyPndbdvcEVcseEovVfp0qjJp7S+i5+xgp/Nfbdc= +github.com/bits-and-blooms/bitset v1.14.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..5fbb5ae --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,8 @@ +package common + +func Trim0x(s string) string { + if len(s) >= 2 && s[0:2] == "0x" { + return s[2:] + } + return s +} diff --git a/internal/common/testutils/common.go b/internal/common/testutils/common.go new file mode 100644 index 0000000..aee613a --- /dev/null +++ b/internal/common/testutils/common.go @@ -0,0 +1,14 @@ +package testutils + +import ( + "log/slog" + "os" +) + +func GetTestLogger() *slog.Logger { + handler := slog.NewTextHandler( + os.Stdout, + &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug}, + ) + return slog.New(handler) +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go new file mode 100644 index 0000000..518ffe8 --- /dev/null +++ b/internal/configuration/configuration.go @@ -0,0 +1,11 @@ +package configuration + +type Configuration struct { + KeystoreDir string + + GrpcPort string + MetricsPort string + + TLSCACert string + TLSServerKey string +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go new file mode 100644 index 0000000..367b43e --- /dev/null +++ b/internal/constants/constants.go @@ -0,0 +1,5 @@ +package constants + +const ( + RequestIdKey = "request_id" +) diff --git a/internal/crypto/bn254.go b/internal/crypto/bn254.go new file mode 100644 index 0000000..ce25699 --- /dev/null +++ b/internal/crypto/bn254.go @@ -0,0 +1,112 @@ +package crypto + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +type G1Point struct { + *bn254.G1Affine +} + +// Add another G1 point to this one +func (p *G1Point) Add(p2 *G1Point) *G1Point { + p.G1Affine.Add(p.G1Affine, p2.G1Affine) + return p +} + +// Sub another G1 point from this one +func (p *G1Point) Sub(p2 *G1Point) *G1Point { + p.G1Affine.Sub(p.G1Affine, p2.G1Affine) + return p +} + +// VerifyEquivalence verifies G1Point is equivalent the G2Point +func (p *G1Point) VerifyEquivalence(p2 *G2Point) (bool, error) { + return CheckG1AndG2DiscreteLogEquality(p.G1Affine, p2.G2Affine) +} + +func (p *G1Point) Serialize() []byte { + return SerializeG1(p.G1Affine) +} + +func (p *G1Point) Deserialize(data []byte) *G1Point { + return &G1Point{DeserializeG1(data)} +} + +type G2Point struct { + *bn254.G2Affine +} + +// Add another G2 point to this one +func (p *G2Point) Add(p2 *G2Point) *G2Point { + p.G2Affine.Add(p.G2Affine, p2.G2Affine) + return p +} + +// Sub another G2 point from this one +func (p *G2Point) Sub(p2 *G2Point) *G2Point { + p.G2Affine.Sub(p.G2Affine, p2.G2Affine) + return p +} + +func (p *G2Point) Serialize() []byte { + return SerializeG2(p.G2Affine) +} + +func (p *G2Point) Deserialize(data []byte) *G2Point { + return &G2Point{DeserializeG2(data)} +} + +type Signature struct { + *G1Point `json:"g1_point"` +} + +func (s *Signature) Add(otherS *Signature) *Signature { + s.G1Point.Add(otherS.G1Point) + return s +} + +// Verify a message against a public key +func (s *Signature) Verify(pubkey *G2Point, message [32]byte) (bool, error) { + ok, err := VerifySig(s.G1Affine, pubkey.G2Affine, message) + if err != nil { + return false, err + } + return ok, nil +} + +type PrivateKey = fr.Element + +type KeyPair struct { + PrivKey *PrivateKey + PubKey *G1Point +} + +func NewKeyPair(sk *PrivateKey) *KeyPair { + pk := MulByGeneratorG1(sk) + return &KeyPair{sk, &G1Point{pk}} +} + +// SignMessage This signs a message on G1, and so will require a G2Pubkey to verify +func (k *KeyPair) SignMessage(message [32]byte) *Signature { + H := MapToCurve(message) + sig := new(bn254.G1Affine).ScalarMultiplication(H, k.PrivKey.BigInt(new(big.Int))) + return &Signature{&G1Point{sig}} +} + +// SignHashedToCurveMessage This signs a message on G1, and so will require a G2Pubkey to verify +func (k *KeyPair) SignHashedToCurveMessage(g1HashedMsg *bn254.G1Affine) *Signature { + sig := new(bn254.G1Affine).ScalarMultiplication(g1HashedMsg, k.PrivKey.BigInt(new(big.Int))) + return &Signature{&G1Point{sig}} +} + +func (k *KeyPair) GetPubKeyG2() *G2Point { + return &G2Point{MulByGeneratorG2(k.PrivKey)} +} + +func (k *KeyPair) GetPubKeyG1() *G1Point { + return k.PubKey +} diff --git a/internal/crypto/utils.go b/internal/crypto/utils.go new file mode 100644 index 0000000..ec36379 --- /dev/null +++ b/internal/crypto/utils.go @@ -0,0 +1,157 @@ +package crypto + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +func VerifySig(sig *bn254.G1Affine, pubkey *bn254.G2Affine, msgBytes [32]byte) (bool, error) { + g2Gen := GetG2Generator() + + msgPoint := MapToCurve(msgBytes) + + var negSig bn254.G1Affine + negSig.Neg(sig) + + P := [2]bn254.G1Affine{*msgPoint, negSig} + Q := [2]bn254.G2Affine{*pubkey, *g2Gen} + + ok, err := bn254.PairingCheck(P[:], Q[:]) + if err != nil { + return false, nil + } + return ok, nil + +} + +// MapToCurve implements the simple hash-and-check (also sometimes try-and-increment) algorithm +// see https://hackmd.io/@benjaminion/bls12-381#Hash-and-check +// Note that this function needs to be the same as the one used in the contract: +// https://github.com/Layr-Labs/eigenlayer-middleware/blob/1feb6ae7e12f33ce8eefb361edb69ee26c118b5d/src/libraries/BN254.sol#L292 +// we don't use the newer constant time hash-to-curve algorithms as they are gas-expensive to +// compute onchain +func MapToCurve(digest [32]byte) *bn254.G1Affine { + one := new(big.Int).SetUint64(1) + three := new(big.Int).SetUint64(3) + x := new(big.Int) + x.SetBytes(digest[:]) + for { + // y = x^3 + 3 + xP3 := new(big.Int).Exp(x, big.NewInt(3), fp.Modulus()) + y := new(big.Int).Add(xP3, three) + y.Mod(y, fp.Modulus()) + + if y.ModSqrt(y, fp.Modulus()) == nil { + x.Add(x, one).Mod(x, fp.Modulus()) + } else { + var fpX, fpY fp.Element + fpX.SetBigInt(x) + fpY.SetBigInt(y) + return &bn254.G1Affine{ + X: fpX, + Y: fpY, + } + } + } +} + +func CheckG1AndG2DiscreteLogEquality( + pointG1 *bn254.G1Affine, + pointG2 *bn254.G2Affine, +) (bool, error) { + negGenG1 := new(bn254.G1Affine).Neg(GetG1Generator()) + return bn254.PairingCheck( + []bn254.G1Affine{*pointG1, *negGenG1}, + []bn254.G2Affine{*GetG2Generator(), *pointG2}, + ) +} + +func GetG1Generator() *bn254.G1Affine { + g1Gen := new(bn254.G1Affine) + _, err := g1Gen.X.SetString("1") + if err != nil { + return nil + } + _, err = g1Gen.Y.SetString("2") + if err != nil { + return nil + } + + return g1Gen +} + +func GetG2Generator() *bn254.G2Affine { + g2Gen := new(bn254.G2Affine) + g2Gen.X.SetString( + "10857046999023057135944570762232829481370756359578518086990519993285655852781", + "11559732032986387107991004021392285783925812861821192530917403151452391805634", + ) + g2Gen.Y.SetString( + "8495653923123431417604973247489272438418190587263600148770280649306958101930", + "4082367875863433681332203403145435568316851327593401208105741076214120093531", + ) + return g2Gen +} + +func MulByGeneratorG1(a *fr.Element) *bn254.G1Affine { + g1Gen := GetG1Generator() + return new(bn254.G1Affine).ScalarMultiplication(g1Gen, a.BigInt(new(big.Int))) +} + +func MulByGeneratorG2(a *fr.Element) *bn254.G2Affine { + g2Gen := GetG2Generator() + return new(bn254.G2Affine).ScalarMultiplication(g2Gen, a.BigInt(new(big.Int))) +} + +func SerializeG1(p *bn254.G1Affine) []byte { + b := make([]byte, 0) + tmp := p.X.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + tmp = p.Y.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + return b +} + +func DeserializeG1(b []byte) *bn254.G1Affine { + p := new(bn254.G1Affine) + p.X.SetBytes(b[0:32]) + p.Y.SetBytes(b[32:64]) + return p +} + +func SerializeG2(p *bn254.G2Affine) []byte { + b := make([]byte, 0) + tmp := p.X.A0.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + tmp = p.X.A1.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + tmp = p.Y.A0.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + tmp = p.Y.A1.Bytes() + for i := 0; i < 32; i++ { + b = append(b, tmp[i]) + } + return b +} + +func DeserializeG2(b []byte) *bn254.G2Affine { + p := new(bn254.G2Affine) + p.X.A0.SetBytes(b[0:32]) + p.X.A1.SetBytes(b[32:64]) + p.Y.A0.SetBytes(b[64:96]) + p.Y.A1.SetBytes(b[96:128]) + return p +} diff --git a/internal/metrics/README.md b/internal/metrics/README.md new file mode 100644 index 0000000..f4bf896 --- /dev/null +++ b/internal/metrics/README.md @@ -0,0 +1,10 @@ +## Metrics +It exposes the following metrics which can be scraped by Prometheus: + +* `rpc_server_request_total`: The total number of RPC requests received by the server. + * Labels: `method` +* `rpc_server_request_duration_seconds`: The duration of RPC requests in seconds. + * Labels: `method` + * Latency buckets for all methods +* `rpc_server_response_total`: The total number of RPC responses sent by the server. + * Labels: `method` and `status` (e.g. `success`, `failed`). \ No newline at end of file diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..de88255 --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,82 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +const ( + SuccessLabel = "success" + FailureLabel = "failure" + + SubsystemRPCServer = "rpc_server" + + MetricRequestTotal = "request_total" + MetricRequestDurationSeconds = "request_duration_seconds" + MetricResponseTotal = "response_total" + + MethodLabelName = "method" + StatusLabelName = "status" +) + +type Recorder interface { + RecordRPCServerRequest(method string) func() + RecordRPCServerResponse(method string, status string) +} + +type RPCServerMetrics struct { + RPCServerRequestTotal *prometheus.CounterVec + RPCServerRequestDurationSeconds *prometheus.SummaryVec + RPCServerResponseTotal *prometheus.CounterVec +} + +func NewRPCServerMetrics(ns string, registry *prometheus.Registry) *RPCServerMetrics { + m := &RPCServerMetrics{ + RPCServerRequestTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: ns, + Subsystem: SubsystemRPCServer, + Name: MetricRequestTotal, + Help: "Total number of RPC server requests.", + }, []string{MethodLabelName}), + RPCServerRequestDurationSeconds: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: ns, + Subsystem: SubsystemRPCServer, + Name: MetricRequestDurationSeconds, + Help: "Duration of RPC server requests in seconds.", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.01, 0.99: 0.001}, + }, []string{MethodLabelName}), + RPCServerResponseTotal: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: ns, + Subsystem: SubsystemRPCServer, + Name: MetricResponseTotal, + Help: "Total number of RPC server responses.", + }, []string{MethodLabelName, StatusLabelName}), + } + registry.MustRegister(m.RPCServerRequestTotal) + registry.MustRegister(m.RPCServerRequestDurationSeconds) + registry.MustRegister(m.RPCServerResponseTotal) + return m +} + +func (m *RPCServerMetrics) RecordRPCServerRequest(method string) func() { + m.RPCServerRequestTotal.WithLabelValues(method).Inc() + timer := prometheus.NewTimer(m.RPCServerRequestDurationSeconds.WithLabelValues(method)) + return func() { + timer.ObserveDuration() + } +} + +func (m *RPCServerMetrics) RecordRPCServerResponse(method string, status string) { + m.RPCServerResponseTotal.WithLabelValues(method, status).Inc() +} + +type NoopRPCMetrics struct{} + +func NewNoopRPCMetrics() *NoopRPCMetrics { + return &NoopRPCMetrics{} +} + +func (NoopRPCMetrics) RecordRPCServerRequest(method string) func() { + return func() {} +} + +func (NoopRPCMetrics) RecordRPCServerResponse(method string, status string) {} + +var _ Recorder = (*NoopRPCMetrics)(nil) diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..cc0058b --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,76 @@ +package server + +import ( + "fmt" + "log" + "log/slog" + "net" + "net/http" + "os" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + + v1 "github.com/Layr-Labs/cerberus-api/pkg/api/v1" + + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/metrics" + "github.com/Layr-Labs/cerberus/internal/services/kms" + "github.com/Layr-Labs/cerberus/internal/services/signing" + "github.com/Layr-Labs/cerberus/internal/store/filesystem" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" +) + +func Start(config *configuration.Configuration, logger *slog.Logger) { + lis, err := net.Listen("tcp", fmt.Sprintf(":%s", config.GrpcPort)) + if err != nil { + logger.Error(fmt.Sprintf("Failed to listen: %v", err)) + os.Exit(1) + } + + registry := prometheus.NewRegistry() + registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + registry.MustRegister(collectors.NewGoCollector()) + rpcMetrics := metrics.NewRPCServerMetrics("remote_bls", registry) + go startMetricsServer(registry, config.MetricsPort, logger) + + keystore := filesystem.NewStore(config.KeystoreDir, logger) + + var opts []grpc.ServerOption + if config.TLSCACert != "" && config.TLSServerKey != "" { + creds, err := credentials.NewServerTLSFromFile(config.TLSCACert, config.TLSServerKey) + if err != nil { + log.Fatalf("Failed to load TLS certificates: %v", err) + } + logger.Info("Server-side TLS support enabled") + + opts = append(opts, grpc.Creds(creds)) + } + + s := grpc.NewServer(opts...) + kmsService := kms.NewService(config, keystore, logger, rpcMetrics) + signingService := signing.NewService(config, keystore, logger, rpcMetrics) + + v1.RegisterKeyManagerServer(s, kmsService) + v1.RegisterSignerServer(s, signingService) + + // Register the reflection service + reflection.Register(s) + + logger.Info(fmt.Sprintf("Starting gRPC server on port %s...", config.GrpcPort)) + if err := s.Serve(lis); err != nil { + log.Fatalf("Failed to serve: %v", err) + } +} + +func startMetricsServer(r *prometheus.Registry, port string, logger *slog.Logger) { + http.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{})) + logger.Info(fmt.Sprintf("Starting metrics server on port %s...", port)) + if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil { + logger.Error(fmt.Sprintf("Failed to start metrics server: %v", err)) + } +} diff --git a/internal/services/kms/kms.go b/internal/services/kms/kms.go new file mode 100644 index 0000000..0ef35e7 --- /dev/null +++ b/internal/services/kms/kms.go @@ -0,0 +1,149 @@ +package kms + +import ( + "context" + "encoding/hex" + "fmt" + "log/slog" + "math/big" + + v1 "github.com/Layr-Labs/cerberus-api/pkg/api/v1" + + "github.com/Layr-Labs/cerberus/internal/common" + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/metrics" + "github.com/Layr-Labs/cerberus/internal/store" + + "github.com/Layr-Labs/bn254-keystore-go/keystore" + "github.com/Layr-Labs/bn254-keystore-go/mnemonic" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Service struct { + config *configuration.Configuration + logger *slog.Logger + store store.Store + metrics metrics.Recorder + + v1.UnimplementedKeyManagerServer +} + +func NewService( + config *configuration.Configuration, + store store.Store, + logger *slog.Logger, + metrics metrics.Recorder, +) *Service { + return &Service{ + config: config, + store: store, + metrics: metrics, + logger: logger.With("component", "kms"), + } +} + +func (k *Service) GenerateKeyPair( + ctx context.Context, + req *v1.GenerateKeyPairRequest, +) (*v1.GenerateKeyPairResponse, error) { + observe := k.metrics.RecordRPCServerRequest("kms/GenerateKeyPair") + defer observe() + password := req.GetPassword() + + // Generate a new BLS key pair + keyPair, err := keystore.NewKeyPair(password, mnemonic.English) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to generate BLS key pair: %v", err)) + k.metrics.RecordRPCServerResponse("kms/GenerateKeyPair", metrics.FailureLabel) + return nil, status.Error(codes.Internal, err.Error()) + } + + pubKeyHex, err := k.store.StoreKey(ctx, keyPair) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to save BLS key pair to file: %v", err)) + k.metrics.RecordRPCServerResponse("kms/GenerateKeyPair", metrics.FailureLabel) + return nil, status.Error(codes.Internal, err.Error()) + } + + // Convert the private key to a hex string + pkBytesSlice := make([]byte, len(keyPair.PrivateKey)) + copy(pkBytesSlice, keyPair.PrivateKey[:]) + privKeyHex := common.Trim0x(hex.EncodeToString(pkBytesSlice)) + + k.metrics.RecordRPCServerResponse("kms/GenerateKeyPair", metrics.SuccessLabel) + return &v1.GenerateKeyPairResponse{ + PublicKey: pubKeyHex, + PrivateKey: privKeyHex, + Mnemonic: keyPair.Mnemonic, + }, nil +} + +func (k *Service) ImportKey( + ctx context.Context, + req *v1.ImportKeyRequest, +) (*v1.ImportKeyResponse, error) { + observe := k.metrics.RecordRPCServerRequest("kms/ImportKey") + defer observe() + pkString := req.GetPrivateKey() + password := req.GetPassword() + pkMnemonic := req.GetMnemonic() + var pkBytes []byte + var err error + + if pkMnemonic != "" { + ks, err := keystore.NewKeyPairFromMnemonic(pkMnemonic, password) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to import key pair from mnemonic: %v", err)) + k.metrics.RecordRPCServerResponse("kms/ImportKey", metrics.FailureLabel) + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + pkBytes = ks.PrivateKey + } else { + pkInt, ok := new(big.Int).SetString(pkString, 10) + if ok { + // It's a bigInt + pkBytes = pkInt.Bytes() + } else { + // It's a hex string + pkHex := common.Trim0x(pkString) + pkBytes, err = hex.DecodeString(pkHex) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to import key pair from string: %v", err)) + k.metrics.RecordRPCServerResponse("kms/ImportKey", metrics.FailureLabel) + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + } + } + + pubKeyHex, err := k.store.StoreKey( + ctx, + &keystore.KeyPair{PrivateKey: pkBytes, Password: password}, + ) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to save BLS key pair to file: %v", err)) + k.metrics.RecordRPCServerResponse("kms/ImportKey", metrics.FailureLabel) + return nil, status.Error(codes.Internal, err.Error()) + } + + k.metrics.RecordRPCServerResponse("kms/ImportKey", metrics.SuccessLabel) + return &v1.ImportKeyResponse{PublicKey: pubKeyHex}, nil +} + +func (k *Service) ListKeys( + ctx context.Context, + req *v1.ListKeysRequest, +) (*v1.ListKeysResponse, error) { + observe := k.metrics.RecordRPCServerRequest("kms/ListKeys") + defer observe() + pubKeys, err := k.store.ListKeys(ctx) + if err != nil { + k.logger.Error(fmt.Sprintf("Failed to list keys: %v", err)) + k.metrics.RecordRPCServerResponse("kms/ListKeys", metrics.FailureLabel) + return nil, status.Error(codes.Internal, err.Error()) + } + + k.metrics.RecordRPCServerResponse("kms/ListKeys", metrics.SuccessLabel) + return &v1.ListKeysResponse{PublicKeys: pubKeys}, nil +} diff --git a/internal/services/kms/kms_test.go b/internal/services/kms/kms_test.go new file mode 100644 index 0000000..58dfdd5 --- /dev/null +++ b/internal/services/kms/kms_test.go @@ -0,0 +1,92 @@ +package kms + +import ( + "context" + "encoding/hex" + "testing" + + v1 "github.com/Layr-Labs/cerberus-api/pkg/api/v1" + + "github.com/Layr-Labs/cerberus/internal/common/testutils" + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/metrics" + "github.com/Layr-Labs/cerberus/internal/store/filesystem" + + "github.com/stretchr/testify/assert" +) + +const testPassword = "p@$$w0rd" + +func setup() (*Service, *filesystem.FileStore) { + logger := testutils.GetTestLogger() + config := &configuration.Configuration{ + KeystoreDir: "testdata/keystore", + } + fs := filesystem.NewStore(config.KeystoreDir, logger) + noopMetrics := metrics.NewNoopRPCMetrics() + service := NewService(config, fs, logger, noopMetrics) + + return service, fs +} + +func TestCreateKey(t *testing.T) { + service, fs := setup() + + ctx := context.Background() + + createResp, err := service.GenerateKeyPair( + ctx, + &v1.GenerateKeyPairRequest{Password: testPassword}, + ) + assert.NoError(t, err) + + storedKeyPair, err := fs.RetrieveKey(ctx, createResp.PublicKey, testPassword) + assert.NoError(t, err) + + pubKeyBytes := storedKeyPair.PubKey.Bytes() + pubKeyHex := hex.EncodeToString(pubKeyBytes[:]) + assert.Equal(t, createResp.PublicKey, pubKeyHex) + + privBytes := storedKeyPair.PrivKey.Bytes() + privKeyHex := hex.EncodeToString(privBytes[:]) + assert.Equal(t, createResp.PrivateKey, privKeyHex) +} + +func TestImportKey(t *testing.T) { + service, _ := setup() + + ctx := context.Background() + + createResp, err := service.GenerateKeyPair( + ctx, + &v1.GenerateKeyPairRequest{Password: testPassword}, + ) + assert.NoError(t, err) + + importResp, err := service.ImportKey(ctx, &v1.ImportKeyRequest{ + PrivateKey: createResp.PrivateKey, + Password: testPassword, + }) + assert.NoError(t, err) + assert.Equal(t, createResp.PublicKey, importResp.PublicKey) +} + +func TestListKeys(t *testing.T) { + service, fs := setup() + + ctx := context.Background() + + createResp, err := service.GenerateKeyPair( + ctx, + &v1.GenerateKeyPairRequest{Password: testPassword}, + ) + assert.NoError(t, err) + + listResp, err := service.ListKeys(ctx, &v1.ListKeysRequest{}) + assert.NoError(t, err) + assert.Contains(t, listResp.PublicKeys, createResp.PublicKey) + + storedKeys, err := fs.ListKeys(ctx) + assert.NoError(t, err) + assert.Contains(t, storedKeys, createResp.PublicKey) +} diff --git a/internal/services/signing/signing.go b/internal/services/signing/signing.go new file mode 100644 index 0000000..7d24c61 --- /dev/null +++ b/internal/services/signing/signing.go @@ -0,0 +1,80 @@ +package signing + +import ( + "context" + "fmt" + "log/slog" + + v1 "github.com/Layr-Labs/cerberus-api/pkg/api/v1" + + "github.com/Layr-Labs/cerberus/internal/common" + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/crypto" + "github.com/Layr-Labs/cerberus/internal/metrics" + "github.com/Layr-Labs/cerberus/internal/store" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Service struct { + config *configuration.Configuration + logger *slog.Logger + store store.Store + metrics metrics.Recorder + keyCache map[string]*crypto.KeyPair + v1.UnimplementedSignerServer +} + +func NewService( + config *configuration.Configuration, + store store.Store, + logger *slog.Logger, + metrics metrics.Recorder, +) *Service { + return &Service{ + config: config, + store: store, + metrics: metrics, + logger: logger.With("component", "signing"), + keyCache: make(map[string]*crypto.KeyPair), + } +} + +func (s *Service) SignGeneric( + ctx context.Context, + req *v1.SignGenericRequest, +) (*v1.SignGenericResponse, error) { + observe := s.metrics.RecordRPCServerRequest("signing/SignGeneric") + defer observe() + // Take the public key and data from the request + pubKeyHex := common.Trim0x(req.GetPublicKey()) + password := req.GetPassword() + + if _, ok := s.keyCache[pubKeyHex]; !ok { + s.logger.Info(fmt.Sprintf("In memory cache miss. Retrieving key for %s", req.PublicKey)) + blsKey, err := s.store.RetrieveKey(ctx, pubKeyHex, password) + if err != nil { + s.logger.Error(fmt.Sprintf("Failed to retrieve key: %v", err)) + s.metrics.RecordRPCServerResponse("signing/SignGeneric", metrics.FailureLabel) + return nil, status.Error(codes.Internal, err.Error()) + } + s.keyCache[pubKeyHex] = blsKey + } + blsKey := s.keyCache[pubKeyHex] + + data := req.GetData() + if len(data) > 32 { + s.logger.Error("Data is too long, must be 32 bytes") + s.metrics.RecordRPCServerResponse("signing/SignGeneric", metrics.FailureLabel) + return nil, status.Error(codes.InvalidArgument, "data is too long, must be 32 bytes") + } + + var byteArray [32]byte + copy(byteArray[:], data) + // Sign the data with the private key + sig := blsKey.SignMessage(byteArray) + s.logger.Info(fmt.Sprintf("Signed a message successfully using %s", req.PublicKey)) + s.metrics.RecordRPCServerResponse("signing/SignGeneric", metrics.SuccessLabel) + return &v1.SignGenericResponse{Signature: sig.Serialize()}, nil +} diff --git a/internal/services/signing/signing_test.go b/internal/services/signing/signing_test.go new file mode 100644 index 0000000..8411133 --- /dev/null +++ b/internal/services/signing/signing_test.go @@ -0,0 +1,42 @@ +package signing + +import ( + "context" + "encoding/hex" + "testing" + + v1 "github.com/Layr-Labs/cerberus-api/pkg/api/v1" + + "github.com/Layr-Labs/cerberus/internal/common/testutils" + "github.com/Layr-Labs/cerberus/internal/configuration" + "github.com/Layr-Labs/cerberus/internal/metrics" + "github.com/Layr-Labs/cerberus/internal/store/filesystem" + + "github.com/stretchr/testify/assert" +) + +func TestSigning(t *testing.T) { + // private key: 0x040ad69253b921aca71dd714cccc3095576fbe1a21f86c9b10cb5b119b1c6899 + pubKeyHex := "a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0" + password := "p@$$w0rd" + expectedSig := "0fea882fc5c936c304b0d79f4c256dbb2d38a2df74b44aaa483dfa87f1a86ede0bbc32080db378a408b90af7e264b9768a4b2f16c6953ec2611a13bc448d27e4" + data := []byte("somedata") + var bytes [32]byte + copy(bytes[:], data) + + config := &configuration.Configuration{ + KeystoreDir: "testdata/keystore", + } + logger := testutils.GetTestLogger() + store := filesystem.NewStore(config.KeystoreDir, logger) + m := metrics.NewNoopRPCMetrics() + signingService := NewService(config, store, logger, m) + + resp, err := signingService.SignGeneric(context.Background(), &v1.SignGenericRequest{ + PublicKey: pubKeyHex, + Data: bytes[:], + Password: password, + }) + assert.NoError(t, err) + assert.Equal(t, expectedSig, hex.EncodeToString(resp.Signature)) +} diff --git a/internal/services/signing/testdata/keystore/a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0.json b/internal/services/signing/testdata/keystore/a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0.json new file mode 100644 index 0000000..49a1394 --- /dev/null +++ b/internal/services/signing/testdata/keystore/a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0.json @@ -0,0 +1 @@ +{"crypto":{"kdf":{"function":"scrypt","params":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"da545c4ce983b0121e02a78cbf97ffbde4fcec6a2a3312b7e5ad0ee67b378425"},"message":""},"checksum":{"function":"sha256","params":{},"message":"f526116f0f8ce0698d6ae3eddcba316268a962080aacc1bd0c0b910d9ed25c78"},"cipher":{"function":"aes-128-ctr","params":{"iv":"7a264654c45ccfe6c93ac04631bb383b"},"message":"1481a16a3e6f8bb6bdc5e24304906e3f84d4e3062ac00ee8afc42bb791dda528"}},"description":"","pubkey":"a3111a2232584734d526d62cbb7c9a0d4ce1984a92b7ecb85bde8878fea5d1b0","path":"m/254/60/0/0","uuid":"bdf7a014d841465e995843f37b48f8c4","version":0,"curve":"bn254"} diff --git a/internal/store/filesystem/filesystem.go b/internal/store/filesystem/filesystem.go new file mode 100644 index 0000000..158bb01 --- /dev/null +++ b/internal/store/filesystem/filesystem.go @@ -0,0 +1,101 @@ +package filesystem + +import ( + "context" + "fmt" + + "log/slog" + "os" + + "github.com/Layr-Labs/cerberus/internal/crypto" + "github.com/Layr-Labs/cerberus/internal/store" + + "github.com/Layr-Labs/bn254-keystore-go/curve" + "github.com/Layr-Labs/bn254-keystore-go/keystore" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +const keyFileExtension = ".json" + +var _ store.Store = (*FileStore)(nil) + +type FileStore struct { + keystoreDir string + + logger *slog.Logger +} + +func NewStore( + keystoreDir string, + logger *slog.Logger, +) *FileStore { + logger = logger.With("component", "filesystem-store") + if err := os.MkdirAll(keystoreDir, 0755); err != nil { + logger.Error(fmt.Sprintf("Error creating keystore directory: %v", err)) + os.Exit(1) + } + logger.Info("Created keystore directory successfully") + return &FileStore{ + keystoreDir: keystoreDir, + logger: logger, + } +} + +func (s *FileStore) RetrieveKey( + ctx context.Context, + pubKey string, + password string, +) (*crypto.KeyPair, error) { + path := s.keystoreDir + "/" + pubKey + ".json" + return readPrivateKeyFromFile(path, password) +} + +func (s *FileStore) StoreKey( + ctx context.Context, + keyPair *keystore.KeyPair, +) (string, error) { + keyStore, err := keyPair.Encrypt(keystore.KDFScrypt, curve.BN254) + if err != nil { + return "", err + } + + err = keyStore.SaveWithPubKeyHex(s.keystoreDir, "") + if err != nil { + return "", err + } + + return keyStore.PubKey, nil +} + +func (s *FileStore) ListKeys(ctx context.Context) ([]string, error) { + files, err := os.ReadDir(s.keystoreDir) + if err != nil { + return nil, err + } + + s.logger.Debug(fmt.Sprintf("Found %d key files", len(files))) + pubKeys := make([]string, len(files)) + for i, file := range files { + pubKeys[i] = file.Name()[0 : len(file.Name())-len(keyFileExtension)] + } + + return pubKeys, nil +} + +func readPrivateKeyFromFile(path string, password string) (*crypto.KeyPair, error) { + ks := new(keystore.Keystore) + err := ks.FromFile(path) + if err != nil { + return nil, err + } + + skBytes, err := ks.Decrypt(password) + if err != nil { + return nil, err + } + + privKey := new(fr.Element).SetBytes(skBytes) + keyPair := crypto.NewKeyPair(privKey) + return keyPair, nil +} diff --git a/internal/store/filesystem/filesystem_test.go b/internal/store/filesystem/filesystem_test.go new file mode 100644 index 0000000..37859e0 --- /dev/null +++ b/internal/store/filesystem/filesystem_test.go @@ -0,0 +1,69 @@ +package filesystem + +import ( + "context" + "encoding/hex" + "os" + "testing" + + "github.com/Layr-Labs/bn254-keystore-go/keystore" + "github.com/Layr-Labs/bn254-keystore-go/mnemonic" + + "github.com/Layr-Labs/cerberus/internal/common/testutils" + + "github.com/stretchr/testify/assert" +) + +const ( + tmpDir = "tmp" + keystoreDir = "keystore" +) + +func TestFileStoreKeyOperators(t *testing.T) { + defer cleanup() + + ctx := context.Background() + logger := testutils.GetTestLogger() + fs := NewStore(tmpDir+"/"+keystoreDir, logger) + testPassword := "p@$$w0rd" + + // Run this 10 times for enough randomness to be allowed + // in generating key pairs + for i := 0; i < 10; i++ { + keyPair, err := keystore.NewKeyPair(testPassword, mnemonic.English) + assert.NoError(t, err, "Failed to generate key pair") + + pubKeyHex, err := fs.StoreKey(ctx, keyPair) + assert.NoError(t, err, "Failed to store key") + + storedKeyPair, err := fs.RetrieveKey(ctx, pubKeyHex, testPassword) + assert.NoError(t, err, "Failed to retrieve key") + + pubKeyBytes := storedKeyPair.PubKey.Bytes() + pubKeySlice := pubKeyBytes[:] + assert.Equal(t, pubKeyHex, hex.EncodeToString(pubKeySlice), "public key mismatch") + + pkBytes := storedKeyPair.PrivKey.Bytes() + slice := pkBytes[:] + assert.Equal( + t, + hex.EncodeToString(keyPair.PrivateKey), + hex.EncodeToString(slice), + "private key mismatch", + ) + + keys, err := fs.ListKeys(ctx) + if err != nil { + return + } + + assert.Contains(t, keys, pubKeyHex, "Expected key not found") + } +} + +func cleanup() { + err := os.RemoveAll(tmpDir) + if err != nil { + return + } +} diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..dbd6cd7 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,26 @@ +package store + +import ( + "context" + + "github.com/Layr-Labs/bn254-keystore-go/keystore" + + "github.com/Layr-Labs/cerberus/internal/crypto" +) + +type Store interface { + // RetrieveKey retrieves the private key from the store + // using the public key and password + // Returns the private key or an error if it fails + // Public key is used to identify the key in the store + RetrieveKey(ctx context.Context, pubKey string, password string) (*crypto.KeyPair, error) + + // StoreKey stores the private key in the store + // using the public key as identifier + // Returns an error if it fails + // Password is used to encrypt the private key before storing if it is provided + StoreKey(ctx context.Context, keyPair *keystore.KeyPair) (string, error) + + // ListKeys returns a list of public keys stored in the store + ListKeys(ctx context.Context) ([]string, error) +} diff --git a/monitoring/signer.json b/monitoring/signer.json new file mode 100644 index 0000000..3ec32a6 --- /dev/null +++ b/monitoring/signer.json @@ -0,0 +1,413 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "default": true, + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "remote_bls_rpc_server_request_duration_seconds{method=\"signing/SignGeneric\", quantile=\"0.95\"}", + "fullMetaSearch": false, + "includeNullMetadata": false, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Sign P95", + "type": "timeseries" + }, + { + "datasource": { + "default": true, + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Request / sec", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "rate(remote_bls_rpc_server_request_total{method=\"signing/SignGeneric\"}[$__range])", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Sign RPS", + "type": "timeseries" + }, + { + "datasource": { + "default": true, + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "C {__name__=\"remote_bls_rpc_server_response_total\", instance=\"localhost:9091\", job=\"prometheus\", method=\"signing/SignGeneric\", status=\"success\"}" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "C {__name__=\"remote_bls_rpc_server_response_total\", instance=\"localhost:9091\", job=\"prometheus\", method=\"signing/SignGeneric\", status=\"success\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "SignGeneric" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "remote_bls_rpc_server_response_total{status=\"success\", method=\"signing/SignGeneric\"}", + "fullMetaSearch": false, + "hide": true, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "A", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "ddzhl7fatdbeob" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "remote_bls_rpc_server_response_total{method=\"signing/SignGeneric\"}", + "fullMetaSearch": false, + "hide": true, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{method}}", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "($A * 100) / $B", + "hide": false, + "refId": "C", + "type": "math" + } + ], + "title": "Sign Request/Response Success %age", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "BLS Signer", + "uid": "bdzhsw9k29am8d", + "version": 17, + "weekStart": "" +} \ No newline at end of file