Skip to content

Commit

Permalink
feature: download circuit artifacts (#13)
Browse files Browse the repository at this point in the history
* initial downloader
* use dummy placeholders
* circuits artifacts URLs
* including artifacts hashes
* new config package with default circuits variables
* set the basedir by default to a ./.cache/circuits-artifacts and remove os reference from artifacts.go
  • Loading branch information
lucasmenendez authored Jan 9, 2025
1 parent df6bcea commit 9fbd910
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 8 deletions.
26 changes: 26 additions & 0 deletions circuits/aggregator/artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package aggregator

import (
"github.com/vocdoni/vocdoni-z-sandbox/circuits"
"github.com/vocdoni/vocdoni-z-sandbox/config"
"github.com/vocdoni/vocdoni-z-sandbox/types"
)

var Artifacts = circuits.NewCircuitArtifacts(
&circuits.Artifact{
RemoteURL: config.AggregatorProvingKeyURL,
Hash: types.HexStringToHexBytes(config.AggregatorProvingKeyHash),
},
&circuits.Artifact{
RemoteURL: config.AggregatorVerificationKeyURL,
Hash: types.HexStringToHexBytes(config.AggregatorVerificationKeyHash),
},
)

var DummyArtifacts = circuits.NewCircuitArtifacts(
&circuits.Artifact{
RemoteURL: config.DummyProvingKeyURL,
Hash: types.HexStringToHexBytes(config.DummyProvingKeyHash),
},
nil,
)
5 changes: 2 additions & 3 deletions circuits/aggregator/dummy_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,9 @@ func FillWithDummyFixed(placeholder, assigments *AggregatorCircuit, main constra
}
// fill placeholders and assigments dummy values
for i := range assigments.VerifyProofs {
placeholder.VerifyProofs[i] = stdgroth16.PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](dummyCCS)
placeholder.VerifyPublicInputs[i] = stdgroth16.PlaceholderWitness[sw_bls12377.ScalarField](dummyCCS)
if i >= fromIdx {
placeholder.VerifyProofs[i] = stdgroth16.PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](dummyCCS)
placeholder.VerifyPublicInputs[i] = stdgroth16.PlaceholderWitness[sw_bls12377.ScalarField](dummyCCS)

assigments.Nullifiers[i] = dummyValue
assigments.Commitments[i] = dummyValue
assigments.Addresses[i] = dummyValue
Expand Down
223 changes: 223 additions & 0 deletions circuits/artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package circuits

import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"time"

"github.com/vocdoni/vocdoni-z-sandbox/log"
"github.com/vocdoni/vocdoni-z-sandbox/types"
)

const downloadCircuitsTimeout = time.Minute * 5

// BaseDir is the path where the artifact cache is expected to be found. If the
// artifacts are not found there, they will be downloaded and stored. It can be
// set to a different path if needed from other packages. Thats why it is not a
// constant.
//
// Defaults to '.cache/circuits-artifacts'
var BaseDir = filepath.Join(".cache", "circuits-artifacts")

// Artifact is a struct that holds the remote URL, the hash of the content and
// the content itself. It provides a method to load the content from the local
// cache or download it from the remote URL provided. It also checks the hash
// of the content to ensure its integrity.
type Artifact struct {
RemoteURL string
Hash types.HexBytes
Content types.HexBytes
}

// Load method checks if the key content is already loaded, if not, it will
// try to load it from the local cache or download it from the remote URL
// provided. If the content is downloaded, it will be stored locally. It also
// checks the hash of the content to ensure its integrity. If the key is not
// already loaded, it returns an error if the hash is not provided, the remote
// URL is not provided, or the content cannot be loaded locally, downloaded or
// written to a local file. It also returns an error if the hash of the content
// does not match the hash provided.
func (k *Artifact) Load(ctx context.Context) error {
// if the key has content, it is already loaded and it will return
if len(k.Content) != 0 {
return nil
}
// if the key has no content, it must have its hash set to check the
// content when it is loaded
if len(k.Hash) == 0 {
return fmt.Errorf("key hash not provided")
}
// create a flag to check if the content should be written to a local
// file or not
shouldBeWritten := false
// check if the content is already stored locally by hash and load it
content, err := loadLocal(k.Hash.String())
if err != nil {
return err
}
// if the content is not stored locally, it must be downloaded
if content == nil {
// if the remote url is not provided, the key cannot be loaded so
// it will return an error
if k.RemoteURL == "" {
return fmt.Errorf("key not loaded and remote url not provided")
}
// download the content from the remote url
if content, err = loadRemote(ctx, k.RemoteURL); err != nil {
return err
}
// mark the content to be written to a local file
shouldBeWritten = true
}
// check the hash of the loaded content
if err := checkHash(content, k.Hash); err != nil {
return err
}
k.Content = content
// if the content should be written, store it locally
if shouldBeWritten {
if err := storeLocal(content, k.Hash.String()); err != nil {
return err
}
}
return nil
}

