diff --git a/lib/2wp-utils.js b/lib/2wp-utils.js index 33e85aaf..c1bf2c08 100644 --- a/lib/2wp-utils.js +++ b/lib/2wp-utils.js @@ -1,6 +1,6 @@ const expect = require('chai').expect; -const { sendFromCow, mineAndSync, sendTxWithCheck, getUnlockedAddress } = require('./rsk-utils'); -const { wait, retryWithCheck } = require('./utils'); +const { sendFromCow, mineAndSync, sendTxWithCheck, getUnlockedAddress, waitForRskMempoolToGetTxs, waitAndUpdateBridge } = require('./rsk-utils'); +const { retryWithCheck, waitForBtcTxToBeInMempool, waitForBtcMempoolToGetTxs } = require('./utils'); const { getBridge, getLatestActiveForkName } = require('./precompiled-abi-forks-util'); const { getBridgeState } = require('@rsksmart/bridge-state-data-parser'); const btcEthUnitConverter = require('@rsksmart/btc-eth-unit-converter'); @@ -62,7 +62,10 @@ const sendTxToBridge = async (rskTxHelper, amountInRbtc, rskFromAddress, mine = if(!mine) { return; } - await wait(1000); + + // Wait for the rsk tx to be in the rsk mempool before mining + await waitForRskMempoolToGetTxs(rskTxHelper); + await mineAndSync(getRskTransactionHelpers()); const result = await txPromise; return result; @@ -103,9 +106,13 @@ const isUtxoRegisteredInBridge = async (rskTxHelper, peginTxHash, expectedUxtosC const mineForPeginRegistration = async (rskTxHelper, btcTxHelper) => { // Enough confirmations to register the coinbase but not the pegin. + // Wait for the pegin to be in the bitcoin mempool before mining + await waitForBtcMempoolToGetTxs(btcTxHelper); await btcTxHelper.mine(BTC_TO_RSK_MINIMUM_CONFIRMATIONS - 1); await waitAndUpdateBridge(rskTxHelper, 500); // One more confirmation to register the pegin. + // Wait for the pegin to be in the bitcoin mempool before mining + await waitForBtcMempoolToGetTxs(btcTxHelper); await btcTxHelper.mine(1); await waitAndUpdateBridge(rskTxHelper, 500); }; @@ -132,6 +139,9 @@ const sendPegin = async (rskTxHelper, btcTxHelper, btcSenderAddressInformation, const peginBtcTxHash = await btcTxHelper.transferBtc(btcSenderAddressInformation, recipientsTransferInformation, data); + // Wait for the pegin to be in the bitcoin mempool before mining + await waitForBtcTxToBeInMempool(btcTxHelper, peginBtcTxHash); + await mineForPeginRegistration(rskTxHelper, btcTxHelper); return peginBtcTxHash; @@ -177,18 +187,6 @@ const ensurePeginIsRegistered = async (rskTxHelper, peginBtcTxHash, expectedUtxo }; -/** - * Waits for the specified time, updates the bridge and mines 1 rsk block - * @param {RskTransactionHelper} rskTxHelper - * @param {number} timeInMilliseconds defaults to 1000 - * @returns {Promise} - */ -const waitAndUpdateBridge = async (rskTxHelper, timeInMilliseconds = 1000) => { - await wait(timeInMilliseconds); - await rskTxHelper.updateBridge(); - await mineAndSync(getRskTransactionHelpers()); -}; - /** * Creates a pegin v1 data for a user to indicate to which rsk address to receive their pegin funds. * @param {string} rskDestinationAddress @@ -273,5 +271,5 @@ module.exports = { createPeginV1TxData, mineForPeginRegistration, MIN_PEGOUT_VALUE_IN_RBTC, - disableWhitelisting + disableWhitelisting, }; diff --git a/lib/rsk-utils.js b/lib/rsk-utils.js index 5507d756..ba41ce86 100644 --- a/lib/rsk-utils.js +++ b/lib/rsk-utils.js @@ -5,7 +5,7 @@ const { getBridge, getLatestActiveForkName } = require('./precompiled-abi-forks- const hopBridgeTxParser = require('bridge-transaction-parser-hop400'); const fingerrootBridgeTxParser = require('bridge-transaction-parser-fingerroot500'); const { getRskTransactionHelpers } = require('../lib/rsk-tx-helper-provider'); -const { removePrefix0x } = require('./utils'); +const { removePrefix0x, waitForBtcMempoolToGetTxs } = require('./utils'); const BTC_TO_RSK_MINIMUM_ACCEPTABLE_CONFIRMATIONS = 3; const RSK_TO_BTC_MINIMUM_ACCEPTABLE_CONFIRMATIONS = 3; @@ -127,6 +127,98 @@ const increaseBlockToNextPegoutHeight = async (rskTransactionHelpers) => { } }; +/** + * Waits for the specified time, updates the bridge and mines 1 rsk block + * @param {RskTransactionHelper} rskTxHelper + * @param {number} timeInMilliseconds defaults to 1000 + * @returns {Promise} + */ +const waitAndUpdateBridge = async (rskTxHelper, timeInMilliseconds = 1000) => { + await wait(timeInMilliseconds); + await rskTxHelper.updateBridge(); + + // Wait for the rsk `updateBridge` tx to be in the rsk mempool before mining + await waitForRskMempoolToGetTxs(rskTxHelper); + + await mineAndSync(getRskTransactionHelpers()); +}; + +/** + * + * @param {RskTransactionHelper} rskTxHelper + * @returns {Promise} array of tx hashes in the mempool + */ +const getRskMempoolTransactionHashes = async (rskTxHelper) => { + const mempoolBlock = await rskTxHelper.getClient().eth.getBlock('pending'); + return mempoolBlock.transactions; +}; + +/** + * + * @param {RskTransactionHelper} rskTxHelper + * @param {string} txHash + * @param {number} maxAttempts Defaults to 20 + * @param {number} checkEveryMilliseconds Defaults to 1000 milliseconds + * @returns {boolean} whether the tx is in the mempool or not + */ +const waitForRskTxToBeInTheMempool = async (rskTxHelper, txHash, maxAttempts = 5, checkEveryMilliseconds = 1000) => { + + const method = async () => { + const tx = await rskTxHelper.getClient().eth.getTransaction(txHash); + if(tx && !tx.blockNumber) { + console.debug(`The tx (${txHash}) is in the mempool`); + return true; + } else if(tx && tx.blockNumber) { + console.debug(`The tx (${txHash}) is already mined in a block`); + return true; + } else { + console.debug(`The tx (${txHash}) is not in the mempool nor in a block yet. Will keep retrying until it is in the mempool, block, or it reaches the max attempts to find it`); + return false; + } + }; + + const check = async (txIsInTheMempool, currentAttempts) => { + console.debug(`Attempting to find the tx ${txHash} in the mempool. Attempt ${currentAttempts} out of ${maxAttempts}`); + return txIsInTheMempool; + }; + + return await retryWithCheck(method, check, maxAttempts, checkEveryMilliseconds); + +}; + +/** + * + * @param {RskTransactionHelper} rskTxHelper + * @param {number} maxAttempts Defaults to 20 + * @param {number} checkEveryMilliseconds Defaults to 1000 milliseconds + * @returns + */ +const waitForRskMempoolToGetTxs = async (rskTxHelper, maxAttempts = 5, checkEveryMilliseconds = 1000) => { + + const initialRskMempoolTxHashes = await getRskMempoolTransactionHashes(rskTxHelper); + + console.debug(`[waitForRskMempoolToGetTxs] initial mempool size: ${initialRskMempoolTxHashes.length}`); + + const method = async () => { + const mempoolTxHashes = await getRskMempoolTransactionHashes(rskTxHelper); + if(mempoolTxHashes.length > initialRskMempoolTxHashes.length) { + console.debug(`The mempool got new ${mempoolTxHashes.length - initialRskMempoolTxHashes.length} transactions`); + return true; + } else { + console.debug(`The mempool did not get new transactions yet. Will keep retrying until it gets new transactions or it reaches the max attempts`); + return false; + } + }; + + const check = async (mempoolHasTxs, currentAttempts) => { + console.debug(`Attempting to find new txs in the mempool. Attempt ${currentAttempts} out of ${maxAttempts}`); + return mempoolHasTxs; + }; + + return await retryWithCheck(method, check, maxAttempts, checkEveryMilliseconds); + + }; + /** * * @param {Array} rskTransactionHelpers RskTransactionHelper instances each belonging to one federator node to make calls to the rsk network @@ -141,12 +233,9 @@ const triggerRelease = async (rskTransactionHelpers, btcClient, callbacks = {}) await increaseBlockToNextPegoutHeight(rskTransactionHelpers); } - // Sync all nodes - await waitForSync(rskTransactionHelpers); - // Adds the pegout to the pegoutsWaitingForConfirmations structure with this 1 confirmation - await rskTxHelper.updateBridge(); - await mineAndSync(rskTransactionHelpers); // release_request_received and batch_pegout_created triggered here (if appropriate fork, RSKIP185 and RSKIP271, is/are active) + // release_request_received and batch_pegout_created triggered here (if appropriate fork, RSKIP185 and RSKIP271, is/are active) + await waitAndUpdateBridge(rskTxHelper); if(callbacks.pegoutCreatedCallback) { await callbacks.pegoutCreatedCallback(rskTxHelper); @@ -156,8 +245,8 @@ const triggerRelease = async (rskTransactionHelpers, btcClient, callbacks = {}) await mineAndSync(rskTransactionHelpers, BTC_TO_RSK_MINIMUM_ACCEPTABLE_CONFIRMATIONS - 1); // Moves the pegout from pegoutsWaitingForConfirmations to pegoutsWaitingForSignatures - await rskTxHelper.updateBridge(); // Makes an updateCollections call which is the tx that will move the pegout to pegoutsWaitingForSignatures - await mineAndSync(rskTransactionHelpers); // pegout_confirmed event triggered here (if appropriate fork, RSKIP326, is active) + // pegout_confirmed event triggered here (if appropriate fork, RSKIP326, is active) + await waitAndUpdateBridge(rskTxHelper); if(callbacks.pegoutConfirmedCallback) { await callbacks.pegoutConfirmedCallback(rskTxHelper); @@ -192,13 +281,16 @@ const triggerRelease = async (rskTransactionHelpers, btcClient, callbacks = {}) await callbacks.releaseBtcCallback(rskTxHelper); } + // Waiting to make sure that the pegout tx is in the bitcoin mempool before mining the required blocks for confirmation. + await waitForBtcMempoolToGetTxs(btcClient); + // From the btc network side, mine `RSK_TO_BTC_MINIMUM_ACCEPTABLE_CONFIRMATIONS + 1` blocks, 1 for the pegout funds to be mined and reflected in the recipient address, // and `RSK_TO_BTC_MINIMUM_ACCEPTABLE_CONFIRMATIONS` more to have enough confirmation for the change balance to be reflected back in the bridge (like a pegin) await btcClient.mine(RSK_TO_BTC_MINIMUM_ACCEPTABLE_CONFIRMATIONS + 1); // Make pegnatories register the change utxo back in the bridge - await rskTxHelper.updateBridge(); - await mineAndSync(rskTransactionHelpers); // At this point the bridge should already have the change uxto registered + // After this point the bridge should already have the change uxto registered + await waitAndUpdateBridge(rskTxHelper); }; @@ -221,7 +313,7 @@ const sendTxWithCheck = async (rskTxHelper, method, from, checkCallback) => { const estimatedGas = await method.estimateGas({ from }); const txReceiptPromise = method.send({ from, value: 0, gasPrice: 0, gas: estimatedGas }); - await wait(1000); + await waitForRskMempoolToGetTxs(rskTxHelper); await mineAndSync(getRskTransactionHelpers()); return await txReceiptPromise; @@ -344,5 +436,9 @@ module.exports = { getUnlockedAddress, getFedsPubKeys, activateFork, - getLatestForkName + getLatestForkName, + getRskMempoolTransactionHashes, + waitForRskTxToBeInTheMempool, + waitForRskMempoolToGetTxs, + waitAndUpdateBridge, }; diff --git a/lib/tests/2wp.js b/lib/tests/2wp.js index 3808fd73..9bcb10e9 100644 --- a/lib/tests/2wp.js +++ b/lib/tests/2wp.js @@ -9,6 +9,7 @@ const { getRskTransactionHelpers, getRskTransactionHelper } = require('../rsk-tx const { getDerivedRSKAddressInformation } = require('@rsksmart/btc-rsk-derivation'); const btcEthUnitConverter = require('@rsksmart/btc-eth-unit-converter'); const { sendTxToBridge, sendPegin, ensurePeginIsRegistered, donateToBridge } = require('../2wp-utils'); +const { waitAndUpdateBridge } = require('../rsk-utils'); const DONATION_AMOUNT = 250; const REJECTED_REASON = 1; @@ -38,8 +39,7 @@ const execute = (description, getRskHost) => { rskTxHelpers = getRskTransactionHelpers(); // Update the bridge to sync btc blockchains - await rskTxHelper.updateBridge(); - await rskUtils.mineAndSync(rskTxHelpers); + await waitAndUpdateBridge(rskTxHelper); // At the moment there are a lot of pegout tests that depend on the bridge to have enough balance. // Those tests are not doing a pegin if needed, so we need to donate to the bridge to ensure it has enough balance. diff --git a/lib/utils.js b/lib/utils.js index cfc96921..abb1b7dd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -99,6 +99,9 @@ const fundAddressAndGetData = async (btcTxHelper, addressToFund, amountToFundInB const txId = await btcTxHelper.transferBtc(btcSenderAddressInformation, recipientsTransactionInformation); + // Wait for the pegin to be in the bitcoin mempool before mining + await waitForBtcTxToBeInMempool(btcTxHelper, txId); + const rawTx = await btcTxHelper.nodeClient.getRawTransaction(txId); await btcTxHelper.importAddress(addressToFund); @@ -212,6 +215,88 @@ const retryWithCheck = async (method, check, maxAttempts = 5, delayInMillisecond return result; }; +/** + * + * @param {BtcTransactionHelper} btcTxHelper + * @returns {Array} the mempool tx ids + */ +const getBitcoinMempool = async (btcTxHelper) => { + const mempool = await btcTxHelper.nodeClient.execute('getrawmempool', []); + return mempool; +}; + +/** + * + * @param {BtcTransactionHelper} btcTxHelper + * @param {string} btcTxHash + */ +const waitForBtcTxToBeInMempool = async (btcTxHelper, btcTxHash) => { + + const bitcoinMempoolHasTx = async () => { + const bitcoinMempool = await getBitcoinMempool(btcTxHelper); + const txIsInMempool = bitcoinMempool.includes(btcTxHash); + if(!txIsInMempool) { + console.debug(`Attempting to check if the btc tx (${btcTxHash}) was already mined since it's not in the mempool yet.`); + const tx = await btcTransactionHelper.getTransaction(btcTxHash); + if(tx) { + console.debug(`The btc tx (${btcTxHash}) was already mined.`); + return true; + } + } + }; + + const checkBitcoinMempoolHasTx = async (btcTxAlreadyFound, currentAttempts) => { + if(btcTxAlreadyFound) { + console.debug(`The btc tx ${btcTxHash} was found in the mempool at attempt ${currentAttempts}.`); + } else { + console.log(`Attempting to get the btc tx ${btcTxHash} in the mempool. Attempt: ${currentAttempts}.`); + } + return btcTxAlreadyFound; + }; + + const onError = async (e) => { + if(e.message.includes('No such mempool or blockchain transaction')) { + console.debug(`The btc tx ${btcTxHash} is not in the mempool nor mined yet. Let's allow some more time before retrying to get it.`); + return true; + } + console.error(`Un expected error while trying to get the btc tx ${btcTxHash} in the mempool.`, e); + throw e; + }; + + await retryWithCheck(bitcoinMempoolHasTx, checkBitcoinMempoolHasTx, 5, 1000, onError); +}; + +/** + * Waits until the btc mempool has at least one tx. + * @param {BtcTransactionHelper} btcTxHelper + * @returns {boolean} + */ +const waitForBtcMempoolToGetTxs = async (btcTxHelper) => { + + const initialBitcoinMempoolSize = (await getBitcoinMempool(btcTxHelper)).length; + + console.debug(`The initial btc mempool size is ${initialBitcoinMempoolSize}.`); + + const getBitcoinMempoolSize = async () => { + const bitcoinMempool = await getBitcoinMempool(btcTxHelper); + const bitcoinMempoolSize = bitcoinMempool.length; + console.debug(`The btc mempool has ${bitcoinMempoolSize} txs.`); + return bitcoinMempoolSize; + }; + + const checkBtcMempoolIsNotEmpty = async (bitcoinMempoolSize, currentAttempts) => { + console.debug(`The btc mempool has ${bitcoinMempoolSize} txs at attempt ${currentAttempts}.`); + return bitcoinMempoolSize > 0; + }; + + const onError = async (e) => { + console.error(`Un expected error while trying to get the btc mempool.`, e); + throw e; + }; + + return await retryWithCheck(getBitcoinMempoolSize, checkBtcMempoolIsNotEmpty, 5, 1000, onError); +} + module.exports = { sequentialPromise: sequentialPromise, mapPromiseAll: mapPromiseAll, @@ -233,4 +318,7 @@ module.exports = { }, removePrefix0x, retryWithCheck, + getBitcoinMempool, + waitForBtcTxToBeInMempool, + waitForBtcMempoolToGetTxs, } diff --git a/tests/01_03_01-lock_whitelist_pre_papyrus.js b/tests/01_03_01-lock_whitelist_pre_papyrus.js index 57ede0a1..6d928799 100644 --- a/tests/01_03_01-lock_whitelist_pre_papyrus.js +++ b/tests/01_03_01-lock_whitelist_pre_papyrus.js @@ -15,6 +15,7 @@ const { sendPegin, ensurePeginIsRegistered } = require('../lib/2wp-utils'); const { getDerivedRSKAddressInformation } = require('@rsksmart/btc-rsk-derivation'); const { btcToWeis, btcToSatoshis, satoshisToBtc } = require('@rsksmart/btc-eth-unit-converter'); const { getBridge, getLatestActiveForkName } = require('../lib/precompiled-abi-forks-util'); +const { waitAndUpdateBridge } = require('../lib/rsk-utils'); let rskTxHelpers; let btcTxHelper; @@ -164,8 +165,7 @@ describe('Lock whitelisting', () => { const federationBalanceAfterPegin = await btcTxHelper.getAddressBalance(federationAddress); expect(Number(btcToSatoshis(federationBalanceAfterPegin))).to.equal(Number(btcToSatoshis(initialFederationBalance + AMOUNT_TO_TRY_TO_LOCK)), `Lock BTC federation ${federationAddress} credit`); - await rskTxHelper.updateBridge(); - await rskUtils.mineAndSync(rskTxHelpers); + await waitAndUpdateBridge(rskTxHelper); const initialBlockNumber = await rskTxHelper.getBlockNumber(); await rskUtils.mineAndSync(rskTxHelpers); @@ -231,8 +231,8 @@ describe('Lock whitelisting', () => { const federationBalanceAfterPegin = await btcTxHelper.getAddressBalance(federationAddress); expect(federationBalanceAfterPegin).to.equal(initialFederationBalance + PEGIN_VALUE_IN_BTC, 'The federation address should have its balance increased by the pegin amount'); - await rskTxHelper.updateBridge(); - await rskUtils.mineAndSync(rskTxHelpers); + // Update the bridge to sync + await waitAndUpdateBridge(rskTxHelper); const recipientRskAddressBalance = Number(await rskTxHelper.getBalance(recipientRskAddressInfo.address)); expect(recipientRskAddressBalance).to.equal(0, 'The recipient rsk address should not have any balance'); diff --git a/tests/01_03_54-post-papyrus_coinbase_information.js b/tests/01_03_54-post-papyrus_coinbase_information.js index fe5856af..cb16cf6b 100644 --- a/tests/01_03_54-post-papyrus_coinbase_information.js +++ b/tests/01_03_54-post-papyrus_coinbase_information.js @@ -31,6 +31,7 @@ describe('Calling coinbase information methods after papyrus', () => { const blockHash = await btcClient.mine(1); await wait(1000); await rskTxHelper.updateBridge(); + await rskUtils.waitForRskMempoolToGetTxs(rskTxHelper); const blockData = await btcClient.nodeClient.getBlock(blockHash[0], false); const block = bitcoinJs.Block.fromHex(blockData);