Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Naming and its align #66

Merged
merged 6 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ If you would like to get a grant to create PLUME applications or improve the lib

## Contributions

If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any of the bugs in our issues! Each of them is quite small. That includes [#28](https://github.com/plume-sig/zk-nullifier-sig/issues/28), [#24](https://github.com/plume-sig/zk-nullifier-sig/issues/24), [#22](https://github.com/plume-sig/zk-nullifier-sig/issues/22), [#14](https://github.com/plume-sig/zk-nullifier-sig/issues/14),and [#13](https://github.com/plume-sig/zk-nullifier-sig/issues/13).
If you'd like to contribute, we offer $50 bounties in Eth/DAI for resolving any of the bugs in our issues! Each of them is quite small. That includes
[#28](https://github.com/plume-sig/zk-nullifier-sig/issues/28), [#24](https://github.com/plume-sig/zk-nullifier-sig/issues/24),
[#14](https://github.com/plume-sig/zk-nullifier-sig/issues/14),and [#13](https://github.com/plume-sig/zk-nullifier-sig/issues/13).

## Implementations

Expand Down
48 changes: 27 additions & 21 deletions circuits/test/vfy_nullifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { join } from 'path';
import { wasm as wasm_tester } from 'circom_tester'
import { describe, expect, test } from '@jest/globals';
import { hexToBigInt } from "../../javascript/src/utils/encoding";
import { c_v1, c_v2, gPowR, hashMPk, hashMPkPowR, nullifier, s_v1, s_v2, testMessage, testPublicKey, testPublicKeyPoint, testR, testSecretKey } from "../../javascript/test/test_consts"
import { c_v1, c_v2, rPoint, hashMPk, hashedToCurveR, nullifier, s_v1, s_v2, testMessage, testPublicKey, testPublicKeyPoint, testR, testSecretKey } from "../../javascript/test/test_consts"
import { Point } from "../../javascript/node_modules/@noble/secp256k1";
import { generate_inputs_from_array } from "secp256k1_hash_to_curve_circom/ts/generate_inputs";
import { bufToSha256PaddedBitArr } from "secp256k1_hash_to_curve_circom/ts/utils";
Expand All @@ -22,15 +22,17 @@ describe("Nullifier Circuit", () => {
hexToBigInt(hashMPk.x.toString()),
hexToBigInt(hashMPk.y.toString())
)
const hash_to_curve_inputs = utils.stringifyBigInts(generate_inputs_from_array(message_bytes.concat(public_key_bytes)));
const hash_to_curve_inputs = utils.stringifyBigInts(generate_inputs_from_array(
message_bytes.concat(public_key_bytes)
));

var sha_preimage_points: Point[] = [
Point.BASE,
testPublicKeyPoint,
hashMPkPoint,
nullifier,
gPowR,
hashMPkPowR,
rPoint,
hashedToCurveR,
]

const v1_sha256_preimage_bits = bufToSha256PaddedBitArr(Buffer.from(
Expand Down Expand Up @@ -107,12 +109,11 @@ describe("Nullifier Circuit", () => {
// Main circuit inputs
c: scalarToCircuitValue(hexToBigInt(c_v1)),
s: scalarToCircuitValue(hexToBigInt(s_v1)),
msg: message_bytes,
public_key: pointToCircuitValue(testPublicKeyPoint),
plume_message: message_bytes,
pk: pointToCircuitValue(testPublicKeyPoint),
nullifier: pointToCircuitValue(nullifier),
...htci,
sha256_preimage_bit_length: v1_sha256_preimage_bit_length,

})
await circuit.checkConstraints(w)
})
Expand All @@ -127,24 +128,29 @@ describe("Nullifier Circuit", () => {
// Main circuit inputs
c: scalarToCircuitValue(hexToBigInt(c_v2)),
s: scalarToCircuitValue(hexToBigInt(s_v2)),
msg: message_bytes,
public_key: pointToCircuitValue(testPublicKeyPoint),
plume_message: message_bytes,
pk: pointToCircuitValue(testPublicKeyPoint),
nullifier: pointToCircuitValue(nullifier),
...htci,
})
await circuit.checkConstraints(w)
// assertOut builds a huge json string containing the whole witness and fails with "Cannot create a string longer than 0x1fffffe8 characters"
// Instead we just slice into the witness, and the outputs start at 1 (where 0 always equals 1 due to a property of the underlying proof system)
expect(w.slice(1, 5)).toEqual(pointToCircuitValue(gPowR)[0])
expect(w.slice(5, 9)).toEqual(pointToCircuitValue(gPowR)[1])
expect(w.slice(9, 13)).toEqual(pointToCircuitValue(hashMPkPowR)[0])
expect(w.slice(13, 17)).toEqual(pointToCircuitValue(hashMPkPowR)[1])
/* assertOut builds a huge json string containing the whole witness and fails
with "Cannot create a string longer than 0x1fffffe8 characters" */
/* Instead we just slice into the witness, and the outputs start at 1
(where 0 always equals 1 due to a property of the underlying proof system) */
expect(w.slice(1, 5)).toEqual(pointToCircuitValue(rPoint)[0])
expect(w.slice(5, 9)).toEqual(pointToCircuitValue(rPoint)[1])
expect(w.slice(9, 13)).toEqual(pointToCircuitValue(hashedToCurveR)[0])
expect(w.slice(13, 17)).toEqual(pointToCircuitValue(hashedToCurveR)[1])

// In v2 we check the challenge point c outside the circuit
// Note, in a real application you would get the nullifier, g^r, and h^r as public outputs/inputs of the proof
expect(createHash("sha256")
.update(concatUint8Arrays([nullifier.toRawBytes(true), gPowR.toRawBytes(true), hashMPkPowR.toRawBytes(true)]))
.digest('hex')).toEqual(c_v2)
/* Note, in a real application you would get the nullifier,
g^r, and h^r as public outputs/inputs of the proof */
expect(
createHash("sha256").update(concatUint8Arrays([
nullifier.toRawBytes(true), rPoint.toRawBytes(true), hashedToCurveR.toRawBytes(true)
])).digest('hex')
).toEqual(c_v2)
})

// This tests that our circuit correctly computes g^s/(g^sk)^c = g^r, and that the first two equations are
Expand All @@ -156,7 +162,7 @@ describe("Nullifier Circuit", () => {
// Verify that gPowS/pkPowC = gPowR outside the circuit, as a sanity check
const gPowS = Point.fromPrivateKey(s_v1);
const pkPowC = testPublicKeyPoint.multiply(hexToBigInt(c_v1))
expect(gPowS.add(pkPowC.negate()).equals(gPowR)).toBe(true);
expect(gPowS.add(pkPowC.negate()).equals(rPoint)).toBe(true);

// Verify that circuit calculates g^s / pk^c = g^r
const w = await circuit.calculateWitness({
Expand All @@ -165,7 +171,7 @@ describe("Nullifier Circuit", () => {
c: scalarToCircuitValue(hexToBigInt(c_v1)),
})
await circuit.checkConstraints(w)
await circuit.assertOut(w, {out: pointToCircuitValue(gPowR)});
await circuit.assertOut(w, {out: pointToCircuitValue(rPoint)});
});

test("bigint <-> register conversion", async () => {
Expand Down
138 changes: 71 additions & 67 deletions circuits/verify_nullifier.circom
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ include "./node_modules/secp256k1_hash_to_curve_circom/circom/hash_to_curve.circ
include "./node_modules/secp256k1_hash_to_curve_circom/circom/Sha256.circom";
include "./node_modules/circomlib/circuits/bitify.circom";

// Verifies that a nullifier belongs to a specific public key
// Verifies that a nullifier belongs to a specific public key \
// This blog explains the intuition behind the construction https://blog.aayushg.com/posts/nullifier
template plume_v1(n, k, msg_length) {
template plume_v1(n, k, message_length) {
signal input c[k];
signal input s[k];
signal input msg[msg_length];
signal input public_key[2][k];
signal input plume_message[message_length];
signal input pk[2][k];
signal input nullifier[2][k];

// precomputed values for the hash_to_curve component
Expand All @@ -32,14 +32,14 @@ template plume_v1(n, k, msg_length) {
// precomputed value for the sha256 component. TODO: calculate internally in circom to simplify API
signal input sha256_preimage_bit_length;

component check_ec_equations = check_ec_equations(n, k, msg_length);
component check_ec_equations = check_ec_equations(n, k, message_length);

check_ec_equations.c <== c;
check_ec_equations.s <== s;
check_ec_equations.public_key <== public_key;
check_ec_equations.pk <== pk;
check_ec_equations.nullifier <== nullifier;

check_ec_equations.msg <== msg;
check_ec_equations.plume_message <== plume_message;

check_ec_equations.q0_gx1_sqrt <== q0_gx1_sqrt;
check_ec_equations.q0_gx2_sqrt <== q0_gx2_sqrt;
Expand All @@ -63,11 +63,11 @@ template plume_v1(n, k, msg_length) {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < k; j++) {
c_sha256.coordinates[i][j] <== g[i][j];
c_sha256.coordinates[2+i][j] <== public_key[i][j];
c_sha256.coordinates[4+i][j] <== check_ec_equations.h[i][j];
c_sha256.coordinates[2+i][j] <== pk[i][j];
c_sha256.coordinates[4+i][j] <== check_ec_equations.hashed_to_curve[i][j];
c_sha256.coordinates[6+i][j] <== nullifier[i][j];
c_sha256.coordinates[8+i][j] <== check_ec_equations.g_pow_r[i][j];
c_sha256.coordinates[10+i][j] <== check_ec_equations.h_pow_r[i][j];
c_sha256.coordinates[8+i][j] <== check_ec_equations.r_point[i][j];
c_sha256.coordinates[10+i][j] <== check_ec_equations.hashed_to_curve_r[i][j];
}
}

Expand All @@ -90,17 +90,17 @@ template plume_v1(n, k, msg_length) {
}

// v2 is the same as v1, except that the sha256 check is done outside the circuit.
// We output g_pow_r and h_pow_r as public values so that the verifier can calculate the hash themselves.
// We output `r_point` ($g^r$) and `hashed_to_curve_r` ($hash^r$) as public values so that the verifier can calculate the hash themselves.
// The change is explained here https://www.notion.so/PLUME-Discussion-6f4b7e7cf63e4e33976f6e697bf349ff
template plume_v2(n, k, msg_length) {
template plume_v2(n, k, message_length) {
signal input c[k];
signal input s[k];
signal input msg[msg_length];
signal input public_key[2][k];
signal input plume_message[message_length];
signal input pk[2][k];
signal input nullifier[2][k];

signal output g_pow_r[2][k];
signal output h_pow_r[2][k];
signal output r_point[2][k];
signal output hashed_to_curve_r[2][k];

// precomputed values for the hash_to_curve component
signal input q0_gx1_sqrt[4];
Expand All @@ -115,14 +115,14 @@ template plume_v2(n, k, msg_length) {
signal input q1_x_mapped[4];
signal input q1_y_mapped[4];

component check_ec_equations = check_ec_equations(n, k, msg_length);
component check_ec_equations = check_ec_equations(n, k, message_length);

check_ec_equations.c <== c;
check_ec_equations.s <== s;
check_ec_equations.public_key <== public_key;
check_ec_equations.pk <== pk;
check_ec_equations.nullifier <== nullifier;

check_ec_equations.msg <== msg;
check_ec_equations.plume_message <== plume_message;

check_ec_equations.q0_gx1_sqrt <== q0_gx1_sqrt;
check_ec_equations.q0_gx2_sqrt <== q0_gx2_sqrt;
Expand All @@ -136,20 +136,20 @@ template plume_v2(n, k, msg_length) {
check_ec_equations.q1_x_mapped <== q1_x_mapped;
check_ec_equations.q1_y_mapped <== q1_y_mapped;

h_pow_r <== check_ec_equations.h_pow_r;
g_pow_r <== check_ec_equations.g_pow_r;
hashed_to_curve_r <== check_ec_equations.hashed_to_curve_r;
r_point <== check_ec_equations.r_point;
}

template check_ec_equations(n, k, msg_length) {
template check_ec_equations(n, k, message_length) {
signal input c[k];
signal input s[k];
signal input msg[msg_length];
signal input public_key[2][k];
signal input plume_message[message_length];
signal input pk[2][k];
signal input nullifier[2][k];

signal output g_pow_r[2][k];
signal output h_pow_r[2][k];
signal output h[2][k];
signal output r_point[2][k];
signal output hashed_to_curve_r[2][k];
signal output hashed_to_curve[2][k];

// precomputed values for the hash_to_curve component
signal input q0_gx1_sqrt[4];
Expand All @@ -170,58 +170,58 @@ template check_ec_equations(n, k, msg_length) {

// Calculates g^s. Note, turning a private key to a public key is the same operation as
// raising the generator g to some power, and we are *not* dealing with private keys in this circuit.
component g_pow_s = ECDSAPrivToPub(n, k);
g_pow_s.privkey <== s;
component s_point = ECDSAPrivToPub(n, k);
s_point.privkey <== s;

component g_pow_r_comp = a_div_b_pow_c(n, k);
g_pow_r_comp.a <== g_pow_s.pubkey;
g_pow_r_comp.b <== public_key;
g_pow_r_comp.c <== c;
component r_point_comp = a_div_b_pow_c(n, k);
r_point_comp.a <== s_point.pubkey;
r_point_comp.b <== pk;
r_point_comp.c <== c;

// Calculate hash[m, pk]^r
// hash[m, pk]^r = hash[m, pk]^s / (hash[m, pk]^sk)^c
// Note this implicitly checks the second equation in the blog

// Calculate hash[m, pk]^r
component h_comp = HashToCurve(msg_length + 33);
for (var i = 0; i < msg_length; i++) {
h_comp.msg[i] <== msg[i];
component hash_to_curve = HashToCurve(message_length + 33);
for (var i = 0; i < message_length; i++) {
hash_to_curve.msg[i] <== plume_message[i];
}

component pk_compressor = compress_ec_point(n, k);

pk_compressor.uncompressed <== public_key;
pk_compressor.uncompressed <== pk;

for (var i = 0; i < 33; i++) {
h_comp.msg[msg_length + i] <== pk_compressor.compressed[i];
hash_to_curve.msg[message_length + i] <== pk_compressor.compressed[i];
}

// Input precalculated values into HashToCurve
h_comp.q0_gx1_sqrt <== q0_gx1_sqrt;
h_comp.q0_gx2_sqrt <== q0_gx2_sqrt;
h_comp.q0_y_pos <== q0_y_pos;
h_comp.q0_x_mapped <== q0_x_mapped;
h_comp.q0_y_mapped <== q0_y_mapped;
h_comp.q1_gx1_sqrt <== q1_gx1_sqrt;
h_comp.q1_gx2_sqrt <== q1_gx2_sqrt;
h_comp.q1_y_pos <== q1_y_pos;
h_comp.q1_x_mapped <== q1_x_mapped;
h_comp.q1_y_mapped <== q1_y_mapped;
hash_to_curve.q0_gx1_sqrt <== q0_gx1_sqrt;
hash_to_curve.q0_gx2_sqrt <== q0_gx2_sqrt;
hash_to_curve.q0_y_pos <== q0_y_pos;
hash_to_curve.q0_x_mapped <== q0_x_mapped;
hash_to_curve.q0_y_mapped <== q0_y_mapped;
hash_to_curve.q1_gx1_sqrt <== q1_gx1_sqrt;
hash_to_curve.q1_gx2_sqrt <== q1_gx2_sqrt;
hash_to_curve.q1_y_pos <== q1_y_pos;
hash_to_curve.q1_x_mapped <== q1_x_mapped;
hash_to_curve.q1_y_mapped <== q1_y_mapped;

component h_pow_s = Secp256k1ScalarMult(n, k);
h_pow_s.scalar <== s;
h_pow_s.point <== h_comp.out;
h_pow_s.point <== hash_to_curve.out;

component h_pow_r_comp = a_div_b_pow_c(n, k);
h_pow_r_comp.a <== h_pow_s.out;
h_pow_r_comp.b <== nullifier;
h_pow_r_comp.c <== c;
component hashed_to_curve_r_comp = a_div_b_pow_c(n, k);
hashed_to_curve_r_comp.a <== h_pow_s.out;
hashed_to_curve_r_comp.b <== nullifier;
hashed_to_curve_r_comp.c <== c;

h <== h_comp.out;
hashed_to_curve <== hash_to_curve.out;

h_pow_r <== h_pow_r_comp.out;
hashed_to_curve_r <== hashed_to_curve_r_comp.out;

g_pow_r <== g_pow_r_comp.out;
r_point <== r_point_comp.out;
}

template a_div_b_pow_c(n, k) {
Expand All @@ -232,7 +232,7 @@ template a_div_b_pow_c(n, k) {

// Calculates b^c. Note that the spec uses multiplicative notation to preserve intuitions about
// discrete log, and these comments follow the spec to make comparison simpler. But the circom-ecdsa library uses
// additive notation. This is why we appear to calculate an expnentiation using a multiplication component.
// additive notation. This is why we appear to calculate an exponentiation using a multiplication component.
component b_pow_c = Secp256k1ScalarMult(n, k);
b_pow_c.scalar <== c;
b_pow_c.point <== b;
Expand Down Expand Up @@ -337,20 +337,23 @@ template compress_ec_point(n, k) {
verify.compressed <== compressed;
}

// We have a separate internal compression verification template for testing purposes. An adversarial prover
// can set any compressed values, so it's useful to be able to test adversarial inputs.
// We have a separate internal compression verification template for testing
// purposes. An adversarial prover can set any compressed values, so it's
// useful to be able to test adversarial inputs.
template verify_ec_compression(n, k) {
signal input uncompressed[2][k];
signal input compressed[33];

// Get the bit string of the smallest register
// Make sure the least significant bit's evenness matches the evenness specified by the first byte in the compressed version
// Get the bit string of the smallest register \
// Make sure the least significant bit's evenness matches the evenness
// specified by the first byte in the compressed version
component num2bits = Num2Bits(n);
num2bits.in <== uncompressed[1][0]; // Note, circom-ecdsa uses little endian, so we check the 0th register of the y value
compressed[0] === num2bits.out[0] + 2;

// Make sure the compressed and uncompressed x coordinates represent the same number
// l_bytes is an algebraic expression for the bytes of each register
// Make sure the compressed and uncompressed x coordinates represent
// the same number \
// `l_bytes` is an algebraic expression for the bytes of each register
var l_bytes[k];
for (var i = 1; i < 33; i++) {
var j = i - 1; // ignores the first byte specifying the compressed y coordinate
Expand All @@ -360,9 +363,10 @@ template verify_ec_compression(n, k) {
uncompressed[0] === l_bytes;
}

// Equivalent to get_gx and get_gy in circom-ecdsa, except we also have values for n = 64, k = 4.
// This is necessary because hash_to_curve is only implemented for n = 64, k = 4 but circom-ecdsa
// only g's coordinates for n = 86, k = 3
// Equivalent to get_gx and get_gy in circom-ecdsa, except we also have values
// for n = 64, k = 4. \
// This is necessary because hash_to_curve is only implemented for n = 64,
// k = 4 but circom-ecdsa only g's coordinates for n = 86, k = 3 \
// TODO: merge this upstream into circom-ecdsa
function get_genx(n, k) {
assert((n == 86 && k == 3) || (n == 64 && k == 4));
Expand Down
3 changes: 3 additions & 0 deletions javascript/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
# node.js
/node_modules
/dist

# https://github.com/plume-sig/zk-nullifier-sig/pull/66/#issuecomment-1780924701
package-lock.json
1 change: 0 additions & 1 deletion javascript/README.md

This file was deleted.

Loading