// CircuitArtifacts is a struct that holds the proving and verifying keys of a
// zkSNARK circuit. It provides a method to load the keys from the local cache
// or download them from the remote URLs provided.
type CircuitArtifacts struct {
provingKey *Artifact
verifyingKey *Artifact
}

// NewCircuitArtifacts creates a new CircuitArtifacts struct with the proving
// and verifying keys provided.
func NewCircuitArtifacts(provingKey, verifyingKey *Artifact) *CircuitArtifacts {
return &CircuitArtifacts{
provingKey: provingKey,
verifyingKey: verifyingKey,
}
}

// LoadAll method loads the proving and verifying keys creating a context with
// a timeout of 5 minutes. It returns an error if the proving or verifying keys
// cannot be loaded.
func (ca *CircuitArtifacts) LoadAll() error {
ctx, cancel := context.WithTimeout(context.Background(), downloadCircuitsTimeout)
defer cancel()
if ca.provingKey != nil {
if err := ca.provingKey.Load(ctx); err != nil {
return fmt.Errorf("error loading proving key: %w", err)
}
}
if ca.verifyingKey != nil {
if err := ca.verifyingKey.Load(ctx); err != nil {
return fmt.Errorf("error loading verifying key: %w", err)
}
}
return nil
}

func loadLocal(name string) ([]byte, error) {
// check if BaseDir exists and create it if it does not
if _, err := os.Stat(BaseDir); err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(BaseDir, os.ModePerm); err != nil {
return nil, fmt.Errorf("error creating the base directory: %w", err)
}
} else {
return nil, fmt.Errorf("error checking the base directory: %w", err)
}
}
// append the name to the base directory and check if the file exists
path := filepath.Join(BaseDir, name)
if _, err := os.Stat(path); err != nil {
// if the file does not exists return nil content and nil error, but if
// the error is not a not exists error, return the error
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("error checking file %s: %w", path, err)
}
// if it exists, read the content of the file and return it
content, err := os.ReadFile(path)
if err != nil {
if err == os.ErrNotExist {
return nil, nil
}
return nil, fmt.Errorf("error reading file %s: %w", path, err)
}
return content, nil
}

func loadRemote(ctx context.Context, fileUrl string) ([]byte, error) {
if _, err := url.Parse(fileUrl); err != nil {
return nil, fmt.Errorf("error parsing the file URL provided: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
if err != nil {
return nil, fmt.Errorf("error creating the file request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

defer func() {
if err := res.Body.Close(); err != nil {
log.Warnf("error closing body response %v", err)
}
}()

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error on download file %s: http status: %d", fileUrl, res.StatusCode)
}
content, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("error reading the file content from the http response: %w", err)
}
return content, nil
}

func checkHash(content, expected []byte) error {
if content == nil {
return fmt.Errorf("no content provided to check")
}
if expected == nil {
return fmt.Errorf("no hash provided to compare")
}
hash := sha256.New()
if _, err := hash.Write(content); err != nil {
return fmt.Errorf("error computing hash function of %s: %w", content, err)
}
if !bytes.Equal(hash.Sum(nil), expected) {
return fmt.Errorf("hash mismatch")
}
return nil
}

func storeLocal(content []byte, name string) error {
path := filepath.Join(BaseDir, name)
if content == nil {
return fmt.Errorf("no content provided")
}
if _, err := os.Stat(filepath.Dir(path)); err != nil {
return fmt.Errorf("destination path parent folder does not exist")
}
fd, err := os.Create(path)
if err != nil {
return fmt.Errorf("error creating the artifact file: %w", err)
}
if _, err := fd.Write(content); err != nil {
return fmt.Errorf("error writing the artifact file: %w", err)
}
return nil
}
70 changes: 70 additions & 0 deletions circuits/artifacts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package circuits

import (
"bytes"
"context"
"crypto/sha256"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"time"

qt "github.com/frankban/quicktest"
)

var (
dummyPath = "dummy.key"
dummyKeyContent = []byte("dummy content")
)

func testDummyKeyServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, dummyPath, time.Now(), bytes.NewReader(dummyKeyContent))
}))
}

func TestMain(m *testing.M) {
// set BaseDir to a temporary directory and create it
BaseDir = filepath.Join(os.TempDir(), BaseDir)
// run the tests
code := m.Run()
// remove BaseDir
if err := os.RemoveAll(BaseDir); err != nil {
panic(err)
}
os.Exit(code)
}

