Skip to content

Commit

Permalink
feat: add flood script
Browse files Browse the repository at this point in the history
  • Loading branch information
hstove committed Mar 22, 2024
1 parent 12c9dac commit 9d423ba
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 9 deletions.
211 changes: 211 additions & 0 deletions stacking/flood.ts
@@ -0,0 +1,211 @@
import { StacksTestnet } from '@stacks/network';
import { StackingClient } from '@stacks/stacking';
import {
TransactionVersion,
getAddressFromPrivateKey,
getNonce,
makeSTXTokenTransfer,
broadcastTransaction,
makeRandomPrivKey,
StacksTransaction,
makeContractDeploy,
makeContractCall,
tupleCV,
uintCV,
} from '@stacks/transactions';
import { readFileSync } from 'fs';
import { config } from 'dotenv';

if (process.argv.slice(2).length > 0) {
config({ path: './tx-broadcaster.env' });
}
import { bytesToHex } from '@stacks/common';
import { logger, parseEnvInt, contractsApi, accountsApi } from './common';

const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2');
const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`;
const network = new StacksTestnet({ url });
const EPOCH_30_START = parseInt(process.env.STACKS_30_HEIGHT ?? '0');

const bootstrapperKey = process.env.BOOTSTRAPPER_KEY!;
const bootstrapper = {
privKey: bootstrapperKey,
stxAddress: getAddressFromPrivateKey(bootstrapperKey, TransactionVersion.Testnet),
};

const client = new StackingClient(bootstrapper.stxAddress, network);

const floodContract = readFileSync('./flooder.clar', { encoding: 'utf-8' });

const NUM_FLOODERS = parseEnvInt('NUM_FLOODERS', false) ?? 0;
// per account, how many tx to make per run
const TX_PER_FLOOD = parseEnvInt('TX_PER_FLOOD', false) ?? 10;

const flooders: { privKey: string; stxAddress: string; nonce: bigint }[] = [];

for (let i = 0; i < NUM_FLOODERS; i++) {
const privKey = makeRandomPrivKey();
flooders.push({
privKey: bytesToHex(privKey.data),
stxAddress: getAddressFromPrivateKey(privKey.data, TransactionVersion.Testnet),
nonce: BigInt(0),
});
}

let hasSentToFlooders = false;
const floodContractDeployer = bootstrapper.stxAddress;

async function bootstrapFlooders() {
const nonce = await getNonce(bootstrapper.stxAddress, network);
logger.info('Bootstrapping flooders');
// sync iterate
let i = 0n;
let allWorked = true;
for (const flooder of flooders) {
const tx = await makeSTXTokenTransfer({
recipient: flooder.stxAddress,
amount: 1000000 * 10_000, // 10k STX
senderKey: bootstrapper.privKey,
network,
nonce: nonce + i,
fee: 300,
anchorMode: 'any',
});
i++;
const ok = await broadcast(tx, bootstrapper.stxAddress);
if (!ok) {
allWorked = false;
break;
}
}
if (!(await isContractDeployed(floodContractDeployer))) {
const ok = await broadcast(
await makeContractDeploy({
senderKey: bootstrapper.privKey,
nonce: nonce + i,
contractName: 'flood',
codeBody: floodContract,
fee: 3000,
anchorMode: 'any',
network,
}),
bootstrapper.stxAddress
);
if (!ok) allWorked = false;
}
if (allWorked) {
hasSentToFlooders = true;
}
}

async function isContractDeployed(address: string) {
try {
const result = await contractsApi.getContractSource({
contractAddress: address,
contractName: 'flood',
});
return !!result.source;
} catch (e) {
return false;
}
}

async function run() {
if (hasSentToFlooders) {
await flood();
} else {
await bootstrapFlooders();
}
}

async function flood() {
const accountFloods = flooders.map(async (flooder, n) => {
// const nonce = await getNonce(flooder.stxAddress, network);
const nonces = accountsApi.getAccountNonces({
principal: flooder.stxAddress,
});
const nonce = ((await nonces).last_executed_tx_nonce ?? -1) + 1;
logger.info(`Flooder ${n} has nonce ${nonce.toString()}`);
// return { ...account, nonce };
let txFloods = new Array(TX_PER_FLOOD).fill(0).map(async (_, i) => {
// const nonce = getFlooderNonce(flooder.stxAddress);
const tx = await makeContractCall({
contractAddress: floodContractDeployer,
contractName: 'flood',
functionName: 'set-data',
functionArgs: [uintCV(1), uintCV(2), uintCV(3)],
senderKey: flooder.privKey,
nonce: nonce + i,
anchorMode: 'any',
network,
fee: 10000,
});
await broadcast(tx, flooder.stxAddress);
});
await Promise.all(txFloods);
});
await Promise.all(accountFloods);
}

async function broadcast(tx: StacksTransaction, sender?: string) {
const txType = tx.payload.payloadType;
const label = sender ? accountLabel(sender) : 'Unknown';
const broadcastResult = await broadcastTransaction(tx, network);
if (broadcastResult.error) {
logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`);
return false;
} else {
if (label.includes('Flooder')) return;
logger.info(`Broadcast ${txType} from ${label} tx=${broadcastResult.txid}`);
return true;
}
}

