Skip to content

chore(contract-manager) Entropy stress test script #2588

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

Closed
wants to merge 3 commits into from
Closed
Changes from 1 commit
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
191 changes: 191 additions & 0 deletions contract_manager/scripts/latency_entropy_with_callback_stress_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
DefaultStore,
EvmChain,
EvmEntropyContract,
PrivateKey,
toPrivateKey,
} from "../src";
import { COMMON_DEPLOY_OPTIONS, findEntropyContract } from "./common";

const parser = yargs(hideBin(process.argv))
.usage(
"Requests random numbers from an entropy contract and measures the\n" +
"latency between request submission and fulfillment by the Fortuna keeper service.\n" +
"Usage: $0 --private-key <private-key> --chain <chain-id> | --all-chains <testnet|mainnet> --nrequests <number> --delay <delay>",
)
.options({
chain: {
type: "string",
desc: "test latency for the contract on this chain",
conflicts: "all-chains",
},
"all-chains": {
type: "string",
conflicts: "chain",
choices: ["testnet", "mainnet"],
desc: "test latency for all entropy contracts deployed either on mainnet or testnet",
},
"private-key": COMMON_DEPLOY_OPTIONS["private-key"],
"nrequests": {
type: "number",
desc: "number of requests to make",
default: 1,
},
"delay": {
type: "number",
desc: "delay between requests",
Copy link
Collaborator

Choose a reason for hiding this comment

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

mention that it is in seconds

Copy link
Member Author

Choose a reason for hiding this comment

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

ms

default: 25,
Copy link
Collaborator

Choose a reason for hiding this comment

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

seems like a very specific number. any reason?

Copy link
Member Author

@aditya520 aditya520 Apr 17, 2025

Choose a reason for hiding this comment

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

Nope, just felt like it. 😅

},
});

async function sendRequest(
contract: EvmEntropyContract,
privateKey: PrivateKey,
requestId: number,
): Promise<{ EntropyRequestResponse: any; startTime: number }> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can't we use something better than any?

Copy link
Member Author

@aditya520 aditya520 Apr 17, 2025

Choose a reason for hiding this comment

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

This is just the transaction turn object that we get from requestRandomness

const fortunaProvider = await contract.getDefaultProvider();
const userRandomNumber = contract.generateUserRandomNumber();
const EntropyRequestResponse = await contract.requestRandomness(
userRandomNumber,
fortunaProvider,
privateKey,
true, // with callback
);
const startTime = Date.now();
console.log(`[Request ${requestId}] Request tx hash : ${EntropyRequestResponse.transactionHash}`);
return { EntropyRequestResponse: EntropyRequestResponse, startTime: startTime };
}

async function waitForCallback(
contract: EvmEntropyContract,
EntropyRequestResponse: any,
startTime: number,
requestId: number,
): Promise<{ success: boolean; latency?: number }> {
const fromBlock = EntropyRequestResponse.blockNumber;
const web3 = contract.chain.getWeb3();
const entropyContract = contract.getContract();

// eslint-disable-next-line no-constant-condition
while (true) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const currentBlock = await web3.eth.getBlockNumber();
if (fromBlock > currentBlock) {
continue;
}

const events = await entropyContract.getPastEvents("RevealedWithCallback", {
fromBlock: fromBlock,
toBlock: currentBlock,
});

const event = events.find(
(event) => event.returnValues.request[1] == EntropyRequestResponse.events.RequestedWithCallback.returnValues.sequenceNumber,
);

if (event !== undefined) {
console.log(`[Request ${requestId}] Random number : ${event.returnValues.randomNumber}`);
const eventBlockTimestamp = Number(await web3.eth.getBlock(event.blockNumber).then(block => block.timestamp));
const entropyRequestBlockTimestamp = Number(await web3.eth.getBlock(EntropyRequestResponse.blockNumber).then(block => block.timestamp));
const latency = eventBlockTimestamp - entropyRequestBlockTimestamp;
console.log(`[Request ${requestId}] Fortuna Latency : ${latency}ms`);
console.log(
`[Request ${requestId}] Revealed after : ${
event.blockNumber - EntropyRequestResponse.blockNumber
} blocks`,
);
return { success: true, latency };
}
if (Date.now() - startTime > 60000) {
console.log(`[Request ${requestId}] Timeout: 60s passed without the callback being called.`);
return { success: false };
}
}
}

async function testParallelLatency(
contract: EvmEntropyContract,
privateKey: PrivateKey,
numRequests: number,
delay: number,
) {
console.log(`Starting ${numRequests} requests...`);

// First send all requests
const requests: { EntropyRequestResponse: any; startTime: number; requestId: number }[] = [];
for (let i = 0; i < numRequests; i++) {
if (i > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
const { EntropyRequestResponse, startTime } = await sendRequest(contract, privateKey, i + 1);
requests.push({ EntropyRequestResponse, startTime, requestId: i + 1 });
}




// Then wait for all callbacks
// The response time won't be accurate here.
const results: { success: boolean; latency?: number }[] = [];
for (const request of requests) {
const sequenceNumber =
request.EntropyRequestResponse.events.RequestedWithCallback.returnValues.sequenceNumber;
console.log(`[Request ${request.requestId}] sequence : ${sequenceNumber}`);
results.push(await waitForCallback(
contract,
request.EntropyRequestResponse,
request.startTime,
request.requestId
));
}

// Calculate statistics
const successfulRequests = results.filter(r => r.success).length;
const failedRequests = numRequests - successfulRequests;
const successRate = (successfulRequests / numRequests) * 100;

// Calculate average latency for successful requests
const successfulLatencies = results
.filter((r): r is { success: true; latency: number } => r.success && r.latency !== undefined)
.map(r => r.latency);
const avgLatency = successfulLatencies.length > 0
? successfulLatencies.reduce((a, b) => a + b, 0) / successfulLatencies.length
: 0;

console.log("\n=== Test Results ===");
console.log(`Total Requests : ${numRequests}`);
console.log(`Successful : ${successfulRequests}`);
console.log(`Failed : ${failedRequests}`);
console.log(`Success Rate : ${successRate.toFixed(2)}%`);
if (successfulLatencies.length > 0) {
console.log(`Average Latency : ${avgLatency.toFixed(2)}ms`);
}
console.log("===================");
}

async function main() {
const argv = await parser.argv;
if (!argv.chain && !argv["all-chains"]) {
throw new Error("Must specify either --chain or --all-chains");
}
const privateKey = toPrivateKey(argv.privateKey);
if (argv["all-chains"]) {
for (const contract of Object.values(DefaultStore.entropy_contracts)) {
if (
contract.getChain().isMainnet() ===
(argv["all-chains"] === "mainnet")
) {
console.log(`Testing latency for ${contract.getId()}...`);
await testParallelLatency(contract, privateKey, argv["nrequests"], argv["delay"]);
}
}
} else if (argv.chain) {
const chain = DefaultStore.getChainOrThrow(argv.chain, EvmChain);
const contract = findEntropyContract(chain);
await testParallelLatency(contract, privateKey, argv["nrequests"], argv["delay"]);
}
}

main();
Loading