From 177e2ee0433adf910bdc7165f745d409d102f8dd Mon Sep 17 00:00:00 2001 From: Apisit Ritreungroj <38898766+apskhem@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:30:57 +0700 Subject: [PATCH 1/4] feat(cat-voices): Add CIP-39 seed phrase utility (#852) * feat: bip39 * fix: constructor * refactor: single class * fix: docs * test: initial test * fix: typo * refactor: seed phrase class * test: seed phrase words * feat: full words * fix: cspell * refactor: move to models * refactor: rename test folder * test: add exceeding cases * refactor: test array * fix: linter for a file --- .../lib/src/seed_phrase.dart | 70 ++++++++++++++++++ .../catalyst_voices_models/pubspec.yaml | 2 + .../test/seed_phrase_test.dart | 74 +++++++++++++++++++ melos.yaml | 1 + 4 files changed, 147 insertions(+) create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart create mode 100644 catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart new file mode 100644 index 0000000000..1b49fe942a --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/seed_phrase.dart @@ -0,0 +1,70 @@ +// cspell: words wordlists WORDLIST +// ignore_for_file: implementation_imports + +import 'dart:typed_data'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip39/src/wordlists/english.dart'; +import 'package:convert/convert.dart'; + +/// Represents a seed phrase consisting of a mnemonic and provides methods for +/// generating and deriving cryptographic data from the mnemonic. +/// +/// The `SeedPhrase` class allows creation of a seed phrase either randomly, +/// from a given mnemonic, or from entropy data. It supports converting between +/// different formats, including Uint8List and hex strings. +class SeedPhrase { + /// The mnemonic phrase + final String mnemonic; + + /// Generates a new seed phrase with a random mnemonic. + /// + /// Throws an [ArgumentError] if the word count is invalid. + /// + /// [wordCount]: The number of words in the mnemonic. + /// The default word count is 12, but can specify 12, 15, 18, 21, or 24 words. + /// with a higher word count providing greater entropy and security. + SeedPhrase({int wordCount = 12}) + : this.fromMnemonic( + bip39.generateMnemonic( + strength: (wordCount * 32) ~/ 3, + ), + ); + + /// Creates a SeedPhrase from an existing [Uint8List] entropy. + /// + /// [encodedData]: The entropy data as a Uint8List. + SeedPhrase.fromUint8ListEntropy(Uint8List encodedData) + : this.fromHexEntropy(hex.encode(encodedData)); + + /// Creates a SeedPhrase from an existing hex-encoded entropy. + /// + /// [encodedData]: The entropy data as a hex string. + SeedPhrase.fromHexEntropy(String encodedData) + : this.fromMnemonic(bip39.entropyToMnemonic(encodedData)); + + /// Creates a SeedPhrase from an existing [mnemonic]. + /// + /// Throws an [ArgumentError] if the mnemonic is invalid. + /// + /// [mnemonic]: The mnemonic to derive the seed from. + SeedPhrase.fromMnemonic(this.mnemonic) + : assert(bip39.validateMnemonic(mnemonic), 'Invalid mnemonic phrase'); + + /// The seed derived from the mnemonic as a Uint8List. + Uint8List get uint8ListSeed => bip39.mnemonicToSeed(mnemonic); + + /// The seed derived from the mnemonic as a hex-encoded string. + String get hexSeed => bip39.mnemonicToSeedHex(mnemonic); + + /// The entropy derived from the mnemonic as a Uint8List. + Uint8List get uint8ListEntropy => Uint8List.fromList(hex.decode(hexEntropy)); + + /// The entropy derived from the mnemonic as a hex-encoded string. + String get hexEntropy => bip39.mnemonicToEntropy(mnemonic); + + /// The mnemonic phrase as a list of individual words. + List get mnemonicWords => mnemonic.split(' '); + + /// The full list of BIP-39 mnemonic words in English. + static List get wordList => WORDLIST; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index a25c85724b..0a336b0d57 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -7,7 +7,9 @@ environment: sdk: ">=3.5.0 <4.0.0" dependencies: + bip39: ^1.0.6 catalyst_cardano_serialization: ^0.4.0 + convert: ^3.1.1 equatable: ^2.0.5 flutter_quill: ^10.5.13 meta: ^1.10.0 diff --git a/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart b/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart new file mode 100644 index 0000000000..de075d6356 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/test/seed_phrase_test.dart @@ -0,0 +1,74 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:catalyst_voices_models/src/seed_phrase.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(SeedPhrase, () { + test('should generate a new SeedPhrase with random mnemonic', () { + final seedPhrase = SeedPhrase(); + expect(seedPhrase.mnemonic, isNotEmpty); + expect(seedPhrase.uint8ListSeed, isNotEmpty); + expect(seedPhrase.hexSeed, isNotEmpty); + expect(seedPhrase.mnemonicWords.length, 12); + }); + + test('should generate a seed phrase with 12, 15, 18, 21, and 24 words', () { + for (final wordCount in [12, 15, 18, 21, 24]) { + final seedPhrase = SeedPhrase(wordCount: wordCount); + expect(seedPhrase.mnemonicWords.length, wordCount); + expect(bip39.validateMnemonic(seedPhrase.mnemonic), isTrue); + } + }); + + test('should throw an error for an invalid word count', () { + expect(() => SeedPhrase(wordCount: 9), throwsA(isA())); + expect(() => SeedPhrase(wordCount: 13), throwsA(isA())); + expect(() => SeedPhrase(wordCount: 27), throwsA(isA())); + }); + + test('should create SeedPhrase from a valid mnemonic', () { + final validMnemonic = bip39.generateMnemonic(); + final seedPhrase = SeedPhrase.fromMnemonic(validMnemonic); + expect(seedPhrase.mnemonic, validMnemonic); + expect(seedPhrase.hexSeed, bip39.mnemonicToSeedHex(validMnemonic)); + }); + + test('should create SeedPhrase from hex-encoded entropy', () { + final entropy = bip39.mnemonicToEntropy(bip39.generateMnemonic()); + final seedPhrase = SeedPhrase.fromHexEntropy(entropy); + + expect(seedPhrase.mnemonic, isNotEmpty); + expect(seedPhrase.hexEntropy, entropy); + }); + + test('should throw an error for invalid mnemonic', () { + const invalidMnemonic = 'invalid mnemonic phrase'; + expect( + () => SeedPhrase.fromMnemonic(invalidMnemonic), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid mnemonic phrase'), + ), + ), + ); + }); + + test('should contain consistent mnemonic and seed in generated SeedPhrase', + () { + final seedPhrase = SeedPhrase(); + final mnemonic = seedPhrase.mnemonic; + final seed = seedPhrase.hexSeed; + + expect(bip39.mnemonicToSeedHex(mnemonic), seed); + }); + + test('should split mnemonic into a list of words', () { + final mnemonic = bip39.generateMnemonic(); + final seedPhrase = SeedPhrase.fromMnemonic(mnemonic); + final expectedWords = mnemonic.split(' '); + expect(seedPhrase.mnemonicWords, expectedWords); + }); + }); +} diff --git a/melos.yaml b/melos.yaml index 2e38ced9f7..345b88fae7 100644 --- a/melos.yaml +++ b/melos.yaml @@ -19,6 +19,7 @@ command: flutter: ">=3.24.1" dependencies: asn1lib: ^1.5.3 + bip39: ^1.0.6 bloc_concurrency: ^0.2.2 collection: ^1.18.0 cryptography: ^2.7.0 From b2a1ddec72257c9a299d00362805e383bb2121e1 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:40:20 +0200 Subject: [PATCH 2/4] feat(dart/catalyst_cardano_serialization): add support for additional transaction body fields (#858) * feat(transaction_builder): add support for additional transaction body fields (#710) * fix(catalyst_cardano_serialization): add missing properties to Transaction constructor in `_buildBody()` * docs(catalyst_cardano_serialization): Correct typo in transaction builder class Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --------- Co-authored-by: Steven Johnson Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .../catalyst_cardano_serialization/README.md | 14 ++--- .../lib/src/builders/transaction_builder.dart | 55 ++++++++++++++++++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/README.md b/catalyst_voices_packages/catalyst_cardano_serialization/README.md index 7354c3ecce..4cb692b5d5 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/README.md +++ b/catalyst_voices_packages/catalyst_cardano_serialization/README.md @@ -194,15 +194,15 @@ Byron era addresses are not supported. | 5 = reward withdrawals | ❌️ | | 6 = protocol parameter update | ❌️ | | 7 = auxiliary_data_hash | ✔️ | -| 8 = validity interval start | ❌️ | -| 9 = mint | ❌️ | -| 11 = script_data_hash | ❌️ | -| 13 = collateral inputs | ❌️ | +| 8 = validity interval start | ✔️ | +| 9 = mint | ✔️ | +| 11 = script_data_hash | ✔️ | +| 13 = collateral inputs | ✔️ | | 14 = required signers | ✔️ | | 15 = network_id | ✔️ | -| 16 = collateral return | ❌️ | -| 17 = total collateral | ❌️ | -| 18 = reference inputs | ❌️ | +| 16 = collateral return | ✔️ | +| 17 = total collateral | ✔️ | +| 18 = reference inputs | ✔️ | ## Reference documentation diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart index b6470a4676..96d071ace6 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/builders/transaction_builder.dart @@ -38,13 +38,34 @@ final class TransactionBuilder extends Equatable { /// The transaction metadata as a list of key-value pairs (a map). final AuxiliaryData? auxiliaryData; - /// The list of public key hashes of addresses - /// that are required to sign the transaction. + /// Validity interval start as integer. + final SlotBigNum? validityStart; + + /// Mint as a non-zero uint64 multiasset. + final MultiAsset? mint; + + /// The transaction metadata as a list of key-value pairs (a map). + final ScriptData? scriptData; + + /// Collateral inputs as nonempty set. + final Set? collateralInputs; + + /// The list of public key hashes of addresses that are required to sign the + /// transaction. A nonempty set of addr key hashes. final Set? requiredSigners; /// Specifies on which network the code will run. final NetworkId? networkId; + /// Collateral return's transaction output. + final ShelleyMultiAssetTransactionOutput? collateralReturn; + + /// Total collateral as coin (uint64). + final Coin? totalCollateral; + + /// Reference inputs as nonempty set of transaction inputs. + final Set? referenceInputs; + /// The builder that builds the witness set of the transaction. /// /// The caller must know in advance how many witnesses there will be to @@ -60,8 +81,15 @@ final class TransactionBuilder extends Equatable { this.fee, this.ttl, this.auxiliaryData, + this.validityStart, + this.mint, + this.scriptData, + this.collateralInputs, this.requiredSigners, this.networkId, + this.collateralReturn, + this.totalCollateral, + this.referenceInputs, this.witnessBuilder = const TransactionWitnessSetBuilder( vkeys: {}, vkeysCount: 1, @@ -232,8 +260,15 @@ final class TransactionBuilder extends Equatable { fee, ttl, auxiliaryData, + validityStart, + mint, + scriptData, + collateralInputs, requiredSigners, networkId, + collateralReturn, + totalCollateral, + referenceInputs, witnessBuilder, ]; @@ -476,8 +511,17 @@ final class TransactionBuilder extends Equatable { auxiliaryDataHash: auxiliaryData != null ? AuxiliaryDataHash.fromAuxiliaryData(auxiliaryData!) : null, + validityStart: validityStart, + mint: mint, + scriptDataHash: scriptData != null + ? ScriptDataHash.fromScriptData(scriptData!) + : null, + collateralInputs: collateralInputs, requiredSigners: requiredSigners, networkId: networkId, + collateralReturn: collateralReturn, + totalCollateral: totalCollateral, + referenceInputs: referenceInputs, ); } @@ -493,8 +537,15 @@ final class TransactionBuilder extends Equatable { fee: fee ?? this.fee, ttl: ttl, auxiliaryData: auxiliaryData, + validityStart: validityStart, + mint: mint, + scriptData: scriptData, + collateralInputs: collateralInputs, requiredSigners: requiredSigners, networkId: networkId, + collateralReturn: collateralReturn, + totalCollateral: totalCollateral, + referenceInputs: referenceInputs, witnessBuilder: witnessBuilder ?? this.witnessBuilder, ); } From 1ce946514bc6d6702ce39debe873fb6f1d93f28e Mon Sep 17 00:00:00 2001 From: Alex Pozhylenkov Date: Mon, 23 Sep 2024 13:56:25 +0300 Subject: [PATCH 3/4] docs: Jormungandr voting crypto proofs spec. (#862) * add Non-Interactive ZK Tally Proof appendix * fix * fix markdown check * fix spelling * add Non-Interactive ZK Vote Proof Proover section and Verifier section * Finish verifier description * wip * fix spelling * fix --- .../08_concepts/voting_transaction/crypto.md | 320 +++++++++++++++--- 1 file changed, 276 insertions(+), 44 deletions(-) diff --git a/docs/src/architecture/08_concepts/voting_transaction/crypto.md b/docs/src/architecture/08_concepts/voting_transaction/crypto.md index f8dd29a8cf..5ae02eeaa1 100644 --- a/docs/src/architecture/08_concepts/voting_transaction/crypto.md +++ b/docs/src/architecture/08_concepts/voting_transaction/crypto.md @@ -28,7 +28,7 @@ and performing tally process for executing "Catalyst" fund events. ### Preliminaries -Through this paper we will use the following notations to refer to some entities of this protocol: +The protocol is based around the following entities: * **Proposal** - voting subject on which each voter will be cast their votes. @@ -49,13 +49,9 @@ Through this paper we will use the following notations to refer to some entities Or it could be defined based on their stake in the blockchain, which is more appropriate for web3 systems. -Important to note that current protocol defined to work with the one specific proposal, -so all definitions and procedures would be applied for some proposal. +Important to note that the protocol defined for some **one** proposal. Obviously, it could be easily scaled for a set of proposals, -performing all this protocol in parallel. - -The voting committee and voters registration/definition -are not subjects of this document. +performing protocol steps in parallel. ### Initial setup @@ -125,10 +121,10 @@ components would be defined as follows: -After the choice is done, -vote **must** be encrypted using shared shared election public key $pk$. +After the choice is done (described in [section](#voting-choice)), +vote **must** be encrypted using shared election public key $pk$. -Lifted ElGamal encryption algorithm is used, +To achieve that, Lifted ElGamal encryption algorithm is used, noted as $ElGamalEnc(message, randomness, public \; key)$. More detailed description of the lifted ElGamal algorithm you can find in the [appendix B](#b-lifted-elgamal-encryptiondecryption). @@ -138,17 +134,16 @@ $ElGamalEnc(message, randomness, public \; key)$ algorithm produces a ciphertext c = ElGamalEnc(message, randomness, public \; key) \end{equation} -To encode previously generated unit vector $\mathbf{e}_i$ ($i$ - voting choice identifier), -in more details you can read in this [section](#voting-choice), +To encrypt previously generated unit vector $\mathbf{e}_i$ ($i$ - voting choice identifier), for each vector component value $e_{i,j}$ generate a corresponding randomness.
Lets denote randomness value as $r_j$, -where $j$ states as the same identifier of the vector component $e_{i,j}$. +where $j$ is the same vector component's index $j$ value, $e_{i, j} => r_j$. -Then, for each vector component $e_{i,j}$ with the corresponding randomness, +Then, for each vector component $e_{i,j}$ with the corresponding randomness $r_j$, perform encryption algorithm applying shared election public key $pk$. \begin{equation} -c_j = Enc(e_{i,j}, r_j, pk) +c_j = ElGamalEnc(e_{i,j}, r_j, pk) \end{equation} As a result getting a vector $\mathbf{c}$ of ciphertext values $c_f$, @@ -156,10 +151,10 @@ with the size equals of the size $\mathbf{e}_t$ unit vector, equals to the amount of the voting options. Lets denote this vector as: \begin{equation} -\mathbf{c} = (c_1, \ldots, c_{M}) +\mathbf{c} = (c_1, \ldots, c_{M}) = (ElGamalEnc(e_{i,j}, r_j, pk), \ldots, ElGamalEnc(e_{i,M}, r_M, pk)) \end{equation} -where $M$ is the voting options amount. +where $M$ is the voting options amount and $i$ is the index of the voting choice. This is a first part of the published vote for a specific proposal. @@ -171,7 +166,7 @@ After the voter's choice is generated and encrypted, it is crucial to prove that [encoding](#voting-choice) and [encryption](#vote-encrypting) are formed correctly (i.e. that the voter indeed encrypt a unit vector). -Because by the definition of the encryption algorithm $Enc(message, randomness, public \; key)$ +Because by the definition of the encryption algorithm $ElGamalEnc(message, randomness, public \; key)$ it is possible to encrypt an any message value, it is not restricted for encryption only $0$ and $1$ values (as it was stated in the previous [section](#voting-choice), @@ -181,22 +176,28 @@ so everyone could validate a correctness of the encrypted vote data, without revealing a voting choice itself. To achieve that a some sophisticated ZK (Zero Knowledge) algorithm is used, -noted as $VotingChoiceProof(\mathbf{c})$. -It takes an encrypted vote vector $\mathbf{c}$ and generates a proof value $\pi$. +noted as $VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk)$. +It takes an encrypted vote vector $\mathbf{c}$, +an original vote unit vector $\mathbf{e}_i$, +a randomness vector $\mathbf{r}$, +which was used during encryption algorithm $ElGamalEnc$ +and an shared election public key $pk$. +As a result it generates a proof value $\pi$. \begin{equation} -\pi = VotingChoiceProof(\mathbf{c}) +\pi = VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk) \end{equation} -So to validate a $VotingChoiceCheck(\mathbf{c}, \pi)$ procedure should be used, -which takes an encrypted vote $\mathbf{c}$ and corresponded proof $\pi$ +So to validate a $VoteCheck(\mathbf{c}, \pi, pk)$ procedure should be used, +which takes an encrypted vote $\mathbf{c}$, corresponded proof $\pi$ +and the same hared election public key $pk$ as arguments and returns `true` or `false`, is it valid or not. \begin{equation} -true | false = VotingChoiceCheck(\mathbf{c}, \pi) +true | false = VoteCheck(\mathbf{c}, \pi, pk) \end{equation} -A more detailed description of how $VotingChoiceProof$, $VotingChoiceCheck$ work -you can find in the section *2.4* of this [paper][treasury_system_spec]. +A more detailed description of how $VoteProof$, $VoteCheck$ work +you can find in the [appendix D](#d-non-interactive-zk-vote-proof). #### Vote publishing @@ -305,7 +306,7 @@ Which proofs that a provided encrypted tally result value $er$ was decrypted int using the exact secret key $sk$, which is corresponded to the already known shared election public key $pk$. \begin{equation} -\pi = TallyProof(er, r, sk) +\pi = TallyProof(er, sk) \end{equation} So to validate a $TallyCheck(er, r, pk, \pi)$ procedure should be used, @@ -317,10 +318,8 @@ is it valid or not. true | false = TallyCheck(er, r, pk, \pi) \end{equation} - A more detailed description of how $TallyProof$, $TallyCheck$ work -you can find in the section *Fig. 10* of this [paper][treasury_system_spec]. - +you can find in the [appendix E](#e-non-interactive-zk-tally-proof). #### Tally publishing @@ -331,7 +330,7 @@ and tally proofs $\pi_i$ corresponded for each voting option of some proposal. It could be published using any public channel, e.g. blockchain, ipfs or through p2p network. -## A: Group definition +## A: Group Definition @@ -350,7 +349,7 @@ And defined as follows: -## B: Lifted ElGamal encryption/decryption +## B: Lifted ElGamal Encryption/Decryption @@ -363,12 +362,16 @@ More detailed how group operations are defined, described in [appendix A](#a-gro ### Encryption Lifted ElGamal encryption algorithm -takes as arguments $m$ message ($m \in \mathbb{Z}_q^*$), -$r$ randomness ($r \in \mathbb{Z}_q^*$), -$pk$ public key ($pk \in \mathbb{G}$): +takes as arguments: + +* $m$ - message ($m \in \mathbb{Z}_q$) +* $r$ - randomness ($r \in \mathbb{Z}_q$) +* $pk$ - public key ($pk \in \mathbb{G}$) + \begin{equation} ElGamalEnc(m, r, pk) = (c_1, c_2) = c, \end{equation} + \begin{equation} c_1 = g^r, \quad c_2 = g^m \circ pk^r \end{equation} @@ -377,8 +380,11 @@ $c$ - is a resulted ciphertext which consists of two elements $c_1, c_2 \in \mat ### Decryption -Lifted ElGamal decryption algorithm takes as arguments $c$ ciphertext, -$sk$ secret key ($sk \in \mathbb{Z}_q^*$): +Lifted ElGamal decryption algorithm takes as arguments: + +* $c$ - ciphertext, +* $sk$ - secret key ($sk \in \mathbb{Z}_q$) + \begin{equation} ElGamalDec(c, sk) = Dlog(c_2 \circ c_1^{-sk}) = m \end{equation} @@ -387,11 +393,11 @@ $m$ - an original message which was encrypted on the previous step, $Dlog(x)$ is a discrete logarithm of $x$. Note that since $Dlog$ is not efficient, the message space should be a small set, -say $m \in {{0,1}}^{\xi}$, for $\xi \le 30$. +say $m \in \{0,1\}^{\xi}$, for $\xi \le 30$. -## C: Homomorphic tally +## C: Homomorphic Tally @@ -399,11 +405,14 @@ Homomorphic tally schema is defined over any cyclic group $\mathbb{G}$ of order
More detailed how group operations are defined, described in [appendix A](#a-group-definition). -Homomorphic tally algorithm takes as arguments $i$ voting choice index, -$[\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}]$ -an array of encrypted votes vector's, -$[\alpha_1, \alpha_2, \ldots, \alpha_N]$ - an array of corresponded voter's voting power. -Where $N$ - votes amount. +Homomorphic tally algorithm takes as arguments: + +* $i$ - voting choice index +* $[\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}]$ - an array of encrypted votes vector's, + where $N$ - votes amount +* $[\alpha_1, \alpha_2, \ldots, \alpha_N]$ - an array of corresponded voter's voting power, + where $N$ - votes amount + \begin{equation} Tally(i, [\mathbf{c_1}, \mathbf{c_2}, \ldots, \mathbf{c_N}], [\alpha_1, \alpha_2, \ldots, \alpha_N]) = c_{1, i}^{\alpha_1} \circ c_{2, i}^{\alpha_2} \circ \ldots \circ c_{N, i}^{\alpha_N} = er_i @@ -419,6 +428,229 @@ it needs a decryption procedure corresponded for which encryption one was made. +## D: Non-Interactive ZK Vote Proof + +Non-Interactive ZK (Zero Knowledge) Vote Proof algorithm helps to solve only one problem, +to prove that the encrypted voting choice is exactly a some unit vector, +which consists of **only one** is $1$ value and others are $0$. + +A more detailed and formal description +you can find in the section *2.4* of this [paper][treasury_system_spec]. + +It is assumed that the original encryption and decryption is performing by ElGamal scheme. +It means that all described operations is also group dependent +(more about groups described in [appendix A](#a-group-definition)). + +### Prover + +The prover algorithm takes as arguments: + +* $\mathbf{c} = (c_0, \ldots, c_{M-1})$ - encrypted vote (a vector of ciphertext), + where $M$ is amount of voting options. +* $\mathbf{e}_i = (e_{i,0},\ldots, e_{i,M-1})$ - original voting choice, a unit vector, + where $M$ is amount of voting options + and $i$ is an index of the voting choice. +* $\mathbf{r} = (r_0, \ldots, r_{M-1})$ - a vector of randomnesses, + which was used during encryption. +* $pk$ - is a public key, which was used to encrypt a unit vector. + +So basically here is the relation between all these values: +\begin{equation} +\mathbf{c} = (c_1, \ldots, c_M) = (ElGamalEnc(e_{i,1}, r_1, pk), \ldots, ElGamalEnc(e_{i,M}, r_M, pk)) +\end{equation} + +\begin{equation} +VoteProof(\mathbf{c}, \mathbf{e}_i, \mathbf{r}, pk) = \pi +\end{equation} + +Important to note that the following notation would be used +$\{a_i\}$ - which is a set of some elements $a_i$. + +$\pi$ is the final proof. +To compute it, prover needs to perform the next steps: + +1. If the number of voting options $M$ is not a perfect power of $2$, + extend the vector $\mathbf{c}$ with $c_j = ElGamalEnc(0, 0, pk)$, + where $N$ is a perfect power of $2$, $j \in [M, \ldots, N - 1]$. + So the resulted $\mathbf{c} = (c_1, \ldots, c_M, \{c_j\})$. +2. Generate a commitment key $ck \in \mathbb{G}$. +3. Let $i_k$ is a bit value of the $i$-th binary representation, + where $k \in [0, log_2(N) - 1]$. + E.g. $i=3$ and $N=8, log_2(N) = 3$, + its binary representation $i=011$, + $i_0=0, i_1=1, i_2=1$. +4. For $l \in [0, \ldots, log_2(N)-1]$ generate a random values + $\alpha_l, \beta_l, \gamma_l, \delta_l, \in \mathbb{Z}_q$. +5. For $l \in [0, \ldots, log_2(N)-1]$ calculate, where $g$ is the group generator: + * $I_l = g^{i_l} \circ ck^{\alpha_l}, I_l \in \mathbb{G}$. + * $B_l = g^{\beta_l} \circ ck^{\gamma_l}, B_l \in \mathbb{G}$. + * $A_l = g^{i_l * \beta_l} \circ ck^{\delta_l}, A_l \in \mathbb{G}$. +6. Calculate a first verifier challenge + $com_1 = H(ck, pk, \{c_j\}, \{I_l\}, \{B_l\}, \{A_l\})$, + where $H$ is a hash function, + $j \in [0, \ldots, N-1]$ + and $l \in [0, \ldots, log_2(N)-1]$. +7. For $j \in [0, \ldots, N-1]$ calculate polynomials + in the following form $p_j(x) = e_{i, j}*x^{log_2(N)} + \sum_{l=0}^{log_2(N)-1} p_{j,l} * x^l$: + * $j_l$ is a bit value of the $j$-th binary representation (same as was described in step `3`). + * $z_l^{1} = i_l * x + \beta_l$. + * $z_l^{0} = x - z_l^{1} = (1 - i_l)*x - \beta_l$. + * Calculate the polynomial itself $p_j(x) = \prod_{l=0}^{log_2(N)-1} z_l^{j_l}$ +8. For $l \in [0, \ldots, log_2(N)-1]$ generate a random $R_l \in \mathbb{Z}_q$. +9. For $l \in [0, \ldots, log_2(N)-1]$ compute + $D_l = ElGamalEnc(sum_l, R_l, pk)$, + where $sum_l = \sum_{j=0}^{N-1}(p_{j,l} * com_1^j)$ + and $p_{j,l}$ - corresponding coefficients of the polynomial $p_j(x)$ calculated on step `7`. +10. Calculate a second verifier challenge + $com_2 = H(com_1, \{D_l\})$, + where $H$ is a hash function + and $l \in [0, \ldots, log_2(N)-1]$. +11. For $l \in [0, \ldots, log_2(N)-1]$ calculate: + * $z_l = i_l * com_2 + \beta_l, z_l \in \mathbb{Z}_q$. + * $w_l = \alpha_l * com_2 + \gamma_l, w_l \in \mathbb{Z}_q$. + * $v_l = \alpha_l * (com_2 - z_l) + \delta_l, v_l \in \mathbb{Z}_q$. +12. Calculate + $R=\sum_{j=0}^{N-1}(r_j * (com_2)^{log_2(N)} * (com_1)^j) + \sum_{l=0}^{log_2(N)-1}(R_l * (com_2)^l)$, + where $r_j$ original random values which was used to encrypt $c_j$ + and $R_l$ random values generated in step `8`. + +Finally, the proof is $\pi = (ck, \{I_l\}, \{B_l\}, \{A_l\}, \{D_l\}, \{z_l\}, \{w_l\}, \{v_l\}, R)$, +where $l \in [0, \ldots, log_2(N)-1]$. + +### Verifier + +The verifier algorithm takes as arguments: + +* $\mathbf{c} = (c_0, \ldots, c_{M-1})$ - encrypted vote (a vector of ciphertext), + where $M$ is amount of voting options. +* $\pi$ - a prover's proof generated on the [previous step](#prover) +* $pk$ - is a public key, which was used to encrypt a unit vector. + +\begin{equation} +VoteCheck(\mathbf{c}, \pi, pk) = true | false +\end{equation} + +As a result algorithm will return `true` or `false`, +is the verification was succeeded or not respectively. + +Knowing that $\pi$ equals to $(ck, \{I_l\}, \{B_l\}, \{A_l\}, \{D_l\}, \{z_l\}, \{w_l\}, \{v_l\}, R)$, +verifier needs to perform the next steps: + +1. If the number of voting options $M$ is not a perfect power of $2$, + extend the vector $\mathbf{c}$ with $c_j = ElGamalEnc(0, 0, pk)$, + where $N$ is a perfect power of $2$, $j \in [M, \ldots, N - 1]$. + So the resulted $\mathbf{c} = (c_1, \ldots, c_M, \{c_j\})$. +2. Calculate the first verifier challenge + $com_1 = H(ck, pk, \{c_j\}, \{I_l\}, \{B_l\}, \{A_l\})$, + where $H$ is a hash function, + $j \in [0, \ldots, N-1]$ + and $l \in [0, \ldots, log_2(N)-1]$. +3. Calculate a second verifier challenge + $com_2 = H(com_1, \{D_l\})$, + where $H$ is a hash function + and $l \in [0, \ldots, log_2(N)-1]$. +4. For $l \in [0, \ldots, log_2(N)-1]$ verify that the following statements are `true`, + where $g$ is the group generator: + * $(I_l)^{com_2} \circ B_l == g^{z_l} \circ ck^{w_l}$. + * $(I_l)^{com_2 - z_l} \circ A_l == g^{0} \circ ck^{v_l}$. +5. Calculate the following $Left = ElGamalEnc(0, R, pk)$. + Note that the $Left$ is a ciphertext, $Left = (Left_1, Left_2)$. +6. Note that $D_l$ is a ciphertext, + $D_l = (D_{l,1}, D_{l,2})$, for $l \in [0, \ldots, log_2(N)-1]$ + calculate the following: + * $Right2_1 = (D_{0,1})^{0} \circ \ldots \circ (D_{log_2(N) - 1,1})^{log_2(N) - 1}$. + * $Right2_2 = (D_{0,2})^{0} \circ \ldots \circ (D_{log_2(N) - 1,2})^{log_2(N) - 1}$. +7. For $j \in [0, \ldots, N-1]$ calculate the $p_j(com_2)$, + where $p_j$ is a prover's defined polynomial defined in step `7`: + * $j_l$ is a bit value of the $j$-th binary representation. + * $z_l^1 = z_j$. + * $z_l^0 = com_2 - z_j^1$. + * $p_j(com_2) = \prod_l^{log_2(N)-1} z_l^{j_l}$. +8. For $j \in [0, \ldots, N-1]$ calculate the $P_j = ElGamalEnc(-p_j(com_2), 0, pk)$. + Note that the $P_j$ is a ciphertext, $P_j = (P_{j,1}, P_{j,2})$. +9. Note that $C_j$ is a ciphertext, + $C_j = (C_{j,1}, C_{j,2})$, for $j \in [0, \ldots, N-1]$ + calculate: + * $Right1_{j,1} = (C_{j,1})^{com_2^{log_2(N)}} \circ (P_{j,1})^{com_1^{j}}$. + * $Right1_{j,2} = (C_{j,2})^{com_2^{log_2(N)}} \circ (P_{j,2})^{com_1^{j}}$. + * $Right1_{1} = Right1_{j,1} \circ \ldots \circ Right1_{N - 1, 1}$. + * $Right1_{2} = Right1_{j,2} \circ \ldots \circ Right1_{N - 1, 2}$. +10. Verify that the following statements are `true`: + * $Right1_{1} \circ Right2_1 == Left_1$. + * $Right1_{2} \circ Right2_2 == Left_2$. + +If step `4` and `10` returns `true` so the final result is `true` otherwise return `false`. + +## E: Non-Interactive ZK Tally Proof + +Non-Interactive ZK (Zero Knowledge) Tally Proof algorithm helps to solve only one problem, +to prove that the specific encrypted message was decrypted into the specific resulted value, +using exactly that secret key, +which is corresponds to the some shared public key. + + +A more detailed and formal description +you can find in the sections *Fig. 10* and *2.1.5* of this [paper][treasury_system_spec]. + + +It is assumed that the original encryption and decryption is performing by ElGamal scheme. +It means that all described operations is also group dependent +(more about groups described in [appendix A](#a-group-definition)). + +### Prover + +The prover algorithm takes as arguments: + +* $enc$ - an encrypted message (ciphertext). +* $sk$ - a secret key which was used to decrypt a message $enc$. + +\begin{equation} +TallyProof(enc, sk) = \pi +\end{equation} + +$\pi$ is the final proof. +To compute it, prover needs to perform the next steps: + +1. Take the first element of the ciphertext $enc = (enc_1, enc_2)$ + and calculate $d = enc_1^{sk}$. +2. Generate a random value $\mu, \quad \mu \in \mathbb{Z}_q$. +3. Compute $A_1 = g^{\mu}$, where $g$ is the group generator ($A_1 \in \mathbb{G}$). +4. Compute $A_2 = (enc_1)^{\mu}, \quad A_2 \in \mathbb{G}$. +5. Compute $e = H(pk, d, g, enc_1, A_1, A_2 )$, + where $pk$ is a corresponding public key of $sk$, $H$ is a hash function. +6. Compute $z = sk * e + \mu, \quad z \in \mathbb{Z}_q$. + +Finally, the proof is $\pi = (A_1, A_2, z)$. + +### Verifier + +The verifier algorithm takes as arguments: + +* $enc$ - an encrypted message (ciphertext). +* $dec$ - a decrypted message from the encrypted ciphertext $enc$. +* $pk$ - a public key corresponded to the $sk$ + which was supposedly used to decrypt a message $enc$. +* $\pi$ - a prover's proof generated on the [previous step](#prover-1). + +\begin{equation} +TallyCheck(enc, dec, pk, \pi) = true | false +\end{equation} + +As a result algorithm will return `true` or `false`, +is the verification was succeeded or not respectively. + +Knowing that $\pi$ equals to $(A_1, A_2, z)$, +verifier needs to perform the next steps: + +1. Take the first and second elements $enc_1, enc_2$ + of the ciphertext $enc = (enc_1, enc_2)$. +2. Calculate $d = g^{dec} \circ (-enc_2), \quad d \in \mathbb{G}$. +3. Compute $e = H(pk, d, g, enc_1, A_1, A_2 )$, where $g$ is the group generator. +4. Verify $g^z == pk^e \circ A_1$. +5. Verify $enc_1^z == d^e \circ A_2$. + +If step `3` and `4` returns `true` so the final result is `true` otherwise return `false`. + ## Rationale ## Path to Active From 24f86587423ab1411e9322a5d29978e5259480bd Mon Sep 17 00:00:00 2001 From: digitalheartxs Date: Mon, 23 Sep 2024 13:25:34 +0200 Subject: [PATCH 4/4] feat(cat-voices): Account popup (#857) * feat: My account, develop account_popup.dart * feat: My account, account_popup, pass callbacks * feat: My account, account_popup, ignore pointer for avatar * feat: My account, account_popup, lint issues * feat: My account, account_popup, lint issues * feat: My account, account_popup, use enum * feat: My account, account_popup, fields above constructor * feat: My account, account_popup, use VoicesDivider --------- Co-authored-by: Dominik Toton <166132265+dtscalac@users.noreply.github.com> --- .config/dictionaries/project.dic | 1 + .../lib/pages/account/account_popup.dart | 253 ++++++++++++++++++ .../app_bar/session/session_state_header.dart | 23 +- 3 files changed, 260 insertions(+), 17 deletions(-) create mode 100644 catalyst_voices/lib/pages/account/account_popup.dart diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 2a20967c22..52715d4bb0 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -1,6 +1,7 @@ aapt aarch abnf +addr addrr adminer afinet diff --git a/catalyst_voices/lib/pages/account/account_popup.dart b/catalyst_voices/lib/pages/account/account_popup.dart new file mode 100644 index 0000000000..44f91a83e2 --- /dev/null +++ b/catalyst_voices/lib/pages/account/account_popup.dart @@ -0,0 +1,253 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class AccountPopup extends StatelessWidget { + final String avatarLetter; + final VoidCallback? onProfileKeychainTap; + final VoidCallback? onLockAccountTap; + + const AccountPopup({ + super.key, + required this.avatarLetter, + this.onProfileKeychainTap, + this.onLockAccountTap, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton<_MenuItemValue>( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, + onSelected: (_MenuItemValue value) { + switch (value) { + case _MenuItemValue.profileAndKeychain: + onProfileKeychainTap?.call(); + break; + case _MenuItemValue.lock: + onLockAccountTap?.call(); + break; + } + }, + itemBuilder: (BuildContext bc) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + enabled: false, + value: null, + child: _Header( + accountLetter: avatarLetter, + walletName: 'Wallet name', + walletBalance: '₳ 1,750,000', + accountType: 'Basis', + walletAddress: 'addr1_H4543...45GH', + ), + ), + const PopupMenuItem( + height: 48, + padding: EdgeInsets.zero, + enabled: false, + value: null, + child: _Section('My account'), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + value: _MenuItemValue.profileAndKeychain, + child: _MenuItem( + 'Profile & Keychain', + VoicesAssets.icons.userCircle, + ), + ), + PopupMenuItem( + padding: EdgeInsets.zero, + value: _MenuItemValue.lock, + child: _MenuItem( + 'Lock account', + VoicesAssets.icons.lockClosed, + showDivider: false, + ), + ), + ]; + }, + offset: const Offset(0, kToolbarHeight), + child: IgnorePointer( + child: VoicesAvatar( + icon: Text(avatarLetter), + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + final String accountLetter; + final String walletName; + final String walletBalance; + final String accountType; + final String walletAddress; + + const _Header({ + required this.accountLetter, + required this.walletName, + required this.walletBalance, + required this.accountType, + required this.walletAddress, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Row( + children: [ + VoicesAvatar( + icon: Text(accountLetter), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(_padding), + child: Wrap( + children: [ + Text( + walletName, + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + walletBalance, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ), + VoicesChip.rectangular( + content: Text( + accountType, + style: TextStyle( + color: Theme.of(context).colors.successContainer, + ), + ), + backgroundColor: Theme.of(context).colors.success, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only( + left: _padding, + right: _padding, + bottom: _padding, + top: 8, + ), + child: Row( + children: [ + Expanded( + child: Text( + walletAddress, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + InkWell( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: walletAddress), + ); + }, + child: VoicesAssets.icons.clipboardCopy.buildIcon(), + ), + ], + ), + ), + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorder, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +class _MenuItem extends StatelessWidget { + final String text; + final SvgGenImage icon; + final bool showDivider; + + const _MenuItem( + this.text, + this.icon, { + this.showDivider = true, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 47, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Row( + children: [ + icon.buildIcon(), + const SizedBox(width: _padding), + Text( + text, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + if (showDivider) + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorderVariant, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + final String text; + + const _Section( + this.text, + ); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + height: 47, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: _padding), + child: Text( + text, + style: Theme.of(context).textTheme.titleSmall, + ), + ), + VoicesDivider( + height: 1, + color: Theme.of(context).colors.outlineBorderVariant, + indent: 0, + endIndent: 0, + ), + ], + ); + } +} + +const _padding = 12.0; + +enum _MenuItemValue { + profileAndKeychain, + lock, +} diff --git a/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart b/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart index 8f110ec863..a84223a205 100644 --- a/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart +++ b/catalyst_voices/lib/widgets/app_bar/session/session_state_header.dart @@ -1,3 +1,4 @@ +import 'package:catalyst_voices/pages/account/account_popup.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; @@ -15,8 +16,11 @@ class SessionStateHeader extends StatelessWidget { return switch (state) { VisitorSessionState() => const _VisitorButton(), GuestSessionState() => const _GuestButton(), - ActiveUserSessionState(:final user) => - _ActiveUserAvatar(letter: user.acronym ?? 'A'), + ActiveUserSessionState(:final user) => AccountPopup( + avatarLetter: user.acronym ?? 'A', + onLockAccountTap: () => debugPrint('Lock account'), + onProfileKeychainTap: () => debugPrint('Open Profile screen'), + ), }; }, ); @@ -46,18 +50,3 @@ class _VisitorButton extends StatelessWidget { ); } } - -class _ActiveUserAvatar extends StatelessWidget { - final String letter; - - const _ActiveUserAvatar({ - required this.letter, - }); - - @override - Widget build(BuildContext context) { - return VoicesAvatar( - icon: Text(letter), - ); - } -}