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

Using 'parameterized tests' for 2wp.js pegin tests #89

49 changes: 47 additions & 2 deletions lib/2wp-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const {
waitForRskMempoolToGetNewTxs,
waitAndUpdateBridge
} = require('./rsk-utils');
const { retryWithCheck } = require('./utils');
const { retryWithCheck, ensure0x } = require('./utils');
const { waitForBitcoinTxToBeInMempool, waitForBitcoinMempoolToGetTxs } = 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');
const btcEthUnitConverter = require('@rsksmart/btc-eth-unit-converter');

const peginVerifier = require('pegin-address-verificator');
Expand Down Expand Up @@ -198,7 +199,7 @@ const ensurePeginIsRegistered = async (rskTxHelper, peginBtcTxHash, expectedUtxo
* @param {RskTransactionHelper} rskTxHelper
* @param {BtcTransactionHelper} btcTxHelper
* @param {number} amountInBtc
* @returns {string} the pegin tx hash
* @returns {Promise<string>} the pegin tx hash
*/
const donateToBridge = async (rskTxHelper, btcTxHelper, donatingBtcAddressInformation, amountInBtc) => {
const data = [];
Expand Down Expand Up @@ -227,6 +228,48 @@ const disableWhitelisting = async (rskTxHelper, btcTxHelper, blockDelay = 1) =>
}
};

/**
* Creates a btc sender and rsk recipient information (private keys and addresses) and funds the btc sender address with the specified amount.
* @param {RskTransactionHelper} rskTxHelper to make transactions to the rsk network.
* @param {BtcTransactionHelper} btcTxHelper to make transactions to the bitcoin network.
* @param {string} type the btc address type to generate. Defaults to 'legacy'.
* @param {number} initialAmountToFundInBtc the initial amount to fund the btc sender address. Defaults to 1.
* @returns {Promise<{btcSenderAddressInfo: {address: string, privateKey: string}, rskRecipientRskAddressInfo: {address: string, privateKey: string}>}}
*/
const createSenderRecipientInfo = async (rskTxHelper, btcTxHelper, type = 'legacy', initialAmountToFundInBtc = 1) => {
const btcSenderAddressInfo = await btcTxHelper.generateBtcAddress(type);
const rskRecipientRskAddressInfo = getDerivedRSKAddressInformation(btcSenderAddressInfo.privateKey, btcTxHelper.btcConfig.network);
await rskTxHelper.importAccount(rskRecipientRskAddressInfo.privateKey);
await rskTxHelper.unlockAccount(rskRecipientRskAddressInfo.address);
initialAmountToFundInBtc && await btcTxHelper.fundAddress(btcSenderAddressInfo.address, initialAmountToFundInBtc);
return {
btcSenderAddressInfo,
rskRecipientRskAddressInfo
};
};

/**
* Creates a pegin_btc event with the specified parameters.
* @param {Object} partialExpectedEvent an object with some pegin_btc event default values.
* @param {string} rskRecipientRskAddress the rsk address that receives the funds expected to be in the event.
* @param {string} btcPeginTxHash the pegin btc tx hash expected to be in the event.
* @param {number} peginValueInSatoshis the pegin value in satoshis expected to be in the event.
* @param {string} protocolVersion the pegin protocol version expected to be in the event. Defaults to '0'.
* @returns {BridgeEvent}
*/
const createExpectedPeginBtcEvent = (partialExpectedEvent, rskRecipientRskAddress, btcPeginTxHash, peginValueInSatoshis, protocolVersion = '0') => {
const expectedEvent = {
...partialExpectedEvent,
arguments: {
receiver: rskRecipientRskAddress,
btcTxHash: ensure0x(btcPeginTxHash),
amount: `${peginValueInSatoshis}`,
protocolVersion,
},
}
return expectedEvent;
};

