diff --git a/.github/workflows/on-pull-request-master.yml b/.github/workflows/on-pull-request-master.yml index 069aef685..687da963c 100644 --- a/.github/workflows/on-pull-request-master.yml +++ b/.github/workflows/on-pull-request-master.yml @@ -895,3 +895,48 @@ jobs: with: name: periodic-payment-test-logs path: ./periodic-payment-test.log + + test-client-sync: + env: + CONFIRMATIONS: 1 + NF_SERVICES_TO_START: blockchain,client,deployer,mongodb,optimist,rabbitmq,worker + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v1 + with: + node-version: '16.17.0' + + - name: Start Containers + run: | + ./bin/setup-nightfall + ./bin/start-nightfall -g -d &> test-client-sync.log &disown + - name: Wait for images to be ready + uses: Wandalen/wretry.action@v1.0.11 + with: + command: | + docker wait nightfall_3_deployer_1 + attempt_limit: 100 + attempt_delay: 20000 + + - name: Debug logs - after image builds + if: always() + run: cat test-client-sync.log + + - name: Run integration test + run: | + npm run test-client-sync + - name: Debug logs - after integration test run + if: always() + run: cat test-client-sync.log + + - name: If integration test failed, shutdown the Containers + if: failure() + run: npm run nightfall-down + + - name: If integration test failed, upload logs files as artifacts + if: failure() + uses: actions/upload-artifact@master + with: + name: test-client-sync-logs + path: ./test-client-sync.log diff --git a/cli/lib/nf3.mjs b/cli/lib/nf3.mjs index e3bd0f160..01f72b48e 100644 --- a/cli/lib/nf3.mjs +++ b/cli/lib/nf3.mjs @@ -677,7 +677,7 @@ class Nf3 { this.shieldContractAddress, 0, ); - resolve(receipt); + resolve({ ...receipt, transactionHashL2: res.data.transaction.transactionHash }); } catch (err) { reject(err); } @@ -740,7 +740,7 @@ class Nf3 { this.shieldContractAddress, 0, ); - resolve(receipt); + resolve({ ...receipt, transactionHashL2: res.data.transaction.transactionHash }); } catch (err) { reject(err); } diff --git a/nightfall-client/src/event-handlers/block-proposed.mjs b/nightfall-client/src/event-handlers/block-proposed.mjs index 60a662184..2f6d62153 100644 --- a/nightfall-client/src/event-handlers/block-proposed.mjs +++ b/nightfall-client/src/event-handlers/block-proposed.mjs @@ -14,8 +14,9 @@ import { setSiblingInfo, countCircuitTransactions, isTransactionHashBelongCircuit, + deleteNonNullifiedCommitments, } from '../services/commitment-storage.mjs'; -import getProposeBlockCalldata from '../services/process-calldata.mjs'; +import { getProposeBlockCalldata } from '../services/process-calldata.mjs'; import { zkpPrivateKeys, nullifierKeys } from '../services/keys.mjs'; import { getTreeByBlockNumberL2, @@ -26,6 +27,8 @@ import { getNumberOfL2Blocks, getTransactionByTransactionHash, updateTransaction, + findDuplicateTransactions, + deleteTransactionsByTransactionHashes, } from '../services/database.mjs'; import { decryptCommitment } from '../services/commitment-sync.mjs'; import { syncState } from '../services/state-sync.mjs'; @@ -81,6 +84,7 @@ async function blockProposedEventHandler(data, syncing) { const dbUpdates = transactions.map(async transaction => { let saveTxToDb = false; + let duplicateTransactions = []; // duplicate tx holding same commitments or nullifiers // filter out non zero commitments and nullifiers const nonZeroCommitments = transaction.commitments.filter(c => c !== ZERO); @@ -143,6 +147,12 @@ async function blockProposedEventHandler(data, syncing) { } else logger.warn(`Duplicate transaction in Proposed Block has been dropped`); } else throw new Error(err); } + + duplicateTransactions = await findDuplicateTransactions( + nonZeroCommitments, + nonZeroNullifiers, + [transaction.transactionHash], + ); } return Promise.all([ @@ -154,6 +164,13 @@ async function blockProposedEventHandler(data, syncing) { data.blockNumber, data.transactionHash, ), + deleteTransactionsByTransactionHashes([...duplicateTransactions.map(t => t.transactionHash)]), + deleteNonNullifiedCommitments([ + ...duplicateTransactions + .map(t => t.commitments) + .flat() + .filter(c => c !== ZERO), + ]), ]); }); diff --git a/nightfall-client/src/event-handlers/index.mjs b/nightfall-client/src/event-handlers/index.mjs index 0c7a8d2a9..af6e0ceaa 100644 --- a/nightfall-client/src/event-handlers/index.mjs +++ b/nightfall-client/src/event-handlers/index.mjs @@ -2,15 +2,18 @@ import { startEventQueue } from './subscribe.mjs'; import blockProposedEventHandler from './block-proposed.mjs'; import rollbackEventHandler from './rollback.mjs'; import removeBlockProposedEventHandler from './chain-reorg.mjs'; +import transactionSubmittedEventHandler from './transaction-submitted.mjs'; const eventHandlers = { BlockProposed: blockProposedEventHandler, + TransactionSubmitted: transactionSubmittedEventHandler, Rollback: rollbackEventHandler, removers: { BlockProposed: removeBlockProposedEventHandler, }, priority: { BlockProposed: 0, + TransactionSubmitted: 1, Rollback: 0, }, }; diff --git a/nightfall-client/src/event-handlers/transaction-submitted.mjs b/nightfall-client/src/event-handlers/transaction-submitted.mjs new file mode 100644 index 000000000..1cf1766fb --- /dev/null +++ b/nightfall-client/src/event-handlers/transaction-submitted.mjs @@ -0,0 +1,63 @@ +import logger from 'common-files/utils/logger.mjs'; +import constants from 'common-files/constants/index.mjs'; +import { getTransactionSubmittedCalldata } from '../services/process-calldata.mjs'; +import { countCommitments, countNullifiers } from '../services/commitment-storage.mjs'; +import { saveTransaction } from '../services/database.mjs'; + +const { ZERO } = constants; + +async function doesAnyOfCommitmentsExistInDB(commitments) { + const count = await countCommitments(commitments); + return Boolean(count); +} + +async function doesAnyOfNullifiersExistInDB(nullifiers) { + const count = await countNullifiers(nullifiers); + return Boolean(count); +} + +/** + * This handler runs whenever a new transaction is submitted to the blockchain + */ +async function transactionSubmittedEventHandler(eventParams) { + const { offchain = false, ...data } = eventParams; + let saveTxInDb = false; + + const transaction = await getTransactionSubmittedCalldata(data); + transaction.blockNumber = data.blockNumber; + transaction.transactionHashL1 = data.transactionHash; + + // logic: if any of non zero commitment in transaction alraedy exist in db + // That means this transaction belong to user using this nightfall-client + // Hence, proceed and save tx in db. + // Note: for deposit we store commitment in transaction submit event handler, + // similarly for transfer we store change commitment in transaction submit event handler. + + // filter out non zero commitments and nullifiers + const nonZeroCommitments = transaction.commitments.filter(c => c !== ZERO); + const nonZeroNullifiers = transaction.nullifiers.filter(n => n !== ZERO); + + if (await doesAnyOfCommitmentsExistInDB(nonZeroCommitments)) { + saveTxInDb = true; + } else if (doesAnyOfNullifiersExistInDB(nonZeroNullifiers)) { + saveTxInDb = true; + } + + if (saveTxInDb) { + await saveTransaction({ ...transaction }).catch(err => + logger.error({ + msg: 'error while saving transaction in transactionSubmittedEventHandler', + err, + }), + ); + } + + logger.info({ + msg: 'Client Transaction Handler - New transaction received.', + transaction, + offchain, + saveTxInDb, + }); +} + +export default transactionSubmittedEventHandler; diff --git a/nightfall-client/src/routes/commitment.mjs b/nightfall-client/src/routes/commitment.mjs index 6cd953a04..b2d98966a 100644 --- a/nightfall-client/src/routes/commitment.mjs +++ b/nightfall-client/src/routes/commitment.mjs @@ -15,10 +15,12 @@ import { getWalletPendingSpentBalance, getCommitments, getCommitmentsByCompressedZkpPublicKeyList, - insertCommitmentsAndResync, + insertCommitments, getCommitmentsByCircuitHash, getCommitmentsDepositedRollbacked, } from '../services/commitment-storage.mjs'; +import { syncState } from '../services/state-sync.mjs'; +import { getAllTransactions } from '../services/database.mjs'; const router = express.Router(); @@ -84,7 +86,8 @@ router.get('/commitments', async (req, res, next) => { router.post('/save', async (req, res, next) => { const listOfCommitments = req.body; try { - const response = await insertCommitmentsAndResync(listOfCommitments); + const response = await insertCommitments(listOfCommitments); + await syncState(); // Sycronize from beggining res.json(response); } catch (err) { next(err); @@ -147,4 +150,13 @@ router.get('/commitmentsRollbacked', async (req, res, next) => { } }); +router.get('/transactions', async (req, res, next) => { + try { + const transactions = await getAllTransactions(); + res.json({ transactions }); + } catch (err) { + next(err); + } +}); + export default router; diff --git a/nightfall-client/src/services/commitment-storage.mjs b/nightfall-client/src/services/commitment-storage.mjs index e532f8f4e..cbc99f51c 100644 --- a/nightfall-client/src/services/commitment-storage.mjs +++ b/nightfall-client/src/services/commitment-storage.mjs @@ -16,7 +16,6 @@ import { getTransactionByTransactionHash, getTransactionHashSiblingInfo, } from './database.mjs'; -import { syncState } from './state-sync.mjs'; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION } = config; const { generalise } = gen; @@ -976,13 +975,13 @@ export async function findUsableCommitmentsMutex( /** * - * @function insertCommitmentsAndResync save a list of commitments in the database + * @function insertCommitments save a list of commitments in the database * @param {[]} listOfCommitments a list of commitments to be saved in the database * @throws if all the commitments in the list already exists in the database * throw an error * @returns return a success message. */ -export async function insertCommitmentsAndResync(listOfCommitments) { +export async function insertCommitments(listOfCommitments) { const connection = await mongo.connection(MONGO_URL); const db = connection.db(COMMITMENTS_DB); @@ -1005,10 +1004,6 @@ export async function insertCommitmentsAndResync(listOfCommitments) { if (onlyNewCommitments.length > 0) { // 4. Insert all await db.collection(COMMITMENTS_COLLECTION).insertMany(onlyNewCommitments); - - // 5. Sycronize from beggining - await syncState(); - return { successMessage: 'Commitments have been saved successfully!' }; } @@ -1072,3 +1067,11 @@ export async function getCommitmentsDepositedRollbacked(compressedZkpPublicKey) return db.collection(COMMITMENTS_COLLECTION).find(query).toArray(); } + +// function to delete non nullified commitments +export async function deleteNonNullifiedCommitments(commitments) { + const connection = await mongo.connection(MONGO_URL); + const query = { _id: { $in: commitments }, isNullifiedOnChain: -1 }; + const db = connection.db(COMMITMENTS_DB); + return db.collection(COMMITMENTS_COLLECTION).deleteMany(query); +} diff --git a/nightfall-client/src/services/database.mjs b/nightfall-client/src/services/database.mjs index 237409571..c1ad432ce 100644 --- a/nightfall-client/src/services/database.mjs +++ b/nightfall-client/src/services/database.mjs @@ -304,3 +304,18 @@ export async function getTransactionsByTransactionHashesByL2Block(transactionHas ); return transactions; } + +/** + * Function to find duplicate transactions for an array of commitments or nullifiers + * this function is used in blockProposedEventHandler + */ +export async function findDuplicateTransactions(commitments, nullifiers, transactionHashes = []) { + const connection = await mongo.connection(MONGO_URL); + const db = connection.db(COMMITMENTS_DB); + const query = { + $or: [{ commitments: { $in: commitments } }, { nullifiers: { $in: nullifiers } }], + transactionHash: { $nin: transactionHashes }, + blockNumberL2: { $eq: -1 }, + }; + return db.collection(TRANSACTIONS_COLLECTION).find(query).toArray(); +} diff --git a/nightfall-client/src/services/process-calldata.mjs b/nightfall-client/src/services/process-calldata.mjs index 844742f85..32f518f78 100644 --- a/nightfall-client/src/services/process-calldata.mjs +++ b/nightfall-client/src/services/process-calldata.mjs @@ -10,7 +10,7 @@ import { unpackBlockInfo } from 'common-files/utils/block-utils.mjs'; const { SIGNATURES } = config; -async function getProposeBlockCalldata(eventData) { +export async function getProposeBlockCalldata(eventData) { const web3 = Web3.connection(); const { transactionHash } = eventData; const tx = await web3.eth.getTransaction(transactionHash); @@ -76,4 +76,47 @@ async function getProposeBlockCalldata(eventData) { return { transactions, block }; } -export default getProposeBlockCalldata; +export async function getTransactionSubmittedCalldata(eventData) { + const web3 = Web3.connection(); + const { transactionHash } = eventData; + const tx = await web3.eth.getTransaction(transactionHash); + // Remove the '0x' and function signature to recove rhte abi bytecode + const abiBytecode = `0x${tx.input.slice(10)}`; + const transactionData = web3.eth.abi.decodeParameter(SIGNATURES.SUBMIT_TRANSACTION, abiBytecode); + const [ + packedTransactionInfo, + historicRootBlockNumberL2Packed, + tokenId, + ercAddress, + recipientAddress, + commitments, + nullifiers, + compressedSecrets, + proof, + ] = transactionData; + + const { value, fee, circuitHash, tokenType } = + Transaction.unpackTransactionInfo(packedTransactionInfo); + + const historicRootBlockNumberL2 = Transaction.unpackHistoricRoot( + nullifiers.length, + historicRootBlockNumberL2Packed, + ); + + const transaction = { + value, + fee, + circuitHash, + tokenType, + historicRootBlockNumberL2, + tokenId, + ercAddress, + recipientAddress, + commitments, + nullifiers, + compressedSecrets, + proof, + }; + transaction.transactionHash = Transaction.calcHash(transaction); + return transaction; +} diff --git a/nightfall-client/src/services/state-sync.mjs b/nightfall-client/src/services/state-sync.mjs index 89d36f599..b26307544 100644 --- a/nightfall-client/src/services/state-sync.mjs +++ b/nightfall-client/src/services/state-sync.mjs @@ -12,8 +12,9 @@ import { unpauseQueue } from 'common-files/utils/event-queue.mjs'; import constants from 'common-files/constants/index.mjs'; import blockProposedEventHandler from '../event-handlers/block-proposed.mjs'; import rollbackEventHandler from '../event-handlers/rollback.mjs'; +import transactionSubmittedEventHandler from '../event-handlers/transaction-submitted.mjs'; -const { STATE_CONTRACT_NAME, CHALLENGES_CONTRACT_NAME } = constants; +const { STATE_CONTRACT_NAME, SHIELD_CONTRACT_NAME, CHALLENGES_CONTRACT_NAME } = constants; const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION, STATE_GENESIS_BLOCK } = config; export const syncState = async ( @@ -24,6 +25,7 @@ export const syncState = async ( logger.info({ msg: 'SyncState parameters', fromBlock, toBlock, eventFilter }); const stateContractInstance = await waitForContract(STATE_CONTRACT_NAME); // BlockProposed + const shieldContractInstance = await waitForContract(SHIELD_CONTRACT_NAME); // TransactionSubmitted const challengesContractInstance = await waitForContract(CHALLENGES_CONTRACT_NAME); // Rollback const pastStateEvents = await stateContractInstance.getPastEvents(eventFilter, { @@ -31,6 +33,11 @@ export const syncState = async ( toBlock, }); + const pastShieldEvents = await shieldContractInstance.getPastEvents(eventFilter, { + fromBlock, + toBlock, + }); + const pastChallengeEvents = await challengesContractInstance.getPastEvents(eventFilter, { fromBlock, toBlock, @@ -38,6 +45,7 @@ export const syncState = async ( // Put all events together and sort chronologically as they appear on Ethereum const splicedList = pastStateEvents + .concat(pastShieldEvents) .concat(pastChallengeEvents) .sort((a, b) => a.blockNumber - b.blockNumber); @@ -52,6 +60,10 @@ export const syncState = async ( // eslint-disable-next-line no-await-in-loop await rollbackEventHandler(pastEvent); break; + case 'TransactionSubmitted': + // eslint-disable-next-line no-await-in-loop + await transactionSubmittedEventHandler(pastEvent); + break; default: break; } @@ -66,10 +78,9 @@ const genGetCommitments = async (query = {}, proj = {}) => { // eslint-disable-next-line import/prefer-default-export export const initialClientSync = async () => { - const allCommitments = await genGetCommitments(); - const commitmentBlockNumbers = allCommitments.map(a => a.blockNumber).filter(n => n >= 0); - - logger.info(`commitmentBlockNumbers: ${commitmentBlockNumbers}`); + const allCommitments = await genGetCommitments({ blockNumber: { $gte: 0 } }); + const commitmentBlockNumbers = allCommitments.map(a => a.blockNumber); + logger.info({ msg: 'commitmentBlockNumbers', commitmentBlockNumbers }); const firstSeenBlockNumber = Math.min(...commitmentBlockNumbers); diff --git a/package.json b/package.json index c2f35f729..6a58e2c5f 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "ping-pong": ". ./bin/export-multiproposer-test-env && npx hardhat test --bail --no-compile test/ping-pong/ping-pong.test.mjs", "test-administrator": "npx hardhat test --bail --no-compile test/multisig/administrator.test.mjs ", "test-optimist-sync": "npx hardhat test --no-compile --bail test/optimist-resync.test.mjs", + "test-client-sync": "npx hardhat test --no-compile --bail test/client-resync.test.mjs", "test-adversary": "npx hardhat test --no-compile --bail test/adversary.test.mjs", "test-general-stuff": "npx hardhat test --bail --no-compile test/kem-dem.test.mjs test/timber.test.mjs", "test-x509": "LOG_LEVEL=debug npx hardhat test --bail --no-compile test/x509.test.mjs", diff --git a/test/client-resync.test.mjs b/test/client-resync.test.mjs new file mode 100644 index 000000000..b80cc1121 --- /dev/null +++ b/test/client-resync.test.mjs @@ -0,0 +1,173 @@ +/* eslint-disable no-await-in-loop */ +import chai, { expect } from 'chai'; +// import gen from 'general-number'; +import chaiHttp from 'chai-http'; +import chaiAsPromised from 'chai-as-promised'; +import config from 'config'; +import logger from 'common-files/utils/logger.mjs'; +import Nf3 from '../cli/lib/nf3.mjs'; +import { + getLayer2Balances, + expectTransaction, + Web3Client, + getUserCommitments, + getClientTransactions, + restartClient, +} from './utils.mjs'; + +chai.use(chaiHttp); +chai.use(chaiAsPromised); + +// const { generalise } = gen; +const environment = config.ENVIRONMENTS[process.env.ENVIRONMENT] || config.ENVIRONMENTS.localhost; +const { + fee, + transferValue, + tokenConfigs: { tokenType, tokenId }, + mnemonics, + signingKeys, +} = config.TEST_OPTIONS; + +const web3Client = new Web3Client(); +const eventLogs = []; +let rollbackCount = 0; + +const nf3User = new Nf3(signingKeys.user1, environment); +const nf3User2 = new Nf3(signingKeys.user2, environment); + +const nf3Proposer = new Nf3(signingKeys.proposer1, environment); + +async function makeBlock() { + logger.debug(`Make block...`); + await nf3Proposer.makeBlockNow(); + await web3Client.waitForEvent(eventLogs, ['blockProposed']); +} + +describe('Client synchronisation tests', () => { + let erc20Address; + + before(async () => { + await nf3User.init(mnemonics.user1); + await nf3User2.init(mnemonics.user2); + + await nf3Proposer.init(mnemonics.proposer); + await nf3Proposer.registerProposer('http://optimist', await nf3Proposer.getMinimumStake()); + + // Proposer listening for incoming events + const newGasBlockEmitter = await nf3Proposer.startProposer(); + newGasBlockEmitter.on('rollback', () => { + rollbackCount += 1; + logger.debug( + `Proposer received a signalRollback complete, Now no. of rollbacks are ${rollbackCount}`, + ); + }); + + erc20Address = await nf3User.getContractAddress('ERC20Mock'); + web3Client.subscribeTo('logs', eventLogs, { address: nf3User.stateContractAddress }); + web3Client.subscribeTo('logs', eventLogs, { address: nf3User.shieldContractAddress }); + }); + + describe('Test nightfall-client', () => { + it('Should do two deposit successfully', async function () { + const userL2BalanceBefore = await getLayer2Balances(nf3User, erc20Address); + + // first deposit + const res = await nf3User.deposit(erc20Address, tokenType, transferValue, tokenId, fee); + expectTransaction(res); + + await web3Client.waitForEvent(eventLogs, ['TransactionSubmitted']); + const transactions = await getClientTransactions(environment.clientApiUrl); + + // passing of below expect proves that transaction are save in + // transactionEventHandler + expect(transactions.length).to.be.equal(1); + expect(res.transactionHashL2).to.be.equal(transactions[0].transactionHash); + + await makeBlock(); + + // second deposit + await nf3User.deposit(erc20Address, tokenType, transferValue, tokenId, fee); + + await makeBlock(); + + const userL2BalanceAfter = await getLayer2Balances(nf3User, erc20Address); + expect(userL2BalanceAfter - userL2BalanceBefore).to.be.equal(transferValue * 2 - fee * 2); + }); + + // this test is to check nightfall-client behaviour in a case + // where two same transfer transactions is created but second one with higher fee + context('Test nightfall-client duplicate transaction deletion logic', () => { + let userCommitments; + let firstTransfer; + let userL2BalanceBefore; + before(async () => { + userCommitments = await getUserCommitments( + environment.clientApiUrl, + nf3User.zkpKeys.compressedZkpPublicKey, + ); + userL2BalanceBefore = await getLayer2Balances(nf3User, erc20Address); + }); + + it('Should successfully create a transfer transaction', async function () { + const res = await nf3User.transfer( + false, + erc20Address, + tokenType, + transferValue, + tokenId, + nf3User2.zkpKeys.compressedZkpPublicKey, + fee, + userCommitments.map(c => c.commitmentHash), + ); + expectTransaction(res); + firstTransfer = res.transactionHashL2; + await web3Client.waitForEvent(eventLogs, ['TransactionSubmitted']); + const transactions = await getClientTransactions(environment.clientApiUrl); + + expect(transactions.length).to.be.equal(3); + expect(res.transactionHashL2).to.be.equal(transactions[2].transactionHash); + }); + + it('Should successfully do a transfer with higher fee with create block', async function () { + let transactions; + const res = await nf3User.transfer( + false, + erc20Address, + tokenType, + transferValue, + tokenId, + nf3User2.zkpKeys.compressedZkpPublicKey, + fee + 1, + userCommitments.map(c => c.commitmentHash), + ); + expectTransaction(res); + + transactions = await getClientTransactions(environment.clientApiUrl); + expect(transactions.length).to.be.equal(3); + expect(firstTransfer).to.be.equal(transactions[2].transactionHash); + + // here we will also test client resync atleast for TransactionSubmitEvent Handler + await restartClient(nf3User); + + transactions = await getClientTransactions(environment.clientApiUrl); + // if below expect passes it proves client resync is working. + expect(transactions.length).to.be.equal(4); + expect(res.transactionHashL2).to.be.equal(transactions[3].transactionHash); + + // client re-sync has made sure higer fee transfer transaction is received in + // transaction submit eventHandler, infact that what passing of above expect proves + // But for optimist to receive same transaction lets wait for TransactionSubmitted + // event trigger, needed before proceeding to makeBLock + await web3Client.waitForEvent(eventLogs, ['TransactionSubmitted']); + await makeBlock(); + const userL2BalanceAfter = await getLayer2Balances(nf3User, erc20Address); + expect(userL2BalanceAfter - userL2BalanceBefore).to.be.equal(-(transferValue + fee + 1)); + + transactions = await getClientTransactions(environment.clientApiUrl); + // if below expect passes it proves blockEventHandler delete duplicate transaction is working. + expect(transactions.length).to.be.equal(3); + expect(res.transactionHashL2).to.be.equal(transactions[2].transactionHash); + }); + }); + }); +}); diff --git a/test/utils.mjs b/test/utils.mjs index dc669e4f7..44c4d9d3d 100644 --- a/test/utils.mjs +++ b/test/utils.mjs @@ -571,6 +571,14 @@ const healthy = async nf3Proposer => { logger.debug('optimist is healthy'); }; +const healthyClient = async nf3User => { + while (!(await nf3User.healthcheck('client'))) { + await waitForTimeout(1000); + } + + logger.debug('client is healthy'); +}; + const dropOptimistMongoDatabase = async () => { logger.debug(`Dropping Optimist's Mongo database`); let mongoConn; @@ -717,3 +725,29 @@ export async function restartOptimist(nf3Proposer, dropDb = true) { await healthy(nf3Proposer); } + +// unlike optimist, client cannot drop its database. +// because of commitments stored in database. +// restartClient function is only used in client-resync.test.mjs +export async function restartClient(nf3User) { + const options = { + config: [ + 'docker/docker-compose.yml', + 'docker/docker-compose.dev.yml', + 'docker/docker-compose.ganache.yml', + ], + log: process.env.LOG_LEVEL || 'silent', + composeOptions: [['-p', 'nightfall_3']], + }; + + await compose.stopOne('client', options); + await compose.rm(options, 'client'); + + await compose.upOne('client', options); + await healthyClient(nf3User); +} + +export async function getClientTransactions(clientApiUrl) { + const { transactions } = (await axios.get(`${clientApiUrl}/commitment/transactions`)).data; + return transactions; +}