Skip to content
Merged
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
33 changes: 20 additions & 13 deletions bouncer/shared/send_btc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,19 @@ export async function sendVaultTransaction(
);
}

export async function waitForBtcTransaction(logger: Logger, txid: string, confirmations = 1) {
export async function waitForBtcTransaction(
logger: Logger,
txid: string,
confirmations = 1,
client = btcClient,
) {
logger.trace(
`Waiting for Btc transaction to be confirmed, txid: ${txid}, required confirmations: ${confirmations}`,
);
// Localnet btc blocktime is 5sec
const timeoutSeconds = 10 + 5 * confirmations;
for (let i = 0; i < timeoutSeconds; i++) {
const transactionDetails = await btcClient.getTransaction(txid);
const transactionDetails = await client.getTransaction(txid);

if (transactionDetails.confirmations < confirmations) {
await sleep(1000);
Expand All @@ -86,6 +91,7 @@ export async function sendBtc(
address: string,
amount: number | string,
confirmations = 1,
client = btcClient,
): Promise<string> {
// Btc client has a limit on the number of concurrent requests
let txid: string;
Expand All @@ -95,11 +101,11 @@ export async function sendBtc(
while (attempts < maxAttempts) {
try {
txid = (await btcClientMutex.runExclusive(async () =>
btcClient.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1),
client.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1),
)) as string;

if (confirmations > 0) {
await waitForBtcTransaction(logger, txid, confirmations);
await waitForBtcTransaction(logger, txid, confirmations, client);
}
return txid;
} catch (error) {
Expand Down Expand Up @@ -131,18 +137,19 @@ export async function sendBtcTransactionWithParent(
amount: number,
parentConfirmations: number,
childConfirmations: number,
client = btcClient,
): Promise<{ parentTxid: string; childTxid: string }> {
// Btc client has a limit on the number of concurrent requests
const txids = await btcClientMutex.runExclusive(async () => {
// create a new address in our wallet that we have the keys for
const intermediateAddress = await btcClient.getNewAddress();
const intermediateAddress = await client.getNewAddress();

// amount to use for the parent tx
// Note: bitcoin has 8 decimal places
const parentAmount = (amount * 1.1).toFixed(8);

// send the parent tx
const parentTxid = (await btcClient.sendToAddress(
const parentTxid = (await client.sendToAddress(
intermediateAddress,
parentAmount,
'',
Expand All @@ -157,11 +164,11 @@ export async function sendBtcTransactionWithParent(

// wait for inclusion in a block
if (parentConfirmations > 0) {
await waitForBtcTransaction(logger, parentTxid, parentConfirmations);
await waitForBtcTransaction(logger, parentTxid, parentConfirmations, client);
}

// Create a raw transaction for the child tx
const childRawTx = await btcClient.createRawTransaction(
const childRawTx = await client.createRawTransaction(
[
{
txid: parentTxid as string,
Expand All @@ -174,23 +181,23 @@ export async function sendBtcTransactionWithParent(
);

// Fund the child tx
const childFundedTx = (await btcClient.fundRawTransaction(childRawTx, {
changeAddress: await btcClient.getNewAddress(),
const childFundedTx = (await client.fundRawTransaction(childRawTx, {
changeAddress: await client.getNewAddress(),
feeRate: 0.00001,
lockUnspents: true,
})) as { hex: string };

// Sign the child tx
const childSignedTx = await btcClient.signRawTransactionWithWallet(childFundedTx.hex);
const childSignedTx = await client.signRawTransactionWithWallet(childFundedTx.hex);

// Send the signed tx
const childTxid = (await btcClient.sendRawTransaction(childSignedTx.hex)) as string;
const childTxid = (await client.sendRawTransaction(childSignedTx.hex)) as string;

return { parentTxid, childTxid };
});

if (childConfirmations > 0) {
await waitForBtcTransaction(logger, txids.childTxid, childConfirmations);
await waitForBtcTransaction(logger, txids.childTxid, childConfirmations, client);
}

return txids;
Expand Down
4 changes: 2 additions & 2 deletions bouncer/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,15 @@ export function ingressEgressPalletForChain(chain: Chain) {
}
}

export function getBtcClient(): Client {
export function getBtcClient(wallet: string = 'watch'): Client {
const endpoint = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332';

return new Client({
host: endpoint.split(':')[1].slice(2),
port: Number(endpoint.split(':')[2]),
username: 'flip',
password: 'flip',
wallet: 'watch',
wallet,
});
}

Expand Down
2 changes: 1 addition & 1 deletion bouncer/shared/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const prettyConsoleTransport = pino.transport({
},
});

const getIsoTime = () => {
export const getIsoTime = () => {
// Getting the time using the time function of pino, there might be a better way to do this.
const { time } = JSON.parse(`{"noop": "nothing"${pino.stdTimeFunctions.isoTime()}}`);
return time as string;
Expand Down
64 changes: 53 additions & 11 deletions bouncer/tests/broker_level_screening.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from 'axios';
import { Chain, InternalAsset } from '@chainflip/cli';
import Web3 from 'web3';
import { sendBtc, sendBtcTransactionWithParent } from 'shared/send_btc';
import { btcClient, sendBtc, sendBtcTransactionWithParent } from 'shared/send_btc';
import {
newAssetAddress,
sleep,
Expand All @@ -18,14 +18,16 @@ import {
observeBalanceIncrease,
observeCcmReceived,
observeFetch,
btcClientMutex,
getBtcClient,
} from 'shared/utils';
import { getChainflipApi, observeEvent } from 'shared/utils/substrate';
import Keyring from 'polkadot/keyring';
import { requestNewSwap } from 'shared/perform_swap';
import { FillOrKillParamsX128 } from 'shared/new_swap';
import { getBtcBalance } from 'shared/get_btc_balance';
import { TestContext } from 'shared/utils/test_context';
import { Logger } from 'shared/utils/logger';
import { getIsoTime, Logger } from 'shared/utils/logger';
import { getBalance } from 'shared/get_balance';
import { send } from 'shared/send';
import { submitGovernanceExtrinsic } from 'shared/cf_governance';
Expand Down Expand Up @@ -533,17 +535,37 @@ async function setWhitelistedBroker(brokerAddress: Uint8Array) {
// 1. No boost and early tx report -> tx is reported early and the swap is refunded.
// 2. Boost and early tx report -> tx is reported early and the swap is refunded.
// 3. Boost and late tx report -> tx is reported late and the swap is not refunded.
export function testBitcoin(testContext: TestContext, doBoost: boolean): Promise<void>[] {
export async function testBitcoin(
testContext: TestContext,
doBoost: boolean,
): Promise<Promise<void>[]> {
const logger = testContext.logger;

// we have to setup a separate wallet in order to not taint our main wallet, otherwise
// the deposit monitor will possibly reject transactions created by other tests, due
// to ancestor screening. This has been a source of bouncer flakiness in the past.
const taintedClient = await btcClientMutex.runExclusive(async () => {
const reply: any = await btcClient.createWallet(`tainted-${getIsoTime()}`, false, false, '');
if (!reply.name) {
throw new Error(`Could not create tainted wallet, with error ${reply.warning}`);
}
testContext.debug(`got new wallet for BLS test: ${reply.name}`);
return getBtcClient(reply.name);
});
const fundingAddress = await taintedClient.getNewAddress();
testContext.debug(`funding tainted wallet with 5btc to ${fundingAddress}`);
await sendBtc(testContext.logger, fundingAddress, 5, 1);
testContext.debug(`funding success!`);

// if we don't boost, we wait with our report for 1 block confirmation, otherwise we submit the report directly
const confirmationsBeforeReport = doBoost ? 0 : 1;

// send a single tx
const simple = brokerLevelScreeningTestBtc(
logger,
doBoost,
async (amount, address) => sendBtc(logger, address, amount, confirmationsBeforeReport),
async (amount, address) =>
sendBtc(logger, address, amount, confirmationsBeforeReport, taintedClient),
async (txId) => setTxRiskScore(txId, 9.0),
);

Expand All @@ -552,8 +574,16 @@ export function testBitcoin(testContext: TestContext, doBoost: boolean): Promise
logger,
doBoost,
async (amount, address) =>
(await sendBtcTransactionWithParent(logger, address, amount, 0, confirmationsBeforeReport))
.childTxid,
(
await sendBtcTransactionWithParent(
logger,
address,
amount,
0,
confirmationsBeforeReport,
taintedClient,
)
).childTxid,
async (txId) => setTxRiskScore(txId, 9.0),
);

Expand All @@ -562,14 +592,23 @@ export function testBitcoin(testContext: TestContext, doBoost: boolean): Promise
logger,
doBoost,
async (amount, address) =>
(await sendBtcTransactionWithParent(logger, address, amount, 2, confirmationsBeforeReport))
.childTxid,
(
await sendBtcTransactionWithParent(
logger,
address,
amount,
2,
confirmationsBeforeReport,
taintedClient,
)
).childTxid,
async (txId) => setTxRiskScore(txId, 9.0),
);

return [simple, sameBlockParentMarked, oldParentMarked];
}

/* eslint-disable @typescript-eslint/no-unused-vars */
async function testBitcoinVaultSwap(testContext: TestContext) {
const logger = testContext.logger;

Expand Down Expand Up @@ -612,6 +651,9 @@ export async function testBrokerLevelScreening(
// An alternative would be to increase the ArbEth safety margin on localnet.
// - ArbUsdc: we also don't test ArbUsdc rejections, they have caused tests to become flaky
// as well (PRO-2488).
// - Btc VaultSwaps: For bitcoin, due to ancestor screening, we have to make sure to use
// a dedicated "tainted" wallet. Since it's somewhat difficult to inject
// a different wallet into the `sendVaultSwap` flow, we disable the test for now.

// test rejection of swaps by the responsible broker
await Promise.all(
Expand All @@ -620,8 +662,8 @@ export async function testBrokerLevelScreening(
testEvm(testContext, 'Usdt', async (txId) => setTxRiskScore(txId, 9.0)),
testEvm(testContext, 'Usdc', async (txId) => setTxRiskScore(txId, 9.0)),
]
.concat(testBitcoin(testContext, false))
.concat(testBoostedDeposits ? testBitcoin(testContext, true) : []),
.concat(await testBitcoin(testContext, false))
.concat(testBoostedDeposits ? await testBitcoin(testContext, true) : []),
);

// test rejection of LP deposits and vault swaps:
Expand All @@ -635,7 +677,7 @@ export async function testBrokerLevelScreening(
testEvmLiquidityDeposit(testContext, 'Usdc', async (txId) => setTxRiskScore(txId, 9.0)),

// --- vault swaps ---
testBitcoinVaultSwap(testContext),
// testBitcoinVaultSwap(testContext),
testEvmVaultSwap(testContext, 'Eth', async (txId) => setTxRiskScore(txId, 9.0)),
testEvmVaultSwap(testContext, 'Usdc', async (txId) => setTxRiskScore(txId, 9.0)),
]);
Expand Down
Loading