async function waitForNakamoto() {
while (true) {
try {
const poxInfo = await client.getPoxInfo();
if (poxInfo.current_burnchain_block_height! <= EPOCH_30_START) {
logger.info(
`Nakamoto not activated yet, waiting... (current=${poxInfo.current_burnchain_block_height}), (epoch3=${EPOCH_30_START})`
);
} else {
logger.info(
`Nakamoto activation height reached, ready to submit txs for Nakamoto block production`
);
break;
}
} catch (error) {
if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) {
logger.info(`Stacks node not ready, waiting...`);
} else {
logger.error('Error getting pox info:', error);
}
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
}

function accountLabel(address: string) {
if (address === bootstrapper.stxAddress) {
return 'Bootstrapper';
}
const flooderIndex = flooders.findIndex(flooder => flooder.stxAddress === address);
if (flooderIndex !== -1) {
return `Flooder #${flooderIndex}`;
}
return `Unknown (${address})`;
}

async function loop() {
await waitForNakamoto();
while (true) {
try {
await run();
} catch (e) {
logger.error(e, 'Error submitting stx-transfer tx:');
}
await new Promise(resolve => setTimeout(resolve, broadcastInterval * 1000));
}
}
loop();
31 changes: 31 additions & 0 deletions stacking/flooder.clar
@@ -0,0 +1,31 @@
(define-map big-map {
a: uint,
b: uint,
c: uint
} {
a: uint,
b: uint,
c: uint,
})

(define-public (set-data (a uint) (b uint) (c uint))
(begin
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(map-get? big-map { a: a, b: b, c: c })
(map-set big-map { a: a, b: b, c: c } { a: a, b: b, c: c })
(ok true)
)
)
3 changes: 3 additions & 0 deletions stacking/monitor.ts
Expand Up @@ -171,6 +171,9 @@ async function loop() {
}
if (showStxBlockMsg && info.txs.length > 0) {
info.txs.forEach(({ contract_call, sender_address, tx_status, ...tx }) => {
if (contract_call.contract_id.includes('flood')) {
return;
}
loopLog.info(
{
sender_address,
Expand Down
5 changes: 4 additions & 1 deletion stacking/package.json
Expand Up @@ -4,7 +4,8 @@
"description": "",
"type": "module",
"scripts": {
"format": "prettier --write *.ts"
"format": "prettier --write *.ts",
"flood": "dotenvx run -f ./tx-broadcaster.env -- npx tsx flood.ts"
},
"keywords": [],
"author": "",
Expand All @@ -18,10 +19,12 @@
"@stacks/stacking": "6.11.4-pr.36558cf.0",
"@stacks/stacks-blockchain-api-types": "7.8.2",
"@stacks/transactions": "6.11.4-pr.36558cf.0",
"dotenv": "^16.4.5",
"pino": "^8.19.0",
"pino-pretty": "^10.3.1"
},
"devDependencies": {
"@dotenvx/dotenvx": "^0.26.0",
"@stacks/prettier-config": "^0.0.10",
"tsx": "4.7.1"
},
Expand Down
12 changes: 12 additions & 0 deletions stacking/tx-broadcaster.env
@@ -0,0 +1,12 @@
STACKS_CORE_RPC_HOST=localhost
STACKS_CORE_RPC_PORT=3999
NAKAMOTO_BLOCK_INTERVAL=2
STACKS_30_HEIGHT=131
ACCOUNT_KEYS=0d2f965b472a82efd5a96e6513c8b9f7edc725d5c96c7d35d6c722cedeb80d1b01,975b251dd7809469ef0c26ec3917971b75c51cd73a022024df4bf3b232cc2dc001,c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01
NUM_FLOODERS=15
TX_PER_FLOOD=15
STACKS_25_HEIGHT=108
STACKING_KEYS: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01,b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401,7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01
POX_PREPARE_LENGTH=5
POX_REWARD_LENGTH=20
BOOTSTRAPPER_KEY=66b7a77a3e0abc2cddaa51ed38fc4553498e19d3620ef08eb141afcfd0e3f5b501
38 changes: 30 additions & 8 deletions stacking/tx-broadcaster.ts
Expand Up @@ -6,7 +6,9 @@ import {
getNonce,
makeSTXTokenTransfer,
broadcastTransaction,
StacksTransaction,
} from '@stacks/transactions';
import { logger } from './common';

const broadcastInterval = parseInt(process.env.NAKAMOTO_BLOCK_INTERVAL ?? '2');
const url = `http://${process.env.STACKS_CORE_RPC_HOST}:${process.env.STACKS_CORE_RPC_PORT}`;
Expand All @@ -33,7 +35,7 @@ async function run() {
const sender = accountNonces[0];
const recipient = accountNonces[1];

console.log(
logger.info(
`Sending stx-transfer from ${sender.stxAddress} (nonce=${sender.nonce}) to ${recipient.stxAddress}`
);

Expand All @@ -46,12 +48,20 @@ async function run() {
fee: 300,
anchorMode: 'any',
});
await broadcast(tx, sender.stxAddress);
}

async function broadcast(tx: StacksTransaction, sender?: string) {
const txType = tx.payload.payloadType;
const label = sender ? accountLabel(sender) : 'Unknown';
const broadcastResult = await broadcastTransaction(tx, network);
if (broadcastResult.error) {
console.error('Error broadcasting stx-transfer', broadcastResult);
logger.error({ ...broadcastResult, account: label }, `Error broadcasting ${txType}`);
return false;
} else {
console.log(`Broadcast stx-transfer tx=${broadcastResult.txid}`);
if (label.includes('Flooder')) return true;
logger.debug(`Broadcast ${txType} from ${label} tx=${broadcastResult.txid}`);
return true;
}
}

Expand All @@ -60,29 +70,41 @@ async function waitForNakamoto() {
try {
const poxInfo = await client.getPoxInfo();
if (poxInfo.current_burnchain_block_height! <= EPOCH_30_START) {
console.log(`Nakamoto not activated yet, waiting... (current=${poxInfo.current_burnchain_block_height}), (epoch3=${EPOCH_30_START})`);
logger.info(
`Nakamoto not activated yet, waiting... (current=${poxInfo.current_burnchain_block_height}), (epoch3=${EPOCH_30_START})`
);
} else {
console.log(`Nakamoto activation height reached, ready to submit txs for Nakamoto block production`);
logger.info(
`Nakamoto activation height reached, ready to submit txs for Nakamoto block production`
);
break;
}
} catch (error) {
if (/(ECONNREFUSED|ENOTFOUND|SyntaxError)/.test(error.cause?.message)) {
console.log(`Stacks node not ready, waiting...`);
logger.info(`Stacks node not ready, waiting...`);
} else {
console.error('Error getting pox info:', error);
logger.error('Error getting pox info:', error);
}
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
}

function accountLabel(address: string) {
const accountIndex = accounts.findIndex(account => account.stxAddress === address);
if (accountIndex !== -1) {
return `Account #${accountIndex}`;
}
return `Unknown (${address})`;
}

async function loop() {
await waitForNakamoto();
while (true) {
try {
await run();
} catch (e) {
console.error('Error submitting stx-transfer tx:', e);
logger.error('Error submitting stx-transfer tx:', e);
}
await new Promise(resolve => setTimeout(resolve, broadcastInterval * 1000));
}
Expand Down

0 comments on commit 9d423ba

Please sign in to comment.