module.exports = {
sendTxToBridge,
assertRefundUtxosSameAsPeginUtxos,
Expand All @@ -240,4 +283,6 @@ module.exports = {
mineForPeginRegistration,
MIN_PEGOUT_VALUE_IN_RBTC,
disableWhitelisting,
createSenderRecipientInfo,
createExpectedPeginBtcEvent,
};
5 changes: 4 additions & 1 deletion lib/assertions/2wp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ const expect = require('chai').expect;
var {wait, removePrefix0x} = require('../utils');
var bitcoin = require('peglib').bitcoin;
var rsk = require('peglib').rsk;
const rskUtils = require('../rsk-utils');
const CustomError = require('../CustomError');

const {MAX_ESTIMATED_FEE_PER_PEGOUT, FEE_DIFFERENCE_PER_PEGOUT} = require('../constants');
const {encodeOutpointValuesAsMap, decodeOutpointValues} = require("../varint");

Expand Down Expand Up @@ -140,5 +143,5 @@ module.exports = {
assertLock: assertLock(btcClient, rskClient, pegClient),
}),
assertCallToPegoutBatchingBridgeMethods,
assertRejectedPeginEvent
assertRejectedPeginEvent,
};
10 changes: 9 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,14 @@ const PEGOUT_EVENTS = {
BATCH_PEGOUT_CREATED: "batch_pegout_created",
PEGOUT_TRANSACTION_CREATED: "pegout_transaction_created",
PEGOUT_CONFIRMED: "pegout_confirmed"
}
};

const PEGIN_EVENTS = {
PEGIN_BTC: {
name: "pegin_btc",
signature: '0x44cdc782a38244afd68336ab92a0b39f864d6c0b2a50fa1da58cafc93cd2ae5a'
}
};

module.exports = {
KEY_TYPE_BTC,
Expand All @@ -77,4 +84,5 @@ module.exports = {
PEGOUT_EVENTS,
FUNDS_MIGRATION_AGE_SINCE_ACTIVATION_BEGIN,
FUNDS_MIGRATION_AGE_SINCE_ACTIVATION_END,
PEGIN_EVENTS,
};
216 changes: 216 additions & 0 deletions lib/tests/2wp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
const expect = require('chai').expect;
const { getBridge } = require('../precompiled-abi-forks-util');
const { getBtcClient } = require('../btc-client-provider');
const { getRskTransactionHelpers, getRskTransactionHelper } = require('../rsk-tx-helper-provider');
const { btcToWeis, btcToSatoshis } = require('@rsksmart/btc-eth-unit-converter');
const { waitAndUpdateBridge, mineAndSync, findEventInBlock } = require('../rsk-utils');
const { PEGIN_EVENTS } = require("../constants");
const { waitForBitcoinTxToBeInMempool } = require('../btc-utils');
const {
ensurePeginIsRegistered,
donateToBridge,
createSenderRecipientInfo,
createExpectedPeginBtcEvent,
mineForPeginRegistration,
} = require('../2wp-utils');
const { ensure0x } = require('../utils');
const bitcoinJsLib = require('bitcoinjs-lib');

const DONATION_AMOUNT = 250;
const MINIMUM_PEGIN_VALUE_IN_BTC = 0.5;

let btcTxHelper;
let rskTxHelper;
let rskTxHelpers;
let bridge;
let federationAddress;

const setupBridgeDonation = async (rskTxHelpers, btcTxHelper) => {
const donatingBtcAddressInformation = await btcTxHelper.generateBtcAddress('legacy');
await mineAndSync(rskTxHelpers);
await btcTxHelper.fundAddress(donatingBtcAddressInformation.address, DONATION_AMOUNT + btcTxHelper.getFee());
await donateToBridge(rskTxHelpers[0], btcTxHelper, donatingBtcAddressInformation, DONATION_AMOUNT);
};

const addInputs = (tx, sendersUtxosInfo) => {
sendersUtxosInfo.flatMap(senderUtxosInfo => senderUtxosInfo.utxos).forEach(uxto => {
tx.addInput(Buffer.from(uxto.txid, 'hex').reverse(), uxto.vout);
});
};

const addChangeOutputs = (tx, sendersInfo, sendersChange) => {
sendersChange.forEach((change, index) => {
if(change > 0) {
tx.addOutput(
bitcoinJsLib.address.toOutputScript(sendersInfo[index].btcSenderAddressInfo.address, btcTxHelper.btcConfig.network),
Number(btcToSatoshis(change))
);
}
});
};

const addOutputsToFed = (tx, outputsToFed) => {
outputsToFed.forEach(outputAmount => {
tx.addOutput(
bitcoinJsLib.address.toOutputScript(federationAddress, btcTxHelper.btcConfig.network),
Number(btcToSatoshis(outputAmount))
);
});
};

const pushPegin = async (btcPeginTxHash, expectedUtxosCount) => {
await waitForBitcoinTxToBeInMempool(btcTxHelper, btcPeginTxHash);
await mineForPeginRegistration(rskTxHelper, btcTxHelper);
await ensurePeginIsRegistered(rskTxHelper, btcPeginTxHash, expectedUtxosCount);
};

const getSendersInfo = async (initialBtcSenderBalancesInBtc) => {
return await Promise.all(initialBtcSenderBalancesInBtc.map(initialBtcSenderBalance => createSenderRecipientInfo(rskTxHelper, btcTxHelper, 'legacy', initialBtcSenderBalance + btcTxHelper.getFee())));
};

const getSendersBtcAddressBalances = async (sendersInfo) => {
return await Promise.all(sendersInfo.map(senderInfo => btcTxHelper.getAddressBalance(senderInfo.btcSenderAddressInfo.address)));
};

const getSendersUtxosInfo = async (sendersInfo, btcSenderAmountsToSendToFed) => {
return await Promise.all(sendersInfo.map((senderInfo, index) => btcTxHelper.selectSpendableUTXOsFromAddress(senderInfo.btcSenderAddressInfo.address, btcSenderAmountsToSendToFed[index])));
};

const getSendersChange = (sendersUtxosInfo) => {
return sendersUtxosInfo.map(senderUtxosInfo => senderUtxosInfo.change - btcTxHelper.getFee());
};

const getSendersPrivateKeys = (sendersInfo) => {
return sendersInfo.map(senderInfo => senderInfo.btcSenderAddressInfo.privateKey);
};

const getTotalAmountToFed = (outputsToFed) => {
return outputsToFed.reduce((total, amount) => total + amount, 0);
};

const getFinalRskRecipientBalances = async (sendersInfo) => {
return await Promise.all(sendersInfo.map(senderInfo => rskTxHelper.getBalance(senderInfo.rskRecipientRskAddressInfo.address)));
};

const legacyPeginTestsHappyPath = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not simple legacy pegins. I consider these legacy pegins as border cases, as well as uncommon/complex legacy pegins, at least those ones sending multiple outputs to the fed, since there's no reason for wallet to do that.

That being said, I would add a prefix to this variable like uncommon/complex-LegacyPeginTests . wdyt?

{
description: 'should do legacy pegin with one input and one output to the federation',
initialBtcSenderBalancesInBtc: [ MINIMUM_PEGIN_VALUE_IN_BTC ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can fetch this value using an async function. You just need to execute it once you are executing the test.

btcSenderAmountsToSendToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC ],
outputsToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC ],
},
{
description: 'should do legacy pegin with multiple inputs from different accounts and one output to the federation',
initialBtcSenderBalancesInBtc: [ MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC ], // Each sender is funded with some amount
btcSenderAmountsToSendToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC ], // Each sender decides how much to send to the federation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btcSenderAmountsToSendToFed seems a bit complex name to me. Maybe peginSenderAmounts by including pegin, we can assume this refers to funds sent to fed. Wdyt?

outputsToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC * 3 ],
},
{
description: 'should do legacy pegin with multiple inputs from different accounts and two outputs to the federation',
initialBtcSenderBalancesInBtc: [ MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC ],
btcSenderAmountsToSendToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC ],
outputsToFed: [ MINIMUM_PEGIN_VALUE_IN_BTC, MINIMUM_PEGIN_VALUE_IN_BTC * 2 ], // Example of a pegin with multiple outputs to fed
},
];

