diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7383688..d227267 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: jobs: fmt: - name: Check Formatting + name: Check formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,21 +21,8 @@ jobs: - name: Check Formatting run: gofmt -l . - lint: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.24' - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - version: v2.1 - - build_and_test: - name: Build and Test + lint_build_and_test: + name: Lint, build and test runs-on: ubuntu-latest needs: fmt steps: @@ -44,6 +31,22 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.24' + - name: Install protoc + run: | + sudo apt-get update + sudo apt-get install -y protobuf-compiler + - name: Install protoc-gen-go and protoc-gen-go-grpc + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + # Ensure GOPATH/bin is in PATH + echo "${GOPATH}/bin" >> $GITHUB_PATH + - name: Generate code from gRPC protocol definition + run: go generate ./... + - name: Lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.1 - name: Build run: go build ./... - name: Test diff --git a/.gitignore b/.gitignore index 80dfcdb..3d875d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ .idea/ -*.ph1 -*.ptau -*.ph2 +# Generated protobuf code +**/ceremony.pb.go +**/ceremony_grpc.pb.go trusted-setup +.DS_Store diff --git a/README.md b/README.md index 5f0774e..63f01cf 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,42 @@ # ZKP Trusted Setup Ceremony Coordinator +**Warning** +Please note that this tool is under development. Please consider it unusable before the first release. + ## Overview This utility program allows for performing a Trusted Setup Ceremony in a Multi-Party Computation fashion. It is meant to be used by the Coordinator of the ceremony, as well as by the Contributors. In the end, the Coordinator will obtain Proving and Verifying Keys, which can be used to generate proofs for the circuit the ceremony was conducted for. +### Online mode + +The primary mode of the program. In this mode, the Coordinator runs the ceremony server, which is responsible for +accepting contributions from the Contributors. The Contributors connect to the Coordinator and contribute to the +ceremony. + +See help for `server` and `client` commands for details. + +### Offline mode + +In this mode, the Coordinator and the Contributors run the ceremony locally. The Coordinator initializes the ceremony +and generates the initial Phase 2 file. The Coordinator sends the file to the first Contributor. The Contributor +generates their contribution and sends them to the Coordinator in the form of a Phase 2 file. The Coordinator verifies +the contributions and, if the verification is positive, sends it to the next Contributor. + +In this mode, sending Phase 2 files must be performed manually by the Coordinator and Contributors. + +At the end of the ceremony, the Coordinator will have a list of accepted contributions. The Coordinator can then +perform the final verification and extract the Proving and the Verifying Keys. + +See help for `init`, `contrib`, `verify` and `extract` commands for details. + +### Snarkjs powers of tau (ptau) -> Phase 1 conversion + +The tool can convert a Snarkjs powers of tau file to a Phase 1 file. This step is performed by the Coordinator before +the initialization of the offline mode ceremony, if the Coordinator has a ptau file that they wish to use in the ceremony. + +This step is not necessary if the Coordinator already has a Phase 1 file. + ## Constraints Gnark version used for implementing the circuit the ceremony will be conducted for must match the Gnark version used @@ -14,20 +46,57 @@ Your Gnark project must satisfy the following constraints: - Supported curve: BN254 - Supported backend: Groth16 +## Prerequisites + +These are one-time steps that must be done in order to build the program. + +Install [Go](https://go.dev/dl/). Any recent version will do. Look into `go.mod` to see the minimum required version. + +Install [Protocol Buffer Compiler](https://protobuf.dev/installation/). + +Install gRPC for Go: + +```shell +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +``` + +## Build + +To build the project, run: +```shell +$ go generate ./... +$ go build . +```` + +in the project's root directory. + +The test suite can be executed with: +```shell +$ go test -v ./... +``` + ## Usage -Run the program with `go run .` or `go run `. +Run the program with: +```shell +$ go run . + +# or, after the program was built +./trusted-setup +``` Running the program with no arguments lists the available commands. Running the program with the command but without options will display the command's help. ## Commands -### `help` +### General purpose commands + +#### `help` Print help. -### `ptau` +#### `ptau` Convert a Snarkjs powers of tau file to a Phase 1 file. This step is performed by the Coordinator. @@ -38,9 +107,54 @@ tau file to a Phase 1 file, which can be used to initialize the Phase 2 of the c - `--ptau` - A Snarkjs powers of tau file, - `--phase1` - The output Phase 1 file. -### `init` +### Online mode commands + +#### `server` + +Start a Ceremony server. This step is performed by the Coordinator. + +The server is responsible for orchestrating the ceremony, receiving contributions from the participants and, in the end, +generating Proving and Verifying Keys. + +The server is configured with a JSON file. An example configuration is shown below: +```json5 +{ + // A human-readable name for the ceremony that will be sent to contributors. + // Used for identification purposes; can be any reasonably sized string. + "ceremonyName": "test ceremony", + // The IP address on which the server will listen on. + "host": "127.0.0.1", + // The TCP port on which the server will listen on. + "port": 7312, + // The path to the R1CS file generated from a Gnark circuit. + "r1cs": "resources/server.r1cs", + // The path to the Phase 1 file (possibly generated from a ptau file - see the `ptau` command for details). + "phase1": "resources/server.ph1", +} +``` + +Coordination of the ceremony is automatic. No action from the Coordinator is required besides starting the server +and stopping it with CTRL+C at any arbitrary moment. At CTRL+C, the server stops accepting new contributions and starts +key extraction from the existing contributions. + +- `--config` - Path to a JSON file containing the server configuration. + +#### `client` + +Connect to a Ceremony server and provide contributions. This step is performed by the Contributors. + +The client is responsible for connecting to the server and providing contributions. The client is configured with +a host and port of the server. Participation in the ceremony is automatic. No action from the Contributor is required +besides starting the client. + +- `--host` - The IP address of the server, +- `--port` - The port of the server. + +### Offline mode commands + +#### `init` -Initialize Phase 2 of the ceremony for the given R1CS with a Phase 1 file. This step is performed by the Coordinator. +Initialize Phase 2 of the ceremony for the given R1CS with a Phase 1 file. This step is performed by the Coordinator. This step outputs a Phase 2 file based on the provided R1CS and Phase 1 file. The Coordinator must provide the R1CS file generated from a Gnark circuit and the Phase 1 file either generated in the previous step or from another @@ -58,7 +172,7 @@ The command outputs a beacon value, which must then be passed as an argument to - `--phase2` - The output path for the Phase 2 file, - `--srscommons` - The output path for circuit-independent components of the Groth16 SRS. -### `contribute` +#### `contribute` Contribute randomness to Phase 2. This step is performed by all the participants of the ceremony. @@ -70,7 +184,7 @@ appended to the name. - `--phase2` - The existing Phase 2 file created in the `init` step or in the previous run of the `contribute` step. -### `verify` +#### `verify` Verify the last randomness contributed to Phase 2. This step is performed by the Coordinator. @@ -85,7 +199,7 @@ If the verification is successful, the Coordinator can either: - `--phase2prev` - A Phase 2 file being an input to the contribution - `--phase2next` - A Phase 2 file that was contributed to. -### `extract-keys` +#### `extract-keys` Extract the Proving and Verifying Keys. This step is performed by the Coordinator. diff --git a/cmd/doc.go b/cmd/doc.go deleted file mode 100644 index bac2458..0000000 --- a/cmd/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package cmd provides handlers for the command line interface for the application. -// The handlers are used in `main.go`. -package cmd diff --git a/go.mod b/go.mod index 776bd95..34773e4 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,12 @@ require ( github.com/consensys/gnark v0.13.0 github.com/consensys/gnark-crypto v0.18.0 github.com/drand/go-clients v0.2.3 + github.com/golang/protobuf v1.5.4 github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v3 v3.3.8 github.com/worldcoin/ptau-deserializer v0.2.0 + google.golang.org/grpc v1.73.0 + google.golang.org/protobuf v1.36.6 ) replace github.com/worldcoin/ptau-deserializer => github.com/reilabs/ptau-deserializer v0.0.0-20250630133456-6f3242b72b0a @@ -41,6 +44,7 @@ require ( github.com/prometheus/procfs v0.17.0 // indirect github.com/ronanh/intcomp v1.1.1 // indirect github.com/rs/zerolog v1.34.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.dedis.ch/fixbuf v1.0.3 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -51,7 +55,5 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect - google.golang.org/grpc v1.71.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ec4ef42..f037cc1 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/ronanh/intcomp v1.1.1/go.mod h1:7FOLy3P3Zj3er/kVrU/pl+Ql7JFZj7bwliMGk github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= @@ -141,8 +143,8 @@ go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/ go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= @@ -173,8 +175,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e h1: google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= -google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 4afcca4..7a20671 100644 --- a/main.go +++ b/main.go @@ -8,14 +8,18 @@ import ( "github.com/urfave/cli/v3" - "github.com/reilabs/trusted-setup/cmd" + "github.com/reilabs/trusted-setup/offline" + "github.com/reilabs/trusted-setup/online" ) func main() { app := &cli.Command{ Name: filepath.Base(os.Args[0]), Usage: "a ZKP Trusted Setup Ceremony Coordinator", - Description: "This program allows for initializing a trusted setup ceremony and contributing to it.\n" + + Description: "This program allows for initializing a trusted setup ceremony and contributing to it.\n\n" + + "The program has two modes:\n" + + "- the online mode: run an automated ceremony server and let clients connect and contribute\n" + + "- the offline mode: orchestrate the ceremony yourself, manually managing contributions\n\n" + "Phase 2 of the ceremony can be initialized from a previously generated Phase 1 file\n" + "or from a Snarkjs powers of tau file. New contributions can be added to Phase 2.\n" + "The contributions can be verified. Proving and verifying keys can be exported from the\n" + @@ -31,124 +35,12 @@ func main() { }, DefaultCommand: "help", Suggest: true, - Commands: []*cli.Command{ - { - Name: "ptau", - Usage: "Convert a Snarkjs powers of tau file to a Phase 1 file", - Action: cmd.PtauToPhase1, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "ptau", - Usage: "Snarkjs powers of tau file", - Required: true, - }, - &cli.StringFlag{ - Name: "phase1", - Usage: "Output Phase 1 file", - Required: true, - }, - }, - }, - { - Name: "init", - Usage: "Initialize Phase 2 for the given R1CS with a Phase 1 file", - Action: cmd.Phase2Init, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "phase1", - Usage: "Phase 1 file", - Required: true, - }, - &cli.StringFlag{ - Name: "r1cs", - Usage: "R1CS file generated from a Gnark circuit", - Required: true, - }, - &cli.StringFlag{ - Name: "phase2", - Usage: "Output path for the Phase 2 file", - Required: true, - }, - &cli.StringFlag{ - Name: "srscommons", - Usage: "Output path for circuit-independent components of the Groth16 SRS", - Required: true, - }, - }, - }, - { - Name: "contribute", - Usage: "Contribute randomness to Phase 2", - Action: cmd.Phase2Contribute, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "phase2", - Usage: "The existing Phase 2 file created in the init step or in the previous run\n" + - "of the contribute step.", - Required: true, - }, - }, - }, - { - Name: "verify", - Usage: "Verify the last randomness contributed to Phase 2", - Action: cmd.Phase2Verify, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "phase2prev", - Usage: "Phase 2 file being an input to the contribution", - Required: true, - }, - &cli.StringFlag{ - Name: "phase2next", - Usage: "Phase 2 file that was contributed to", - Required: true, - }, - }, - }, - { - Name: "extract-keys", - Usage: "Extract Proving and Verifying Keys", - Action: cmd.Phase2ExtractKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "r1cs", - Usage: "R1CS file generated from a gnark circuit", - Required: true, - }, - &cli.StringFlag{ - Name: "srscommons", - Usage: "Circuit-independent components of the Groth16 SRS file generated on the Phase 2" + - " initialization", - Required: true, - }, - &cli.StringFlag{ - Name: "beacon", - Usage: "Random string generated on the Phase 2 initialization", - Required: true, - }, - &cli.StringSliceFlag{ - Name: "phase2", - Usage: "List of Phase 2 files to verify the contributions in the order they were\n" + - "created. Contributions are verified in pairs, so at least two files must be provided.\n" + - " This DOES NOT INCLUDE the original Phase 2 file generated on initialization.", - Required: true, - }, - &cli.StringFlag{ - Name: "pk", - Usage: "Output path for the proving key", - Required: true, - }, - &cli.StringFlag{ - Name: "vk", - Usage: "Output path for the verifying key", - Required: true, - }, - }, - }, - }, + Commands: []*cli.Command{}, } + app.Commands = append(app.Commands, offline.Commands...) + app.Commands = append(app.Commands, online.Commands...) + if err := app.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } diff --git a/cmd/contribute.go b/offline/actions/contribute.go similarity index 95% rename from cmd/contribute.go rename to offline/actions/contribute.go index c8416fb..a7b1944 100644 --- a/cmd/contribute.go +++ b/offline/actions/contribute.go @@ -1,4 +1,4 @@ -package cmd +package actions import ( "context" @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli/v3" - "github.com/reilabs/trusted-setup/phase2" + "github.com/reilabs/trusted-setup/offline/phase2" ) // appendTimestamp appends a timestamp to the given string if not present, or updates the timestamp if present. diff --git a/offline/actions/doc.go b/offline/actions/doc.go new file mode 100644 index 0000000..50c3ffb --- /dev/null +++ b/offline/actions/doc.go @@ -0,0 +1,3 @@ +// Package actions provides handlers for the command line interface for the application. +// The handlers are used in `main.go`. +package actions diff --git a/cmd/init.go b/offline/actions/init.go similarity index 91% rename from cmd/init.go rename to offline/actions/init.go index 323acdc..9c169d5 100644 --- a/cmd/init.go +++ b/offline/actions/init.go @@ -1,4 +1,4 @@ -package cmd +package actions import ( "context" @@ -7,12 +7,12 @@ import ( "github.com/urfave/cli/v3" - "github.com/reilabs/trusted-setup/phase2" + "github.com/reilabs/trusted-setup/offline/phase2" "github.com/reilabs/trusted-setup/utils/randomness" ) func Phase2Init(_ context.Context, cmd *cli.Command) error { - rand, err := randomness.NewDrandProvider() + rand, err := randomness.New() if err != nil { return err } diff --git a/cmd/ptau.go b/offline/actions/ptau.go similarity index 89% rename from cmd/ptau.go rename to offline/actions/ptau.go index fe57df7..ddf82bc 100644 --- a/cmd/ptau.go +++ b/offline/actions/ptau.go @@ -1,4 +1,4 @@ -package cmd +package actions import ( "context" @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli/v3" - "github.com/reilabs/trusted-setup/phase1" + "github.com/reilabs/trusted-setup/offline/phase1" ) func PtauToPhase1(_ context.Context, cmd *cli.Command) error { diff --git a/cmd/verify.go b/offline/actions/verify.go similarity index 96% rename from cmd/verify.go rename to offline/actions/verify.go index 1dae0a0..71de6a6 100644 --- a/cmd/verify.go +++ b/offline/actions/verify.go @@ -1,4 +1,4 @@ -package cmd +package actions import ( "context" @@ -8,7 +8,7 @@ import ( "github.com/urfave/cli/v3" - "github.com/reilabs/trusted-setup/phase2" + "github.com/reilabs/trusted-setup/offline/phase2" ) func Phase2Verify(_ context.Context, cmd *cli.Command) error { diff --git a/offline/commands.go b/offline/commands.go new file mode 100644 index 0000000..a5fa3fb --- /dev/null +++ b/offline/commands.go @@ -0,0 +1,129 @@ +package offline + +import ( + "github.com/urfave/cli/v3" + + "github.com/reilabs/trusted-setup/offline/actions" +) + +// Commands defines all CLI commands related to offline operations. +var Commands = []*cli.Command{ + { + Name: "ptau", + Usage: "Convert a Snarkjs powers of tau file to a Phase 1 file", + Action: actions.PtauToPhase1, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "ptau", + Usage: "Snarkjs powers of tau file", + Required: true, + }, + &cli.StringFlag{ + Name: "phase1", + Usage: "Output Phase 1 file", + Required: true, + }, + }, + }, + { + Name: "init", + Category: "offline mode", + Usage: "Initialize Phase 2 for the given R1CS with a Phase 1 file", + Action: actions.Phase2Init, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "phase1", + Usage: "Phase 1 file", + Required: true, + }, + &cli.StringFlag{ + Name: "r1cs", + Usage: "R1CS file generated from a Gnark circuit", + Required: true, + }, + &cli.StringFlag{ + Name: "phase2", + Usage: "Output path for the Phase 2 file", + Required: true, + }, + &cli.StringFlag{ + Name: "srscommons", + Usage: "Output path for circuit-independent components of the Groth16 SRS", + Required: true, + }, + }, + }, + { + Name: "contribute", + Category: "offline mode", + Usage: "Contribute randomness to Phase 2", + Action: actions.Phase2Contribute, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "phase2", + Usage: "The existing Phase 2 file created in the init step or in the previous run\n" + + "of the contribute step.", + Required: true, + }, + }, + }, + { + Name: "verify", + Category: "offline mode", + Usage: "Verify the last randomness contributed to Phase 2", + Action: actions.Phase2Verify, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "phase2prev", + Usage: "Phase 2 file being an input to the contribution", + Required: true, + }, + &cli.StringFlag{ + Name: "phase2next", + Usage: "Phase 2 file that was contributed to", + Required: true, + }, + }, + }, + { + Name: "extract-keys", + Category: "offline mode", + Usage: "Extract Proving and Verifying Keys", + Action: actions.Phase2ExtractKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "r1cs", + Usage: "R1CS file generated from a gnark circuit", + Required: true, + }, + &cli.StringFlag{ + Name: "srscommons", + Usage: "Circuit-independent components of the Groth16 SRS file generated on the Phase 2" + + " initialization", + Required: true, + }, + &cli.StringFlag{ + Name: "beacon", + Usage: "Random string generated on the Phase 2 initialization", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "phase2", + Usage: "List of Phase 2 files to verify the contributions in the order they were\n" + + "created. Contributions are verified in pairs, so at least two files must be provided.\n" + + " This DOES NOT INCLUDE the original Phase 2 file generated on initialization.", + Required: true, + }, + &cli.StringFlag{ + Name: "pk", + Usage: "Output path for the proving key", + Required: true, + }, + &cli.StringFlag{ + Name: "vk", + Usage: "Output path for the verifying key", + Required: true, + }, + }, + }, +} diff --git a/phase1/marshal.go b/offline/phase1/marshal.go similarity index 84% rename from phase1/marshal.go rename to offline/phase1/marshal.go index 19a5e2d..7830ee6 100644 --- a/phase1/marshal.go +++ b/offline/phase1/marshal.go @@ -10,7 +10,7 @@ import ( // ToFile writes the Phase 1 object to the file specified by phase1Path. // // Returns nil on success and error on failure. -func ToFile(phase1 mpcsetup.Phase1, phase1Path string) error { +func ToFile(phase1 *mpcsetup.Phase1, phase1Path string) error { writer, err := os.Create(phase1Path) if err != nil { return err @@ -34,10 +34,10 @@ func ToFile(phase1 mpcsetup.Phase1, phase1Path string) error { // FromFile reads the Phase 1 object from the file specified by phase1Path. // // Returns the Phase 1 object and nil on success and empty Phase 1 object and error on failure. -func FromFile(phase1Path string) (phase1 mpcsetup.Phase1, err error) { +func FromFile(phase1Path string) (phase1 *mpcsetup.Phase1, err error) { reader, err := os.Open(phase1Path) if err != nil { - return mpcsetup.Phase1{}, err + return nil, err } defer func(reader *os.File) { err := reader.Close() @@ -47,9 +47,10 @@ func FromFile(phase1Path string) (phase1 mpcsetup.Phase1, err error) { }(reader) log.Printf("Loading Phase 1 from %s", phase1Path) + phase1 = &mpcsetup.Phase1{} _, err = phase1.ReadFrom(reader) if err != nil { - return mpcsetup.Phase1{}, err + return nil, err } return diff --git a/phase1/phase1.go b/offline/phase1/phase1.go similarity index 94% rename from phase1/phase1.go rename to offline/phase1/phase1.go index e7a0362..a2faf42 100644 --- a/phase1/phase1.go +++ b/offline/phase1/phase1.go @@ -24,5 +24,5 @@ func FromPtau(ptauFilePath string, outputPhase1FilePath string) error { return err } - return ToFile(phase1, outputPhase1FilePath) + return ToFile(&phase1, outputPhase1FilePath) } diff --git a/phase2/marshal.go b/offline/phase2/marshal.go similarity index 90% rename from phase2/marshal.go rename to offline/phase2/marshal.go index c800b7d..147ca84 100644 --- a/phase2/marshal.go +++ b/offline/phase2/marshal.go @@ -12,7 +12,7 @@ import ( // ToFile writes the Phase 2 object provided in phase2 to a file specified by phase2Path. // // Returns nil on success and error on failure. -func ToFile(phase2 mpcsetup.Phase2, phase2Path string) error { +func ToFile(phase2 *mpcsetup.Phase2, phase2Path string) error { writer, err := os.Create(phase2Path) if err != nil { return err @@ -36,10 +36,10 @@ func ToFile(phase2 mpcsetup.Phase2, phase2Path string) error { // FromFile reads the Phase 2 object from the file specified by phase2Path and returns the Phase 2 object. // // Returns the Phase 2 object and nil on success and nil and error on failure. -func FromFile(phase2Path string) (phase2 mpcsetup.Phase2, err error) { +func FromFile(phase2Path string) (phase2 *mpcsetup.Phase2, err error) { reader, err := os.Open(phase2Path) if err != nil { - return mpcsetup.Phase2{}, err + return nil, err } defer func(reader *os.File) { err := reader.Close() @@ -49,9 +49,10 @@ func FromFile(phase2Path string) (phase2 mpcsetup.Phase2, err error) { }(reader) log.Printf("Loading Phase 2 from %s", phase2Path) + phase2 = &mpcsetup.Phase2{} _, err = phase2.ReadFrom(reader) if err != nil { - return mpcsetup.Phase2{}, err + return nil, err } return @@ -61,7 +62,7 @@ func FromFile(phase2Path string) (phase2 mpcsetup.Phase2, err error) { // specified by srsCommonsPath. // // Returns nil on success and error on failure. -func SrsCommonsToFile(srsCommons mpcsetup.SrsCommons, srsCommonsPath string) error { +func SrsCommonsToFile(srsCommons *mpcsetup.SrsCommons, srsCommonsPath string) error { writer, err := os.Create(srsCommonsPath) if err != nil { return err @@ -86,10 +87,10 @@ func SrsCommonsToFile(srsCommons mpcsetup.SrsCommons, srsCommonsPath string) err // srsCommonsPath and returns the SrsCommons object. // // Returns the SrsCommons object and nil on success and nil and error on failure. -func SrsCommonsFromFile(srsCommonsPath string) (srsCommons mpcsetup.SrsCommons, err error) { +func SrsCommonsFromFile(srsCommonsPath string) (srsCommons *mpcsetup.SrsCommons, err error) { reader, err := os.Open(srsCommonsPath) if err != nil { - return mpcsetup.SrsCommons{}, err + return nil, err } defer func(reader *os.File) { err := reader.Close() @@ -99,9 +100,10 @@ func SrsCommonsFromFile(srsCommonsPath string) (srsCommons mpcsetup.SrsCommons, }(reader) log.Printf("Loading SRS commons from %s", srsCommonsPath) + srsCommons = &mpcsetup.SrsCommons{} _, err = srsCommons.ReadFrom(reader) if err != nil { - return mpcsetup.SrsCommons{}, err + return nil, err } return diff --git a/phase2/phase2.go b/offline/phase2/phase2.go similarity index 91% rename from phase2/phase2.go rename to offline/phase2/phase2.go index 722ac4b..c89215e 100644 --- a/phase2/phase2.go +++ b/offline/phase2/phase2.go @@ -6,10 +6,9 @@ import ( "log" "github.com/consensys/gnark/backend/groth16/bn254/mpcsetup" - cs "github.com/consensys/gnark/constraint/bn254" - "github.com/reilabs/trusted-setup/phase1" - "github.com/reilabs/trusted-setup/r1cs" + "github.com/reilabs/trusted-setup/offline/phase1" + "github.com/reilabs/trusted-setup/offline/r1cs" ) // Init initializes the multi-party computation Phase 2 object based on a serialized Phase 1 and R1CS objects. @@ -35,15 +34,15 @@ func Init( log.Print("Generating SRS commons form Phase 1") srsCommons := p1.Seal(beacon) - err = SrsCommonsToFile(srsCommons, outputSrsCommonsPath) + err = SrsCommonsToFile(&srsCommons, outputSrsCommonsPath) if err != nil { return err } log.Print("Initializing Phase 2") p2 := mpcsetup.Phase2{} - _ = p2.Initialize(ccs.(*cs.R1CS), &srsCommons) - err = ToFile(p2, outputPhase2FilePath) + _ = p2.Initialize(ccs, &srsCommons) + err = ToFile(&p2, outputPhase2FilePath) if err != nil { return err } @@ -88,7 +87,7 @@ func Verify(phase2prevFilePath, phase2nextFilePath string) error { } log.Print("Verifying the most recent Phase 2 against the previous step") - err = prev.Verify(&next) + err = prev.Verify(next) if err != nil { return err } @@ -135,11 +134,11 @@ func ExtractKeys( if err != nil { return err } - phase2s = append(phase2s, &p2) + phase2s = append(phase2s, p2) } log.Print("Verifying all Phase 2 contributions and generating Keys") - pk, vk, err := mpcsetup.VerifyPhase2(ccs.(*cs.R1CS), &srsCommons, beacon, phase2s...) + pk, vk, err := mpcsetup.VerifyPhase2(ccs, srsCommons, beacon, phase2s...) if err != nil { return err } diff --git a/r1cs/marshal.go b/offline/r1cs/marshal.go similarity index 54% rename from r1cs/marshal.go rename to offline/r1cs/marshal.go index 9359f3e..91e7c4e 100644 --- a/r1cs/marshal.go +++ b/offline/r1cs/marshal.go @@ -7,38 +7,14 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bn254" ) -// ToFile writes the R1CS constraint system provided in ccs to a file specified by r1csPath. -// -// Returns nil on success and error on failure. -func ToFile(ccs constraint.ConstraintSystem, r1csPath string) error { - writer, err := os.Create(r1csPath) - if err != nil { - return err - } - defer func(writer *os.File) { - err := writer.Close() - if err != nil { - log.Printf("Error closing r1cs writer: %v", err) - } - }(writer) - - log.Printf("Storing R1CS to %s", r1csPath) - _, err = ccs.WriteTo(writer) - if err != nil { - return err - } - - return nil -} - // FromFile reads the R1CS constraint system from the file specified by r1csPath and returns the constraint system // object. // // Returns the constraint system object and nil on success and nil and error on failure. -func FromFile(r1csPath string) (constraint.ConstraintSystem, error) { +func FromFile(r1csPath string) (*cs.R1CS, error) { reader, err := os.Open(r1csPath) if err != nil { return nil, err @@ -57,5 +33,5 @@ func FromFile(r1csPath string) (constraint.ConstraintSystem, error) { return nil, err } - return r1cs, nil + return r1cs.(*cs.R1CS), nil } diff --git a/test/offline_ceremony_test.go b/offline/test/offline_test.go similarity index 77% rename from test/offline_ceremony_test.go rename to offline/test/offline_test.go index 5d4f58c..60b74e7 100644 --- a/test/offline_ceremony_test.go +++ b/offline/test/offline_test.go @@ -1,4 +1,4 @@ -package test +package test_test import ( "bytes" @@ -11,10 +11,10 @@ import ( "github.com/stretchr/testify/assert" - "github.com/reilabs/trusted-setup/phase1" - "github.com/reilabs/trusted-setup/phase2" - "github.com/reilabs/trusted-setup/r1cs" - "github.com/reilabs/trusted-setup/utils/randomness" + "github.com/reilabs/trusted-setup/offline/phase1" + "github.com/reilabs/trusted-setup/offline/phase2" + "github.com/reilabs/trusted-setup/offline/r1cs" + test_circuit "github.com/reilabs/trusted-setup/test" ) // testOfflineCeremony verifies the trusted setup ceremony in the offline mode. @@ -36,27 +36,50 @@ func TestOfflineCeremony(t *testing.T) { teardown() } -const phase1FileName = "test.phase1" -const phase2FileName = "test.phase2" -const srsCommonsFileName = "test.srscommons" -const r1csFileName = "test.r1cs" -const pkFileName = "test.pk" -const vkFileName = "test.vk" +var ( + r1csFileName string + phase1FileName string + phase2FileName string + srsCommonsFileName string + pkFileName string + vkFileName string + rand []byte +) -var rand randomness.MockProvider +func createTempFile(pattern string) *os.File { + f, err := os.CreateTemp("", pattern) + if err != nil { + panic(err) + } + + return f +} func setup() { - ccs, err := buildCcs() + ccs, err := test_circuit.BuildCcs() if err != nil { panic(err) } - err = r1cs.ToFile(ccs, r1csFileName) + + fCcs := createTempFile("r1cs") + r1csFileName = fCcs.Name() + _, err = ccs.WriteTo(fCcs) if err != nil { panic(err) } - mockBeacon := bytes.Repeat([]byte{0x42}, 32) - rand = randomness.MockProvider{Beacon: mockBeacon} + fP1 := createTempFile("phase1") + phase1FileName = fP1.Name() + fP2 := createTempFile("phase2") + phase2FileName = fP2.Name() + fSrs := createTempFile("srsCommons") + srsCommonsFileName = fSrs.Name() + fPk := createTempFile("pk") + pkFileName = fPk.Name() + fVk := createTempFile("vk") + vkFileName = fVk.Name() + + rand = bytes.Repeat([]byte{0x42}, 32) } func teardown() { @@ -100,12 +123,12 @@ func testPtau(t *testing.T) { p1beforeContribution, err := phase1.FromFile(phase1FileName) assert.NoError(t, err) p1.Contribute() - err = p1beforeContribution.Verify(&p1) + err = p1beforeContribution.Verify(p1) assert.NoError(t, err) } func testInit(t *testing.T) { - assert.NoError(t, phase2.Init(phase1FileName, r1csFileName, phase2FileName, srsCommonsFileName, rand.GetBeacon())) + assert.NoError(t, phase2.Init(phase1FileName, r1csFileName, phase2FileName, srsCommonsFileName, rand)) p2, err := phase2.FromFile(phase2FileName) assert.NoError(t, err) @@ -162,7 +185,7 @@ func testExtractKeys(t *testing.T) { assert.NoError( t, phase2.ExtractKeys( - r1csFileName, srsCommonsFileName, phase2Contributions[1:], pkFileName, vkFileName, rand.GetBeacon(), + r1csFileName, srsCommonsFileName, phase2Contributions[1:], pkFileName, vkFileName, rand, ), ) } @@ -173,6 +196,6 @@ func testProveAndVerify(t *testing.T) { ccs, err := r1cs.FromFile(r1csFileName) assert.NoError(t, err) - err = proveAndVerify(ccs, pk, vk) + err = test_circuit.ProveAndVerify(ccs, &pk, &vk) assert.NoError(t, err) } diff --git a/test/resources/test.ptau b/offline/test/resources/test.ptau similarity index 100% rename from test/resources/test.ptau rename to offline/test/resources/test.ptau diff --git a/online/actions/client.go b/online/actions/client.go new file mode 100644 index 0000000..535cb6a --- /dev/null +++ b/online/actions/client.go @@ -0,0 +1,21 @@ +package actions + +import ( + "context" + + "github.com/urfave/cli/v3" + + "github.com/reilabs/trusted-setup/online/client" +) + +func Client(_ context.Context, cmd *cli.Command) error { + host := cmd.String("host") + port := cmd.String("port") + + c, err := client.New(host, port) + if err != nil { + return err + } + + return c.Contribute() +} diff --git a/online/actions/server.go b/online/actions/server.go new file mode 100644 index 0000000..b6dca32 --- /dev/null +++ b/online/actions/server.go @@ -0,0 +1,99 @@ +package actions + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/urfave/cli/v3" + + "github.com/reilabs/trusted-setup/offline/phase1" + offline_phase2 "github.com/reilabs/trusted-setup/offline/phase2" + "github.com/reilabs/trusted-setup/offline/r1cs" + server_config "github.com/reilabs/trusted-setup/online/config" + "github.com/reilabs/trusted-setup/online/contribution" + "github.com/reilabs/trusted-setup/online/server" + "github.com/reilabs/trusted-setup/online/server/ceremony_service" + "github.com/reilabs/trusted-setup/online/server/contributors_manager" + "github.com/reilabs/trusted-setup/online/server/coordinator" + "github.com/reilabs/trusted-setup/utils/randomness" +) + +func Server(_ context.Context, cmd *cli.Command) error { + configFilePath := cmd.String("config") + + log.Printf("Loading config file: %s", configFilePath) + config, err := server_config.New(configFilePath) + if err != nil { + log.Fatal(err) + } + + ccs, err := r1cs.FromFile(config.R1cs) + if err != nil { + return err + } + + p1, err := phase1.FromFile(config.Phase1) + if err != nil { + return err + } + + beaconProvider, err := randomness.New() + if err != nil { + return err + } + + log.Print("Initializing Phase 2") + last := contribution.New(p1, ccs, beaconProvider.GetBeacon()) + + service := ceremony_service.New( + config.CeremonyName, + coordinator.New( + last, + contributors_manager.New(), + ), + ) + + s := server.New(service) + + err = s.Start(config.Host, config.Port) + if err != nil { + log.Fatal(err) + } + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + fmt.Println("Press Ctrl+C to end Ceremony and generate Keys") + <-sigs + s.Stop() + + // TODO: this is temporary, keys will go to S3 + pkTemp, err := getTempFilePath("pk") + if err != nil { + return err + } + vkTemp, err := getTempFilePath("vk") + if err != nil { + return err + } + fmt.Println("Generating keys...") + pk, vk := last.ExtractKeys() + return offline_phase2.PkVkToFile(pk, pkTemp, vk, vkTemp) +} + +func getTempFilePath(pattern string) (string, error) { + tempFile, err := os.CreateTemp("", pattern) + if err != nil { + return "", err + } + // Close immediately because we're not writing to these files, we just need paths + err = tempFile.Close() + if err != nil { + log.Printf("error closing %s", tempFile.Name()) + } + + return tempFile.Name(), nil +} diff --git a/online/api/api.go b/online/api/api.go new file mode 100644 index 0000000..fa5d4b9 --- /dev/null +++ b/online/api/api.go @@ -0,0 +1,9 @@ +// Package api contains definitions for the gRPC protocol between the ceremony client and ceremony server. +// +// The protocol definition is in ceremony.proto. To regenerate the Go code, run: `go generate ./...`. +// +// Below is the go-generate directive that will be invoked automatically on code regeneration. This is an input +// for the go compiler, please do not remove this comment. +// +//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ceremony.proto +package api diff --git a/online/api/ceremony.proto b/online/api/ceremony.proto new file mode 100644 index 0000000..029261c --- /dev/null +++ b/online/api/ceremony.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +// For local testing, run `go generate ./...` after changing this file. + +package ceremony; + +option go_package = "github.com/reilabs/trusted-setup/api;api"; + +service CeremonyService { + // Contribute handles the whole ceremony process. + // + // After calling the method, the client will receive the stream of ContributeResponse containing + // TurnNotification messages. They indicate the position of the contributor in the queue + // and serve as a notification of the waiting progress. + // + // After TurnNotification with CanContribute == true arrives, the stream of DataChunk will arrive. + // They contain the last verified contribution accepted by the server. The client can reconstruct + // the Phase 2 object from them and contribute to it. + // + // Afterwards, the client is allow to stream back DataChunk of their contribution. + // + // When this is done, the server will reply with ValidationResponse. This ends the contribution. + rpc Contribute(stream DataChunk) returns (stream ContributeResponse); +} + +// ContributeResponse defines possible messages streamed by the server to the client. +message ContributeResponse { + oneof response { + Hello hello = 1; + // TurnNotification notifies the client when it's their turn to contribute. + TurnNotification turn = 2; + // ValidationResponse inform the client if their contribution was accepted. + ValidationResponse validation = 3; + // DataChunk contains the last verified contribution accepted by the server. + DataChunk lastContribution = 4; + } +} + +// Hello is sent once by the server when Contribute is first called. +message Hello { + // ceremonyName is a human-readable name of the ceremony for identification purposes. + string ceremonyName = 1; +} + +// TurnNotification notifies the client when it's their turn to contribute. +message TurnNotification { + // canContribute is true if the client is now allowed to contribute. + bool canContribute = 1; + // positionInQueue updates the client's current position in queue. + // If positionInQueue == 0, this is the client's turn. + uint32 positionInQueue = 2; +} + +// DataChunk contains a chunk of data sent between client and server. +message DataChunk { + bytes data = 1; +} + +// UploadResponse is sent by the server to the client after they submit their contribution. +message ValidationResponse { + // isValid is true if the contribution was considered valid and was accepted by server. + bool isValid = 1; + // rejectionReason contains the reason of the rejection, if server does not accept client's contribution. + // Valid only if isValid == false. + string rejectionReason = 2; +} diff --git a/online/api/responses.go b/online/api/responses.go new file mode 100644 index 0000000..28dd83b --- /dev/null +++ b/online/api/responses.go @@ -0,0 +1,58 @@ +package api + +import ( + "fmt" +) + +// NewHello returns a Hello message with the given ceremony name. +func NewHello(ceremonyName string) *ContributeResponse { + return &ContributeResponse{ + Response: &ContributeResponse_Hello{ + Hello: &Hello{ + CeremonyName: ceremonyName, + }, + }, + } +} + +// NewDataChunk returns a DataChunk message with the given data. +func NewDataChunk(data []byte) *DataChunk { + return &DataChunk{ + Data: data, + } +} + +// NewLastContribution returns a ContributeResponse_LastContribution message with the given data. +func NewLastContribution(data []byte) *ContributeResponse { + return &ContributeResponse{ + Response: &ContributeResponse_LastContribution{ + LastContribution: NewDataChunk(data), + }, + } +} + +// NewTurnNotification returns a TurnNotification message with the given position. +func NewTurnNotification(position int) *ContributeResponse { + return &ContributeResponse{ + Response: &ContributeResponse_Turn{ + Turn: &TurnNotification{ + CanContribute: position == 0, + PositionInQueue: uint32(position), + }, + }, + } +} + +// NewValidationResponse returns a ValidationResponse message with the given error. +// +// If the error is nil, the response is considered valid. +func NewValidationResponse(err error) *ContributeResponse { + return &ContributeResponse{ + Response: &ContributeResponse_Validation{ + Validation: &ValidationResponse{ + IsValid: err == nil, + RejectionReason: fmt.Sprintf("%v", err), + }, + }, + } +} diff --git a/online/api/stream_utils/stream_reader.go b/online/api/stream_utils/stream_reader.go new file mode 100644 index 0000000..837987e --- /dev/null +++ b/online/api/stream_utils/stream_reader.go @@ -0,0 +1,55 @@ +package stream_utils + +import ( + "bytes" + "fmt" + "io" + + "github.com/reilabs/trusted-setup/online/api" +) + +type dataChunkReceiver interface { + Recv() (*api.DataChunk, error) +} + +type dataChunkResponseReceiver interface { + Recv() (*api.ContributeResponse, error) +} + +// NewStreamReader creates a new streamReader instance. +// +// The stream is meant to be a gRPC stream of either *api.DataChunk or *api.ContributeResponse. +func NewStreamReader(stream interface{}) io.Reader { + return &streamReader{stream, bytes.Buffer{}} +} + +type streamReader struct { + stream interface{} + buffer bytes.Buffer +} + +// Read implements the io.Reader interface. It reads data from the stream set by NewStreamReader into p. +func (d *streamReader) Read(p []byte) (n int, err error) { + switch d.stream.(type) { + case dataChunkReceiver: + if d.buffer.Len() == 0 { + resp, err := d.stream.(dataChunkReceiver).Recv() + if err != nil { + return 0, err + } + d.buffer.Write(resp.Data) + } + case dataChunkResponseReceiver: + if d.buffer.Len() == 0 { + resp, err := d.stream.(dataChunkResponseReceiver).Recv() + if err != nil { + return 0, err + } + d.buffer.Write(resp.GetLastContribution().Data) + } + default: + return 0, fmt.Errorf("unsupported stream type: %T", d.stream) + } + + return d.buffer.Read(p) +} diff --git a/online/api/stream_utils/stream_writer.go b/online/api/stream_utils/stream_writer.go new file mode 100644 index 0000000..cacfc81 --- /dev/null +++ b/online/api/stream_utils/stream_writer.go @@ -0,0 +1,47 @@ +package stream_utils + +import ( + "fmt" + "io" + + "github.com/reilabs/trusted-setup/online/api" +) + +type dataChunkSender interface { + Send(contribution *api.DataChunk) error +} + +type dataChunkResponseSender interface { + Send(contribution *api.ContributeResponse) error +} + +// NewStreamWriter creates a new streamWriter instance. +// +// The stream is meant to be a gRPC stream of either *api.DataChunk or *api.ContributeResponse. +func NewStreamWriter(stream interface{}) io.Writer { + return &streamWriter{stream} +} + +type streamWriter struct { + stream interface{} +} + +// Write implements the io.Writer interface. It sends the data from p to the stream set by NewStreamWriter. +func (u *streamWriter) Write(p []byte) (n int, err error) { + switch u.stream.(type) { + case dataChunkSender: + err = u.stream.(dataChunkSender).Send(api.NewDataChunk(p)) + if err != nil { + return 0, err + } + case dataChunkResponseSender: + err = u.stream.(dataChunkResponseSender).Send(api.NewLastContribution(p)) + if err != nil { + return 0, err + } + default: + return 0, fmt.Errorf("unsupported stream type: %T", u.stream) + } + + return len(p), nil +} diff --git a/online/client/client.go b/online/client/client.go new file mode 100644 index 0000000..b865292 --- /dev/null +++ b/online/client/client.go @@ -0,0 +1,59 @@ +// Package client provides a client for the ceremony service. +package client + +import ( + "context" + "log" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/reilabs/trusted-setup/online/api" +) + +// Client is used by contributors to connect to the coordinator and contribute to the ceremony. +type Client struct { + connection *grpc.ClientConn + stream api.CeremonyService_ContributeClient +} + +// New creates a new Client for the trusted ceremony. +// +// The client immediately connects to the given host and port. +// +// On success, a Client object is returned. On failure, an error is returned. +func New(host string, port string) (*Client, error) { + hostPort := host + ":" + port + log.Printf("Connecting to %s...", hostPort) + + c := Client{} + var err error + c.connection, err = grpc.NewClient( + hostPort, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, err + } + + client := api.NewCeremonyServiceClient(c.connection) + c.stream, err = client.Contribute(context.Background()) + if err != nil { + return nil, err + } + return &c, nil +} + +// Contribute contributes to the ceremony. +// +// On success, nil is returned. On failure, an error is returned. +func (c *Client) Contribute() error { + defer func() { + err := c.connection.Close() + if err != nil { + log.Printf("failed to close connection: %v", err) + } + }() + + return c.messageLoop() +} diff --git a/online/client/handlers.go b/online/client/handlers.go new file mode 100644 index 0000000..09bed3a --- /dev/null +++ b/online/client/handlers.go @@ -0,0 +1,83 @@ +package client + +import ( + "fmt" + "io" + "log" + + "github.com/reilabs/trusted-setup/online/api" + "github.com/reilabs/trusted-setup/online/api/stream_utils" + "github.com/reilabs/trusted-setup/online/contribution" +) + +func (c *Client) messageLoop() error { + for { + resp, err := c.stream.Recv() + if err == io.EOF { + log.Print("Contribution stream closed") + break + } + if err != nil { + log.Printf("error receiving from stream: %v", err) + return err + } + switch r := resp.Response.(type) { + case *api.ContributeResponse_Hello: + c.onHello(r) + case *api.ContributeResponse_Turn: + c.onTurn(r) + case *api.ContributeResponse_Validation: + return c.onValidation(r) + default: + log.Printf("unexpected response type: %T", r) + } + } + + return nil +} + +func (c *Client) onHello(msg *api.ContributeResponse_Hello) { + log.Printf("Joined ceremony: %s", msg.Hello.CeremonyName) +} + +func (c *Client) onTurn(msg *api.ContributeResponse_Turn) { + log.Printf("Contribution slot assigned, position in queue: %d", msg.Turn.PositionInQueue) + if !msg.Turn.CanContribute { + log.Printf("Waiting for our turn...") + return + } + + c.contribute() +} + +func (c *Client) onValidation(msg *api.ContributeResponse_Validation) error { + if !msg.Validation.IsValid { + return fmt.Errorf("contribution rejected: %s", msg.Validation.RejectionReason) + } + + log.Print("Contribution accepted") + return nil +} + +func (c *Client) contribute() { + log.Print("Our turn, downloading last contribution") + + p2 := contribution.NewContributable() + downloader := stream_utils.NewStreamReader(c.stream) + n, err := p2.ReadFrom(downloader) + if err != nil { + log.Fatalf("failed to download last phase 2: %v", err) + } + log.Printf("Received %d bytes", n) + + log.Print("Generating contribution") + p2.Contribute() + + log.Print("Uploading our contribution") + uploader := stream_utils.NewStreamWriter(c.stream) + n, err = p2.WriteTo(uploader) + if err != nil { + log.Fatalf("failed to upload next phase 2: %v", err) + } + log.Printf("Sent %d bytes", n) +} diff --git a/online/commands.go b/online/commands.go new file mode 100644 index 0000000..440588a --- /dev/null +++ b/online/commands.go @@ -0,0 +1,42 @@ +package online + +import ( + "github.com/urfave/cli/v3" + + "github.com/reilabs/trusted-setup/online/actions" +) + +// Commands defines all CLI commands related to online operations. +var Commands = []*cli.Command{ + { + Name: "server", + Category: "online mode", + Usage: "Start a Ceremony server", + Action: actions.Server, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "JSON file containing the server configuration", + Required: true, + }, + }, + }, + { + Name: "client", + Category: "online mode", + Usage: "Connect to a Ceremony server and provide contributions", + Action: actions.Client, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Usage: "address of the Ceremony server", + Required: true, + }, + &cli.StringFlag{ + Name: "port", + Usage: "port the Ceremony server listens on", + Required: true, + }, + }, + }, +} diff --git a/online/config/config.go b/online/config/config.go new file mode 100644 index 0000000..aeca0a8 --- /dev/null +++ b/online/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" +) + +type Config struct { + CeremonyName string `json:"ceremonyName"` + Host string `json:"host"` + Port int `json:"port"` + R1cs string `json:"r1cs"` + Phase1 string `json:"phase1"` +} + +func (c *Config) Validate() error { + const msg = "config validation error: " + if c.CeremonyName == "" { + return fmt.Errorf(msg + "ceremony name must be provided") + } + if c.Host == "" { + return fmt.Errorf(msg + "host must be provided") + } + if c.Port == 0 { + return fmt.Errorf(msg + "port must be provided and non-zero") + } + if c.R1cs == "" { + return fmt.Errorf(msg + "R1CS input path must be provided") + } + if c.Phase1 == "" { + return fmt.Errorf(msg + "Phase 1 input path must be provided") + } + return nil +} + +func New(filePath string) (*Config, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %w", err) + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + log.Printf("error closing config file: %v", err) + } + }(file) + + byteValue, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := json.Unmarshal(byteValue, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + if err = config.Validate(); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/online/config/config_test.go b/online/config/config_test.go new file mode 100644 index 0000000..0d02f44 --- /dev/null +++ b/online/config/config_test.go @@ -0,0 +1,156 @@ +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/reilabs/trusted-setup/online/config" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + config config.Config + wantError bool + }{ + { + name: "valid config", + config: config.Config{ + CeremonyName: "Test ceremony", + Host: "localhost", + Port: 8080, + R1cs: "input.r1cs", + Phase1: "phase1.dat", + }, + wantError: false, + }, + { + name: "missing name", + config: config.Config{ + Port: 8080, + R1cs: "input.r1cs", + Phase1: "phase1.dat", + }, + wantError: true, + }, + { + name: "missing host", + config: config.Config{ + CeremonyName: "Test ceremony", + Port: 8080, + R1cs: "input.r1cs", + Phase1: "phase1.dat", + }, + wantError: true, + }, + { + name: "port zero", + config: config.Config{ + CeremonyName: "Test ceremony", + Host: "localhost", + Port: 0, + R1cs: "input.r1cs", + Phase1: "phase1.dat", + }, + wantError: true, + }, + { + name: "missing R1cs", + config: config.Config{ + CeremonyName: "Test ceremony", + Host: "localhost", + Port: 8080, + Phase1: "phase1.dat", + }, + wantError: true, + }, + { + name: "missing Phase1", + config: config.Config{ + CeremonyName: "Test ceremony", + Host: "localhost", + Port: 8080, + R1cs: "input.r1cs", + }, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + err := tt.config.Validate() + if (err != nil) != tt.wantError { + t.Errorf("validate() error = %v, wantError %v", err, tt.wantError) + } + }, + ) + } +} + +func TestNew(t *testing.T) { + // Create a temporary directory for test config files + tempDir := t.TempDir() + + validConfigContent := `{ + "ceremonyName": "Test ceremony", + "host": "localhost", + "port": 8080, + "r1cs": "input.r1cs", + "phase1": "phase1.dat" + }` + + invalidConfigContent := `{ + "host": "", + "port": 0, + "r1cs": "", + "phase1": "" + }` + + invalidJSONContent := `{ + "host": "localhost", + "port": 8080, + "r1cs": "input.r1cs", + "phase1": "phase1.dat", + ` // malformed JSON + + tests := []struct { + name string + content string + expectError bool + }{ + {name: "valid config file", content: validConfigContent, expectError: false}, + {name: "invalid config file (validation fail)", content: invalidConfigContent, expectError: true}, + {name: "invalid JSON file", content: invalidJSONContent, expectError: true}, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + filePath := filepath.Join(tempDir, tt.name+".json") + err := os.WriteFile(filePath, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("failed to write temp config file: %v", err) + } + + cfg, err := config.New(filePath) + if (err != nil) != tt.expectError { + t.Errorf("New() error = %v, expectError %v", err, tt.expectError) + } + if err == nil && cfg == nil { + t.Errorf("New() returned nil config without error") + } + }, + ) + } + + t.Run( + "non-existent file", func(t *testing.T) { + _, err := config.New(filepath.Join(tempDir, "nonexistent.json")) + if err == nil { + t.Errorf("expected error when loading non-existent config file, got nil") + } + }, + ) +} diff --git a/online/contribution/contribution.go b/online/contribution/contribution.go new file mode 100644 index 0000000..a500a20 --- /dev/null +++ b/online/contribution/contribution.go @@ -0,0 +1,115 @@ +// Package contribution implements all phases (initialization, contribution, verification) +// of a multi-party computation setup ceremony. +package contribution + +import ( + "io" + + "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/groth16/bn254/mpcsetup" + cs "github.com/consensys/gnark/constraint/bn254" +) + +type contribution struct { + contribution *mpcsetup.Phase2 + srsCommons *mpcsetup.SrsCommons + evals *mpcsetup.Phase2Evaluations + beacon []byte +} + +// Verifiable is an interface for a contribution object that is going to be verified by a verifier. +type Verifiable interface { + ReadFrom(reader io.Reader) (int64, error) +} + +// Contributable is an interface for a contribution object that is going to be contributed to. +type Contributable interface { + Contribute() + ReadFrom(reader io.Reader) (int64, error) + WriteTo(writer io.Writer) (int64, error) +} + +// NewContributable returns a new empty contribution instance implementing Contributable. +func NewContributable() Contributable { + return &contribution{contribution: new(mpcsetup.Phase2)} +} + +// Contribute contributes randomness to contribution. +func (p *contribution) Contribute() { + p.contribution.Contribute() +} + +// ReadFrom reads the contribution from the reader. +// +// It returns the number of bytes read and any error encountered. +func (p *contribution) ReadFrom(reader io.Reader) (int64, error) { + return p.contribution.ReadFrom(reader) +} + +// WriteTo writes the contribution object to the writer. +// +// It returns the number of bytes written and any error encountered. +func (p *contribution) WriteTo(writer io.Writer) (int64, error) { + return p.contribution.WriteTo(writer) +} + +// Contribution is an interface for doing operations on a contribution from the ceremony coordinator perspective. +type Contribution interface { + NewVerifiable() Verifiable + AddContribution(next Verifiable) error + ExtractKeys() (groth16.ProvingKey, groth16.VerifyingKey) + WriteTo(writer io.Writer) (int64, error) +} + +// New creates a new Contribution from a Phase 1 object, R1CS and beacon. +// +// phase1 is the Phase 1 object produced either by Gnark of by the `ptau` command from a Snarkjs file. +// r1cs is the R1CS constraint system object generated by Gnark for a circuit the ceremony is performed for. +// beacon is a 32-byte array of random bytes. It is used for generating Phase 1 and later for keys generation. +func New(phase1 *mpcsetup.Phase1, r1cs *cs.R1CS, beacon []byte) Contribution { + p2 := new(mpcsetup.Phase2) + srsCommons := phase1.Seal(beacon) + evals := p2.Initialize(r1cs, &srsCommons) + + return &contribution{ + p2, + &srsCommons, + &evals, + beacon, + } +} + +// NewVerifiable returns a new empty contribution instance implementing Verifiable. +// +// This object can be reconstructed from a stream of bytes with ReadFrom and passed to AddContribution for verification. +func (p *contribution) NewVerifiable() Verifiable { + return &contribution{contribution: new(mpcsetup.Phase2)} +} + +// AddContribution adds a new contribution to the existing one. +// +// The next object is verified against the current one. If verification fails, an error is returned. +// If the contribution is valid, next considered the new state. +func (p *contribution) AddContribution(next Verifiable) error { + err := p.contribution.Verify(next.(*contribution).contribution) + if err != nil { + return err + } + p.contribution = next.(*contribution).contribution + return nil +} + +// ExtractKeys extracts the proving and verifying keys from the contribution. +func (p *contribution) ExtractKeys() (groth16.ProvingKey, groth16.VerifyingKey) { + // We intentionally don't use mpcsetup.VerifyPhase2() here. + // + // mpcsetup.VerifyPhase2() accepts a list of all accepted contributions and runs + // them in pairs through .Verify(). Then, the last contribution of this chain + // is used to call .Seal() to get the keys. + // + // This is exactly what has been already done in p.AddContribution() for every incoming + // contribution. Our p.contribution is the result of .Verify() passing + // for every submitted contribution. It is enough to implement only the last step + // of mpcsetup.VerifyPhase2(), which is .Seal(). + return p.contribution.Seal(p.srsCommons, p.evals, p.beacon) +} diff --git a/online/contribution/contribution_test.go b/online/contribution/contribution_test.go new file mode 100644 index 0000000..4ec6d5d --- /dev/null +++ b/online/contribution/contribution_test.go @@ -0,0 +1,68 @@ +package contribution_test + +import ( + "bytes" + "testing" + + "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/groth16/bn254/mpcsetup" + cs "github.com/consensys/gnark/constraint/bn254" + "github.com/stretchr/testify/assert" + + "github.com/reilabs/trusted-setup/offline/phase1" + "github.com/reilabs/trusted-setup/offline/r1cs" + "github.com/reilabs/trusted-setup/online/contribution" + test_circuit "github.com/reilabs/trusted-setup/test" +) + +func setup() (*cs.R1CS, *mpcsetup.Phase1, []byte) { + ccs, err := r1cs.FromFile("../test/resources/server.r1cs") + if err != nil { + panic(err) + } + + p1, err := phase1.FromFile("../test/resources/server.ph1") + if err != nil { + panic(err) + } + + return ccs, p1, bytes.Repeat([]byte{0x42}, 32) +} + +func teardown(ccs *cs.R1CS, pk *groth16.ProvingKey, vk *groth16.VerifyingKey) { + err := test_circuit.ProveAndVerify(ccs, pk, vk) + if err != nil { + panic(err) + } +} + +func Test(t *testing.T) { + // Generate initial data: constraint system, Phase 1 and random beacon + ccs, p1, beacon := setup() + + // Initialize Phase 2 from Phase 1, circuit constraint system and random beacon + p2 := contribution.New(p1, ccs, beacon) + + // Serialize initial Phase 2 to a buffer + var buf bytes.Buffer + _, err := p2.WriteTo(&buf) + assert.NoError(t, err) + + // Recreate the initial contribution from a buffer + contrib := contribution.NewContributable() + _, err = contrib.ReadFrom(&buf) + assert.NoError(t, err) + + // Contribute + contrib.Contribute() + + // Submit contribution + err = p2.AddContribution(contrib.(contribution.Verifiable)) + assert.NoError(t, err) + + // One contribution should be enough to generate keys + pk, vk := p2.ExtractKeys() + + // Check that keys can be used for proof generation and verification + teardown(ccs, &pk, &vk) +} diff --git a/online/server/ceremony_service/ceremony_service.go b/online/server/ceremony_service/ceremony_service.go new file mode 100644 index 0000000..88eaad7 --- /dev/null +++ b/online/server/ceremony_service/ceremony_service.go @@ -0,0 +1,86 @@ +// Package ceremony_service implements the gRPC service for the multi-party computation setup ceremony. +package ceremony_service + +import ( + "context" + "log" + + "google.golang.org/grpc/peer" + + "github.com/reilabs/trusted-setup/online/api" + "github.com/reilabs/trusted-setup/online/api/stream_utils" + "github.com/reilabs/trusted-setup/online/server/coordinator" +) + +type ceremonyService struct { + api.UnimplementedCeremonyServiceServer + + name string + coordinator coordinator.Coordinator +} + +// New returns a new instance of CeremonyServiceServer. +// +// # The returned object can be passed to the gRPC server constructor +// +// Accepts a name of the ceremony and a ceremony coordinator instance. +func New( + name string, coordinator coordinator.Coordinator, +) api.CeremonyServiceServer { + return &ceremonyService{name: name, coordinator: coordinator} +} + +func clientAddressFromContext(ctx context.Context) string { + peerInfo, ok := peer.FromContext(ctx) + clientIP := "unknown" + if ok && peerInfo.Addr != nil { + clientIP = peerInfo.Addr.String() + } + + return clientIP +} + +func onContributorPositionUpdate(newPosition int, clientIp string, stream api.CeremonyService_ContributeServer) { + log.Printf("contributor %s got slot %d in the queue", clientIp, newPosition) + if err := stream.Send(api.NewTurnNotification(newPosition)); err != nil { + log.Printf("failed to send position update to %s: %v", clientIp, err) + } +} + +// Contribute implements the flow of a single contribution coming from a contributor client. +func (s *ceremonyService) Contribute( + stream api.CeremonyService_ContributeServer, +) error { + err := stream.Send(api.NewHello(s.name)) + if err != nil { + return err + } + + clientIp := clientAddressFromContext(stream.Context()) + waitForThisContributorsTurn := s.coordinator.AddContributor( + func(newPosition int) { + onContributorPositionUpdate(newPosition, clientIp, stream) + }, + ) + + waitForThisContributorsTurn() + + log.Printf("Sending last contribution to %s", clientIp) + n, err := s.coordinator.WriteLastContribution(stream_utils.NewStreamWriter(stream)) + if err != nil { + log.Printf("error sending last contribution to %s", clientIp) + return err + } + log.Printf("Sent %d bytes", n) + + log.Printf("Contribution to be received from %s", clientIp) + n, err = s.coordinator.ReadNextContribution(stream_utils.NewStreamReader(stream)) + log.Printf("Received %d bytes", n) + if err != nil { + log.Printf("%s: %v", clientIp, err) + return stream.Send(api.NewValidationResponse(err)) + } + log.Printf("Contribution from %s accepted", clientIp) + + return stream.Send(api.NewValidationResponse(nil)) +} diff --git a/online/server/contributors_manager/contributors_manager.go b/online/server/contributors_manager/contributors_manager.go new file mode 100644 index 0000000..017603b --- /dev/null +++ b/online/server/contributors_manager/contributors_manager.go @@ -0,0 +1,105 @@ +// Package contributors_manager implements a manager for contributors in the multi-party computation setup ceremony. +// Contributors Manager handles the queue of the contributors. It allows for adding new contributors to the ceremony, +// removing the current contributor after they've been handled and notifying other contributors about their position +// in the queue. +package contributors_manager + +import ( + "github.com/reilabs/trusted-setup/online/server/contributors_manager/unique_fifo" +) + +type contributor struct { + positionChannel chan int +} + +func newContributor() contributor { + return contributor{make(chan int, 1)} +} + +type manager struct { + contributorsQueue unique_fifo.Queue[contributor] +} + +// OnPositionUpdate is a type of function that is called whenever a contributor's position in the queue +// is updated. +// +// It accepts the current position of the contributor in the queue. It is used by the user +// of ContributorsManager to process the updates. +type OnPositionUpdate func(int) + +// PositionUpdateNotifier is a type of function returned by AddContributor when a new contributor is added. +// +// It blocks, calling OnPositionUpdate repeatedly on every update of the contributor's position in the queue. +// It returns when there will be no more updates. +type PositionUpdateNotifier func() + +type ContributorsManager interface { + AddContributor(notify OnPositionUpdate) PositionUpdateNotifier + RemoveCurrentContributor() error +} + +// New returns a new instance of ContributorsManager. +func New() ContributorsManager { + return &manager{ + contributorsQueue: unique_fifo.New[contributor](), + } +} + +// AddContributor adds a new contributor to the queue. +// +// It returns a PositionUpdateNotifier that can be used to get notified about the updates. +// PositionUpdateNotifier blocks, calling OnPositionUpdate repeatedly on every update of the contributor's position in the queue. +// It returns when there will be no more updates. +// +// Example: +// +// cm := contributors_manager.New() +// onUpdate := notifier(func(position int) { +// log.Printf("contributor %s is at position %d", ip, position) +// }) +// notifier := cm.AddContributor(onUpdate) +// notifier() // blocks, calling onUpdate() on contributor's position update, until contributor reaches position 0. + +func (c *manager) AddContributor(notify OnPositionUpdate) PositionUpdateNotifier { + nc := newContributor() + ncPosition := c.contributorsQueue.Enqueue(nc) + nc.positionChannel <- ncPosition + return func() { + for positionUpdate := range nc.positionChannel { + notify(positionUpdate) + if positionUpdate == 0 { + return + } + } + } +} + +// RemoveCurrentContributor removes the current contributor from the queue. +// +// It returns an error if the queue is empty. +func (c *manager) RemoveCurrentContributor() error { + cc, err := c.contributorsQueue.Dequeue() + if err != nil { + return err + } + close(cc.positionChannel) + + return c.notifyWaitingContributors() +} + +func (c *manager) notifyWaitingContributors() error { + if c.contributorsQueue.Len() == 0 { + return nil + } + + waitingContributors, err := c.contributorsQueue.PeekAll() + if err != nil { + return err + } + + for i, wc := range waitingContributors { + wc.positionChannel <- i + } + + return nil +} diff --git a/online/server/contributors_manager/contributors_manager_test.go b/online/server/contributors_manager/contributors_manager_test.go new file mode 100644 index 0000000..6d3e8e7 --- /dev/null +++ b/online/server/contributors_manager/contributors_manager_test.go @@ -0,0 +1,48 @@ +package contributors_manager_test + +import ( + "log" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/reilabs/trusted-setup/online/server/contributors_manager" +) + +func TestNotify(t *testing.T) { + cm := contributors_manager.New() + + aHandlerCalledCount := 0 + onAPositionUpdate := func(newPosition int) { + aHandlerCalledCount++ + log.Printf("Contributor A position updated to %d", newPosition) + assert.Equal(t, 0, newPosition) + assert.NoError(t, cm.RemoveCurrentContributor()) + } + + bExpectedPosition := 1 + bHandlerCalledCount := 0 + var wg sync.WaitGroup + wg.Add(2) + onBPositionUpdate := func(newPosition int) { + defer wg.Done() + bHandlerCalledCount++ + log.Printf("Contributor B position updated to %d", newPosition) + assert.Equal(t, bExpectedPosition, newPosition) + bExpectedPosition -= 1 + } + + notifyA := cm.AddContributor(onAPositionUpdate) + assert.NotNil(t, notifyA) + notifyB := cm.AddContributor(onBPositionUpdate) + assert.NotNil(t, notifyB) + + go notifyB() // run in background because it will block waiting for A + notifyA() + + wg.Wait() + assert.Equal(t, 1, aHandlerCalledCount) // Only learned about position 0 + assert.Equal(t, 2, bHandlerCalledCount) // Learned about position 1, then update to 0 + +} diff --git a/online/server/contributors_manager/unique_fifo/unique_fifo.go b/online/server/contributors_manager/unique_fifo/unique_fifo.go new file mode 100644 index 0000000..faf9158 --- /dev/null +++ b/online/server/contributors_manager/unique_fifo/unique_fifo.go @@ -0,0 +1,190 @@ +package unique_fifo + +import ( + "errors" + "sync" +) + +// Queue defines the contract for a queue. +// +// It allows adding and removing elements and peeking at elements without +// removing them and checking the count of elements. +type Queue[T comparable] interface { + // Enqueue adds value to the queue. It returns the key of the value, that can be used in PeekByKey. + Enqueue(value T) int + // Dequeue removes the next eligible value from the queue and returns it. + Dequeue() (value T, err error) + // Peek returns the next eligible value from the queue without removing it. + Peek() (value T, err error) + // PeekByKey returns the value by the key without removing it. + PeekByKey(key int) (value T, err error) + // PeekAll returns all values from the queue without removing them. + PeekAll() (value []T, err error) + // Len returns the number of elements in the queue. + Len() int +} + +var ( + ErrEmpty = errors.New("fifo empty") + ErrKeyOutOfRange = errors.New("key out of range") +) + +// uniqueFifo is a thread-safe queue that only stores unique values. +// +// The queue can hold any type of values that satisfy the comparable interface. +type uniqueFifo[T comparable] struct { + lock sync.RWMutex + values []T + seen map[T]int +} + +// New creates a new unique FIFO queue. +// +// Example: +// +// q := New[int]() +func New[T comparable]() Queue[T] { + return &uniqueFifo[T]{ + values: make([]T, 0), + seen: make(map[T]int), + } +} + +// Enqueue adds a new value to the queue if it is not already present. +// +// If the value is already present, it is not added. +// +// Example: +// +// k := q.Enqueue(1) // k == 0 +// l := q.Enqueue(2) // l == 1 +// m := q.Enqueue(1) // 1 is not added twice, m == 0 +// n := q.Enqueue(3) // n == 2 +// o := q.Enqueue(2) // 2 is not added twice // o == 1 +func (q *uniqueFifo[T]) Enqueue(value T) int { + q.lock.Lock() + defer q.lock.Unlock() + + if key, exists := q.seen[value]; exists { + return key + } + + q.values = append(q.values, value) + key := len(q.values) - 1 + q.seen[value] = key + return key +} + +// Dequeue removes the mos recently enqueued value from the queue and returns it. +// +// If the queue is empty, an error is returned. +// +// Example: +// +// q.Enqueue(1) +// q.Enqueue(2) +// q.Enqueue(3) +// q.Dequeue() // returns 1 +// q.Dequeue() // returns 2 +// q.Dequeue() // returns 3 +// q.Dequeue() // returns error +func (q *uniqueFifo[T]) Dequeue() (value T, err error) { + q.lock.Lock() + defer q.lock.Unlock() + + if len(q.values) == 0 { + err = ErrEmpty + return + } + value = q.values[0] + q.values = q.values[1:] + delete(q.seen, value) + + // Update keys + for k, v := range q.values { + q.seen[v] = k + } + return +} + +// Peek returns the most recently enqueued value from the queue without removing it. +// +// If the queue is empty, an error is returned. +// +// Example: +// +// q.Enqueue(1) +// q.Enqueue(2) +// q.Enqueue(3) +// q.Peek() // returns 1 +// q.Peek() // returns 1 +// q.Dequeue() // returns 1 +// q.Peek() // returns 2 +func (q *uniqueFifo[T]) Peek() (value T, err error) { + return q.PeekByKey(0) +} + +// PeekByKey returns the value at the given key from the queue without removing it. +// +// If the queue is empty, an error is returned. +// +// Example: +// +// q.Enqueue(1) +// q.Enqueue(2) +// q.Enqueue(3) +// q.PeekByKey(0) // returns 1 +// q.PeekByKey(1) // returns 2 +// q.PeekByKey(2) // returns 3 +// q.PeekByKey(3) // returns error +func (q *uniqueFifo[T]) PeekByKey(key int) (value T, err error) { + q.lock.RLock() + defer q.lock.RUnlock() + + if len(q.values) == 0 { + err = ErrEmpty + return + } else if key >= len(q.values) { + err = ErrKeyOutOfRange + return + } + value = q.values[key] + return +} + +// PeekAll returns all the values in the queue without removing them. +// +// If the queue is empty, an error is returned. +// +// Example: +// +// q.Enqueue(1) +// q.Enqueue(2) +// q.Enqueue(3) +// q.PeekAll() // returns [1, 2, 3] +func (q *uniqueFifo[T]) PeekAll() (value []T, err error) { + q.lock.RLock() + defer q.lock.RUnlock() + + if len(q.values) == 0 { + err = ErrEmpty + return + } + value = q.values + return +} + +// Len returns the number of elements in the queue. +// +// Example: +// +// q.Enqueue(1) +// q.Enqueue(2) +// q.Enqueue(3) +// q.Len() // returns 3 +func (q *uniqueFifo[T]) Len() int { + q.lock.RLock() + defer q.lock.RUnlock() + + return len(q.values) +} diff --git a/online/server/contributors_manager/unique_fifo/unique_fifo_test.go b/online/server/contributors_manager/unique_fifo/unique_fifo_test.go new file mode 100644 index 0000000..b0b590c --- /dev/null +++ b/online/server/contributors_manager/unique_fifo/unique_fifo_test.go @@ -0,0 +1,149 @@ +package unique_fifo_test + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/reilabs/trusted-setup/online/server/contributors_manager/unique_fifo" +) + +func TestFifo(t *testing.T) { + q := unique_fifo.New[int]() + + // Initially queue is empty + assert.Equal(t, 0, q.Len(), "initial len should be zero") + + // Enqueue some values + key0 := q.Enqueue(10) + key1 := q.Enqueue(20) + key2 := q.Enqueue(30) + + // Check keys are in order + assert.Equal(t, 0, key0) + assert.Equal(t, 1, key1) + assert.Equal(t, 2, key2) + + // Check length after push + assert.Equal(t, 3, q.Len(), "len after pushing 3 elements") + + // Peek at the first element + val, err := q.Peek() + assert.NoError(t, err, "peek should not error") + assert.Equal(t, 10, val, "peek should return first element") + + // Length should not change after peek + assert.Equal(t, 3, q.Len(), "len after peek should remain unchanged") + + // PeekByKey + val, err = q.PeekByKey(key0) + assert.NoError(t, err) + assert.Equal(t, 10, val) + val, err = q.PeekByKey(key1) + assert.NoError(t, err) + assert.Equal(t, 20, val) + val, err = q.PeekByKey(key2) + assert.NoError(t, err) + assert.Equal(t, 30, val) + + // Length should not change after peek + assert.Equal(t, 3, q.Len(), "len after peek should remain unchanged") + + // PeekAll + vals, err := q.PeekAll() + assert.NoError(t, err) + assert.Equal(t, []int{10, 20, 30}, vals) + + // Length should not change after peek + assert.Equal(t, 3, q.Len(), "len after peek should remain unchanged") + + // Dequeue all the elements one by one and check length each time + id, err := q.Dequeue() + assert.NoError(t, err) + assert.Equal(t, 10, id) + assert.Equal(t, 2, q.Len()) + + id, err = q.Dequeue() + assert.NoError(t, err) + assert.Equal(t, 20, id) + assert.Equal(t, 1, q.Len()) + + id, err = q.Dequeue() + assert.NoError(t, err) + assert.Equal(t, 30, id) + assert.Equal(t, 0, q.Len()) + + // Now queue is empty, pop should error + _, err = q.Dequeue() + assert.ErrorIs(t, err, unique_fifo.ErrEmpty, "pop from empty queue should error") + + // Ensure peek on empty queue errors + _, err = q.Peek() + assert.ErrorIs(t, err, unique_fifo.ErrEmpty, "peek on empty queue should error") + + // Ensure peekKey on empty queue errors + _, err = q.PeekByKey(key0) + assert.ErrorIs(t, err, unique_fifo.ErrEmpty, "peekKey on empty queue should error") +} + +func TestFifoUnique(t *testing.T) { + q := unique_fifo.New[int]() + key0 := q.Enqueue(10) + key1 := q.Enqueue(10) + assert.Equal(t, 1, q.Len()) + assert.True(t, key0 == key1) + v, err := q.Dequeue() + assert.NoError(t, err) + assert.Equal(t, 10, v) + assert.Equal(t, 0, q.Len()) +} + +func TestFifoConcurrent(t *testing.T) { + q := unique_fifo.New[int]() + const numGoroutines = 10 + const numPerGoroutine = 1000 + + var wg sync.WaitGroup + wg.Add(numGoroutines * 2) // for pushers and poppers + + // Pushing goroutines + for i := 0; i < numGoroutines; i++ { + go func(base int) { + defer wg.Done() + for j := 0; j < numPerGoroutine; j++ { + _ = q.Enqueue(base*numPerGoroutine + j) + } + }(i) + } + + // Popping goroutines + popped := make(chan int, numGoroutines*numPerGoroutine) + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + count := 0 + for count < numPerGoroutine { + val, err := q.Dequeue() + if err == nil { + popped <- val + count++ + } + } + }() + } + + wg.Wait() + close(popped) + + // Check we got all expected values + got := make(map[int]bool) + for val := range popped { + got[val] = true + } + + expectedTotal := numGoroutines * numPerGoroutine + if len(got) != expectedTotal { + t.Fatalf("expected %d unique popped values but got %d", expectedTotal, len(got)) + } +} diff --git a/online/server/coordinator/coordinator.go b/online/server/coordinator/coordinator.go new file mode 100644 index 0000000..ca52265 --- /dev/null +++ b/online/server/coordinator/coordinator.go @@ -0,0 +1,94 @@ +// Package coordinator implements the coordinator logic for the multi-party computation setup ceremony. +// Coordinator manages contributors and their contributions during the ceremony. +package coordinator + +import ( + "fmt" + "io" + "log" + + "github.com/reilabs/trusted-setup/online/contribution" + "github.com/reilabs/trusted-setup/online/server/contributors_manager" +) + +// Coordinator is an interface for managing contributors and their contributions during the ceremony. +type Coordinator interface { + AddContributor(notify contributors_manager.OnPositionUpdate) contributors_manager.PositionUpdateNotifier + WriteLastContribution(client io.Writer) (int64, error) + ReadNextContribution(client io.Reader) (int64, error) +} + +type coordinator struct { + last contribution.Contribution + manager contributors_manager.ContributorsManager +} + +// New creates a new coordinator object. +// +// p2 implements Phase 2 management logic from the coordinator perspective. +// manager implements contributor management logic. +func New( + p2 contribution.Contribution, manager contributors_manager.ContributorsManager, +) Coordinator { + return &coordinator{ + last: p2, + manager: manager, + } +} + +// AddContributor adds a new contributor to the ceremony. +// +// clientId is a string identifying contributor in a ceremony. +// +// Returns a PositionUpdateNotifier function to be called by the caller. The function blocks, waiting +// for updates of the added contributor's position in the queue. Whenever there is an update, the callback +// being and argument to PositionUpdateNotifier is called. +func (s *coordinator) AddContributor(notify contributors_manager.OnPositionUpdate) contributors_manager.PositionUpdateNotifier { + return s.manager.AddContributor(notify) +} + +// WriteLastContribution writes the last known contribution to the given contributor's writer. +// +// Returns the number of bytes written to the contributor and error (if happened). +// In case of error, the current contributor is removed from the ceremony. +func (s *coordinator) WriteLastContribution( + contributor io.Writer, +) (int64, error) { + n, err := s.last.WriteTo(contributor) + if err != nil { + if err := s.manager.RemoveCurrentContributor(); err != nil { + log.Printf("error removing current contributor: %v", err) + } + } + return n, err +} + +// ReadNextContribution reads and verifies the next upcoming contribution from the given contributor's reader. +// +// If the verification is positive, the new contribution is considered the last verified +// and will be handed out to the next contributor in the queue. +// +// Returns the number of bytes read from the contributor and error (if happened). +// Upon return, regardless if any error happened, the current contributor is removed from the ceremony. +func (s *coordinator) ReadNextContribution(contributor io.Reader) (int64, error) { + next := s.last.NewVerifiable() + n, err := next.ReadFrom(contributor) + if err != nil { + if err := s.manager.RemoveCurrentContributor(); err != nil { + log.Printf("error removing current contributor: %v", err) + } + return n, fmt.Errorf("error reading next contribution: %w", err) + } + + err = s.last.AddContribution(next) + + if err := s.manager.RemoveCurrentContributor(); err != nil { + log.Printf("error removing current contributor: %v", err) + } + + if err != nil { + return n, fmt.Errorf("error verifying next contribution: %w", err) + } + + return n, nil +} diff --git a/online/server/coordinator/coordinator_test.go b/online/server/coordinator/coordinator_test.go new file mode 100644 index 0000000..3f297c6 --- /dev/null +++ b/online/server/coordinator/coordinator_test.go @@ -0,0 +1,122 @@ +package coordinator_test + +import ( + "bytes" + "errors" + "io" + "testing" + + "github.com/consensys/gnark/backend/groth16" + "github.com/stretchr/testify/assert" + + "github.com/reilabs/trusted-setup/online/contribution" + "github.com/reilabs/trusted-setup/online/server/contributors_manager" + "github.com/reilabs/trusted-setup/online/server/contributors_manager/unique_fifo" + "github.com/reilabs/trusted-setup/online/server/coordinator" +) + +type mockPhase2 struct { + phase2 bytes.Buffer +} + +func (m *mockPhase2) NewVerifiable() contribution.Verifiable { + return &mockPhase2{} +} + +func (m *mockPhase2) GetContribution() interface{} { + return &m.phase2 +} + +func (m *mockPhase2) AddContribution(next contribution.Verifiable) error { + contribBuf := next.(*mockPhase2).phase2 + // Simulate an error mpcsetup.Verify could return if it dislikes the contribution + if contribBuf.Len() != 0x37 { + return errors.New("malformed contribution") + } + _, err := m.phase2.Write(contribBuf.Bytes()) + return err +} + +func (m *mockPhase2) ExtractKeys() (groth16.ProvingKey, groth16.VerifyingKey) { + // Not necessary for this test + panic("not implemented") +} + +func (m *mockPhase2) ReadFrom(reader io.Reader) (int64, error) { + return m.phase2.ReadFrom(reader) +} + +func (m *mockPhase2) WriteTo(writer io.Writer) (int64, error) { + fakeContrib := bytes.NewBuffer(bytes.Repeat([]byte{0x21}, 0x37)) + return fakeContrib.WriteTo(writer) +} + +type mockContributorsManager struct { + contributorsCount int +} + +func (m *mockContributorsManager) AddContributor(notify contributors_manager.OnPositionUpdate) contributors_manager.PositionUpdateNotifier { + m.contributorsCount++ + return func() { + notify(m.contributorsCount - 1) + } +} + +func (m *mockContributorsManager) RemoveCurrentContributor() error { + if m.contributorsCount == 0 { + return unique_fifo.ErrEmpty + } + // Noop, let's pretend the contributor was removed + return nil +} + +func TestAddContributor(t *testing.T) { + coord := coordinator.New(&mockPhase2{}, &mockContributorsManager{}) + + onPositionUpdate0 := coord.AddContributor( + func(position int) { + assert.Equal(t, 0, position) + }, + ) + assert.NotNil(t, onPositionUpdate0) + onPositionUpdate0() + + onPositionUpdate1 := coord.AddContributor( + func(position int) { + assert.Equal(t, 1, position) + }, + ) + assert.NotNil(t, onPositionUpdate1) + onPositionUpdate1() +} + +func TestWriteLastContribution(t *testing.T) { + coord := coordinator.New(&mockPhase2{}, &mockContributorsManager{}) + + var clientBuf bytes.Buffer + + _ = coord.AddContributor(func(int) {}) + n, err := coord.WriteLastContribution(&clientBuf) + assert.NoError(t, err) + assert.Equal(t, n, int64(0x37), n) // known fake contribution size + assert.Equal(t, n, int64(clientBuf.Len()), n) +} + +func TestReadNextContribution(t *testing.T) { + coord := coordinator.New(&mockPhase2{}, &mockContributorsManager{}) + + _ = coord.AddContributor(func(int) {}) + + goodContrib := bytes.NewBuffer(bytes.Repeat([]byte{0x21}, 0x37)) + _, err := coord.ReadNextContribution(goodContrib) + assert.NoError(t, err) + + // Shortcut here - during the real ceremony the contributor would be + // removed from the queue, and we'd had to test bad contribution with + // another contributor. This test however does not implement removal, + // so we can accept more contributions from one contributor and test + // for other conditions. + badContrib := bytes.NewBuffer([]byte{0x1, 0x01}) + _, err = coord.ReadNextContribution(badContrib) + assert.Error(t, err) +} diff --git a/online/server/server.go b/online/server/server.go new file mode 100644 index 0000000..c0b14aa --- /dev/null +++ b/online/server/server.go @@ -0,0 +1,60 @@ +package server + +import ( + "fmt" + "log" + "net" + + "google.golang.org/grpc" + + "github.com/reilabs/trusted-setup/online/api" +) + +// CeremonyServer represents a server that handles the trusted setup ceremony by coordinating contributors. +type CeremonyServer struct { + server *grpc.Server + service api.CeremonyServiceServer +} + +// New creates a new instance of the ceremony. +// +// service implements handlers for the ceremony protocol. +// +// The function returns a handler to the ceremony server. The handler can later be used to start and stop the ceremony. +func New(service api.CeremonyServiceServer) *CeremonyServer { + s := grpc.NewServer() + api.RegisterCeremonyServiceServer(s, service) + + return &CeremonyServer{ + server: s, + service: service, + } +} + +// Start initializes the ceremony and starts listening for the incoming contributors' connections. +// +// The server will listen on the address specified by host and TCP port specified by port. +// +// The function is not blocking, it spawns a goroutine that listens for connections in the background. +func (s *CeremonyServer) Start(host string, port int) error { + hostPort := fmt.Sprintf("%s:%d", host, port) + listener, err := net.Listen("tcp", hostPort) + if err != nil { + return err + } + + go func() { + err = s.server.Serve(listener) + if err != nil { + log.Fatalf("Server failed: %v", err) + } + }() + log.Printf("Server started, waiting for Contributors on %s...\n", hostPort) + + return nil +} + +// Stop prevents the server from accepting new connections from contributors. +func (s *CeremonyServer) Stop() { + s.server.GracefulStop() +} diff --git a/online/test/online_test.go b/online/test/online_test.go new file mode 100644 index 0000000..95d5d27 --- /dev/null +++ b/online/test/online_test.go @@ -0,0 +1,108 @@ +package online_test + +import ( + "bytes" + "strconv" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/reilabs/trusted-setup/offline/phase1" + "github.com/reilabs/trusted-setup/offline/r1cs" + "github.com/reilabs/trusted-setup/online/client" + server_config "github.com/reilabs/trusted-setup/online/config" + "github.com/reilabs/trusted-setup/online/contribution" + "github.com/reilabs/trusted-setup/online/server" + "github.com/reilabs/trusted-setup/online/server/ceremony_service" + "github.com/reilabs/trusted-setup/online/server/contributors_manager" + "github.com/reilabs/trusted-setup/online/server/coordinator" + test_circuit "github.com/reilabs/trusted-setup/test" +) + +// testOnlineCeremony verifies the trusted setup ceremony in the client/server mode. +// +// The whole ceremony is done automatically. The test logic only orchestrates the ceremony +// server and clients startup. Then, the whole ceremony, from converting ptau to Phase 1, +// through Phase 2 initialization, contribution and verification is done without supervision +// by the server and clients. In the end, keys are extracted, proof is created and verified. +func TestOnlineCeremony(t *testing.T) { + t.Run("Start server", testStartServer) + t.Run("Run contributions", testRunContributions) + t.Run("Stop server", testStopServer) + t.Run("Extract keys, prove and verify", testProveAndVerifyOnline) +} + +var serv *server.CeremonyServer +var config *server_config.Config +var last contribution.Contribution + +func testStartServer(t *testing.T) { + var err error + + config, err = server_config.New("resources/config.json") + assert.NoError(t, err) + + ccs, err := r1cs.FromFile(config.R1cs) + assert.NoError(t, err) + + p1, err := phase1.FromFile(config.Phase1) + assert.NoError(t, err) + + beacon := bytes.Repeat([]byte{0x42}, 32) + + last = contribution.New(p1, ccs, beacon) + + service := ceremony_service.New(config.CeremonyName, coordinator.New(last, contributors_manager.New())) + + serv = server.New(service) + + assert.NoError(t, serv.Start(config.Host, config.Port)) +} + +func testRunContributions(t *testing.T) { + const clientsCount = 50 + // Run some clients synchronously to simulate contributors connecting slowly one by one + for i := 0; i < clientsCount; i++ { + c, err := client.New(config.Host, strconv.Itoa(config.Port)) + assert.NoError(t, err) + assert.NoError(t, c.Contribute()) + } + + // Now run a group of clients simultaneously to simulate a more real-life case of multiple + // contributors connecting randomly and waiting for their turn + var wg sync.WaitGroup + errCh := make(chan error, clientsCount) + for i := 0; i < clientsCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + c, err := client.New(config.Host, strconv.Itoa(config.Port)) + if err != nil { + errCh <- err + return + } + errCh <- c.Contribute() + }() + } + + wg.Wait() + close(errCh) + + for err := range errCh { + assert.NoError(t, err) + } +} + +func testStopServer(t *testing.T) { + serv.Stop() +} + +func testProveAndVerifyOnline(t *testing.T) { + ccs, err := r1cs.FromFile(config.R1cs) + assert.NoError(t, err) + + pk, vk := last.ExtractKeys() + err = test_circuit.ProveAndVerify(ccs, &pk, &vk) + assert.NoError(t, err) +} diff --git a/online/test/resources/config.json b/online/test/resources/config.json new file mode 100644 index 0000000..4f6500e --- /dev/null +++ b/online/test/resources/config.json @@ -0,0 +1,7 @@ +{ + "ceremonyName": "test ceremony", + "host": "127.0.0.1", + "port": 7312, + "r1cs": "resources/server.r1cs", + "phase1": "resources/server.ph1" +} diff --git a/online/test/resources/server.ph1 b/online/test/resources/server.ph1 new file mode 100644 index 0000000..9673463 Binary files /dev/null and b/online/test/resources/server.ph1 differ diff --git a/online/test/resources/server.r1cs b/online/test/resources/server.r1cs new file mode 100644 index 0000000..c8d8421 Binary files /dev/null and b/online/test/resources/server.r1cs differ diff --git a/test/circuit.go b/test/circuit.go index 521fc03..69237af 100644 --- a/test/circuit.go +++ b/test/circuit.go @@ -1,22 +1,22 @@ -package test +package test_circuit import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254/fr" native_mimc "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/constraint" + cs "github.com/consensys/gnark/constraint/bn254" "github.com/consensys/gnark/frontend" gnark_r1cs "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/std/hash/mimc" ) -type testCircuit struct { +type TestCircuit struct { PreImage frontend.Variable Hash frontend.Variable `gnark:",public"` } -func (circuit *testCircuit) Define(api frontend.API) error { +func (circuit *TestCircuit) Define(api frontend.API) error { mimc, _ := mimc.NewMiMC(api) mimc.Write(circuit.PreImage) api.AssertIsEqual(circuit.Hash, mimc.Sum()) @@ -24,12 +24,13 @@ func (circuit *testCircuit) Define(api frontend.API) error { return nil } -func buildCcs() (constraint.ConstraintSystem, error) { - circuit := &testCircuit{} - return frontend.Compile(ecc.BN254.ScalarField(), gnark_r1cs.NewBuilder, circuit) +func BuildCcs() (*cs.R1CS, error) { + circuit := &TestCircuit{} + r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), gnark_r1cs.NewBuilder, circuit) + return r1cs.(*cs.R1CS), err } -func proveAndVerify(ccs constraint.ConstraintSystem, pk groth16.ProvingKey, vk groth16.VerifyingKey) error { +func ProveAndVerify(ccs *cs.R1CS, pk *groth16.ProvingKey, vk *groth16.VerifyingKey) error { var preImage, hash fr.Element m := native_mimc.NewMiMC() _, err := m.Write(preImage.Marshal()) @@ -38,7 +39,7 @@ func proveAndVerify(ccs constraint.ConstraintSystem, pk groth16.ProvingKey, vk g } hash.SetBytes(m.Sum(nil)) - witness, err := frontend.NewWitness(&testCircuit{PreImage: preImage, Hash: hash}, ecc.BN254.ScalarField()) + witness, err := frontend.NewWitness(&TestCircuit{PreImage: preImage, Hash: hash}, ecc.BN254.ScalarField()) if err != nil { return err } @@ -48,12 +49,12 @@ func proveAndVerify(ccs constraint.ConstraintSystem, pk groth16.ProvingKey, vk g return err } - proof, err := groth16.Prove(ccs, pk, witness) + proof, err := groth16.Prove(ccs, *pk, witness) if err != nil { return err } - err = groth16.Verify(proof, vk, pubWitness) + err = groth16.Verify(proof, *vk, pubWitness) if err != nil { return err } diff --git a/test/integration_test.go b/test/integration_test.go deleted file mode 100644 index b33cd3e..0000000 --- a/test/integration_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package test - -import ( - "testing" -) - -func TestIntegration(t *testing.T) { - t.Run("Test offline ceremony", TestOfflineCeremony) -} diff --git a/utils/randomness/drand_provider.go b/utils/randomness/drand_provider.go deleted file mode 100644 index 5b4eed9..0000000 --- a/utils/randomness/drand_provider.go +++ /dev/null @@ -1,64 +0,0 @@ -package randomness - -import ( - "context" - "encoding/hex" - "fmt" - - "github.com/drand/go-clients/client" - "github.com/drand/go-clients/client/http" - "github.com/drand/go-clients/drand" -) - -// DrandProvider implements BeaconProvider by retrieving randomness from drand network. -type DrandProvider struct { - client drand.Client -} - -// newDrandProvider initializes the randomness module. It connects to the drand network, so random value can be -// obtained with GetBeacon. -func NewDrandProvider() (*DrandProvider, error) { - // Default network chain hash as per the drand project documentation. - const chainHash = "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce" - // API returning the randomness, as per the drand project documentation. - const apiHost = "https://api.drand.sh/" - - httpClient, err := http.NewSimpleClient(apiHost, chainHash) - if err != nil { - panic(err) - } - chb, err := hex.DecodeString(chainHash) - if err != nil { - panic(err) - } - - p := DrandProvider{} - p.client, err = client.New( - client.From(httpClient), - client.WithChainHash(chb), - ) - if err != nil { - panic(err) - } - - return &p, nil -} - -// GetBeacon returns the 32 bytes of randomness. -func (d *DrandProvider) GetBeacon() []byte { - const mostRecentKnownRound = 0 - r, err := d.client.Get(context.Background(), mostRecentKnownRound) - if err != nil { - panic(err) - } - - beacon := r.GetRandomness() - if len(beacon) != 32 { - panic(fmt.Errorf("randomness: expected 32 bytes, got %d", len(beacon))) - } - if beacon == nil { - panic(fmt.Errorf("randomness: drand did not return randomness")) - } - - return beacon -} diff --git a/utils/randomness/mock_provider.go b/utils/randomness/mock_provider.go deleted file mode 100644 index f13f159..0000000 --- a/utils/randomness/mock_provider.go +++ /dev/null @@ -1,11 +0,0 @@ -package randomness - -// MockProvider implements BeaconProvider and returns a fixed 32-byte beacon. -type MockProvider struct { - Beacon []byte -} - -// GetBeacon returns the 32 bytes of randomness acquired at the module initialization. -func (m *MockProvider) GetBeacon() []byte { - return m.Beacon -} diff --git a/utils/randomness/randomness.go b/utils/randomness/randomness.go index 17ec1f9..eecf637 100644 --- a/utils/randomness/randomness.go +++ b/utils/randomness/randomness.go @@ -2,7 +2,70 @@ // by the drand organization (https://github.com/drand/drand). package randomness +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/drand/go-clients/client" + "github.com/drand/go-clients/client/http" + "github.com/drand/go-clients/drand" +) + // BeaconProvider defines an interface to get 32 bytes of randomness (beacon). type BeaconProvider interface { GetBeacon() []byte } + +// drandProvider implements BeaconProvider by retrieving randomness from drand network. +type drandProvider struct { + client drand.Client +} + +// New initializes the randomness module. It connects to the drand network, so random value can be +// obtained with GetBeacon. +func New() (BeaconProvider, error) { + // Default network chain hash as per the drand project documentation. + const chainHash = "8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce" + // API returning the randomness, as per the drand project documentation. + const apiHost = "https://api.drand.sh/" + + httpClient, err := http.NewSimpleClient(apiHost, chainHash) + if err != nil { + panic(err) + } + chb, err := hex.DecodeString(chainHash) + if err != nil { + panic(err) + } + + p := drandProvider{} + p.client, err = client.New( + client.From(httpClient), + client.WithChainHash(chb), + ) + if err != nil { + panic(err) + } + + return &p, nil +} + +// GetBeacon returns the 32 bytes of randomness. +func (d *drandProvider) GetBeacon() []byte { + const mostRecentKnownRound = 0 + r, err := d.client.Get(context.Background(), mostRecentKnownRound) + if err != nil { + panic(err) + } + + beacon := r.GetRandomness() + if len(beacon) != 32 { + panic(fmt.Errorf("randomness: expected 32 bytes, got %d", len(beacon))) + } + if beacon == nil { + panic(fmt.Errorf("randomness: drand did not return randomness")) + } + + return beacon +} diff --git a/utils/randomness/randomness_test.go b/utils/randomness/randomness_test.go index bfc6f10..0066d26 100644 --- a/utils/randomness/randomness_test.go +++ b/utils/randomness/randomness_test.go @@ -1,7 +1,6 @@ package randomness_test import ( - "bytes" "testing" "github.com/stretchr/testify/assert" @@ -9,21 +8,11 @@ import ( "github.com/reilabs/trusted-setup/utils/randomness" ) -func TestGetBeaconWithMock(t *testing.T) { - mockValue := bytes.Repeat([]byte{0x42}, 32) - mock := &randomness.MockProvider{Beacon: mockValue} - - got := mock.GetBeacon() - if !bytes.Equal(got, mockValue) { - t.Fatalf("GetBeacon() = %x; want %x", got, mockValue) - } -} - -func TestGetBeaconWithDrand(t *testing.T) { - drand, err := randomness.NewDrandProvider() +func Test(t *testing.T) { + r, err := randomness.New() assert.NoError(t, err) - got := drand.GetBeacon() + got := r.GetBeacon() assert.NotEmpty(t, got) assert.Equal(t, 32, len(got)) }