-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: download circuit artifacts (#13)
* 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
1 parent
df6bcea
commit 9fbd910
Showing
7 changed files
with
355 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |