From 7e686ddf8ff3aee4761e5f537cd6cd0c45d7899e Mon Sep 17 00:00:00 2001 From: Gui Iribarren Date: Tue, 28 Nov 2023 10:06:07 +0100 Subject: [PATCH] vochain fork: refactor ZkCircuits handling * circuit package now has a Global() circuit, and all packages (including TransactionHandler) use that * circuit/config.go: rename `dev` -> `v0.0.1` and add voceremony as `v1.0.0` * circuit/config.go: rename `tag` concept into `version` * circuit/config.go: now ZkCircuitConfig has Version field, drop app.circuitConfigTag * circuit/config.go: now PublicSignals is a property of each circuit (previously hardcoded in prover) * prover: use PubSignals from circuit.Global() instead of hardcoded indexes * api: /chain/info now returns circuitVersion (instead of misspelt cicuitConfigurationTag) * apiclient: small fix, LoadZkCircuit once on NewHTTPclient instead of every Vote * testsuite: mount zkCircuits cache dir in test container as well * vochain/app.go: SetZkCircuit during beginBlock * add config/forks.go * circuit: add DownloadArtifacts funcs * DownloadArtifactsForChainID * DownloadDefaultArtifacts * NewBaseApplication now calls circuit.DownloadDefaultArtifacts instead of transactionHandler.LoadZkCircuit * newTendermint now calls circuit.DownloadArtifactsForChainID --- api/api_types.go | 28 +++--- api/censuses.go | 9 +- api/chain.go | 36 ++++---- apiclient/client.go | 9 +- apiclient/vote.go | 10 +- benchmark/zk_census_benchmark_test.go | 2 +- config/forks.go | 27 ++++++ crypto/zk/circuit/circuit.go | 112 ++++++++++++++++++++--- crypto/zk/circuit/circuit_test.go | 10 +- crypto/zk/circuit/config.go | 89 ++++++++++++------ crypto/zk/prover/prover.go | 60 ++++-------- crypto/zk/utils.go | 14 +-- dockerfiles/testsuite/docker-compose.yml | 1 + vochain/app.go | 33 +++---- vochain/hysteresis_test.go | 5 +- vochain/start.go | 7 ++ vochain/transaction/election_tx.go | 5 +- vochain/transaction/transaction.go | 12 --- vochain/transaction/vote_tx.go | 7 +- vochain/transaction_zk_test.go | 5 +- 20 files changed, 290 insertions(+), 191 deletions(-) create mode 100644 config/forks.go diff --git a/api/api_types.go b/api/api_types.go index d43ce98b6..c9cead4ac 100644 --- a/api/api_types.go +++ b/api/api_types.go @@ -200,20 +200,20 @@ type GenericTransactionWithInfo struct { } type ChainInfo struct { - ID string `json:"chainId" example:"azeno"` - BlockTime [5]uint64 `json:"blockTime" example:"12000,11580,11000,11100,11100"` - ElectionCount uint64 `json:"electionCount" example:"120"` - OrganizationCount uint64 `json:"organizationCount" example:"20"` - GenesisTime time.Time `json:"genesisTime" format:"date-time" example:"2022-11-17T18:00:57.379551614Z"` - Height uint32 `json:"height" example:"5467"` - Syncing bool `json:"syncing" example:"true"` - Timestamp int64 `json:"blockTimestamp" swaggertype:"string" format:"date-time" example:"2022-11-17T18:00:57.379551614Z"` - TransactionCount uint64 `json:"transactionCount" example:"554"` - ValidatorCount uint32 `json:"validatorCount" example:"5"` - VoteCount uint64 `json:"voteCount" example:"432"` - CircuitConfigurationTag string `json:"cicuitConfigurationTag" example:"dev"` - MaxCensusSize uint64 `json:"maxCensusSize" example:"50000"` - NetworkCapacity uint64 `json:"networkCapacity" example:"2000"` + ID string `json:"chainId" example:"azeno"` + BlockTime [5]uint64 `json:"blockTime" example:"12000,11580,11000,11100,11100"` + ElectionCount uint64 `json:"electionCount" example:"120"` + OrganizationCount uint64 `json:"organizationCount" example:"20"` + GenesisTime time.Time `json:"genesisTime" format:"date-time" example:"2022-11-17T18:00:57.379551614Z"` + Height uint32 `json:"height" example:"5467"` + Syncing bool `json:"syncing" example:"true"` + Timestamp int64 `json:"blockTimestamp" swaggertype:"string" format:"date-time" example:"2022-11-17T18:00:57.379551614Z"` + TransactionCount uint64 `json:"transactionCount" example:"554"` + ValidatorCount uint32 `json:"validatorCount" example:"5"` + VoteCount uint64 `json:"voteCount" example:"432"` + CircuitVersion string `json:"circuitVersion" example:"v1.0.0"` + MaxCensusSize uint64 `json:"maxCensusSize" example:"50000"` + NetworkCapacity uint64 `json:"networkCapacity" example:"2000"` } type Account struct { diff --git a/api/censuses.go b/api/censuses.go index a4f06d885..0f971dbae 100644 --- a/api/censuses.go +++ b/api/censuses.go @@ -209,14 +209,9 @@ func (a *API) censusCreateHandler(msg *apirest.APIdata, ctx *httprouter.HTTPCont return ErrCensusTypeUnknown } - // get census max levels from vochain app if available - maxLevels := circuit.CircuitsConfigurations[circuit.DefaultCircuitConfigurationTag].Levels - if a.vocapp != nil { - maxLevels = a.vocapp.TransactionHandler.ZkCircuit.Config.Levels - } - + // census max levels is limited by global ZkCircuit Levels censusID := util.RandomBytes(32) - _, err = a.censusdb.New(censusID, censusType, "", &token, maxLevels) + _, err = a.censusdb.New(censusID, censusType, "", &token, circuit.Global().Config.Levels) if err != nil { return err } diff --git a/api/chain.go b/api/chain.go index c57a2209c..c1f77205a 100644 --- a/api/chain.go +++ b/api/chain.go @@ -301,20 +301,20 @@ func (a *API) chainInfoHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) } data, err := json.Marshal(&ChainInfo{ - ID: a.vocapp.ChainID(), - BlockTime: a.vocinfo.BlockTimes(), - ElectionCount: a.indexer.CountTotalProcesses(), - OrganizationCount: a.indexer.CountTotalEntities(), - Height: a.vocapp.Height(), - Syncing: a.vocapp.IsSynchronizing(), - TransactionCount: transactionCount, - ValidatorCount: uint32(len(validators)), - Timestamp: a.vocapp.Timestamp(), - VoteCount: voteCount, - GenesisTime: a.vocapp.Genesis().GenesisTime, - CircuitConfigurationTag: a.vocapp.CircuitConfigurationTag(), - MaxCensusSize: maxCensusSize, - NetworkCapacity: networkCapacity, + ID: a.vocapp.ChainID(), + BlockTime: a.vocinfo.BlockTimes(), + ElectionCount: a.indexer.CountTotalProcesses(), + OrganizationCount: a.indexer.CountTotalEntities(), + Height: a.vocapp.Height(), + Syncing: a.vocapp.IsSynchronizing(), + TransactionCount: transactionCount, + ValidatorCount: uint32(len(validators)), + Timestamp: a.vocapp.Timestamp(), + VoteCount: voteCount, + GenesisTime: a.vocapp.Genesis().GenesisTime, + CircuitVersion: circuit.Version(), + MaxCensusSize: maxCensusSize, + NetworkCapacity: networkCapacity, }) if err != nil { return err @@ -329,13 +329,11 @@ func (a *API) chainInfoHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) // @Tags Chain // @Accept json // @Produce json -// @Success 200 {object} circuit.ZkCircuitConfig +// @Success 200 {object} circuit.Config // @Router /chain/info/circuit [get] func (a *API) chainCircuitInfoHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { - // Get current circuit tag - circuitConfig := circuit.GetCircuitConfiguration(a.vocapp.CircuitConfigurationTag()) - // Encode the circuit configuration to JSON - data, err := json.Marshal(circuitConfig) + // Encode the current circuit configuration to JSON + data, err := json.Marshal(circuit.Global().Config) if err != nil { return err } diff --git a/apiclient/client.go b/apiclient/client.go index b4077e150..2da6f1db6 100644 --- a/apiclient/client.go +++ b/apiclient/client.go @@ -41,7 +41,7 @@ type HTTPclient struct { addr *url.URL account *ethereum.SignKeys chainID string - circuit *circuit.ZkCircuitConfig + circuit *circuit.ZkCircuit retries int } @@ -72,8 +72,11 @@ func NewHTTPclient(addr *url.URL, bearerToken *uuid.UUID) (*HTTPclient, error) { return nil, fmt.Errorf("cannot get chain ID from API server") } c.chainID = info.ID - // Get the default circuit config - c.circuit = circuit.GetCircuitConfiguration(info.CircuitConfigurationTag) + + c.circuit, err = circuit.LoadVersion(info.CircuitVersion) + if err != nil { + return nil, fmt.Errorf("error loading circuit: %w", err) + } return c, nil } diff --git a/apiclient/vote.go b/apiclient/vote.go index 23b06dfd8..cd8ce64b6 100644 --- a/apiclient/vote.go +++ b/apiclient/vote.go @@ -1,7 +1,6 @@ package apiclient import ( - "context" "encoding/hex" "encoding/json" "fmt" @@ -102,14 +101,9 @@ func (cl *HTTPclient) Vote(v *VoteData) (types.HexBytes, error) { if err != nil { return nil, fmt.Errorf("error encoding inputs: %w", err) } - // load the correct circuit from the ApiClient configuration - currentCircuit, err := circuit.LoadZkCircuit(context.Background(), c.circuit) - if err != nil { - return nil, fmt.Errorf("error loading circuit: %w", err) - } // instance the prover with the circuit config loaded and generate the // proof for the calculated inputs - proof, err := prover.Prove(currentCircuit.ProvingKey, currentCircuit.Wasm, inputs) + proof, err := prover.Prove(c.circuit.ProvingKey, c.circuit.Wasm, inputs) if err != nil { return nil, fmt.Errorf("could not generate anonymous proof: %w", err) } @@ -119,7 +113,7 @@ func (cl *HTTPclient) Vote(v *VoteData) (types.HexBytes, error) { return nil, err } // include vote nullifier and the encoded proof in a VoteEnvelope - nullifier, err := proof.Nullifier() + nullifier, err := proof.ExtractPubSignal("nullifier") if err != nil { return nil, err } diff --git a/benchmark/zk_census_benchmark_test.go b/benchmark/zk_census_benchmark_test.go index f79c92c01..ce7f29f1a 100644 --- a/benchmark/zk_census_benchmark_test.go +++ b/benchmark/zk_census_benchmark_test.go @@ -158,7 +158,7 @@ func genProofZk(b *testing.B, electionID []byte, acc *ethereum.SignKeys, censusD "nullifier", nullifier.String()) // Get artifacts of the current circuit - currentCircuit, err := circuit.LoadZkCircuit(context.Background(), zkCircuitTest) + currentCircuit, err := circuit.LoadConfig(context.Background(), zkCircuitTest) qt.Assert(b, err, qt.IsNil) // Calculate the proof for the current apiclient circuit config and the // inputs encoded. diff --git a/config/forks.go b/config/forks.go new file mode 100644 index 000000000..acf6b994b --- /dev/null +++ b/config/forks.go @@ -0,0 +1,27 @@ +package config + +// ForksCfg allows applying softforks at specified heights +type ForksCfg struct { + VoceremonyForkBlock uint32 +} + +// Forks is a map of chainIDs +var Forks = map[string]*ForksCfg{ + "vocdoni/DEV/29": { + VoceremonyForkBlock: 216600, // estimated 2023-12-05T09:49:02.224626473Z + }, + "vocdoni/STAGE/9": { + VoceremonyForkBlock: 247000, // estimated 2023-12-11T08:47:56.552083308Z + }, + "vocdoni/LTS/1.2": { + VoceremonyForkBlock: 393000, // estimated 2023-12-11T11:51:47.046130989Z + }, +} + +// ForksForChainID returns the ForksCfg of chainID, if found, or an empty ForksCfg otherwise +func ForksForChainID(chainID string) *ForksCfg { + if cfg, found := Forks[chainID]; found { + return cfg + } + return &ForksCfg{} +} diff --git a/crypto/zk/circuit/circuit.go b/crypto/zk/circuit/circuit.go index 33a2d5ea8..b3ef3e496 100644 --- a/crypto/zk/circuit/circuit.go +++ b/crypto/zk/circuit/circuit.go @@ -10,12 +10,14 @@ import ( "net/url" "os" "path/filepath" + "sync" "time" + "go.vocdoni.io/dvote/config" "go.vocdoni.io/dvote/log" ) -var downloadCircuitsTimeout = time.Minute * 5 +const downloadCircuitsTimeout = time.Minute * 5 // BaseDir is where the artifact cache is expected to be found. // If the artifacts are not found there, they will be downloaded and stored. @@ -31,29 +33,102 @@ var BaseDir = func() string { return filepath.Join(home, ".cache", "vocdoni", "zkCircuits") }() +// Global circuit +var ( + mtx sync.Mutex + + globalCircuit = &ZkCircuit{ + Config: CircuitsConfigurations[DefaultZkCircuitVersion], + } +) + // ZkCircuit struct wraps the circuit configuration and contains the file // content of the circuit artifacts (provingKey, verificationKey and wasm) type ZkCircuit struct { ProvingKey []byte VerificationKey []byte Wasm []byte - Config *ZkCircuitConfig + Config *Config +} + +// Global returns the global ZkCircuit +func Global() *ZkCircuit { + mtx.Lock() + defer mtx.Unlock() + return globalCircuit +} + +// SetGlobal will LoadVersion into the global ZkCircuit +// +// If current version is already equal to the passed version, and the artifacts are loaded into memory, +// it returns immediately +func SetGlobal(version string) error { + mtx.Lock() + defer mtx.Unlock() + if globalCircuit.Version() == version && globalCircuit.IsLoaded() { + return nil + } + circuit, err := LoadVersion(version) + if err != nil { + return fmt.Errorf("could not load zk verification keys: %w", err) + } + globalCircuit = circuit + return nil +} + +// Version returns the version of the global ZkCircuit +func Version() string { + return Global().Version() +} + +// IsLoaded returns true if all needed keys (Proving, Verification and Wasm) are loaded into memory +func IsLoaded() bool { + return Global().IsLoaded() +} + +// Init will load (or download) the default circuit artifacts into memory, ready to be used globally. +func Init() error { + return SetGlobal(DefaultZkCircuitVersion) } -// LoadZkCircuitByTag gets the circuit configuration associated to the provided -// tag or gets the default one and load its artifacts to prepare the circuit to -// be used. -func LoadZkCircuitByTag(configTag string) (*ZkCircuit, error) { - circuitConf := GetCircuitConfiguration(configTag) +// DownloadDefaultArtifacts ensures the default circuit is cached locally +func DownloadDefaultArtifacts() error { + _, err := LoadVersion(DefaultZkCircuitVersion) + if err != nil { + return fmt.Errorf("could not load zk verification keys: %w", err) + } + return nil +} + +// DownloadArtifactsForChainID ensures all circuits needed for chainID are cached locally +func DownloadArtifactsForChainID(chainID string) error { + if config.ForksForChainID(chainID).VoceremonyForkBlock > 0 { + _, err := LoadVersion(PreVoceremonyForkZkCircuitVersion) + if err != nil { + return fmt.Errorf("could not load zk verification keys: %w", err) + } + } + return DownloadDefaultArtifacts() +} + +// LoadVersion loads the circuit artifacts based on the version provided. +// First, tries to load the artifacts from local storage, if they are not +// available, tries to download from their remote location. +// +// Stores the loaded circuit in the global variable, and returns it as well +func LoadVersion(version string) (*ZkCircuit, error) { + circuitConf := GetCircuitConfiguration(version) ctx, cancel := context.WithTimeout(context.Background(), downloadCircuitsTimeout) defer cancel() - return LoadZkCircuit(ctx, circuitConf) + return LoadConfig(ctx, circuitConf) } -// LoadZkCircuit load the circuit artifacts based on the configuration provided. +// LoadConfig loads the circuit artifacts based on the configuration provided. // First, tries to load the artifacts from local storage, if they are not // available, tries to download from their remote location. -func LoadZkCircuit(ctx context.Context, config *ZkCircuitConfig) (*ZkCircuit, error) { +// +// Stores the loaded circuit in the global variable, and returns it as well +func LoadConfig(ctx context.Context, config *Config) (*ZkCircuit, error) { circuit := &ZkCircuit{Config: config} // load the artifacts of the provided circuit from the local storage if err := circuit.LoadLocal(); err == nil { @@ -77,15 +152,28 @@ func LoadZkCircuit(ctx context.Context, config *ZkCircuitConfig) (*ZkCircuit, er if !correct { return nil, fmt.Errorf("hashes from downloaded artifacts don't match the expected ones") } + globalCircuit = circuit return circuit, nil } +// Version returns the version of the ZkCircuit +func (circuit *ZkCircuit) Version() string { + return circuit.Config.Version +} + +// IsLoaded returns true if all needed keys (Proving, Verification and Wasm) are loaded into memory +func (circuit *ZkCircuit) IsLoaded() bool { + return (circuit.ProvingKey != nil && + circuit.VerificationKey != nil && + circuit.Wasm != nil) +} + // LoadLocal tries to read the content of current circuit artifacts from its // local path (provingKey, verificationKey and wasm). If any of the read // operations fails, returns an error. func (circuit *ZkCircuit) LoadLocal() error { var err error - log.Debugw("loading circuit locally...", "BaseDir", BaseDir) + log.Debugw("loading circuit locally...", "BaseDir", BaseDir, "version", circuit.Config.Version) files := map[string][]byte{ circuit.Config.ProvingKeyFilename: nil, circuit.Config.VerificationKeyFilename: nil, @@ -112,7 +200,7 @@ func (circuit *ZkCircuit) LoadLocal() error { // remote location. If any of the downloads fails, returns an error. func (circuit *ZkCircuit) LoadRemote(ctx context.Context) error { log.Debugw("circuit not downloaded yet, downloading...", - "BaseDir", BaseDir) + "BaseDir", BaseDir, "version", circuit.Config.Version) baseUri, err := url.Parse(circuit.Config.URI) if err != nil { return err diff --git a/crypto/zk/circuit/circuit_test.go b/crypto/zk/circuit/circuit_test.go index e0957480e..981814526 100644 --- a/crypto/zk/circuit/circuit_test.go +++ b/crypto/zk/circuit/circuit_test.go @@ -46,7 +46,7 @@ func TestLoadZkCircuit(t *testing.T) { server := testFileServer(testFiles) defer server.Close() - config := &ZkCircuitConfig{ + config := &Config{ URI: server.URL, CircuitPath: "/test/", ProvingKeyFilename: testProvingKey, @@ -69,7 +69,7 @@ func TestLoadZkCircuit(t *testing.T) { testCircuits := filepath.Join(BaseDir, config.CircuitPath) defer os.RemoveAll(testCircuits) - circuit, err := LoadZkCircuit(context.Background(), config) + circuit, err := LoadConfig(context.Background(), config) c.Assert(err, qt.IsNil) c.Assert(circuit.ProvingKey, qt.DeepEquals, testFiles[testProvingKey]) c.Assert(circuit.VerificationKey, qt.DeepEquals, testFiles[testVerificationKey]) @@ -90,7 +90,7 @@ func TestLoadLocal(t *testing.T) { c := qt.New(t) circuit := &ZkCircuit{ - Config: &ZkCircuitConfig{ + Config: &Config{ CircuitPath: "/test/", ProvingKeyFilename: testProvingKey, VerificationKeyFilename: testVerificationKey, @@ -135,7 +135,7 @@ func TestLoadRemote(t *testing.T) { defer server.Close() circuit := &ZkCircuit{ - Config: &ZkCircuitConfig{ + Config: &Config{ URI: server.URL, CircuitPath: "/test/", ProvingKeyFilename: testProvingKey, @@ -199,7 +199,7 @@ func TestVerifiedCircuitArtifacts(t *testing.T) { ProvingKey: testFiles[testProvingKey], VerificationKey: testFiles[testVerificationKey], Wasm: testFiles[testWasm], - Config: &ZkCircuitConfig{}, + Config: &Config{}, } hashFn := sha256.New() diff --git a/crypto/zk/circuit/config.go b/crypto/zk/circuit/config.go index e22afd8a6..1a5c1ab4b 100644 --- a/crypto/zk/circuit/config.go +++ b/crypto/zk/circuit/config.go @@ -10,14 +10,10 @@ import ( "go.vocdoni.io/dvote/util" ) -// DefaultCircuitConfigurationTag constant contains the tag value that points -// to the default ZkSnark circuit configuration. It ensures that at least one -// circuit configuration is available so the configuration referred by this tag -// must be defined. -const DefaultCircuitConfigurationTag = "dev" - -// ZkCircuitConfig defines the configuration of the files to be downloaded -type ZkCircuitConfig struct { +// Config defines the configuration of the files to be downloaded +type Config struct { + // Version of the published circuit + Version string // URI defines the URI from where to download the files URI string `json:"uri"` // CircuitPath defines the path from where the files are downloaded. @@ -41,6 +37,8 @@ type ZkCircuitConfig struct { // FilenameWasm defines the name of the file of the circuit wasm compiled // version WasmFilename string `json:"wasmFilename"` // circuit.wasm + // PublicSignals indicates the index of each public signal + PublicSignals map[string]int // maxCensusSize contains a precomputed max size of a census for the // circuit, which is defined by the expresion: // maxCensusSize = 2^circuitLevels @@ -49,7 +47,7 @@ type ZkCircuitConfig struct { // KeySize returns the maximum number of bytes of a leaf key according to the // number of levels of the current circuit (nBytes = nLevels / 8). -func (conf *ZkCircuitConfig) KeySize() int { +func (conf *Config) KeySize() int { return conf.Levels / 8 } @@ -57,7 +55,7 @@ func (conf *ZkCircuitConfig) KeySize() int { // for the census supports. The method checks if it is already precalculated // or not. If it is not precalculated, it will calculate and initialise it. In // any case, the value is returned as big.Int. -func (conf *ZkCircuitConfig) MaxCensusSize() *big.Int { +func (conf *Config) MaxCensusSize() *big.Int { if conf.maxCensusSize != nil { return conf.maxCensusSize } @@ -71,18 +69,30 @@ func (conf *ZkCircuitConfig) MaxCensusSize() *big.Int { // SupportsCensusSize returns if the provided censusSize is supported by the // current circuit configuration. It ensures that the provided value is lower // than 2^config.Levels. -func (conf *ZkCircuitConfig) SupportsCensusSize(maxCensusSize uint64) bool { +func (conf *Config) SupportsCensusSize(maxCensusSize uint64) bool { return conf.MaxCensusSize().Cmp(new(big.Int).SetUint64(maxCensusSize)) > 0 } +// DefaultZkCircuitVersion is the circuit version used by default +const DefaultZkCircuitVersion = V1_0_0 + +// PreVoceremonyForkZkCircuitVersion is the circuit version used before VoceremonyForkBlock +const PreVoceremonyForkZkCircuitVersion = V0_0_1 + +// Version strings +const ( + V0_0_1 = "v0.0.1" + V1_0_0 = "v1.0.0" +) + // CircuitsConfigurations stores the relation between the different vochain nets // and the associated circuit configuration. Any circuit configuration must have // the remote and local location of the circuits artifacts and their metadata // such as artifacts hash or the number of parameters. -var CircuitsConfigurations = map[string]*ZkCircuitConfig{ - "dev": { - URI: "https://raw.githubusercontent.com/vocdoni/" + - "zk-franchise-proof-circuit/master", +var CircuitsConfigurations = map[string]*Config{ + V0_0_1: { + Version: V0_0_1, + URI: "https://raw.githubusercontent.com/vocdoni/zk-franchise-proof-circuit/master", CircuitPath: "artifacts/zkCensus/dev/160", Levels: 160, // ZkCircuit number of levels ProvingKeyHash: hexToBytes("0xe359b256e5e3c78acaccf8dab5dc4bea99a2f07b2a05e935b5ca658c714dea4a"), @@ -91,30 +101,53 @@ var CircuitsConfigurations = map[string]*ZkCircuitConfig{ VerificationKeyFilename: "verification_key.json", WasmHash: hexToBytes("0x80a73567f6a4655d4332301efcff4bc5711bb48176d1c71fdb1e48df222ac139"), WasmFilename: "circuit.wasm", + PublicSignals: map[string]int{ + "electionId[0]": 0, + "electionId[1]": 1, + "nullifier": 2, + "voteHash[0]": 3, + "voteHash[1]": 4, + "sikRoot": 5, + "censusRoot": 6, + "voteWeight": 7, + }, }, - "prod": { - URI: "https://raw.githubusercontent.com/vocdoni/" + - "zk-franchise-proof-circuit/master", - CircuitPath: "artifacts/zkCensus/dev/160", + V1_0_0: { + Version: V1_0_0, + URI: "https://raw.githubusercontent.com/vocdoni/zk-voceremony", + CircuitPath: "ceremony/vocdoni-zkcensus-ceremony/results", Levels: 160, // ZkCircuit number of levels - ProvingKeyHash: hexToBytes("0xe359b256e5e3c78acaccf8dab5dc4bea99a2f07b2a05e935b5ca658c714dea4a"), - ProvingKeyFilename: "proving_key.zkey", - VerificationKeyHash: hexToBytes("0x235e55571812f8e324e73e37e53829db0c4ac8f68469b9b953876127c97b425f"), - VerificationKeyFilename: "verification_key.json", - WasmHash: hexToBytes("0x80a73567f6a4655d4332301efcff4bc5711bb48176d1c71fdb1e48df222ac139"), - WasmFilename: "circuit.wasm", + ProvingKeyHash: hexToBytes("0x94f4062db3e43175ac1136f285551d547a177e37b0616a41900a38ed5ec3d478"), + ProvingKeyFilename: "census_proving_key.zkey", + VerificationKeyHash: hexToBytes("0x2a47ff7e511926290fedfa406886944eeb0a3df9021ca26333c0c124c89aa7b0"), + VerificationKeyFilename: "census_verification_key.json", + WasmHash: hexToBytes("0xc98133cf4d84ced677549e0d848739f4e80ddf78af678cbc8b95377247a92773"), + WasmFilename: "census.wasm", + // Due to a bug in this circuit definition, voteWeight ended up being a private signal, + // and the only public weight-related signal is availableWeight (on index 3). + // but we don't yet support voteWeight < availableWeight anyway, so we take just availableWeight == voteWeight + PublicSignals: map[string]int{ + "electionId[0]": 0, + "electionId[1]": 1, + "nullifier": 2, + "voteWeight": 3, // see comment above + "voteHash[0]": 4, + "voteHash[1]": 5, + "sikRoot": 6, + "censusRoot": 7, + }, }, } // GetCircuitConfiguration returns the circuit configuration associated with the // provided tag or gets the default one. -func GetCircuitConfiguration(configTag string) *ZkCircuitConfig { +func GetCircuitConfiguration(version string) *Config { // check if the provided config tag exists and return it if it does - if conf, ok := CircuitsConfigurations[configTag]; ok { + if conf, ok := CircuitsConfigurations[version]; ok { return conf } // if not, return default configuration - return CircuitsConfigurations[DefaultCircuitConfigurationTag] + return CircuitsConfigurations[DefaultZkCircuitVersion] } // hexToBytes parses a hex string and returns the byte array from it. Warning, diff --git a/crypto/zk/prover/prover.go b/crypto/zk/prover/prover.go index 4c06b2c31..89f351bc8 100644 --- a/crypto/zk/prover/prover.go +++ b/crypto/zk/prover/prover.go @@ -14,6 +14,7 @@ import ( "github.com/iden3/go-rapidsnark/types" "github.com/iden3/go-rapidsnark/verifier" "github.com/iden3/go-rapidsnark/witness" + "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/tree/arbo" ) @@ -21,9 +22,8 @@ import ( // into the error returned. var ( ErrPublicSignalFormat = fmt.Errorf("invalid proof public signals format") - ErrParsingWeight = fmt.Errorf("error parsing proof weight string to big.Int") - ErrParsingNullifier = fmt.Errorf("error parsing proof nullifier string to big.Int") - ErrParsingSIKRoot = fmt.Errorf("error parsing proof sIKRoot string to []byte") + ErrPubSignalNotFound = fmt.Errorf("public signal not found in circuit definition") + ErrParsingProofSignal = fmt.Errorf("error parsing proof signal string to big.Int") ErrParsingWitness = fmt.Errorf("error parsing provided circuit inputs, it must be a not empty marshalled bytes of a json") ErrInitWitnessCalc = fmt.Errorf("error parsing circuit wasm during calculator instance") ErrWitnessCalc = fmt.Errorf("error during witness calculation") @@ -35,10 +35,6 @@ var ( ErrVerifyProof = fmt.Errorf("error during zksnark verification") ) -// DefaultPubSignals constant contains the default number of public signal that -// a proof has. -const DefaultPubSignals = 8 - // ProofData struct contains the calculated parameters of a Proof. It allows to // encode and decode go-rapidsnark inputs and outputs easily. type ProofData struct { @@ -86,51 +82,35 @@ func (p *Proof) Bytes() ([]byte, []byte, error) { return proofData, pubSignals, nil } -// VoteWeight decodes the vote weight value from the current proof public -// signals and return it as a big.Int. -func (p *Proof) VoteWeight() (*big.Int, error) { +// ExtractPubSignal decodes the requested public signal (identified by a string: "nullifier", "sikRoot", etc) +// from the current proof and returns it as a big.Int. +func (p *Proof) ExtractPubSignal(id string) (*big.Int, error) { // Check if the current proof contains public signals and it contains the // correct number of positions. - if p.PubSignals == nil || len(p.PubSignals) != DefaultPubSignals { + if p.PubSignals == nil || len(p.PubSignals) != len(circuit.Global().Config.PublicSignals) { return nil, ErrPublicSignalFormat } - // Get the weight from the fifth public signal of the proof - strWeight := p.PubSignals[7] - // Parse it into a big.Int - weight, ok := new(big.Int).SetString(strWeight, 10) - if !ok { - return nil, ErrParsingWeight - } - return weight, nil -} - -// Nullifier decodes the vote nullifier value from the current proof public -// signals and return it as a big.Int -func (p *Proof) Nullifier() (*big.Int, error) { - if p.PubSignals == nil || len(p.PubSignals) != DefaultPubSignals { - return nil, ErrPublicSignalFormat + idx, found := circuit.Global().Config.PublicSignals[id] + if !found { + return nil, ErrPubSignalNotFound } - // Get the nullifier from the third public signal of the proof - strNullifier := p.PubSignals[2] + s := p.PubSignals[idx] // Parse it into a big.Int - nullifier, ok := new(big.Int).SetString(strNullifier, 10) + i, ok := new(big.Int).SetString(s, 10) if !ok { - return nil, ErrParsingNullifier + return nil, ErrParsingProofSignal } - return nullifier, nil + return i, nil + } -// SIKRoot function returns the sIKRoot included into the current proof. +// SIKRoot function returns the SIKRoot included into the current proof. func (p *Proof) SIKRoot() ([]byte, error) { - if p.PubSignals == nil || len(p.PubSignals) != DefaultPubSignals { - return nil, ErrPublicSignalFormat - } - arboSIK, ok := new(big.Int).SetString(p.PubSignals[5], 10) - if !ok { - return nil, ErrParsingSIKRoot + sikRoot, err := p.ExtractPubSignal("sikRoot") + if err != nil { + return nil, err } - - return arbo.BigIntToBytes(arbo.HashFunctionPoseidon.Len(), arboSIK), nil + return arbo.BigIntToBytes(arbo.HashFunctionPoseidon.Len(), sikRoot), nil } // calcWitness perform the witness calculation using go-rapidsnark library based diff --git a/crypto/zk/utils.go b/crypto/zk/utils.go index f013aba82..942bfc540 100644 --- a/crypto/zk/utils.go +++ b/crypto/zk/utils.go @@ -7,6 +7,7 @@ import ( "math/big" "go.vocdoni.io/dvote/censustree" + "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/crypto/zk/prover" "go.vocdoni.io/dvote/tree/arbo" "go.vocdoni.io/dvote/types" @@ -18,16 +19,6 @@ import ( // A: [3]bigint, // B: [3][2]bigint, // C: [3]bigint, -// PublicSignals: [8]bigint{ -// 0: electionId[0], -// 1: electionId[1], -// 2: nullifier, -// 3: voteHash[0], -// 4: voteHash[1], -// 5: sikRoot, -// 6: censusRoot -// 7: voteWeight, -// } // } // Default length of each proof parameters @@ -36,7 +27,6 @@ const ( proofBLen = 6 // flatted proofBEncLen = 3 // matrix proofCLen = 3 - publicSigLen = 8 ) // ProtobufZKProofToProverProof parses the provided protobuf ready proof struct @@ -83,7 +73,7 @@ func ProverProofToProtobufZKProof(p *prover.Proof, electionId, sikRoot, // if public signals are provided, check their format proof.PublicInputs = p.PubSignals - if p.PubSignals != nil && len(p.PubSignals) != publicSigLen { + if p.PubSignals != nil && len(p.PubSignals) != len(circuit.Global().Config.PublicSignals) { return nil, fmt.Errorf("wrong ZkSnark prover public signals format") } // if not, check if the rest of the arguments are provided and try to diff --git a/dockerfiles/testsuite/docker-compose.yml b/dockerfiles/testsuite/docker-compose.yml index 291a1acdd..0b2937132 100644 --- a/dockerfiles/testsuite/docker-compose.yml +++ b/dockerfiles/testsuite/docker-compose.yml @@ -101,6 +101,7 @@ services: networks: - blockchain volumes: + - /tmp/.vochain-zkCircuits/:/root/.cache/vocdoni/zkCircuits/ - gocoverage-test:/app/run/gocoverage environment: - GOCOVERDIR=/app/run/gocoverage diff --git a/vochain/app.go b/vochain/app.go index ae990d7f5..8e2c341b2 100644 --- a/vochain/app.go +++ b/vochain/app.go @@ -78,7 +78,6 @@ type BaseApplication struct { // abcitypes.RequestBeginBlock.Header.Time startBlockTimestamp atomic.Int64 chainID string - circuitConfigTag string dataDir string genesisInfo *tmtypes.GenesisDoc @@ -136,10 +135,11 @@ func NewBaseApplication(vochainCfg *config.VochainCfg) (*BaseApplication, error) istc, filepath.Join(vochainCfg.DataDir, "txHandler"), ) - // Load or download the zk verification keys - if err := transactionHandler.LoadZkCircuit(circuit.DefaultCircuitConfigurationTag); err != nil { + + if err := circuit.Init(); err != nil { return nil, fmt.Errorf("cannot load zk circuit: %w", err) } + blockCache, err := lru.New[int64, *tmtypes.Block](32) if err != nil { return nil, err @@ -150,7 +150,6 @@ func NewBaseApplication(vochainCfg *config.VochainCfg) (*BaseApplication, error) TransactionHandler: transactionHandler, blockCache: blockCache, dataDir: vochainCfg.DataDir, - circuitConfigTag: circuit.DefaultCircuitConfigurationTag, genesisInfo: &tmtypes.GenesisDoc{}, }, nil } @@ -260,6 +259,10 @@ func (app *BaseApplication) beginBlock(t time.Time, height uint32) { app.State.Rollback() app.startBlockTimestamp.Store(t.Unix()) app.State.SetHeight(height) + err := app.SetZkCircuit() + if err != nil { + log.Fatalf("failed to set ZkCircuit: %w", err) + } go app.State.CachePurge(height) app.State.OnBeginBlock(vstate.BeginBlock{ Height: int64(height), @@ -352,22 +355,14 @@ func (app *BaseApplication) Genesis() *tmtypes.GenesisDoc { return app.genesisInfo } -// SetCircuitConfigTag sets the current BaseApplication circuit config tag -// attribute to the provided one and loads the circuit configuration based on -// it. The available circuit config tags are defined in -// /crypto/zk/circuit/config.go -func (app *BaseApplication) SetCircuitConfigTag(tag string) error { - // Update the loaded circuit of the current app transactionHandler - if err := app.TransactionHandler.LoadZkCircuit(tag); err != nil { - return fmt.Errorf("cannot load zk circuit: %w", err) +// SetZkCircuit ensures the global ZkCircuit is the correct for a chain that implements forks +func (app *BaseApplication) SetZkCircuit() error { + switch { + case app.Height() < config.ForksForChainID(app.chainID).VoceremonyForkBlock: + return circuit.SetGlobal(circuit.PreVoceremonyForkZkCircuitVersion) + default: // for example, if VoceremonyForkBlock == 0, or if Height is past the fork + return circuit.SetGlobal(circuit.DefaultZkCircuitVersion) } - app.circuitConfigTag = tag - return nil -} - -// CircuitConfigurationTag returns the Node CircuitConfigurationTag -func (app *BaseApplication) CircuitConfigurationTag() string { - return app.circuitConfigTag } // IsSynchronizing informs if the blockchain is synchronizing or not. diff --git a/vochain/hysteresis_test.go b/vochain/hysteresis_test.go index 64e5b9cb3..18394f1af 100644 --- a/vochain/hysteresis_test.go +++ b/vochain/hysteresis_test.go @@ -23,9 +23,8 @@ func TestHysteresis(t *testing.T) { // create test app and load zk circuit app := TestBaseApplication(t) - devCircuit, err := circuit.LoadZkCircuitByTag(circuit.DefaultCircuitConfigurationTag) + err := circuit.Init() c.Assert(err, qt.IsNil) - app.TransactionHandler.ZkCircuit = devCircuit // initial accounts testWeight := big.NewInt(10) @@ -94,7 +93,7 @@ func TestHysteresis(t *testing.T) { encInputs, err := json.Marshal(inputs) c.Assert(err, qt.IsNil) - zkProof, err := prover.Prove(devCircuit.ProvingKey, devCircuit.Wasm, encInputs) + zkProof, err := prover.Prove(circuit.Global().ProvingKey, circuit.Global().Wasm, encInputs) c.Assert(err, qt.IsNil) protoZkProof, err := zk.ProverProofToProtobufZKProof(zkProof, nil, nil, nil, nil, nil) diff --git a/vochain/start.go b/vochain/start.go index 1aee974fc..f69d8ce06 100644 --- a/vochain/start.go +++ b/vochain/start.go @@ -12,6 +12,7 @@ import ( "go.vocdoni.io/dvote/config" "go.vocdoni.io/dvote/crypto/ethereum" + "go.vocdoni.io/dvote/crypto/zk/circuit" vocdoniGenesis "go.vocdoni.io/dvote/vochain/genesis" tmcfg "github.com/cometbft/cometbft/config" @@ -275,6 +276,12 @@ func newTendermint(app *BaseApplication, log.Infow("genesis file", "genesis", tconfig.GenesisFile(), "chainID", genesisCID.ChainID) app.SetChainID(genesisCID.ChainID) + // the chain might need additional ZkCircuits, now that we know the chainID ensure they are downloaded now, + // to avoid delays at beginBlock during a fork + if err := circuit.DownloadArtifactsForChainID(genesisCID.ChainID); err != nil { + return nil, fmt.Errorf("cannot download zk circuits for chainID: %w", err) + } + // assign the default tendermint methods app.SetDefaultMethods() node, err := tmnode.NewNode(tconfig, diff --git a/vochain/transaction/election_tx.go b/vochain/transaction/election_tx.go index 853b3e56b..dbbae63b2 100644 --- a/vochain/transaction/election_tx.go +++ b/vochain/transaction/election_tx.go @@ -6,6 +6,7 @@ import ( "go.vocdoni.io/dvote/crypto/ethereum" "go.vocdoni.io/dvote/crypto/nacl" + "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/log" "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/vochain/processid" @@ -72,10 +73,10 @@ func (t *TransactionHandler) NewProcessTxCheck(vtx *vochaintx.Tx) (*models.Proce fmt.Errorf("maxCensusSize is greater than the maximum allowed (%d)", maxProcessSize) } // check that the census size is not bigger than the circuit levels - if tx.Process.EnvelopeType.Anonymous && !t.ZkCircuit.Config.SupportsCensusSize(txMaxCensusSize) { + if tx.Process.EnvelopeType.Anonymous && !circuit.Global().Config.SupportsCensusSize(txMaxCensusSize) { return nil, ethereum.Address{}, fmt.Errorf("maxCensusSize for anonymous envelope "+ "cannot be bigger than the number of levels of the circuit (max:%d provided:%d)", - t.ZkCircuit.Config.MaxCensusSize().Int64(), txMaxCensusSize) + circuit.Global().Config.MaxCensusSize().Int64(), txMaxCensusSize) } // check signature diff --git a/vochain/transaction/transaction.go b/vochain/transaction/transaction.go index 8f8e94001..4a63bc4d4 100644 --- a/vochain/transaction/transaction.go +++ b/vochain/transaction/transaction.go @@ -7,7 +7,6 @@ import ( cometCrypto256k1 "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/ethereum/go-ethereum/common" "go.vocdoni.io/dvote/crypto/ethereum" - "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/log" "go.vocdoni.io/dvote/vochain/ist" vstate "go.vocdoni.io/dvote/vochain/state" @@ -46,8 +45,6 @@ type TransactionHandler struct { istc *ist.Controller // dataDir is the path for storing some files dataDir string - // ZkCircuit contains the current chain circuit - ZkCircuit *circuit.ZkCircuit } // NewTransactionHandler creates a new TransactionHandler. @@ -59,15 +56,6 @@ func NewTransactionHandler(state *vstate.State, istc *ist.Controller, dataDir st } } -func (t *TransactionHandler) LoadZkCircuit(configTag string) error { - circuit, err := circuit.LoadZkCircuitByTag(configTag) - if err != nil { - return fmt.Errorf("could not load zk verification keys: %w", err) - } - t.ZkCircuit = circuit - return nil -} - // CheckTx check the validity of a transaction and adds it to the state if forCommit=true. // It returns a bytes value which depends on the transaction type: // diff --git a/vochain/transaction/vote_tx.go b/vochain/transaction/vote_tx.go index b355843bf..191155e16 100644 --- a/vochain/transaction/vote_tx.go +++ b/vochain/transaction/vote_tx.go @@ -6,6 +6,7 @@ import ( "go.vocdoni.io/dvote/crypto/ethereum" "go.vocdoni.io/dvote/crypto/zk" + "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/log" vstate "go.vocdoni.io/dvote/vochain/state" "go.vocdoni.io/dvote/vochain/transaction/vochaintx" @@ -131,7 +132,7 @@ func (t *TransactionHandler) VoteTxCheck(vtx *vochaintx.Tx, forCommit bool) (*vs // verify the proof associated with the vote if process.EnvelopeType.Anonymous { - if t.ZkCircuit == nil { + if !circuit.IsLoaded() { return nil, fmt.Errorf("anonymous voting not supported, missing zk circuits data") } // get snark proof from vote envelope @@ -154,7 +155,7 @@ func (t *TransactionHandler) VoteTxCheck(vtx *vochaintx.Tx, forCommit bool) (*vs return nil, fmt.Errorf("expired sik root provided, generate the proof again") } // get vote weight from proof publicSignals - vote.Weight, err = proof.VoteWeight() + vote.Weight, err = proof.ExtractPubSignal("voteWeight") if err != nil { return nil, fmt.Errorf("failed on parsing vote weight from public inputs provided: %w", err) } @@ -165,7 +166,7 @@ func (t *TransactionHandler) VoteTxCheck(vtx *vochaintx.Tx, forCommit bool) (*vs "electionID", fmt.Sprintf("%x", voteEnvelope.ProcessId), ) // verify the proof with the circuit verification key - if err := proof.Verify(t.ZkCircuit.VerificationKey); err != nil { + if err := proof.Verify(circuit.Global().VerificationKey); err != nil { return nil, fmt.Errorf("zkSNARK proof verification failed: %w", err) } diff --git a/vochain/transaction_zk_test.go b/vochain/transaction_zk_test.go index 74b70bfb4..bdf24e570 100644 --- a/vochain/transaction_zk_test.go +++ b/vochain/transaction_zk_test.go @@ -22,9 +22,8 @@ func TestVoteCheckZkSNARK(t *testing.T) { c := qt.New(t) // create test app and load zk circuit app := TestBaseApplication(t) - devCircuit, err := circuit.LoadZkCircuitByTag(circuit.DefaultCircuitConfigurationTag) + err := circuit.Init() c.Assert(err, qt.IsNil) - app.TransactionHandler.ZkCircuit = devCircuit // set initial inputs testWeight := big.NewInt(10) accounts, censusRoot, proofs := testCreateKeysAndBuildWeightedZkCensus(t, 10, testWeight) @@ -84,7 +83,7 @@ func TestVoteCheckZkSNARK(t *testing.T) { c.Assert(err, qt.IsNil) encInputs, err := json.Marshal(inputs) c.Assert(err, qt.IsNil) - proof, err := prover.Prove(devCircuit.ProvingKey, devCircuit.Wasm, encInputs) + proof, err := prover.Prove(circuit.Global().ProvingKey, circuit.Global().Wasm, encInputs) c.Assert(err, qt.IsNil) // generate nullifier nullifier, err := testAccount.AccountSIKnullifier(electionId, nil)