func TestLoadKey(t *testing.T) {
c := qt.New(t)
// create a dummy key server
server := testDummyKeyServer()
defer server.Close()
// get the expected hash
hashFn := sha256.New()
hashFn.Write(dummyKeyContent)
expectedHash := hashFn.Sum(nil)
// create a dummy key
remoteURL, err := url.JoinPath(server.URL, dummyPath)
c.Assert(err, qt.IsNil)
dummyKey := &Artifact{
RemoteURL: remoteURL,
Hash: expectedHash,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// test no downloaded file
c.Assert(dummyKey.Load(ctx), qt.IsNil)
c.Assert([]byte(dummyKey.Content), qt.DeepEquals, dummyKeyContent)
// test downloaded file but no locally stored file
dummyKey.Content = nil
c.Assert(dummyKey.Load(ctx), qt.IsNil)
c.Assert([]byte(dummyKey.Content), qt.DeepEquals, dummyKeyContent)
// test wrong hash
dummyKey.Content = nil
dummyKey.Hash = []byte("wrong hash")
c.Assert(dummyKey.Load(ctx), qt.IsNotNil)
}
5 changes: 0 additions & 5 deletions circuits/test/aggregator/aggregator_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,6 @@ func AggregarorInputsForTest(processId []byte, nValidVoters int) (
return nil, nil, nil, err
}
finalPlaceholder.VerificationKeys[1] = fixedVk
// set the vote verififer proofs and pubInputa
for i := 0; i < nValidVoters; i++ {
finalPlaceholder.VerifyPublicInputs[i] = stdgroth16.PlaceholderWitness[sw_bls12377.ScalarField](vvCCS)
finalPlaceholder.VerifyProofs[i] = stdgroth16.PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](vvCCS)
}
// fill placeholder and witness with dummy circuits
if err := aggregator.FillWithDummyFixed(finalPlaceholder, finalAssigments, vvCCS, nValidVoters); err != nil {
return nil, nil, nil, err
Expand Down
18 changes: 18 additions & 0 deletions circuits/voteverifier/artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package voteverifier

import (
"github.com/vocdoni/vocdoni-z-sandbox/circuits"
"github.com/vocdoni/vocdoni-z-sandbox/config"
"github.com/vocdoni/vocdoni-z-sandbox/types"
)

var Artifacts = circuits.NewCircuitArtifacts(
&circuits.Artifact{
RemoteURL: config.VoteVerifierProvingKeyURL,
Hash: types.HexStringToHexBytes(config.VoteVerifierProvingKeyHash),
},
&circuits.Artifact{
RemoteURL: config.VoteVerifierVerificationKeyURL,
Hash: types.HexStringToHexBytes(config.VoteVerifierVerificationKeyHash),
},
)
16 changes: 16 additions & 0 deletions config/circuit_artifacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package config

const (
// CircuitArtifacts constants for circuits/voteverifier package
VoteVerifierProvingKeyURL = "https://media.githubusercontent.com/media/vocdoni/vocdoni-circuits-artifacts/main/voteverifier/voteverifier.pk"
VoteVerifierProvingKeyHash = "4bcb2de78562f400a3f96e5adcdcc00d32ebd0e29c7af4145f857f05281eb9e8"
VoteVerifierVerificationKeyURL = "https://media.githubusercontent.com/media/vocdoni/vocdoni-circuits-artifacts/main/voteverifier/voteverifier.vk"
VoteVerifierVerificationKeyHash = "a3a3874b6a1d4c568f6ee0d221e3213bf408f4e66d67e3f1eaf3c73f02994309"
// CircuitArtifacts constants for circuits/aggregator package
AggregatorProvingKeyURL = "https://media.githubusercontent.com/media/vocdoni/vocdoni-circuits-artifacts/main/aggregator/aggregator.pk"
AggregatorProvingKeyHash = "aecef25b7f5cd6c28df19d5398a7c9d6922149fdc60d7ebfee549eb42d84abe9"
AggregatorVerificationKeyURL = "https://media.githubusercontent.com/media/vocdoni/vocdoni-circuits-artifacts/main/aggregator/aggregator.vk"
AggregatorVerificationKeyHash = "c748f9e234d70c0123f116a5a88b81ad1bcf782a9d9d0d50d2caa196aac2c0fb"
DummyProvingKeyURL = "https://media.githubusercontent.com/media/vocdoni/vocdoni-circuits-artifacts/main/aggregator/dummy.pk"
DummyProvingKeyHash = "fa587e9f24473de364d8950c70be11f6c33118b0be12df4ee100eed0dabecff2"
)

0 comments on commit 9fbd910

Please sign in to comment.