const execute = (description, getRskHost) => {

describe(description, () => {

before(async () => {

rskTxHelpers = getRskTransactionHelpers();
btcTxHelper = getBtcClient();
rskTxHelper = getRskTransactionHelper(getRskHost());
bridge = getBridge(rskTxHelper.getClient());

federationAddress = await bridge.methods.getFederationAddress().call();
await btcTxHelper.importAddress(federationAddress, 'federation');

await waitAndUpdateBridge(rskTxHelper);
await setupBridgeDonation(rskTxHelpers, btcTxHelper);

});

legacyPeginTestsHappyPath.forEach(test => {

it(test.description, async () => {

// Arrange

const { initialBtcSenderBalancesInBtc, btcSenderAmountsToSendToFed, outputsToFed } = test;

const initialFederationAddressBalanceInBtc = Number(await btcTxHelper.getAddressBalance(federationAddress));

const sendersInfo = await getSendersInfo(initialBtcSenderBalancesInBtc);
const senderInfo1 = sendersInfo[0];

const initialSendersBtcAddressBalances = await getSendersBtcAddressBalances(sendersInfo);
const sendersUtxosInfo = await getSendersUtxosInfo(sendersInfo, btcSenderAmountsToSendToFed);
const sendersChange = getSendersChange(sendersUtxosInfo);

const tx = new bitcoinJsLib.Transaction();
addInputs(tx, sendersUtxosInfo);
addOutputsToFed(tx, outputsToFed);
addChangeOutputs(tx, sendersInfo, sendersChange);

const sendersPrivateKeys = getSendersPrivateKeys(sendersInfo);
const signedTx = await btcTxHelper.nodeClient.signTransaction(tx.toHex(), [], sendersPrivateKeys);

// Act

// Sending the pegin and ensuring the pegin is registered
const btcPeginTxHash = await btcTxHelper.nodeClient.sendTransaction(signedTx);
await pushPegin(btcPeginTxHash, outputsToFed.length);

// Assert

const isBtcTxHashAlreadyProcessed = await bridge.methods.isBtcTxHashAlreadyProcessed(btcPeginTxHash).call();
expect(isBtcTxHashAlreadyProcessed).to.be.true;

const totalAmountToFed = getTotalAmountToFed(outputsToFed);

// The expected pegin_btc event should be emitted
const recipient1RskAddressChecksumed = rskTxHelper.getClient().utils.toChecksumAddress(ensure0x(senderInfo1.rskRecipientRskAddressInfo.address));
const expectedEvent = createExpectedPeginBtcEvent(PEGIN_EVENTS.PEGIN_BTC, recipient1RskAddressChecksumed, btcPeginTxHash, btcToSatoshis(totalAmountToFed));
const btcTxHashProcessedHeight = Number(await bridge.methods.getBtcTxHashProcessedHeight(btcPeginTxHash).call());
const peginBtcEvent = await findEventInBlock(rskTxHelper, expectedEvent.name, btcTxHashProcessedHeight);
expect(peginBtcEvent).to.be.deep.equal(expectedEvent);

// The federation address should have received the total amount sent by the senders
const finalFederationAddressBalanceInBtc = Number(await btcTxHelper.getAddressBalance(federationAddress));
expect(finalFederationAddressBalanceInBtc).to.be.equal(initialFederationAddressBalanceInBtc + totalAmountToFed);

// The senders should have their balances reduced by the amount sent to the federation and the fee
const finalSendersBtcAddressBalances = await getSendersBtcAddressBalances(sendersInfo);

for(let i = 0; i < finalSendersBtcAddressBalances.length; i++) {
const actualFinalBalance = Number(btcToSatoshis(finalSendersBtcAddressBalances[i]));
const expectedFinalBalance = Number(btcToSatoshis(initialSendersBtcAddressBalances[i])) - Number(btcToSatoshis(btcSenderAmountsToSendToFed[i])) - Number(btcToSatoshis(btcTxHelper.getFee()));
expect(actualFinalBalance).to.be.equal(expectedFinalBalance);
}

const finalRskRecipientBalances = await getFinalRskRecipientBalances(sendersInfo);

// Extracting the first rsk recipient balance since only the first sender should have the total amount in rsk.
const [ firstRskRecipientBalance, ...restOfFinalRskRecipientBalances ] = finalRskRecipientBalances;
expect(Number(firstRskRecipientBalance)).to.be.equal(Number(btcToWeis(totalAmountToFed)));

// The other rsk recipients should have their balances unchanged
for(let i = 0; i < restOfFinalRskRecipientBalances.length; i++) {
const actualFinalBalance = Number(restOfFinalRskRecipientBalances[i]);
const expectedFinalBalance = 0;
expect(actualFinalBalance).to.be.equal(expectedFinalBalance);
}

});

});

});

}

module.exports = {
execute,
};
3 changes: 3 additions & 0 deletions tests/01_01_01-2wp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const twoWpTests = require('../lib/tests/2wp');

twoWpTests.execute('BTC <=> RSK 2WP', () => Runners.hosts.federate.host);