Skip to content

Commit

Permalink
tests: Bouncer test for broker level screening (#5377)
Browse files Browse the repository at this point in the history
* tests: Added first draft of bouncer test

* chore: added current work in progress

* chore: added current status

* chore: restored

* chore: added current status

* chore: added current state

* tests: integrated broker level screening test

* chore: Tidy up

* chore: eslint / prettier

* chore: added some comments + smallish refactors

* chore: parse unknown to string type

* chore: Make test verification more explicit

* chore: addressed comments

* chore: removed from runAllConcurrentTests

* feature: added mode to run some tests concurrent in localnet

* chore: Fixed var name
  • Loading branch information
Janislav authored and dandanlen committed Nov 6, 2024
1 parent 7bbe120 commit 6e1fb1a
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 5 deletions.
2 changes: 2 additions & 0 deletions bouncer/commands/run_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { testDeltaBasedIngress } from '../tests/delta_based_ingress';
import { testCancelOrdersBatch } from '../tests/create_and_delete_multiple_orders';
import { depositChannelCreation } from '../tests/request_swap_deposit_address_with_affiliates';
import { testDCASwaps } from '../tests/DCA_test';
import { testBrokerLevelScreening } from '../tests/broker_level_screening';

async function main() {
const testName = process.argv[2];
Expand Down Expand Up @@ -67,6 +68,7 @@ async function main() {
testDeltaBasedIngress,
testCancelOrdersBatch,
depositChannelCreation,
testBrokerLevelScreening,
];

// Help message
Expand Down
18 changes: 13 additions & 5 deletions bouncer/shared/send_btc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Client from 'bitcoin-core';
import { sleep, btcClientMutex } from './utils';

export async function sendBtc(address: string, amount: number | string) {
export async function sendBtcAndReturnTxId(
address: string,
amount: number | string,
): Promise<string> {
const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332';
const client = new Client({
host: BTC_ENDPOINT.split(':')[1].slice(2),
Expand All @@ -12,19 +15,24 @@ export async function sendBtc(address: string, amount: number | string) {
});

// Btc client has a limit on the number of concurrent requests
const txid = await btcClientMutex.runExclusive(async () =>
const txId = (await btcClientMutex.runExclusive(async () =>
client.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1),
);
)) as string;

for (let i = 0; i < 50; i++) {
const transactionDetails = await client.getTransaction(txid);
const transactionDetails = await client.getTransaction(txId);

const confirmations = transactionDetails.confirmations;

if (confirmations < 1) {
await sleep(1000);
} else {
return;
break;
}
}
return Promise.resolve(txId);
}

export async function sendBtc(address: string, amount: number | string) {
await sendBtcAndReturnTxId(address, amount);
}
4 changes: 4 additions & 0 deletions bouncer/test_commands/broker_level_screening.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env -S pnpm tsx
import { testBrokerLevelScreening } from '../tests/broker_level_screening';

await testBrokerLevelScreening.run();
9 changes: 9 additions & 0 deletions bouncer/tests/all_concurrent_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { testCancelOrdersBatch } from './create_and_delete_multiple_orders';
import { testAllSwaps } from './all_swaps';
import { depositChannelCreation } from './request_swap_deposit_address_with_affiliates';
import { testDCASwaps } from './DCA_test';
import { testBrokerLevelScreening } from './broker_level_screening';

async function runAllConcurrentTests() {
// Specify the number of nodes via providing an argument to this script.
// Using regex because the localnet script passes in "3-node".
const match = process.argv[2] ? process.argv[2].match(/\d+/) : null;
const givenNumberOfNodes = match ? parseInt(match[0]) : null;
const numberOfNodes = givenNumberOfNodes ?? 1;
// If the third argument is not explicitly false, we assume it's true and we are in a localnet environment.
const addConcurrentLocalnetTests = process.argv[3] !== 'false';

const broadcastAborted = observeBadEvent(':BroadcastAborted', {
label: 'Concurrent broadcast aborted',
Expand Down Expand Up @@ -52,6 +55,12 @@ async function runAllConcurrentTests() {
tests.push(...multiNodeTests);
}

// Tests that only work with localnet but can be run concurrent.
if (addConcurrentLocalnetTests) {
const localnetTests = [testBrokerLevelScreening.run()];
tests.push(...localnetTests);
}

await Promise.all(tests);

await Promise.all([broadcastAborted.stop(), feeDeficitRefused.stop()]);
Expand Down
207 changes: 207 additions & 0 deletions bouncer/tests/broker_level_screening.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { randomBytes } from 'crypto';
import { execSync } from 'child_process';
import { InternalAsset } from '@chainflip/cli';
import { ExecutableTest } from '../shared/executable_test';
import { sendBtcAndReturnTxId } from '../shared/send_btc';
import {
hexStringToBytesArray,
newAddress,
sleep,
handleSubstrateError,
brokerMutex,
} 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';

const keyring = new Keyring({ type: 'sr25519' });
const broker = keyring.createFromUri('//BROKER_1');

/* eslint-disable @typescript-eslint/no-use-before-define */
export const testBrokerLevelScreening = new ExecutableTest('Broker-Level-Screening', main, 300);

/**
* Observes the balance of a BTC address and returns true if the balance changes. Times out after 100 seconds and returns false if the balance does not change.
*
* @param address - The address to observe the balance of.
* @returns - Whether the balance changed.
*/
async function observeBtcAddressBalanceChange(address: string): Promise<boolean> {
const MAX_RETRIES = 100;
const initialBalance = await getBtcBalance(address);
for (let i = 0; i < MAX_RETRIES; i++) {
await sleep(1000);
const balance = await getBtcBalance(address);
if (balance !== initialBalance) {
return Promise.resolve(true);
}
}
console.error(`BTC balance for ${address} did not change after ${MAX_RETRIES} seconds.`);
return Promise.resolve(false);
}

/**
* Generates a new address for an asset.
*
* @param asset - The asset to generate an address for.
* @param seed - The seed to generate the address with. If no seed is provided, a random one is generated.
* @returns - The new address.
*/
async function newAssetAddress(asset: InternalAsset, seed = null): Promise<string> {
return Promise.resolve(newAddress(asset, seed || randomBytes(32).toString('hex')));
}

/**
* Submits a transaction as tainted to the extrinsic on the state chain.
*
* @param txId - The txId to submit as tainted as byte array in the order it is on the Bitcoin chain - which
* is reverse of how it's normally displayed in block explorers.
*/
async function submitTxAsTainted(txId: number[]) {
await using chainflip = await getChainflipApi();
return brokerMutex.runExclusive(async () =>
chainflip.tx.bitcoinIngressEgress
.markTransactionAsTainted(txId)
.signAndSend(broker, { nonce: -1 }, handleSubstrateError(chainflip)),
);
}

/**
* Pauses or resumes the bitcoin block production. We send a command to the docker container to start or stop mining blocks.
*
* @param pause - Whether to pause or resume the block production.
* @returns - Whether the command was successful.
*/
function pauseBtcBlockProduction(pause: boolean): boolean {
try {
execSync(
pause
? 'docker exec bitcoin rm /root/mine_blocks'
: 'docker exec bitcoin touch /root/mine_blocks',
);
return true;
} catch (error) {
console.error(error);
return false;
}
}

/**
* Runs a test scenario for broker level screening based on the given parameters.
*
* @param amount - The deposit amount.
* @param doBoost - Whether to boost the deposit.
* @param refundAddress - The address to refund to.
* @param stopBlockProductionFor - The number of blocks to stop block production for. We need this to ensure that the tainted tx is on chain before the deposit is witnessed/prewitnessed.
* @param waitBeforeReport - The number of milliseconds to wait before reporting the tx as tainted.
* @returns - The the channel id of the deposit channel.
*/
async function brokerLevelScreeningTestScenario(
amount: string,
doBoost: boolean,
refundAddress: string,
stopBlockProductionFor = 0,
waitBeforeReport = 0,
): Promise<string> {
const destinationAddressForUsdc = await newAssetAddress('Usdc');
const refundParameters: FillOrKillParamsX128 = {
retryDurationBlocks: 0,
refundAddress,
minPriceX128: '0',
};
const swapParams = await requestNewSwap(
'Btc',
'Usdc',
destinationAddressForUsdc,
'brokerLevelScreeningTest',
undefined,
0,
true,
doBoost ? 100 : 0,
refundParameters,
);
const txId = await sendBtcAndReturnTxId(swapParams.depositAddress, amount);
if (stopBlockProductionFor > 0) {
pauseBtcBlockProduction(true);
}
await sleep(waitBeforeReport);
// Note: The bitcoin core js lib returns the txId in reverse order.
// On chain we expect the txId to be in the correct order (like the Bitcoin internal representation).
// Because of this we need to reverse the txId before submitting it as tainted.
await submitTxAsTainted(hexStringToBytesArray(txId).reverse());
await sleep(stopBlockProductionFor);
if (stopBlockProductionFor > 0) {
pauseBtcBlockProduction(false);
}
return Promise.resolve(swapParams.channelId.toString());
}

// -- Test suite for broker level screening --
//
// In this tests we are interested in the following scenarios:
//
// 1. No boost and early tx report -> Tainted tx is reported early and the swap is refunded.
// 2. Boost and early tx report -> Tainted tx is reported early and the swap is refunded.
// 3. Boost and late tx report -> Tainted tx is reported late and the swap is not refunded.
async function main() {
const MILLI_SECS_PER_BLOCK = 6000;
const BLOCKS_TO_WAIT = 2;

// 1. -- Test no boost and early tx report --
testBrokerLevelScreening.log('Testing broker level screening with no boost...');
let btcRefundAddress = await newAssetAddress('Btc');

await brokerLevelScreeningTestScenario(
'0.2',
false,
btcRefundAddress,
MILLI_SECS_PER_BLOCK * BLOCKS_TO_WAIT,
);

await observeEvent('bitcoinIngressEgress:TaintedTransactionRejected').event;
if (!(await observeBtcAddressBalanceChange(btcRefundAddress))) {
throw new Error(`Didn't receive funds refund to address ${btcRefundAddress} within timeout!`);
}

testBrokerLevelScreening.log(`Tainted transaction was rejected and refunded 👍.`);

// 2. -- Test boost and early tx report --
testBrokerLevelScreening.log(
'Testing broker level screening with boost and a early tx report...',
);
btcRefundAddress = await newAssetAddress('Btc');

await brokerLevelScreeningTestScenario(
'0.2',
true,
btcRefundAddress,
MILLI_SECS_PER_BLOCK * BLOCKS_TO_WAIT,
);
await observeEvent('bitcoinIngressEgress:TaintedTransactionRejected').event;

if (!(await observeBtcAddressBalanceChange(btcRefundAddress))) {
throw new Error(`Didn't receive funds refund to address ${btcRefundAddress} within timeout!`);
}
testBrokerLevelScreening.log(`Tainted transaction was rejected and refunded 👍.`);

// 3. -- Test boost and late tx report --
// Note: We expect the swap to be executed and not refunded because the tainted tx was reported too late.
testBrokerLevelScreening.log('Testing broker level screening with boost and a late tx report...');
btcRefundAddress = await newAssetAddress('Btc');

const channelId = await brokerLevelScreeningTestScenario(
'0.2',
true,
btcRefundAddress,
0,
MILLI_SECS_PER_BLOCK * BLOCKS_TO_WAIT,
);

await observeEvent('bitcoinIngressEgress:DepositFinalised', {
test: (event) => event.data.channelId === channelId,
}).event;

testBrokerLevelScreening.log(`Swap was executed and tainted transaction was not refunded 👍.`);
}

0 comments on commit 6e1fb1a

Please sign in to comment.