Skip to content

Commit

Permalink
Merge pull request #426 from EYBlockchain/westlad/ping-pong-test
Browse files Browse the repository at this point in the history
Wait for twelve L1 confirmations before updating L2 data.
  • Loading branch information
Westlad authored Feb 4, 2022
2 parents 0192862 + c933b08 commit 34aec0e
Show file tree
Hide file tree
Showing 47 changed files with 801 additions and 5,075 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-docker-app-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- master
- westlad/ping-pong
- westlad/ping-pong-test

jobs:
release:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-docker-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- master
- westlad/ping-pong
- westlad/ping-pong-test

jobs:
release:
Expand Down
36 changes: 31 additions & 5 deletions cli/lib/nf3.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class Nf3 {

currentEnvironment;

notConfirmed = 0;

constructor(web3WsUrl, ethereumSigningKey, environment = ENVIRONMENTS.localhost, zkpKeys) {
this.clientBaseUrl = environment.clientApiUrl;
this.optimistBaseUrl = environment.optimistApiUrl;
Expand Down Expand Up @@ -183,8 +185,27 @@ class Nf3 {

if (this.ethereumSigningKey) {
const signed = await this.web3.eth.accounts.signTransaction(tx, this.ethereumSigningKey);
return this.web3.eth.sendSignedTransaction(signed.rawTransaction);
// rather than waiting until we have a receipt, wait until we have enough confirmation blocks
// then return the receipt.
// TODO does this still work if there is a chain reorg or do we have to handle that?
return new Promise(resolve => {
console.log(`Confirming transaction ${signed.transactionHash}`);
this.notConfirmed++;
this.web3.eth
.sendSignedTransaction(signed.rawTransaction)
.on('confirmation', (number, receipt) => {
if (number === 12) {
this.notConfirmed--;
console.log(
`Transaction ${receipt.transactionHash} has been confirmed ${number} times.`,
`Number of unconfirmed transactions is ${this.notConfirmed}`,
);
resolve(receipt);
}
});
});
}
// TODO add wait for confirmations to the wallet functionality
return this.web3.eth.sendTransaction(tx);
}

Expand Down Expand Up @@ -317,6 +338,9 @@ class Nf3 {
ask: this.zkpKeys.ask,
fee,
});
if (res.data.error && res.data.error === 'No suitable commitments') {
throw new Error('No suitable commitments');
}
if (!offchain) {
return this.submitTransaction(res.data.txDataToSign, this.shieldContractAddress, fee);
}
Expand Down Expand Up @@ -708,7 +732,7 @@ class Nf3 {
@method
@async
@param {Array} ercList - list of erc contract addresses to filter.
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
ones compressed pkd
@returns {Promise} This promise resolves into an object whose properties are the
addresses of the ERC contracts of the tokens held by this account in Layer 2. The
Expand Down Expand Up @@ -748,7 +772,7 @@ class Nf3 {
@method
@async
@param {Array} ercList - list of erc contract addresses to filter.
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
ones compressed pkd
@returns {Promise} This promise resolves into an object whose properties are the
addresses of the ERC contracts of the tokens held by this account in Layer 2. The
Expand All @@ -769,7 +793,7 @@ class Nf3 {
@method
@async
@param {Array} ercList - list of erc contract addresses to filter.
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
@param {Boolean} filterByCompressedPkd - flag to indicate if request is filtered
ones compressed pkd
@returns {Promise} This promise resolves into an object whose properties are the
addresses of the ERC contracts of the tokens held by this account in Layer 2. The
Expand Down Expand Up @@ -816,7 +840,9 @@ class Nf3 {
Set a Web3 Provider URL
*/
async setWeb3Provider() {
this.web3 = new Web3(this.web3WsUrl, { transactionBlockTimeout: 200 }); // set a longer timeout
this.web3 = new Web3(this.web3WsUrl);
this.web3.eth.transactionBlockTimeout = 200;
this.web3.eth.transactionConfirmationBlocks = 12;
if (typeof window !== 'undefined') {
if (window.ethereum && this.ethereumSigningKey === '') {
this.web3 = new Web3(window.ethereum);
Expand Down
1 change: 1 addition & 0 deletions common-files/classes/public-inputs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PublicInputs {
hash;

constructor(publicInputs) {
// some inputs may be general numbers and some strings. We convert all to string, process and generalise.
this.publicInputs = generalise(publicInputs.flat(Infinity));
[, this.hash] = generalise(sha256(this.publicInputs).limbs(248, 2));
}
Expand Down
5 changes: 0 additions & 5 deletions common-files/package-lock.json

This file was deleted.

2 changes: 1 addition & 1 deletion common-files/utils/contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import config from 'config';
import Web3 from './web3.mjs';
import logger from './logger.mjs';

const web3 = Web3.connection();
export const web3 = Web3.connection();

const options = config.WEB3_OPTIONS;

Expand Down
105 changes: 65 additions & 40 deletions common-files/utils/event-queue.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,15 @@ and catch these removals, processing them appropriately.
import Queue from 'queue';
import config from 'config';
import logger from 'common-files/utils/logger.mjs';
import { web3 } from 'common-files/utils/contract.mjs';

const { MAX_QUEUE } = config;
const { MAX_QUEUE, CONFIRMATION_POLL_TIME, CONFIRMATIONS } = config;
const fastQueue = new Queue({ autostart: true, concurrency: 1 });
const slowQueue = new Queue({ autostart: true, concurrency: 1 });
const removed = {}; // singleton holding transaction hashes of any removed events
const stopQueue = new Queue({ autostart: false, concurrency: 1 });
export const queues = [fastQueue, slowQueue, stopQueue];

/**
This function will return a promise that resolves to true when the next highest
priority queue is empty (priority goes in reverse order, prioity 0 is highest
priority)
*/
function nextHigherPriorityQueueHasEmptied(priority) {
return new Promise(resolve => {
const listener = () => resolve();
if (priority === 0) resolve(); // resolve if we're the highest priority queue
queues[priority - 1].once('end', listener); // or when the higher priority queue empties
if (queues[priority - 1].length === 0) {
queues[priority - 1].removeListener('end', listener);
resolve(); // or if it's already empty
}
});
}

/**
This function will wait until all the functions currently in a queue have been
processed. It's useful if you want to ensure that Nightfall has had an opportunity
Expand All @@ -63,17 +48,64 @@ function flushQueue(priority) {

async function enqueueEvent(callback, priority, args) {
queues[priority].push(async () => {
// await nextHigherPriorityQueueHasEmptied(priority);
// prevent conditionalmakeblock from running until fastQueue is emptied
return callback(args);
});
}

/**
This function will return when the event has been on chain for <confirmations> blocks
It's useful to call this if you want to be sure that your event has been confirmed
multiple times before you go ahead and process it.
*/

function waitForConfirmation(eventObject) {
logger.debug(`Confirming event ${eventObject.event}`);
const { transactionHash, blockNumber } = eventObject;
return new Promise((resolve, reject) => {
let confirmedBlocks = 0;
const id = setInterval(async () => {
// get the transaction that caused the event
// if it's been in a chain reorg then it will have been removed.
if (removed[transactionHash] > 0) {
clearInterval(id);
removed[eventObject.transactionHash]--;
reject(
new Error(
`Event removed; probable chain reorg. Event was ${eventObject.event}, transaction hash was ${transactionHash}`,
),
);
}
const currentBlock = await web3.eth.getBlock('latest');
if (currentBlock.number - blockNumber > confirmedBlocks) {
confirmedBlocks = currentBlock.number - blockNumber;
}
if (confirmedBlocks >= CONFIRMATIONS) {
clearInterval(id);
logger.debug(
`Event ${eventObject.event} has been confirmed ${
currentBlock.number - blockNumber
} times`,
);
resolve(eventObject);
}
}, CONFIRMATION_POLL_TIME);
});
}

async function dequeueEvent(priority) {
queues[priority].shift();
}

async function queueManager(eventObject, eventArgs) {
if (eventObject.removed) {
// in this model we don't queue removals but we can use them to reject the event
// Note the event object and its removal have the same transactionHash.
// Also note that we can get more than one removal because the event could be re-mined
// and removed again - so we need to keep count of the removals.
if (!removed[eventObject.transactionHash]) removed[eventObject.transactionHash] = 0;
removed[eventObject.transactionHash]++; // store the removal; waitForConfirmation will read this and reject.
return;
}
// First element of eventArgs must be the eventHandlers object
const [eventHandlers, ...args] = eventArgs;
// handlers contains the functions needed to handle particular types of event,
Expand All @@ -84,30 +116,23 @@ async function queueManager(eventObject, eventArgs) {
}
// pull up the priority for the event being handled (removers have identical priority)
const priority = eventHandlers.priority[eventObject.event];
// if the event was removed then we have a chain reorg and need to reset our
// layer 2 state accordingly.
if (eventObject.removed) {
if (!eventHandlers.removers[eventObject.event]) {
logger.debug(`Unknown event removal ${eventObject.event} ignored`);
return;
}
logger.info(`Queueing event removal ${eventObject.event}`);
queues[priority].push(async () => {
await nextHigherPriorityQueueHasEmptied(priority); // prevent eventHandlers running until the higher priority queue has emptied
return eventHandlers.removers[eventObject.event](eventObject, args);
});
// otherwise queue the event for processing.
} else {
logger.info(`Queueing event ${eventObject.event}`);
queues[priority].push(async () => {
// await nextHigherPriorityQueueHasEmptied(priority); // prevent eventHandlers running until the higher priority queue has emptied
logger.info(
`Queueing event ${eventObject.event}, with transaction hash ${eventObject.transactionHash} and priority ${priority}`,
);
queues[priority].push(async () => {
// we won't even think about processing an event until it's been confirmed many times
try {
await waitForConfirmation(eventObject);
return eventHandlers[eventObject.event](eventObject, args);
});
}
} catch (err) {
return logger.warn(err.message);
}
});
// }
// the queue shouldn't get too long if we're keeping up with the blockchain.
if (queues[priority].length > MAX_QUEUE)
logger.warn(`The event queue has more than ${MAX_QUEUE} events`);
}

/* ignore unused exports */
export { flushQueue, enqueueEvent, dequeueEvent, queueManager };
export { flushQueue, enqueueEvent, queueManager, dequeueEvent, waitForConfirmation };
6 changes: 6 additions & 0 deletions compress-keys.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// utility to compress a pkd
import { generalise } from 'general-number';
import { compressPublicKey } from './nightfall-client/src/services/keys.mjs';

const inputs = generalise(process.argv.slice(2, 4));
console.log(`Compressed value = ${compressPublicKey(inputs).hex()}`);
3 changes: 2 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ module.exports = {
LOG_LEVEL: process.env.LOG_LEVEL || 'debug',
MONGO_URL: process.env.MONGO_URL || 'mongodb://localhost:27017/',
ZKP_KEY_LENGTH: 32, // use a 32 byte key length for SHA compatibility
CONFIRMATION_POLL_TIME: 1000, // time to wait before querying the blockchain (ms). Must be << block interval
CONFIRMATIONS: 12, // number of confirmations to wait before accepting a transaction
PROTOCOL: 'http://', // connect to zokrates microservice like this
WEBSOCKET_PORT: process.env.WEBSOCKET_PORT || 8080,
ZOKRATES_WORKER_HOST: process.env.ZOKRATES_WORKER_HOST || 'worker',
Expand All @@ -31,7 +33,6 @@ module.exports = {
USE_INFURA: process.env.USE_INFURA === 'true',
ETH_PRIVATE_KEY: process.env.ETH_PRIVATE_KEY, // owner's/deployer's private key
ETH_ADDRESS: process.env.ETH_ADDRESS,
USE_ROPSTEN_NODE: process.env.USE_ROPSTEN_NODE === 'true',
OPTIMIST_HOST: process.env.OPTIMIST_HOST || 'optimist',
OPTIMIST_PORT: process.env.OPTIMIST_PORT || 80,
clientBaseUrl: `http://${process.env.CLIENT_HOST}:${process.env.CLIENT_PORT}`,
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.ganache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
ports:
- 8546:8546
command:
ganache-cli --accounts=10 --defaultBalanceEther=1000 --gasLimit=0x3B9ACA00 --deterministic -i 4378921 -p 8546
ganache-cli --accounts=10 --defaultBalanceEther=1000 --gasLimit=0x3B9ACA00 --deterministic -i 4378921 -p 8546 -b 1
--account="0x4775af73d6dc84a0ae76f8726bda4b9ecf187c377229cb39e1afa7a18236a69e,10000000000000000000000"
--account="0x4775af73d6dc84a0ae76f8726bda4b9ecf187c377229cb39e1afa7a18236a69d,10000000000000000000000"
--account="0xd42905d0582c476c4b74757be6576ec323d715a0c7dcff231b6348b7ab0190eb,10000000000000000000000"
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ services:
OPTIMIST_HOST: optimist1
OPTIMIST_PORT: 80
USE_STUBS: 'false' # make sure this flag is the same as in deployer service
RETRIES: 80
command: ['npm', 'run', 'dev']

client2:
Expand Down
31 changes: 18 additions & 13 deletions nightfall-client/src/event-handlers/block-proposed.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
storeCommitment,
countCommitments,
setSiblingInfo,
countTransactionHashes,
} from '../services/commitment-storage.mjs';
import getProposeBlockCalldata from '../services/process-calldata.mjs';
import Secrets from '../classes/secrets.mjs';
Expand All @@ -20,17 +19,20 @@ const { ZERO } = config;
This handler runs whenever a BlockProposed event is emitted by the blockchain
*/
async function blockProposedEventHandler(data) {
logger.info(`Received Block Proposed event`);
// ivk will be used to decrypt secrets whilst nsk will be used to calculate nullifiers for commitments and store them
const { blockNumber: currentBlockCount, transactionHash: transactionHashL1 } = data;
const { transactions, block } = await getProposeBlockCalldata(data);
logger.info(
`Received Block Proposed event with layer 2 block number ${block.blockNumberL2} and tx hash ${transactionHashL1}`,
);
const latestTree = await getLatestTree();
const blockCommitments = transactions.map(t => t.commitments.filter(c => c !== ZERO)).flat();

if ((await countTransactionHashes(block.transactionHashes)) > 0) {
await saveBlock({ blockNumber: currentBlockCount, transactionHashL1, ...block });
await Promise.all(transactions.map(t => saveTransaction({ transactionHashL1, ...t })));
}
// if ((await countCommitments(blockCommitments)) > 0) {
await saveBlock({ blockNumber: currentBlockCount, transactionHashL1, ...block });
logger.debug(`Saved L2 block ${block.blockNumberL2}, with tx hash ${transactionHashL1}`);
await Promise.all(transactions.map(t => saveTransaction({ transactionHashL1, ...t })));
// }

const dbUpdates = transactions.map(async transaction => {
// filter out non zero commitments and nullifiers
Expand All @@ -41,6 +43,7 @@ async function blockProposedEventHandler(data) {
(transaction.transactionType === '1' || transaction.transactionType === '2') &&
(await countCommitments(nonZeroCommitments)) === 0
) {
let keysTried = 1;
ivks.forEach((key, i) => {
// decompress the secrets first and then we will decryp t the secrets from this
const decompressedSecrets = Secrets.decompressSecrets(transaction.compressedSecrets);
Expand All @@ -51,20 +54,22 @@ async function blockProposedEventHandler(data) {
nonZeroCommitments[0],
);
if (Object.keys(commitment).length === 0)
logger.info("This encrypted message isn't for this recipient");
logger.info(
`This encrypted message isn't for this recipient, keys tried = ${keysTried++}`,
);
else {
// console.log('PUSHED', commitment, 'nsks', nsks[i]);
storeCommitments.push(storeCommitment(commitment, nsks[i]));
}
} catch (err) {
logger.info(err);
logger.info("This encrypted message isn't for this recipient");
logger.info(
`*This encrypted message isn't for this recipient, keys tried = ${keysTried++}`,
);
}
});
}
await Promise.all(storeCommitments).catch(function (err) {
logger.info(err);
}); // control errors when storing commitments in order to ensure next Promise being executed
await Promise.all(storeCommitments);
return Promise.all([
markOnChain(nonZeroCommitments, block.blockNumberL2, data.blockNumber, data.transactionHash),
markNullifiedOnChain(
Expand All @@ -79,8 +84,8 @@ async function blockProposedEventHandler(data) {
// await Promise.all(toStore);
await Promise.all(dbUpdates);
const updatedTimber = Timber.statelessUpdate(latestTree, blockCommitments);
await saveTree(data.blockNumber, block.blockNumberL2, updatedTimber);

await saveTree(transactionHashL1, block.blockNumberL2, updatedTimber);
logger.debug(`Saved tree for L2 block ${block.blockNumberL2}`);
await Promise.all(
// eslint-disable-next-line consistent-return
blockCommitments.map(async (c, i) => {
Expand Down
Loading

0 comments on commit 34aec0e

Please sign in to comment.