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

Adds 'should reject and refund a legacy pegin from a multisig account… #99

Closed
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
14 changes: 2 additions & 12 deletions lib/2wp-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const {
waitAndUpdateBridge
} = require('./rsk-utils');
const { retryWithCheck, ensure0x } = require('./utils');
const { waitForBitcoinTxToBeInMempool, waitForBitcoinMempoolToGetTxs, getBtcAddressBalanceInSatoshis } = require('./btc-utils');
const { waitForBitcoinTxToBeInMempool, waitForBitcoinMempoolToGetTxs, getBtcAddressBalanceInSatoshis, fundBtcAddressCheckingBalance } = require('./btc-utils');
const { getBridge } = require('./precompiled-abi-forks-util');
const { getBridgeState } = require('@rsksmart/bridge-state-data-parser');
const { getDerivedRSKAddressInformation } = require('@rsksmart/btc-rsk-derivation');
Expand Down Expand Up @@ -248,17 +248,7 @@ const createSenderRecipientInfo = async (rskTxHelper, btcTxHelper, type = 'legac
await rskTxHelper.importAccount(rskRecipientRskAddressInfo.privateKey);
await rskTxHelper.unlockAccount(rskRecipientRskAddressInfo.address);
if(Number(initialAmountToFundInBtc) > 0) {
const initialAddressBalance = await btcTxHelper.getAddressBalance(btcSenderAddressInfo.address);
const fundTxHash = await btcTxHelper.fundAddress(btcSenderAddressInfo.address, initialAmountToFundInBtc);
const finalAddressBalance = await btcTxHelper.getAddressBalance(btcSenderAddressInfo.address);
// If the final address balance is the same as the initial balance, then the tx has not yet reached the mempool or has not been mined. Rare condition that may happen randomly.
if(finalAddressBalance === initialAddressBalance) {
const inMempool = await waitForBitcoinTxToBeInMempool(btcTxHelper, fundTxHash);
if(inMempool) {
await btcTxHelper.mine();
}
}

await fundBtcAddressCheckingBalance(btcTxHelper, btcSenderAddressInfo.address, initialAmountToFundInBtc);
}
return {
btcSenderAddressInfo,
Expand Down
94 changes: 57 additions & 37 deletions lib/btc-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,44 +154,64 @@ const waitForBitcoinTxToBeInMempool = async (btcTxHelper, btcTxHash, maxAttempts
* @returns {Promise<boolean>}
*/
const waitForBitcoinMempoolToGetTxs = async (btcTxHelper, maxAttempts = 3, checkEveryMilliseconds = 500) => {
const initialBitcoinMempoolSize = (await getBitcoinTransactionsInMempool(btcTxHelper)).length;
logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] The initial bitcoin mempool size is ${initialBitcoinMempoolSize}.`);
logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] Will wait and attempt to check if the bitcoin mempool has received any new transactions ${maxAttempts} times.`);

const getCountOfTransactionsInMempool = async () => {
const bitcoinMempool = await getBitcoinTransactionsInMempool(btcTxHelper);
const bitcoinMempoolSize = bitcoinMempool.length;
return bitcoinMempoolSize;
};

const checkBtcMempoolIsNotEmpty = async (bitcoinMempoolSize) => {
return bitcoinMempoolSize > 0;
};

const { result: bitcoinMempoolHasTx, attempts } = await retryWithCheck(
getCountOfTransactionsInMempool,
checkBtcMempoolIsNotEmpty,
maxAttempts,
checkEveryMilliseconds
);

const txsInMempool = await getBitcoinTransactionsInMempool(btcTxHelper);
const finalBitcoinMempoolSize = txsInMempool.length;

logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] The final bitcoin mempool size is ${finalBitcoinMempoolSize}, after ${attempts} attempts. Difference with initial mempool size: ${finalBitcoinMempoolSize - initialBitcoinMempoolSize}.`);

return bitcoinMempoolHasTx;
}
const initialBitcoinMempoolSize = (await getBitcoinTransactionsInMempool(btcTxHelper)).length;
logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] The initial bitcoin mempool size is ${initialBitcoinMempoolSize}.`);
logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] Will wait and attempt to check if the bitcoin mempool has received any new transactions ${maxAttempts} times.`);

const getBtcAddressBalanceInSatoshis = async (btcTxHelper, btcAddress) => {
return Number(btcToSatoshis(await btcTxHelper.getAddressBalance(btcAddress)));
const getCountOfTransactionsInMempool = async () => {
const bitcoinMempool = await getBitcoinTransactionsInMempool(btcTxHelper);
const bitcoinMempoolSize = bitcoinMempool.length;
return bitcoinMempoolSize;
};

module.exports = {
publicKeyToCompressed,
fundAddressAndGetData,
getBitcoinTransactionsInMempool,
waitForBitcoinTxToBeInMempool,
waitForBitcoinMempoolToGetTxs,
getBtcAddressBalanceInSatoshis,
const checkBtcMempoolIsNotEmpty = async (bitcoinMempoolSize) => {
return bitcoinMempoolSize > 0;
};

const { result: bitcoinMempoolHasTx, attempts } = await retryWithCheck(
getCountOfTransactionsInMempool,
checkBtcMempoolIsNotEmpty,
maxAttempts,
checkEveryMilliseconds
);

const txsInMempool = await getBitcoinTransactionsInMempool(btcTxHelper);
const finalBitcoinMempoolSize = txsInMempool.length;

logger.debug(`[${waitForBitcoinMempoolToGetTxs.name}] The final bitcoin mempool size is ${finalBitcoinMempoolSize}, after ${attempts} attempts. Difference with initial mempool size: ${finalBitcoinMempoolSize - initialBitcoinMempoolSize}.`);

return bitcoinMempoolHasTx;
};

const getBtcAddressBalanceInSatoshis = async (btcTxHelper, btcAddress) => {
return Number(btcToSatoshis(await btcTxHelper.getAddressBalance(btcAddress)));
};

/**
* Funds a btc address and checks if the balance has been updated. If the balance has not been updated, then the tx has not yet reached the mempool or has not been mined. Rare condition that may happen randomly.
* In that case, the code waits for the tx to be in the mempool and then mines a block.
* @param {BtcTransactionHelper} btcTxHelper to make transactions to the bitcoin network
* @param {string} btcAddress address to fund
* @param {number} amountInBtc amount to fund in btc
*/
const fundBtcAddressCheckingBalance = async (btcTxHelper, btcAddress, amountInBtc) => {
const initialAddressBalance = await btcTxHelper.getAddressBalance(btcAddress);
const fundTxHash = await btcTxHelper.fundAddress(btcAddress, Number(amountInBtc));
const finalAddressBalance = await btcTxHelper.getAddressBalance(btcAddress);
if(finalAddressBalance === initialAddressBalance) {
const inMempool = await waitForBitcoinTxToBeInMempool(btcTxHelper, fundTxHash, 10);
if(inMempool) {
await btcTxHelper.mine();
}
}
};

module.exports = {
publicKeyToCompressed,
fundAddressAndGetData,
getBitcoinTransactionsInMempool,
waitForBitcoinTxToBeInMempool,
waitForBitcoinMempoolToGetTxs,
getBtcAddressBalanceInSatoshis,
fundBtcAddressCheckingBalance,
};
1 change: 1 addition & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FUNDS_MIGRATION_AGE_SINCE_ACTIVATION_END = 150;

const PEGIN_REJECTION_REASONS = {
PEGIN_CAP_SURPASSED_REASON: '1',
LEGACY_PEGIN_MULTISIG_SENDER: '2',
PEGIN_V1_INVALID_PAYLOAD_REASON: '4',
INVALID_AMOUNT: '5',
};
Expand Down
108 changes: 88 additions & 20 deletions lib/tests/2wp.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { sendPegin,
mineForPeginRegistration,
createExpectedRejectedPeginEvent,
} = require('../2wp-utils');
const { getBtcAddressBalanceInSatoshis, waitForBitcoinMempoolToGetTxs } = require('../btc-utils');
const { getBtcAddressBalanceInSatoshis, waitForBitcoinMempoolToGetTxs, fundBtcAddressCheckingBalance } = require('../btc-utils');
const { ensure0x } = require('../utils');
const bitcoinJsLib = require('bitcoinjs-lib');
const { createPeginV1TxData } = require('pegin-address-verificator');
Expand All @@ -40,7 +40,7 @@ const execute = (description, getRskHost) => {

federationAddress = await bridge.methods.getFederationAddress().call();
minimumPeginValueInSatoshis = Number(await bridge.methods.getMinimumLockTxValue().call());
btcFeeInSatoshis = btcToSatoshis(await btcTxHelper.getFee());
btcFeeInSatoshis = Number(btcToSatoshis(await btcTxHelper.getFee()));

await btcTxHelper.importAddress(federationAddress, 'federation');

Expand All @@ -57,7 +57,7 @@ const execute = (description, getRskHost) => {

// Act

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis));
const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)));

// Assert

Expand Down Expand Up @@ -90,7 +90,7 @@ const execute = (description, getRskHost) => {

// Act

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis));
const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)));

// Assert

Expand All @@ -116,8 +116,8 @@ const execute = (description, getRskHost) => {
// Arrange

const initial2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
const senderRecipientInfo1 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper);
const senderRecipientInfo2 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper);
const senderRecipientInfo1 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper, 'legacy', 2);
const senderRecipientInfo2 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper, 'legacy', 1);
const initialSender1AddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo1.btcSenderAddressInfo.address);
const initialSender2AddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo2.btcSenderAddressInfo.address);

Expand Down Expand Up @@ -184,8 +184,8 @@ const execute = (description, getRskHost) => {
// Arrange

const initial2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
const senderRecipientInfo1 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper);
const senderRecipientInfo2 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper);
const senderRecipientInfo1 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper, 'legacy', 2);
const senderRecipientInfo2 = await createSenderRecipientInfo(rskTxHelper, btcTxHelper, 'legacy', 1);
const initialSender1AddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo1.btcSenderAddressInfo.address);
const initialSender2AddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo2.btcSenderAddressInfo.address);

Expand Down Expand Up @@ -262,7 +262,7 @@ const execute = (description, getRskHost) => {

const peginV1Data = [Buffer.from(createPeginV1TxData(peginV1RskRecipientAddress), 'hex')];

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis), peginV1Data);
const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)), peginV1Data);

// Assert

Expand Down Expand Up @@ -304,7 +304,7 @@ const execute = (description, getRskHost) => {

const blockNumberBeforePegin = await rskTxHelper.getBlockNumber();

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis));
const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)));
// Funds of a pegin with value below minimum are lost. But calling triggerRelease here to ensure that nothing will be refunded.
await triggerRelease(rskTxHelpers, btcTxHelper);

Expand Down Expand Up @@ -348,7 +348,7 @@ const execute = (description, getRskHost) => {

const blockNumberBeforePegin = await rskTxHelper.getBlockNumber();

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, satoshisToBtc(peginValueInSatoshis), peginV1Data);
const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)), peginV1Data);
// Funds of a pegin with value below minimum are lost. But calling triggerRelease here to ensure that nothing will be refunded.
await triggerRelease(rskTxHelpers, btcTxHelper);

Expand All @@ -375,6 +375,46 @@ const execute = (description, getRskHost) => {

});

it('should reject and refund a legacy pegin from a multisig account with the value exactly minimum', async () => {

// Arrange

const initial2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
const multisigSenderAddressInfo = await btcTxHelper.generateMultisigAddress(3, 2, 'legacy');
const peginValueInSatoshis = minimumPeginValueInSatoshis;
await fundBtcAddressCheckingBalance(btcTxHelper, multisigSenderAddressInfo.address, Number(satoshisToBtc(peginValueInSatoshis + btcFeeInSatoshis)));

// Act

const btcPeginTxHash = await sendPegin(rskTxHelper, btcTxHelper, multisigSenderAddressInfo, Number(satoshisToBtc(peginValueInSatoshis)));

// Assert

// The btc pegin tx is marked as processed by the bridge.
const isBtcTxHashAlreadyProcessed = await bridge.methods.isBtcTxHashAlreadyProcessed(btcPeginTxHash).call();
expect(isBtcTxHashAlreadyProcessed).to.be.true;

// The rejected_pegin event is emitted with the expected values
const btcTxHashProcessedHeight = Number(await bridge.methods.getBtcTxHashProcessedHeight(btcPeginTxHash).call());
await assertExpectedRejectedPeginEventIsEmitted(btcPeginTxHash, btcTxHashProcessedHeight, PEGIN_REJECTION_REASONS.LEGACY_PEGIN_MULTISIG_SENDER);

await assert2wpBalancesDuringPeginRefundSetup(initial2wpBalances, peginValueInSatoshis);

// Expecting the multisig btc sender balance to be zero after the pegin.
const senderAddressBalanceAfterPegin = await getBtcAddressBalanceInSatoshis(btcTxHelper, multisigSenderAddressInfo.address);
expect(Number(senderAddressBalanceAfterPegin)).to.be.equal(0);

// We are expecting a refund pegout to go through. So, let's push it.
await triggerRelease(rskTxHelpers, btcTxHelper);

await assert2wpBalanceIsUnchanged(initial2wpBalances);

// Finally, the multisig btc sender address should have received the funds back minus certain fee.
const finalSenderAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, multisigSenderAddressInfo.address);
expect(finalSenderAddressBalanceInSatoshis).to.be.above(peginValueInSatoshis - btcFeeInSatoshis).and.below(peginValueInSatoshis)

});

});

}
Expand Down Expand Up @@ -417,23 +457,51 @@ const assert2wpBalancesAfterSuccessfulPegin = async (initial2wpBalances, peginVa
};

/**
* Gets the final 2wp balances (Federation, Bridge utxos and bridge rsk balances) and compares them to the `initial2wpBalances` to assert the expected values based on a rejected pegin due to low amount.
* Checks that after a rejected pegin because of a low amount (below minimum), the federation balance is increased by the peginValueInSatoshis amount, because the funds will not be refunded, while the Bridge utxos and Bridge rsk balances stay intact.
* Gets the final 2wp balances (Federation, Bridge utxos and bridge rsk balances) and compares them to the `initial2wpBalances` to assert the expected values based on a rejected pegin with refund.
* Checks that after a rejected pegin because of a low amount (below minimum), the federation balance is increased by the peginValueInSatoshis amount, while the Bridge utxos and Bridge rsk balances stay intact.
* @param {{federationAddressBalanceInSatoshis: number, bridgeUtxosBalanceInSatoshis: number, bridgeBalanceInWeisBN: BN}} initial2wpBalances
* @param {number} peginValueInSatoshis the value of the pegin in satoshis by which only the federation balance is to be increased.
* @param {number} peginValueInSatoshis the value of the pegin in satoshis by which only the federation balance is expected to have increased.
* @returns {Promise<void>}
*/
const assert2wpBalancesPeginRejectedBelowMinimum = async (initial2wpBalances, peginValueInSatoshis) => {
const assert2wpBalancesMidRejection = async (initial2wpBalances, peginValueInSatoshis) => {

const final2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);

expect(final2wpBalances.federationAddressBalanceInSatoshis).to.be.equal(initial2wpBalances.federationAddressBalanceInSatoshis + peginValueInSatoshis);

expect(final2wpBalances.bridgeUtxosBalanceInSatoshis).to.be.equal(initial2wpBalances.bridgeUtxosBalanceInSatoshis);

const final2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
expect(final2wpBalances.bridgeBalanceInWeisBN.eq(initial2wpBalances.bridgeBalanceInWeisBN)).to.be.true;

};
/**
* Calls `assert2wpBalancesMidRejection` since this logic matches with that function's logic.
* @param {{federationAddressBalanceInSatoshis: number, bridgeUtxosBalanceInSatoshis: number, bridgeBalanceInWeisBN: BN}} initial2wpBalances
* @param {number} peginValueInSatoshis
* @returns {Promise<void>}
*/
const assert2wpBalancesPeginRejectedBelowMinimum = async (initial2wpBalances, peginValueInSatoshis) => {

await assert2wpBalancesMidRejection(initial2wpBalances, peginValueInSatoshis);

expect(final2wpBalances.federationAddressBalanceInSatoshis).to.be.equal(initial2wpBalances.federationAddressBalanceInSatoshis + peginValueInSatoshis);
};

expect(final2wpBalances.bridgeUtxosBalanceInSatoshis).to.be.equal(initial2wpBalances.bridgeUtxosBalanceInSatoshis);
/**
* Calls `assert2wpBalancesMidRejection` since this logic matches with that function's logic.
* @param {{federationAddressBalanceInSatoshis: number, bridgeUtxosBalanceInSatoshis: number, bridgeBalanceInWeisBN: BN}} initial2wpBalances
* @param {number} peginValueInSatoshis
* @returns {Promise<void>}
*/
const assert2wpBalancesDuringPeginRefundSetup = async (initial2wpBalances, peginValueInSatoshis) => {

expect(final2wpBalances.bridgeBalanceInWeisBN.eq(initial2wpBalances.bridgeBalanceInWeisBN)).to.be.true;
await assert2wpBalancesMidRejection(initial2wpBalances, peginValueInSatoshis);

};

};
const assert2wpBalanceIsUnchanged = async (initial2wpBalances) => {
const final2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
expect(final2wpBalances).to.be.deep.equal(initial2wpBalances);
};

const addInputs = (tx, utxos) => {
utxos.forEach(utxo => {
Expand Down