diff --git a/Makefile b/Makefile index 3dc38a1..0ddf68b 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ test: .PHONY: test build: - go build -o kzgcli ./cmd/kzgcli + go build ./cmd/kzgcli .PHONY: build bench: diff --git a/README.md b/README.md index 4b54e09..66961dd 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,17 @@ This repository contains an implementation of a client to participate in the Pow For _bls12-381_ elliptic curve operations such as group multiplication and pairings, the implementation uses the [gnark-crypto](https://github.com/ConsenSys/gnark-crypto) library ([audited Oct-2022](https://github.com/ConsenSys/gnark-crypto/blob/master/audit_oct2022.pdf)). +Used by: +- Multiple individual contributors +- [Proof-of-cat](https://proofof.cat/) +- [Dappnode](https://twitter.com/eduadiez/status/1623963202500304896) +- [Dodgekzg](https://www.dogekzg.com/) (WASM) +- [Raspberry Pi contributor](https://twitter.com/bruderbuck/status/1617424902256041985) +- [cryptosat](https://twitter.com/cryptosat) (Planned for "Special contributions" phase) +- [KZGamer](https://hackmd.io/@RoboCopsGoneMad/Bk3zqWDij) (Planned for "Special contributions" phase) + + + ## Table of content - [Ethereum EIP-4844 Powers of Tau ceremony client](#ethereum-eip-4844-powers-of-tau-ceremony-client) - [Table of content](#table-of-content) @@ -18,7 +29,9 @@ For _bls12-381_ elliptic curve operations such as group multiplication and pairi - [Step 3 - Contribute!](#step-3---contribute) - [Step 4 (optional) - Check that your contribution is in the new transcript](#step-4-optional---check-that-your-contribution-is-in-the-new-transcript) - [External entropy](#external-entropy) - - [Verify the current sequencer transcript ourselves](#verify-the-current-sequencer-transcript-ourselves) + - [Offline contributions](#offline-contributions) + - [Testing ceremony environment](#testing-ceremony-environment) + - [Verify the current sequencer transcript](#verify-the-current-sequencer-transcript) - [Tests and benchmarks](#tests-and-benchmarks) - [Side-effects of this ceremony client work](#side-effects-of-this-ceremony-client-work) - [Potential improvements](#potential-improvements) @@ -117,7 +130,40 @@ Success! If you want to understand in more detail how the external entropy is mixed with the CSRNG, please see [this code section](https://github.com/jsign/go-kzg-ceremony-client/blob/main/contribution/batchcontribution.go#L24-L35). -## Verify the current sequencer transcript ourselves +## Offline contributions +This section is only interesting if you're contributing from constrained environments. + +Apart from conforming to the specification for the Powers of Tau protocol, participating in the ceremony involves interacting with the sequencer in a defined API flow. If you are contributing from a constraint environment (e.g: air-gapped or bandwidth constrained), you might be interested in narrowing down the contribution step independently from getting the state and sending the contribution. + +The CLI tool provides an _offline_ subcommand: + +- `kzgcli offline download-state `: downloads the current state of the ceremony from the sequencer and saves it in a file. +- `kzgcli offline contribute `: opens a previously downloaded current state of the ceremony, makes the contribution and saves it in a new file. +- `kzgcli offline send-contribution --session-id <...> `: sends a previously generated contribution file to the sequencer. + +You might not need `kzgcli offline download-state` you're pulling the current state out-of-band (e.g: direct download or the sequencer sent it to you). If that isn't the case, you can use it in an environment that has internet access (not necessarily your contribution environment). + +The `kzgcli offline contribute` command doesn't require internet access, and will probably be the only command you'll run in your constrained environment. This command also accepts the `--urlrand` and `--hex-entropy` flag if you want to pull entropy from an external source of randomness available in your environment or provided directly to the client, respectively. + +The `kzgcli offline send-contribution` command sends the previously generated file by `kzgcli offline contribute` to the sequencer. + +An example of running the first two commands: +``` +$ kzgcli offline download-state current.json +Downloading current state... OK +Encoding and saving to current.json... OK +Saved current state in current.json +$ kzgcli offline contribute current.json new.json +Opening and parsing offline current state file...OK +Calculating contribution... OK +Success, saved contribution in new.json +``` + +## Testing ceremony environment + +In all commands you can use the `--sequencer-url` flag to override the sequencer API URL to target a different sequencer than in the _mainnet_ environment. For example, `--sequencer-url "https://kzg-ceremony-sequencer-dev.fly.dev"`. + +## Verify the current sequencer transcript The sequencer has [an API that provides a full transcript](https://seq.ceremony.ethereum.org/info/current_state) of all the contributions, so anyone can double-check the calculations to see if the result matches all the received contributions. Having clients double-check sequencer calculations avoids having to trust that the sequencer is in the latest powers of Tau calculation. diff --git a/cmd/kzgcli/contribute.go b/cmd/kzgcli/contribute.go index fb0c5fc..d08153a 100644 --- a/cmd/kzgcli/contribute.go +++ b/cmd/kzgcli/contribute.go @@ -60,7 +60,12 @@ var contributeCmd = &cobra.Command{ extRandomness = append(extRandomness, urlBytes) } - client, err := sequencerclient.New() + sequencerURL, err := cmd.Flags().GetString("sequencer-url") + if err != nil { + log.Fatalf("get --sequencer-url flag value: %s", err) + } + + client, err := sequencerclient.New(sequencerURL) if err != nil { log.Fatalf("creating sequencer client: %s", err) } diff --git a/cmd/kzgcli/main.go b/cmd/kzgcli/main.go index 307452d..2fb26e6 100644 --- a/cmd/kzgcli/main.go +++ b/cmd/kzgcli/main.go @@ -29,17 +29,40 @@ https://github.com/jsign/go-kzg-ceremony-client#i-want-to-participate-in-the-cer }, } +var offlineCmd = &cobra.Command{ + Use: "offline", + Short: "Contains commands for offline contributions", + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Usage(); err != nil { + log.Fatalf("cmd usage failed: %s", err) + } + }, +} + func init() { rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.PersistentFlags().String("sequencer-url", "https://seq.ceremony.ethereum.org", "The URL of the ceremony sequencer") rootCmd.AddCommand(statusCmd) + // Online contribution commands. contributeCmd.Flags().String("session-id", "", "The sesion id as generated in the 'session_id' field in the authentication process") contributeCmd.Flags().Bool("drand", false, "Pull entropy from the Drand network to be mixed with local CSRNG") contributeCmd.Flags().String("urlrand", "", "Pull entropy from an HTTP endpoint mixed with local CSRNG") rootCmd.AddCommand(contributeCmd) + // Verification commands. rootCmd.AddCommand(verifyTranscriptCmd) + + // Offline commands. + offlineContributeCmd.Flags().String("urlrand", "", "Pull entropy from an HTTP endpoint mixed with local CSRNG") + offlineContributeCmd.Flags().String("hex-entropy", "", "Hex encoded entropy to be mixed with local CSRNG") + offlineSendContributionCmd.Flags().String("session-id", "", "The sesion id as generated in the 'session_id' field in the authentication process") + + rootCmd.AddCommand(offlineCmd) + offlineCmd.AddCommand(offlineDownloadStateCmd) + offlineCmd.AddCommand(offlineContributeCmd) + offlineCmd.AddCommand(offlineSendContributionCmd) } func Execute() { diff --git a/cmd/kzgcli/offline_contribute.go b/cmd/kzgcli/offline_contribute.go new file mode 100644 index 0000000..aa50e01 --- /dev/null +++ b/cmd/kzgcli/offline_contribute.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/hex" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/jsign/go-kzg-ceremony-client/contribution" + "github.com/jsign/go-kzg-ceremony-client/extrand" + "github.com/spf13/cobra" +) + +var offlineContributeCmd = &cobra.Command{ + Use: "contribute ", + Short: "Opens a file with the current state of the ceremony, makes the contribution, and saves the new state to a file.", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 2 { + log.Fatalf("two arguments expected") + } + + urlrand, err := cmd.Flags().GetString("urlrand") + if err != nil { + log.Fatalf("get --urlrand flag value: %s", err) + } + var extRandomness [][]byte + if urlrand != "" { + fmt.Printf("Pulling entropy from %s... ", urlrand) + urlBytes, err := extrand.GetFromURL(cmd.Context(), urlrand) + if err != nil { + log.Fatalf("get bytes from url: %s", err) + } + fmt.Printf("Got it! (length: %d)\n", len(urlBytes)) + extRandomness = append(extRandomness, urlBytes) + } + hexEntropy, err := cmd.Flags().GetString("hex-entropy") + if err != nil { + log.Fatalf("get --hex-entropy flag value: %s", err) + } + if hexEntropy != "" { + hexEntropy := strings.TrimPrefix(hexEntropy, "0x") + hb, err := hex.DecodeString(hexEntropy) + if err != nil { + log.Fatalf("decoding hex entropy: %s", err) + } + extRandomness = append(extRandomness, hb) + } + + fmt.Printf("Opening and parsing offline current state file...") + f, err := os.Open(args[0]) + if err != nil { + log.Fatalf("opening current state file at %s: %s", args[0], err) + } + defer f.Close() + + bytes, err := io.ReadAll(f) + if err != nil { + log.Fatalf("reading current state file: %s", err) + } + contributionBatch, err := contribution.DecodeBatchContribution(bytes) + if err != nil { + log.Fatalf("deserializing file content: %s", err) + } + fmt.Printf("OK\nCalculating contribution... ") + + if err := contributionBatch.Contribute(extRandomness...); err != nil { + log.Fatalf("failed on calculating contribution: %s", err) + } + + nbytes, err := contribution.Encode(contributionBatch, true) + if err != nil { + log.Fatalf("encoding contribution: %s", err) + } + + if err := os.WriteFile(args[1], nbytes, os.ModePerm); err != nil { + log.Fatalf("writing contribution file to %s: %s", args[1], err) + } + + fmt.Printf("OK\nSuccess, saved contribution in %s\n", args[1]) + }, +} diff --git a/cmd/kzgcli/offline_currentstate.go b/cmd/kzgcli/offline_currentstate.go new file mode 100644 index 0000000..087118e --- /dev/null +++ b/cmd/kzgcli/offline_currentstate.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/jsign/go-kzg-ceremony-client/contribution" + "github.com/jsign/go-kzg-ceremony-client/sequencerclient" + "github.com/spf13/cobra" +) + +var offlineDownloadStateCmd = &cobra.Command{ + Use: "download-state ", + Short: "Downloads the current state of the ceremony, and saves it in a file", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + log.Fatalf("one argument exected") + } + sequencerURL, err := cmd.Flags().GetString("sequencer-url") + if err != nil { + log.Fatalf("get --sequencer-url flag value: %s", err) + } + client, err := sequencerclient.New(sequencerURL) + if err != nil { + log.Fatalf("creating sequencer client: %s", err) + } + + fmt.Printf("Downloading current state... ") + transcript, err := client.GetCurrentTranscript(cmd.Context()) + if err != nil { + log.Fatalf("getting current transcript: %s", err) + } + fmt.Printf("OK\n") + + bc := contribution.BatchContribution{ + Contributions: make([]contribution.Contribution, len(transcript.Transcripts)), + } + for i, transcript := range transcript.Transcripts { + bc.Contributions[i].NumG1Powers = transcript.NumG1Powers + bc.Contributions[i].NumG2Powers = transcript.NumG2Powers + bc.Contributions[i].PowersOfTau = transcript.PowersOfTau + } + + fmt.Printf("Encoding and saving to %s... ", args[0]) + bytes, err := contribution.Encode(&bc, true) + if err != nil { + log.Fatalf("encoding current state to json: %s", err) + } + + if err := os.WriteFile(args[0], bytes, os.ModePerm); err != nil { + log.Fatalf("writing current state to file: %s", err) + } + + fmt.Printf("OK\nSaved current state in %s\n", args[0]) + }, +} diff --git a/cmd/kzgcli/offline_sendcontribution.go b/cmd/kzgcli/offline_sendcontribution.go new file mode 100644 index 0000000..d23981b --- /dev/null +++ b/cmd/kzgcli/offline_sendcontribution.go @@ -0,0 +1,90 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "time" + + "github.com/jsign/go-kzg-ceremony-client/contribution" + "github.com/jsign/go-kzg-ceremony-client/sequencerclient" + "github.com/spf13/cobra" +) + +var offlineSendContributionCmd = &cobra.Command{ + Use: "send-contribution ", + Short: "Sends a previously generated contribution to the sequencer", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + log.Fatalf("one argument expected") + } + + sessionID, err := cmd.Flags().GetString("session-id") + if err != nil { + log.Fatalf("get --session-id flag value: %s", err) + } + if sessionID == "" { + log.Fatalf("the session id can't be empty") + } + + contributionBytes, err := os.ReadFile(args[0]) + if err != nil { + log.Fatalf("reading contribution file: %s", err) + } + contributionBatch, err := contribution.DecodeBatchContribution(contributionBytes) + if err != nil { + log.Fatalf("decoding contribution file: %s", err) + } + + sequencerURL, err := cmd.Flags().GetString("sequencer-url") + if err != nil { + log.Fatalf("get --sequencer-url flag value: %s", err) + } + client, err := sequencerclient.New(sequencerURL) + if err != nil { + log.Fatalf("creating sequencer client: %s", err) + } + + for { + _, ok, err := client.TryContribute(cmd.Context(), sessionID) + if err != nil { + fmt.Printf("%v Waiting for our turn failed (err: %s), retrying in %v...\n", time.Now().Format("2006-01-02 15:04:05"), err, tryContributeAttemptDelay) + time.Sleep(tryContributeAttemptDelay) + continue + } + if !ok { + fmt.Printf("%v Can't enter the lobby, are you sure we're on your reserved slot? Waiting %v for retrying...\n", time.Now().Format("2006-01-02 15:04:05"), tryContributeAttemptDelay) + time.Sleep(tryContributeAttemptDelay) + continue + } + break + } + + fmt.Printf("Sending our precomputed contribution %s to the sequencer...\n", args[0]) + var contributionReceipt *sequencerclient.ContributionReceipt + for { + var err error + contributionReceipt, err = client.Contribute(cmd.Context(), sessionID, contributionBatch) + if err != nil { + fmt.Printf("Failed sending contribution!: %s\n", err) + fmt.Printf("Retrying sending contribution in %v\n", sendContributionRetryDelay) + time.Sleep(sendContributionRetryDelay) + continue + } + break + } + + // Persist the receipt and contribution. + receiptJSON, _ := json.Marshal(contributionReceipt) + if err := os.WriteFile(fmt.Sprintf("contribution_receipt_%s.json", sessionID), receiptJSON, os.ModePerm); err != nil { + log.Fatalf("failed to save the contribution receipt (err: %s), printing to stdout as last resort: %s", err, receiptJSON) + } + ourContributionBatchJSON, _ := contribution.Encode(contributionBatch, true) + if err := os.WriteFile(fmt.Sprintf("my_contribution_%s.json", sessionID), ourContributionBatchJSON, os.ModePerm); err != nil { + log.Fatalf("failed to save the contribution (err: %s), printing to stdout as last resort: %s", err, ourContributionBatchJSON) + } + + fmt.Printf("Success!\n") + }, +} diff --git a/cmd/kzgcli/status.go b/cmd/kzgcli/status.go index 5f29930..b94654a 100644 --- a/cmd/kzgcli/status.go +++ b/cmd/kzgcli/status.go @@ -12,7 +12,11 @@ var statusCmd = &cobra.Command{ Use: "status", Short: "Returns the current status of the sequencer", Run: func(cmd *cobra.Command, args []string) { - client, err := sequencerclient.New() + sequencerURL, err := cmd.Flags().GetString("sequencer-url") + if err != nil { + log.Fatalf("get --sequencer-url flag value: %s", err) + } + client, err := sequencerclient.New(sequencerURL) if err != nil { log.Fatalf("creating sequencer client: %s", err) } diff --git a/cmd/kzgcli/verifytranscript.go b/cmd/kzgcli/verifytranscript.go index 718af8f..7ce59bc 100644 --- a/cmd/kzgcli/verifytranscript.go +++ b/cmd/kzgcli/verifytranscript.go @@ -13,7 +13,11 @@ var verifyTranscriptCmd = &cobra.Command{ Use: "verify-transcript", Short: "Pulls and verifies the current sequencer transcript", Run: func(cmd *cobra.Command, args []string) { - client, err := sequencerclient.New() + sequencerURL, err := cmd.Flags().GetString("sequencer-url") + if err != nil { + log.Fatalf("get --sequencer-url flag value: %s", err) + } + client, err := sequencerclient.New(sequencerURL) if err != nil { log.Fatalf("creating sequencer client: %s", err) } diff --git a/sequencerclient/sequencerclient.go b/sequencerclient/sequencerclient.go index 40e6c32..0e1fea2 100644 --- a/sequencerclient/sequencerclient.go +++ b/sequencerclient/sequencerclient.go @@ -13,13 +13,11 @@ import ( "github.com/jsign/go-kzg-ceremony-client/transcript" ) -const sequencerURL = "https://seq.ceremony.ethereum.org" - type Client struct { sequencerURL string } -func New() (*Client, error) { +func New(sequencerURL string) (*Client, error) { return &Client{ sequencerURL: sequencerURL, }, nil diff --git a/sequencerclient/sequencerclient_test.go b/sequencerclient/sequencerclient_test.go index 01dcc43..763fe01 100644 --- a/sequencerclient/sequencerclient_test.go +++ b/sequencerclient/sequencerclient_test.go @@ -36,7 +36,7 @@ func TestVerifyTranscript(t *testing.T) { } func createClient(t *testing.T) *Client { - c, err := New() + c, err := New("https://seq.ceremony.ethereum.org") require.NoError(t, err) return c }