Skip to content

Commit 7c12214

Browse files
authored
chore: (A-115) add reqresp effectiveness network test (#17905)
- test tx dropping at various percentages (ie. 10%, 30%, 50%) at high TPS and log included/failed transactions and latency of tx inclusion - add validator pod rolling with tx dropping flags via helm upgrade function to utils Note: Only logging for now as these tests won't be run outside of testing networks
2 parents cb51d0b + 02f65af commit 7c12214

File tree

2 files changed

+247
-2
lines changed

2 files changed

+247
-2
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee';
2+
import type { AztecNode } from '@aztec/aztec.js/node';
3+
import { readFieldCompressedString } from '@aztec/aztec.js/utils';
4+
import { createLogger } from '@aztec/foundation/log';
5+
import { sleep } from '@aztec/foundation/sleep';
6+
import { ProvenTx, TestWallet, proveInteraction } from '@aztec/test-wallet/server';
7+
8+
import { jest } from '@jest/globals';
9+
import type { ChildProcess } from 'child_process';
10+
11+
import { getSponsoredFPCAddress } from '../fixtures/utils.js';
12+
import { createWalletAndAztecNodeClient, deploySponsoredTestAccounts } from './setup_test_wallets.js';
13+
import type { TestAccounts } from './setup_test_wallets.js';
14+
import { type TestConfig, setValidatorTxDrop, setupEnvironment, startPortForwardForRPC } from './utils.js';
15+
16+
describe('reqresp effectiveness under tx drop', () => {
17+
jest.setTimeout(60 * 60 * 1000);
18+
19+
const logger = createLogger(`e2e:spartan-test:reqresp-effectiveness`);
20+
21+
const config: TestConfig = { ...setupEnvironment(process.env) };
22+
const TEST_DURATION_SECONDS = 10;
23+
const TARGET_TPS = 10;
24+
const TOTAL_TXS = TEST_DURATION_SECONDS * TARGET_TPS;
25+
const MINT_AMOUNT = 10000n;
26+
27+
let wallet: TestWallet;
28+
let aztecNode: AztecNode;
29+
let cleanup: undefined | (() => Promise<void>);
30+
let testAccounts: TestAccounts;
31+
let recipient: any;
32+
const forwardProcesses: ChildProcess[] = [];
33+
34+
afterAll(async () => {
35+
// Reset validators to default (no tx drop)
36+
try {
37+
await setValidatorTxDrop({
38+
namespace: config.NAMESPACE,
39+
enabled: false,
40+
probability: 0,
41+
logger,
42+
});
43+
} catch (e) {
44+
logger.warn(`Failed to reset validator tx drop flags: ${String(e)}`);
45+
}
46+
await cleanup?.();
47+
forwardProcesses.forEach(p => p.kill());
48+
});
49+
50+
beforeAll(async () => {
51+
logger.info('Starting port forward for PXE');
52+
const { process: aztecRpcProcess, port: aztecRpcPort } = await startPortForwardForRPC(config.NAMESPACE);
53+
forwardProcesses.push(aztecRpcProcess);
54+
const rpcUrl = `http://127.0.0.1:${aztecRpcPort}`;
55+
56+
const {
57+
wallet: _wallet,
58+
aztecNode: _aztecNode,
59+
cleanup: _cleanup,
60+
} = await createWalletAndAztecNodeClient(rpcUrl, config.REAL_VERIFIER, logger);
61+
cleanup = _cleanup;
62+
wallet = _wallet;
63+
aztecNode = _aztecNode;
64+
testAccounts = await deploySponsoredTestAccounts(wallet, aztecNode, MINT_AMOUNT, logger);
65+
recipient = testAccounts.recipientAddress;
66+
const name = readFieldCompressedString(
67+
await testAccounts.tokenContract.methods.private_get_name().simulate({ from: testAccounts.tokenAdminAddress }),
68+
);
69+
expect(name).toBe(testAccounts.tokenName);
70+
});
71+
72+
async function runLoadAndMeasure(probability: number) {
73+
logger.info(`Applying tx drop: enabled=true, probability=${probability}`);
74+
75+
// Pre-prove load
76+
const sponsor = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress());
77+
const transferAmount = 1n;
78+
const txs: ProvenTx[] = await Promise.all(
79+
Array.from({ length: TOTAL_TXS }, () =>
80+
proveInteraction(
81+
wallet,
82+
testAccounts.tokenContract.methods.transfer_in_public(
83+
testAccounts.tokenAdminAddress,
84+
recipient,
85+
transferAmount,
86+
0,
87+
),
88+
{ from: testAccounts.tokenAdminAddress, fee: { paymentMethod: sponsor } },
89+
),
90+
),
91+
);
92+
93+
if (!(probability == 0)) {
94+
await setValidatorTxDrop({
95+
namespace: config.NAMESPACE,
96+
enabled: true,
97+
probability,
98+
logger,
99+
});
100+
}
101+
102+
const sends: Array<{ sentAt: number; promise: ReturnType<ProvenTx['send']> }[]> = [];
103+
let sentSoFar = 0;
104+
for (let sec = 0; sec < TEST_DURATION_SECONDS; sec++) {
105+
const secondStart = Date.now();
106+
const batch = txs.splice(0, TARGET_TPS);
107+
const sentBatch = batch.map((tx, i) => {
108+
const sent = tx.send();
109+
logger.info(`p=${probability} sec ${sec + 1}: sent tx ${sentSoFar + i + 1}`);
110+
return { sentAt: Date.now(), promise: sent };
111+
});
112+
sends.push(sentBatch);
113+
sentSoFar += batch.length;
114+
const elapsed = Date.now() - secondStart;
115+
if (elapsed < 1000) {
116+
await sleep(1000 - elapsed);
117+
}
118+
}
119+
120+
// Collect tx inclusion time
121+
const latencies: number[] = [];
122+
let included = 0;
123+
let failed = 0;
124+
await Promise.all(
125+
sends.flat().map(async ({ sentAt, promise }, idx) => {
126+
try {
127+
await promise.wait({ timeout: 180, interval: 1, ignoreDroppedReceiptsFor: 2 });
128+
const receipt = await promise.getReceipt();
129+
if (receipt?.blockNumber !== undefined) {
130+
included++;
131+
const l = Date.now() - sentAt;
132+
latencies.push(l);
133+
} else {
134+
failed++;
135+
}
136+
} catch (err) {
137+
failed++;
138+
logger.warn(`tx ${idx + 1} failed: ${String(err)}`);
139+
}
140+
}),
141+
);
142+
143+
const pct = (p: number) => latencies[Math.floor((latencies.length - 1) * p)] ?? 0;
144+
latencies.sort((a, b) => a - b);
145+
const p50 = pct(0.5);
146+
const p90 = pct(0.9);
147+
const p99 = pct(0.99);
148+
149+
logger.info(
150+
`Drop p=${probability}: included=${included}/${TOTAL_TXS}, failed=${failed}, latency(ms) p50=${p50}, p90=${p90}, p99=${p99}`,
151+
);
152+
153+
expect(included + failed).toBe(TOTAL_TXS);
154+
// Soft assertion: inclusion should remain reasonable even under drop
155+
expect(included).toBeGreaterThan(0);
156+
}
157+
158+
it('measures req/resp effectiveness across drop probabilities', async () => {
159+
// Tx drop probabilities
160+
for (const p of [0.1, 0.3, 0.5]) {
161+
await runLoadAndMeasure(p);
162+
}
163+
});
164+
});

