From ba96e999109326484dd897965f89535cd8d89c1a Mon Sep 17 00:00:00 2001 From: SMOKAHONTAS <69312778+bayyubenjamin@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:22:39 +0700 Subject: [PATCH 1/3] test: add live Vault latency diagnostic --- scripts/diagnose-vault.mjs | 154 +++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 scripts/diagnose-vault.mjs diff --git a/scripts/diagnose-vault.mjs b/scripts/diagnose-vault.mjs new file mode 100644 index 00000000..9f90f483 --- /dev/null +++ b/scripts/diagnose-vault.mjs @@ -0,0 +1,154 @@ +import { + cvToHex, + hexToCV, + standardPrincipalCV, + tupleCV, + uintCV, +} from '@stacks/transactions'; + +const API = 'https://api.mainnet.hiro.so'; +const DEPLOYER = 'SP3GHKMV4GSYNA8WGBX83DACG80K1RRVQZAZMB9J3'; +const CONTRACT = 'staking-refinery-v7'; +const TEST_ADDRESS = DEPLOYER; +const SAMPLE_LIMIT = 20; +const TIMEOUT_MS = 15_000; + +async function timedFetch(url, options = {}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS); + const startedAt = performance.now(); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + const elapsedMs = performance.now() - startedAt; + return { response, elapsedMs }; + } finally { + clearTimeout(timeout); + } +} + +function readUint(cv) { + const value = cv?.value?.value ?? cv?.value ?? 0; + const number = Number(value); + if (!Number.isSafeInteger(number) || number < 0) { + throw new Error(`Invalid uint value: ${String(value)}`); + } + return number; +} + +function percentile(values, ratio) { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const index = Math.min(sorted.length - 1, Math.floor(sorted.length * ratio)); + return sorted[index]; +} + +function makeSampleIds(maxId, count) { + if (maxId <= 0) return []; + if (maxId <= count) return Array.from({ length: maxId }, (_, id) => id); + + const ids = new Set([0, maxId - 1]); + for (let index = 1; ids.size < count; index += 1) { + ids.add(Math.floor((index * (maxId - 1)) / (count - 1))); + } + return [...ids].sort((a, b) => a - b).slice(0, count); +} + +async function fetchStake(id) { + const key = tupleCV({ + user: standardPrincipalCV(TEST_ADDRESS), + id: uintCV(id), + }); + + const { response, elapsedMs } = await timedFetch( + `${API}/v2/map_entry/${DEPLOYER}/${CONTRACT}/stakes`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(cvToHex(key)), + }, + ); + + const body = await response.text(); + if (!response.ok) { + throw new Error(`Stake ${id} returned ${response.status}: ${body.slice(0, 180)}`); + } + + return { id, elapsedMs, bodyBytes: Buffer.byteLength(body) }; +} + +async function main() { + const infoResult = await timedFetch(`${API}/v2/info`); + const info = await infoResult.response.json(); + if (!infoResult.response.ok) { + throw new Error(`/v2/info returned ${infoResult.response.status}`); + } + + const nonceResult = await timedFetch( + `${API}/v2/data_var/${DEPLOYER}/${CONTRACT}/nonce`, + ); + const noncePayload = await nonceResult.response.json(); + if (!nonceResult.response.ok || !noncePayload.data) { + throw new Error(`nonce read returned ${nonceResult.response.status}`); + } + + const maxId = readUint(hexToCV(noncePayload.data)); + const sampleIds = makeSampleIds(maxId, SAMPLE_LIMIT); + + const serialStartedAt = performance.now(); + const serialResults = []; + for (const id of sampleIds) { + serialResults.push(await fetchStake(id)); + } + const serialTotalMs = performance.now() - serialStartedAt; + + const concurrentStartedAt = performance.now(); + await Promise.all(sampleIds.map((id) => fetchStake(id))); + const concurrentTotalMs = performance.now() - concurrentStartedAt; + + const requestTimes = serialResults.map((item) => item.elapsedMs); + const averageRequestMs = requestTimes.length + ? requestTimes.reduce((sum, value) => sum + value, 0) / requestTimes.length + : 0; + + const projectedFullSerialMs = averageRequestMs * maxId; + + const report = { + testedAt: new Date().toISOString(), + api: API, + burnBlockHeight: info.burn_block_height, + stacksTipHeight: info.stacks_tip_height, + infoLatencyMs: Math.round(infoResult.elapsedMs), + nonceLatencyMs: Math.round(nonceResult.elapsedMs), + globalStakeNonce: maxId, + sampledMapEntries: sampleIds.length, + serialSampleTotalMs: Math.round(serialTotalMs), + concurrentSampleTotalMs: Math.round(concurrentTotalMs), + averageMapEntryMs: Math.round(averageRequestMs), + p50MapEntryMs: Math.round(percentile(requestTimes, 0.5)), + p95MapEntryMs: Math.round(percentile(requestTimes, 0.95)), + projectedCurrentVaultSerialMs: Math.round(projectedFullSerialMs), + projectedCurrentVaultSerialSeconds: Number((projectedFullSerialMs / 1000).toFixed(1)), + speedupForSample: concurrentTotalMs > 0 + ? Number((serialTotalMs / concurrentTotalMs).toFixed(2)) + : null, + }; + + console.log('VAULT_DIAGNOSTIC_REPORT'); + console.log(JSON.stringify(report, null, 2)); + + if (maxId > 100) { + console.log( + `Finding confirmed: Vault scans ${maxId} global stake IDs serially before clearing its shared loading state.`, + ); + } +} + +main().catch((error) => { + console.error('VAULT_DIAGNOSTIC_FAILED'); + console.error(error); + process.exitCode = 1; +}); From e2db5e4c879c26d06b25abd5c662a3c82a25c9f1 Mon Sep 17 00:00:00 2001 From: SMOKAHONTAS <69312778+bayyubenjamin@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:22:54 +0700 Subject: [PATCH 2/3] ci: run live Vault performance diagnostic --- .github/workflows/vault-diagnostics.yaml | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/vault-diagnostics.yaml diff --git a/.github/workflows/vault-diagnostics.yaml b/.github/workflows/vault-diagnostics.yaml new file mode 100644 index 00000000..9114f722 --- /dev/null +++ b/.github/workflows/vault-diagnostics.yaml @@ -0,0 +1,30 @@ +name: Vault Live Diagnostics + +on: + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + diagnose: + name: Measure Vault mainnet reads + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install locked dependencies + run: npm ci --ignore-scripts + + - name: Measure live Hiro API and Vault scan cost + run: node scripts/diagnose-vault.mjs From 1f02d4a98898a76e7b0fc3aca87d0114f4f23657 Mon Sep 17 00:00:00 2001 From: SMOKAHONTAS <69312778+bayyubenjamin@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:24:19 +0700 Subject: [PATCH 3/3] ci: upload Vault diagnostic report --- .github/workflows/vault-diagnostics.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/vault-diagnostics.yaml b/.github/workflows/vault-diagnostics.yaml index 9114f722..5b7e0719 100644 --- a/.github/workflows/vault-diagnostics.yaml +++ b/.github/workflows/vault-diagnostics.yaml @@ -27,4 +27,13 @@ jobs: run: npm ci --ignore-scripts - name: Measure live Hiro API and Vault scan cost - run: node scripts/diagnose-vault.mjs + run: node scripts/diagnose-vault.mjs | tee vault-diagnostic-report.log + + - name: Upload diagnostic report + if: always() + uses: actions/upload-artifact@v4 + with: + name: vault-diagnostic-report + path: vault-diagnostic-report.log + if-no-files-found: error + retention-days: 3