fix+feat: fixes some aliasing bugs, adds an optional permissioning to broadcast (#23)

* fixes aliasing bugs, adds optional permissioning to broadcast

* fixes nullifiers hash, makes permissioning work with tests

* fix: clears cache

* fix: adds another account for ci testing

* fix: addresses comments
This commit is contained in:
Kobi Gurkan
2019-08-01 08:02:19 +03:00
committed by GitHub
parent fccedd776f
commit fc7c15fb5c
10 changed files with 250 additions and 48 deletions

View File

@@ -19,14 +19,14 @@ jobs:
- restore_cache:
name: restore-npm-cache
keys:
- v1.8-dependencies-{{ checksum "package-lock.json" }}
- v1.9-dependencies-{{ checksum "package-lock.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: v1.8-dependencies-{{ checksum "package-lock.json" }}
key: v1.9-dependencies-{{ checksum "package-lock.json" }}
# checksum the snarks definitions
- run:
@@ -37,7 +37,7 @@ jobs:
- restore_cache:
name: restore-snark-cache
keys:
- v1.8-dependencies-{{ checksum "build/.snark_checksum" }}
- v1.9-dependencies-{{ checksum "build/.snark_checksum" }}
# build snarks
- run:
@@ -47,7 +47,7 @@ jobs:
# cache generated snark circuit and keys
- save_cache:
key: v1.8-dependencies-{{ checksum "build/.snark_checksum" }}
key: v1.9-dependencies-{{ checksum "build/.snark_checksum" }}
paths:
- build/circuit.json
- build/proving_key.bin

View File

@@ -27,6 +27,7 @@ import "./Ownable.sol";
contract Semaphore is Verifier, MultipleMerkleTree, Ownable {
uint256 public external_nullifier;
uint8 public id_tree_index;
bool public is_broadcast_permissioned = true;
mapping (uint256 => bool) root_history;
uint8 current_root_index = 0;
@@ -38,17 +39,17 @@ contract Semaphore is Verifier, MultipleMerkleTree, Ownable {
event SignalBroadcast(bytes signal, uint256 nullifiers_hash, uint256 external_nullifier);
modifier onlyOwnerIfPermissioned() {
require(!is_broadcast_permissioned || isOwner(), "Semaphore: broadcast permission denied");
_;
}
constructor(uint8 tree_levels, uint256 zero_value, uint256 external_nullifier_in) Ownable() public
{
external_nullifier = external_nullifier_in;
id_tree_index = init_tree(tree_levels, zero_value);
}
event Funded(uint256 amount);
function fund() public payable {
emit Funded(msg.value);
}
function insertIdentity(uint256 leaf) public onlyOwner {
insert(id_tree_index, leaf);
@@ -89,9 +90,6 @@ contract Semaphore is Verifier, MultipleMerkleTree, Ownable {
uint[4] memory input,
uint256 signal_hash
) public {
// Note that we only verify the broadcaster's address (input[4]) in the
// snark via verifyProof().
require(hasNullifier(input[1]) == false, "Semaphore: nullifier already seen");
require(signal_hash == input[2], "Semaphore: signal hash mismatch");
require(external_nullifier == input[3], "Semaphore: external nullifier mismatch");
@@ -104,10 +102,10 @@ contract Semaphore is Verifier, MultipleMerkleTree, Ownable {
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[4] memory input // [root, nullifiers_hash, signal_hash, external_nullifier]
) public {
uint[4] memory input // (root, nullifiers_hash, signal_hash, external_nullifier)
) public onlyOwnerIfPermissioned {
// Hash the signal
uint256 signal_hash = uint256(sha256(signal)) >> 8;
uint256 signal_hash = uint256(keccak256(signal)) >> 8;
// Check the inputs
preBroadcastRequire(a, b, c, input, signal_hash);
@@ -136,4 +134,8 @@ contract Semaphore is Verifier, MultipleMerkleTree, Ownable {
function setExternalNullifier(uint256 new_external_nullifier) public onlyOwner {
external_nullifier = new_external_nullifier;
}
function setPermissioning(bool _newPermission) public onlyOwner {
is_broadcast_permissioned = _newPermission;
}
}

View File

@@ -3505,7 +3505,7 @@
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
},
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#8431eab7b3384e65e8126a4602520b78031666fb",
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#572d4bafe08a8a231137e1f9daeb0f8a23f197d2",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.11.8",

View File

@@ -24,6 +24,7 @@
"circomlib": "0.0.10",
"cors": "^2.8.5",
"del": "^4.1.0",
"ethers": "^4.0.33",
"file-saver": "^2.0.1",
"ganache-cli": "^6.4.1",
"ganache-core": "^2.5.3",

View File

@@ -22,7 +22,7 @@
echo "Working directory: `pwd`"
npx mocha --recursive ../test/circuits
../node_modules/.bin/ganache-cli -p 7545 -l 8800000 -i 5777 --account='0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21,100000000000000000000000000' --account='0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21,10000000000000000000000000' -q &
../node_modules/.bin/ganache-cli -p 7545 -l 8800000 -i 5777 --account='0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21,100000000000000000000000000' --account='0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd22,10000000000000000000000000' -q &
../node_modules/.bin/truffle migrate --reset --network=local
../node_modules/.bin/truffle test ../test/contracts/* --network=local

View File

@@ -25,7 +25,7 @@ export CHAIN_ID=5777
export NODE_URL=http://localhost:7545
export SERVER_NODE_URL=http://localhost:7545
export DEPLOY_TO=local
export SLEEP_TIME=7
export SLEEP_TIME=10
if [ "$1" == "goerli" ]; then
CHAIN_ID=5
@@ -66,6 +66,6 @@ IDENTITY_COMMITMENT=`cat ./semaphore_identity.json | jq '.identity_commitment' |
echo ${IDENTITY_COMMITMENT}
tmux \
new-session "source ~/.bashrc; export CONFIG_ENV=true LOG_LEVEL=${LOG_LEVEL} CHAIN_ID=${CHAIN_ID} CONTRACT_ADDRESS=$ADDRESS NODE_URL=${SERVER_NODE_URL} SEMAPHORE_PORT=3000 FROM_ADDRESS=0x1929c15f4e818abf2549510622a50c440c474223 FROM_PRIVATE_KEY=0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21 TRANSACTION_CONFIRMATION_BLOCKS=1 CREATION_HASH=${CREATION_HASH}; npx semaphorejs-server fund; npx semaphorejs-server; bash -i" \; \
new-session "source ~/.bashrc; export CONFIG_ENV=true LOG_LEVEL=${LOG_LEVEL} CHAIN_ID=${CHAIN_ID} CONTRACT_ADDRESS=$ADDRESS NODE_URL=${SERVER_NODE_URL} SEMAPHORE_PORT=3000 FROM_ADDRESS=0x1929c15f4e818abf2549510622a50c440c474223 FROM_PRIVATE_KEY=0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21 TRANSACTION_CONFIRMATION_BLOCKS=1 CREATION_HASH=${CREATION_HASH}; npx semaphorejs-server disable_permissioning; npx semaphorejs-server; bash -i" \; \
split-window -h -t 0 "source ~/.bashrc; sleep ${SLEEP_TIME}; LOG_LEVEL=${LOG_LEVEL} curl http://localhost:3000/add_identity -X POST -d'{\"leaf\": \"${IDENTITY_COMMITMENT}\"}' -H 'Content-Type: application/json'; bash -i" \; \
split-window -t 0 "source ~/.bashrc; sleep ${SLEEP_TIME}; ./wait_for_element.sh ${IDENTITY_COMMITMENT}; LOG_LEVEL=${LOG_LEVEL} TRANSACTION_CONFIRMATION_BLOCKS=1 CHAIN_ID=${CHAIN_ID} CONTRACT_ADDRESS=$ADDRESS FROM_ADDRESS=0x1929c15f4e818abf2549510622a50c440c474223 FROM_PRIVATE_KEY=0x6738837df169e8d6ffc6e33a2947e58096d644fa4aa6d74358c8d9d57c12cd21 NODE_URL=${NODE_URL} EXTERNAL_NULLIFIER=12312 SEMAPHORE_SERVER_URL=http://localhost:3000 BROADCASTER_ADDRESS=0x1929c15f4e818abf2549510622a50c440c474223 CONFIG_ENV=true BASE_DIR=`pwd`/.. npx semaphorejs-client signal `shuf -i 1-100000000 -n 1`; bash -i"

View File

@@ -76,17 +76,26 @@ template Semaphore(jubjub_field_size, n_levels, n_rounds) {
// END signals
component identity_nullifier_bits = Num2Bits(256);
component identity_nullifier_bits = Num2Bits(248);
identity_nullifier_bits.in <== identity_nullifier;
component identity_pk_0_bits = Num2Bits(256);
component identity_pk_0_bits = Num2Bits_strict();
identity_pk_0_bits.in <== dbl3.xout;
component identity_commitment = Pedersen(2*256);
// BEGIN identity commitment
for (var i = 0; i < 256; i++) {
identity_commitment.in[i] <== identity_pk_0_bits.out[i];
identity_commitment.in[i + 256] <== identity_nullifier_bits.out[i];
if (i < 254) {
identity_commitment.in[i] <== identity_pk_0_bits.out[i];
} else {
identity_commitment.in[i] <== 0;
}
if (i < 248) {
identity_commitment.in[i + 256] <== identity_nullifier_bits.out[i];
} else {
identity_commitment.in[i + 256] <== 0;
}
}
// END identity commitment
@@ -115,21 +124,24 @@ template Semaphore(jubjub_field_size, n_levels, n_rounds) {
// END tree
// BEGIN nullifiers
component external_nullifier_bits = Num2Bits(256);
component external_nullifier_bits = Num2Bits(232);
external_nullifier_bits.in <== external_nullifier;
component nullifiers_hasher = Blake2s(512, 0);
for (var i = 0; i < 256; i++) {
for (var i = 0; i < 248; i++) {
nullifiers_hasher.in_bits[i] <== identity_nullifier_bits.out[i];
if (i < 224) {
nullifiers_hasher.in_bits[256 + i] <== external_nullifier_bits.out[i];
} else {
if ( (i-224) < n_levels ) {
nullifiers_hasher.in_bits[256 + i] <== identity_path_index[i - 224];
} else {
nullifiers_hasher.in_bits[256 + i] <== 0;
}
}
}
for (var i = 0; i < 232; i++) {
nullifiers_hasher.in_bits[248 + i] <== external_nullifier_bits.out[i];
}
for (var i = 0; i < n_levels; i++) {
nullifiers_hasher.in_bits[248 + 232 + i] <== identity_path_index[i];
}
for (var i = (248 + 232 + n_levels); i < 512; i++) {
nullifiers_hasher.in_bits[i] <== 0;
}
component nullifiers_hash_num = Bits2Num(253);

View File

@@ -21,7 +21,6 @@
const crypto = require('crypto');
const path = require('path');
const {unstringifyBigInts, stringifyBigInts} = require('websnark/tools/stringifybigint.js');
const blake2 = require('blakejs');
const chai = require('chai');
const assert = chai.assert;
@@ -40,6 +39,8 @@ const fetch = require('node-fetch');
const Web3 = require('web3');
const ethers = require('ethers');
let logger;
/* uint8array to hex */
@@ -112,7 +113,6 @@ class SemaphoreClient {
this.chain_id = chain_id;
this.server_broadcast = server_broadcast;
this.broadcaster_address = server_broadcast_address;
}
async broadcast_signal(signal_str) {
@@ -132,11 +132,16 @@ class SemaphoreClient {
external_nullifier = bigInt(this.external_nullifier);
}
const signal_to_contract = this.web3.utils.asciiToHex(signal_str);
const signal_hash_raw = crypto.createHash('sha256').update(signal_to_contract.slice(2), 'hex').digest();
const signal_hash = beBuff2int(signal_hash_raw.slice(0, 31));
const broadcaster_address = bigInt(this.broadcaster_address);
const signal_to_contract_bytes = new Buffer(signal_to_contract.slice(2), 'hex');
const msg = mimcsponge.multiHash([external_nullifier, signal_hash, broadcaster_address]);
const signal_hash_raw = ethers.utils.solidityKeccak256(
['bytes'],
[signal_to_contract_bytes],
);
const signal_hash_raw_bytes = new Buffer(signal_hash_raw.slice(2), 'hex');
const signal_hash = beBuff2int(signal_hash_raw_bytes.slice(0, 31));
const msg = mimcsponge.multiHash([external_nullifier, signal_hash]);
const signature = eddsa.signMiMCSponge(prvKey, msg);
assert(eddsa.verifyMiMCSponge(msg, signature, pubKey));
@@ -174,7 +179,6 @@ class SemaphoreClient {
identity_nullifier,
identity_path_elements,
identity_path_index,
broadcaster_address,
};
const w = this.circuit.calculateWitness(inputs);
const witness_bin = proof_util.convertWitness(snarkjs.stringifyBigInts(w));
@@ -196,8 +200,8 @@ class SemaphoreClient {
logger.debug(`publicSignals: ${publicSignals}`);
// publicSignals = (root, nullifiers_hash, signal_hash, external_nullifier, broadcaster_address)
const public_signals_to_broadcast = [ publicSignals[0].toString(), publicSignals[1].toString(), publicSignals[2].toString(), publicSignals[3].toString(), publicSignals[4].toString() ];
// publicSignals = (root, nullifiers_hash, signal_hash, external_nullifier)
const public_signals_to_broadcast = [ publicSignals[0].toString(), publicSignals[1].toString(), publicSignals[2].toString(), publicSignals[3].toString() ];
const proof_to_broadcast = [
[ proof.pi_a[0].toString(), proof.pi_a[1].toString() ],
[ [ proof.pi_b[0][1].toString(), proof.pi_b[0][0].toString() ], [ proof.pi_b[1][1].toString(), proof.pi_b[1][0].toString() ] ],

View File

@@ -350,6 +350,11 @@ async function fund() {
);
}
async function disablePermissioning() {
const encoded = await semaphore.contract.methods.disablePermissioning().encodeABI();
await send_transaction(encoded);
}
if (process.argv[2] == 'fund') {
fund()
.then(() => {
@@ -359,6 +364,15 @@ if (process.argv[2] == 'fund') {
logger.error(`error funding: ${err.stack}`);
process.exit(1);
});
} else if (process.argv[2] == 'disable_permissioning') {
disablePermissioning()
.then(() => {
process.exit(0);
})
.catch((err) => {
logger.error(`error funding: ${err.stack}`);
process.exit(1);
});
} else {
const express = require('express');
var cors = require('cors');

View File

@@ -29,6 +29,8 @@ const path = require('path');
const snarkjs = require('snarkjs');
const circomlib = require('circomlib');
const ethers = require('ethers');
const test_util = require('../../src/test_util');
const bigInt = snarkjs.bigInt;
@@ -77,6 +79,7 @@ const cutDownBits = function(b, bits) {
contract('Semaphore', function (accounts) {
let semaphore;
let identity_commitment1;
before(async () => {
semaphore = await Semaphore.deployed();
@@ -96,9 +99,16 @@ contract('Semaphore', function (accounts) {
const external_nullifier = bigInt('12312');
const signal_str = 'hello!';
const signal_hash_raw = crypto.createHash('sha256').update(signal_str, 'utf8').digest();
const signal_hash = beBuff2int(signal_hash_raw.slice(0, 31));
const signal_to_contract = web3.utils.asciiToHex(signal_str);
const signal_to_contract_bytes = new Buffer(signal_to_contract.slice(2), 'hex');
const signal_hash_raw = ethers.utils.solidityKeccak256(
['bytes'],
[signal_to_contract_bytes],
);
const signal_hash_raw_bytes = new Buffer(signal_hash_raw.slice(2), 'hex');
const signal_hash = beBuff2int(signal_hash_raw_bytes.slice(0, 31));
const accounts = await web3.eth.getAccounts();
const msg = mimcsponge.multiHash([bigInt(external_nullifier), bigInt(signal_hash)]);
@@ -106,7 +116,7 @@ contract('Semaphore', function (accounts) {
assert(eddsa.verifyMiMCSponge(msg, signature, pubKey));
const identity_nullifier = bigInt('230');
const identity_nullifier = bigInt('231');
const storage_path = '/tmp/rocksdb_semaphore_test';
if (fs.existsSync(storage_path)) {
@@ -134,18 +144,18 @@ contract('Semaphore', function (accounts) {
);
const identity_commitment = pedersenHash([bigInt(circomlib.babyJub.mulPointEscalar(pubKey, 8)[0]), bigInt(identity_nullifier)]);
identity_commitment1 = identity_commitment;
const semaphore = await Semaphore.deployed();
const receipt = await semaphore.insertIdentity(identity_commitment.toString());
assert.equal(receipt.logs[0].event, 'LeafAdded');
const next_index = parseInt(receipt.logs[0].args.leaf_index.toString());
await semaphore.fund({value: web3.utils.toWei('10')});
await tree.update(next_index, identity_commitment.toString());
await memTree.update(next_index, identity_commitment.toString());
const identity_path = await tree.path(next_index);
const mem_identity_path = await memTree.path(next_index);
assert.equal(JSON.stringify(identity_path), JSON.stringify(mem_identity_path))
const identity_path_elements = identity_path.path_elements;
@@ -244,4 +254,163 @@ contract('Semaphore', function (accounts) {
console.log(evs);
*/
});
it('tests permissioning', async () => {
const cirDef = JSON.parse(fs.readFileSync(path.join(__dirname,'../../build/circuit.json')).toString());
circuit = new snarkjs.Circuit(cirDef);
const prvKey = Buffer.from('0001020304050607080900010203040506070809000102030405060708080001', 'hex');
const pubKey = eddsa.prv2pub(prvKey);
const external_nullifier = bigInt('12312');
const signal_str = 'hello!';
const signal_to_contract = web3.utils.asciiToHex(signal_str);
const signal_to_contract_bytes = new Buffer(signal_to_contract.slice(2), 'hex');
const signal_hash_raw = ethers.utils.solidityKeccak256(
['bytes'],
[signal_to_contract_bytes],
);
const signal_hash_raw_bytes = new Buffer(signal_hash_raw.slice(2), 'hex');
const signal_hash = beBuff2int(signal_hash_raw_bytes.slice(0, 31));
const accounts = await web3.eth.getAccounts();
const msg = mimcsponge.multiHash([bigInt(external_nullifier), bigInt(signal_hash)]);
const signature = eddsa.signMiMCSponge(prvKey, msg);
assert(eddsa.verifyMiMCSponge(msg, signature, pubKey));
const identity_nullifier = bigInt('230');
const storage_path = '/tmp/rocksdb_semaphore_test';
if (fs.existsSync(storage_path)) {
del.sync(storage_path, { force: true });
}
const default_value = '0';
const storage = new RocksDb(storage_path);
const memStorage = new MemStorage();
const hasher = new MimcSpongeHasher();
const prefix = 'semaphore';
const tree = new MerkleTree(
prefix,
storage,
hasher,
20,
default_value,
);
const memTree = new MerkleTree(
prefix,
memStorage,
hasher,
20,
default_value,
);
const identity_commitment = pedersenHash([bigInt(circomlib.babyJub.mulPointEscalar(pubKey, 8)[0]), bigInt(identity_nullifier)]);
const semaphore = await Semaphore.deployed();
const receipt = await semaphore.insertIdentity(identity_commitment.toString());
assert.equal(receipt.logs[0].event, 'LeafAdded');
const next_index = parseInt(receipt.logs[0].args.leaf_index.toString());
await tree.update(0, identity_commitment1.toString());
await memTree.update(0, identity_commitment1.toString());
await tree.update(next_index, identity_commitment.toString());
await memTree.update(next_index, identity_commitment.toString());
const identity_path = await tree.path(next_index);
const mem_identity_path = await memTree.path(next_index);
assert.equal(JSON.stringify(identity_path), JSON.stringify(mem_identity_path))
const identity_path_elements = identity_path.path_elements;
const identity_path_index = identity_path.path_index;
//console.log(identity_commitment.toString());
//console.log(identity_path_elements, identity_path_index, identity_path.root);
const w = circuit.calculateWitness({
'identity_pk[0]': pubKey[0],
'identity_pk[1]': pubKey[1],
'auth_sig_r[0]': signature.R8[0],
'auth_sig_r[1]': signature.R8[1],
auth_sig_s: signature.S,
signal_hash,
external_nullifier,
identity_nullifier,
identity_path_elements,
identity_path_index,
});
const root = w[circuit.getSignalIdx('main.root')];
const nullifiers_hash = w[circuit.getSignalIdx('main.nullifiers_hash')];
assert(circuit.checkWitness(w));
assert.equal(w[circuit.getSignalIdx('main.root')].toString(), identity_path.root);
//console.log(w[circuit.getSignalIdx('main.root')]);
/*
console.log(tree[0]);
console.log(w[circuit.getSignalIdx('main.signal_hash')]);
console.log(w[circuit.getSignalIdx('main.external_nullifier')]);
console.log(w[circuit.getSignalIdx('main.root')]);
console.log(w[circuit.getSignalIdx('main.nullifiers_hash')]);
console.log(w[circuit.getSignalIdx('main.identity_commitment.out')]);
*/
const vk_proof = fs.readFileSync(path.join(__dirname,'../../build/proving_key.bin'));
const witness_bin = proof_util.convertWitness(snarkjs.stringifyBigInts(w));
const publicSignals = w.slice(1, circuit.nPubInputs + circuit.nOutputs+1);
const proof = await proof_util.prove(witness_bin.buffer, vk_proof.buffer);
let failed = false;
let reason = '';
await semaphore.transferOwnership(accounts[1]);
assert.equal(await semaphore.owner(), accounts[1]);
failed = false;
try {
await semaphore.broadcastSignal(
signal_to_contract,
[ proof.pi_a[0].toString(), proof.pi_a[1].toString() ],
[ [ proof.pi_b[0][1].toString(), proof.pi_b[0][0].toString() ], [ proof.pi_b[1][1].toString(), proof.pi_b[1][0].toString() ] ],
[ proof.pi_c[0].toString(), proof.pi_c[1].toString() ],
[ publicSignals[0].toString(), publicSignals[1].toString(), publicSignals[2].toString(), publicSignals[3].toString() ],
);
} catch(e) {
failed = true;
reason = e.reason
}
assert.equal(failed, true);
assert.equal(reason, 'Semaphore: broadcast permission denied');
await semaphore.setPermissioning(false, { from: accounts[1] });
const a = [ proof.pi_a[0].toString(), proof.pi_a[1].toString() ]
const b = [ [ proof.pi_b[0][1].toString(), proof.pi_b[0][0].toString() ], [ proof.pi_b[1][1].toString(), proof.pi_b[1][0].toString() ] ]
const c = [ proof.pi_c[0].toString(), proof.pi_c[1].toString() ]
const input = [ publicSignals[0].toString(), publicSignals[1].toString(), publicSignals[2].toString(), publicSignals[3].toString() ]
const check = await semaphore.preBroadcastCheck(a, b, c, input, bigInt(signal_hash).toString())
assert.isTrue(check)
const broadcastTx = await semaphore.broadcastSignal(
signal_to_contract,
a, b, c, input
);
assert.isTrue(broadcastTx.receipt.status)
/*
const evs = await semaphore.getPastEvents('allEvents', {
fromBlock: 0,
toBlock: 'latest'
});
console.log(evs);
*/
});
});