yarn-project/end-to-end/src/spartan/utils.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ function createHelmCommand({
278278
namespace: string;
279279
valuesFile: string | undefined;
280280
timeout: string;
281-
values: Record<string, string | number>;
281+
values: Record<string, string | number | boolean>;
282282
reuseValues?: boolean;
283283
}) {
284284
const valuesFileArgs = valuesFile ? `--values ${helmChartDir}/values/${valuesFile}` : '';
@@ -633,7 +633,7 @@ export async function installTransferBot({
633633
namespace,
634634
valuesFile: undefined,
635635
timeout,
636-
values: values as unknown as Record<string, string | number>,
636+
values: values as unknown as Record<string, string | number | boolean>,
637637
reuseValues,
638638
});
639639

@@ -661,6 +661,87 @@ export async function uninstallTransferBot(namespace: string, logger: Logger) {
661661
);
662662
}
663663

664+
/**
665+
* Enables or disables probabilistic transaction dropping on validators and waits for rollout.
666+
* Wired to env vars P2P_DROP_TX and P2P_DROP_TX_CHANCE via Helm values.
667+
*/
668+
export async function setValidatorTxDrop({
669+
namespace,
670+
enabled,
671+
probability,
672+
logger,
673+
}: {
674+
namespace: string;
675+
enabled: boolean;
676+
probability: number;
677+
logger: Logger;
678+
}) {
679+
const drop = enabled ? 'true' : 'false';
680+
const prob = String(probability);
681+
682+
const selectors = ['app=validator', 'app.kubernetes.io/component=validator'];
683+
let updated = false;
684+
for (const selector of selectors) {
685+
try {
686+
const list = await execAsync(`kubectl get statefulset -l ${selector} -n ${namespace} --no-headers -o name | cat`);
687+
const names = list.stdout
688+
.split('\n')
689+
.map(s => s.trim())
690+
.filter(Boolean);
691+
if (names.length === 0) {
692+
continue;
693+
}
694+
const cmd = `kubectl set env statefulset -l ${selector} -n ${namespace} P2P_DROP_TX=${drop} P2P_DROP_TX_CHANCE=${prob}`;
695+
logger.info(`command: ${cmd}`);
696+
await execAsync(cmd);
697+
updated = true;
698+
} catch (e) {
699+
logger.warn(`Failed to update validators with selector ${selector}: ${String(e)}`);
700+
}
701+
}
702+
703+
if (!updated) {
704+
logger.warn(`No validator StatefulSets found in ${namespace}. Skipping tx drop toggle.`);
705+
return;
706+
}
707+
708+
// Restart validator pods to ensure env vars take effect and wait for readiness
709+
await restartValidators(namespace, logger);
710+
}
711+
712+
export async function restartValidators(namespace: string, logger: Logger) {
713+
const selectors = ['app=validator', 'app.kubernetes.io/component=validator'];
714+
let any = false;
715+
for (const selector of selectors) {
716+
try {
717+
const { stdout } = await execAsync(`kubectl get pods -l ${selector} -n ${namespace} --no-headers -o name | cat`);
718+
if (!stdout || stdout.trim().length === 0) {
719+
continue;
720+
}
721+
any = true;
722+
await deleteResourceByLabel({ resource: 'pods', namespace, label: selector });
723+
} catch (e) {
724+
logger.warn(`Error restarting validator pods with selector ${selector}: ${String(e)}`);
725+
}
726+
}
727+
728+
if (!any) {
729+
logger.warn(`No validator pods found to restart in ${namespace}.`);
730+
return;
731+
}
732+
733+
// Wait for either label to be Ready
734+
for (const selector of selectors) {
735+
try {
736+
await waitForResourceByLabel({ resource: 'pods', namespace, label: selector });
737+
return;
738+
} catch {
739+
// try next
740+
}
741+
}
742+
logger.warn(`Validator pods did not report Ready; continuing.`);
743+
}
744+
664745
export async function enableValidatorDynamicBootNode(
665746
instanceName: string,
666747
namespace: string,

0 commit comments

Comments
 (0)