Cache verifier salt on executor hot path
All checks were successful
deploy / deploy (push) Successful in 46s

Proof: targeted verifier salt cache and executor/dashboard tests pass; npm test passes 244/244; operator dashboard bundle builds; git diff --check passes.

Assumptions: a 500 ms verifier salt freshness bound with 250 ms background prefetch is conservative for quote-response signing, and stale or malformed salt must block signing instead of using cached data.

Still fake: venue-native terminal fill ids and fee-complete realized PnL remain unavailable.
This commit is contained in:
philipp 2026-05-18 22:54:03 +02:00
parent a4db57182c
commit c2675df141
6 changed files with 258 additions and 2 deletions

View file

@ -16,6 +16,7 @@ import {
assertIntentRequestSubmissionResultEvent, assertIntentRequestSubmissionResultEvent,
assertTradeResult, assertTradeResult,
} from '../core/schemas.mjs'; } from '../core/schemas.mjs';
import { createVerifierSaltCache } from '../core/verifier-salt-cache.mjs';
import { loadConfig } from '../lib/config.mjs'; import { loadConfig } from '../lib/config.mjs';
import { import {
createPostgresPool, createPostgresPool,
@ -74,6 +75,11 @@ const verifierClient = createVerifierClient({
verifierContract: config.nearVerifierContract, verifierContract: config.nearVerifierContract,
signerPrivateKey: config.nearIntentsSignerPrivateKey, signerPrivateKey: config.nearIntentsSignerPrivateKey,
}); });
const verifierSaltCache = createVerifierSaltCache({
loadSalt: () => verifierClient.currentSalt(),
logger: logger.child({ component: 'verifier-salt-cache' }),
});
verifierSaltCache.start();
const signer = verifierClient.getSigner(); const signer = verifierClient.getSigner();
const solverRelayRpcClient = createSolverRelayRpcClient({ const solverRelayRpcClient = createSolverRelayRpcClient({
rpcUrl: config.nearIntentsRpcUrl, rpcUrl: config.nearIntentsRpcUrl,
@ -247,7 +253,10 @@ async function handleCommand(event) {
const saltStartMs = performance.now(); const saltStartMs = performance.now();
let currentSaltHex; let currentSaltHex;
try { try {
currentSaltHex = await verifierClient.currentSalt(); const salt = await verifierSaltCache.getFreshSalt();
currentSaltHex = salt.currentSaltHex;
timing.current_salt_source = salt.source;
timing.current_salt_age_ms = roundTimingMs(salt.ageMs);
} finally { } finally {
recordExecutorTiming(timing, 'current_salt_ms', saltStartMs); recordExecutorTiming(timing, 'current_salt_ms', saltStartMs);
} }
@ -343,6 +352,8 @@ function finishExecutorTiming(timing) {
received_at: timing.received_at, received_at: timing.received_at,
command_event_age_ms: timing.command_event_age_ms, command_event_age_ms: timing.command_event_age_ms,
current_salt_ms: timing.current_salt_ms ?? null, current_salt_ms: timing.current_salt_ms ?? null,
current_salt_source: timing.current_salt_source ?? null,
current_salt_age_ms: timing.current_salt_age_ms ?? null,
sign_ms: timing.sign_ms ?? null, sign_ms: timing.sign_ms ?? null,
relay_response_ms: timing.relay_response_ms ?? null, relay_response_ms: timing.relay_response_ms ?? null,
executor_total_ms: roundTimingMs(performance.now() - timing.started_at_ms), executor_total_ms: roundTimingMs(performance.now() - timing.started_at_ms),
@ -395,6 +406,7 @@ const controlApi = startControlApi({
signer_public_key: signer.getPublicKey().toString(), signer_public_key: signer.getPublicKey().toString(),
signer_registered: signerRegistered, signer_registered: signerRegistered,
relay: relayClient.getState(), relay: relayClient.getState(),
verifier_salt_cache: verifierSaltCache.getState(),
trading_config: tradingConfigStore.getState(), trading_config: tradingConfigStore.getState(),
...state, ...state,
durable_control_state: armedStateStore.getState(), durable_control_state: armedStateStore.getState(),
@ -589,6 +601,7 @@ function createIntentRequestStore() {
async function shutdown() { async function shutdown() {
await controlApi.close().catch(() => {}); await controlApi.close().catch(() => {});
verifierSaltCache.stop();
relayClient.close(); relayClient.close();
await consumer.disconnect(); await consumer.disconnect();
await producer.disconnect(); await producer.disconnect();

View file

@ -0,0 +1,143 @@
const DEFAULT_MAX_AGE_MS = 500;
const DEFAULT_REFRESH_INTERVAL_MS = 250;
const ERROR_LOG_SUPPRESSION_MS = 30_000;
export function createVerifierSaltCache({
loadSalt,
maxAgeMs = DEFAULT_MAX_AGE_MS,
refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
now = () => Date.now(),
setIntervalFn = setInterval,
clearIntervalFn = clearInterval,
logger = null,
} = {}) {
if (typeof loadSalt !== 'function') {
throw new Error('verifier salt cache requires loadSalt');
}
let cached = null;
let refreshInFlight = null;
let timer = null;
let lastRefreshError = null;
let lastRefreshErrorLoggedAtMs = 0;
async function refresh({ reason = 'manual' } = {}) {
if (refreshInFlight) return refreshInFlight;
refreshInFlight = Promise.resolve()
.then(() => loadSalt())
.then((salt) => {
cached = {
currentSaltHex: normalizeSalt(salt),
refreshedAtMs: now(),
refreshedReason: reason,
};
lastRefreshError = null;
return buildSaltResult(cached, 'refresh');
})
.catch((error) => {
logRefreshError(error, reason);
lastRefreshError = serializeSaltError(error);
throw error;
})
.finally(() => {
refreshInFlight = null;
});
return refreshInFlight;
}
async function getFreshSalt() {
if (isFresh(cached)) return buildSaltResult(cached, 'cache');
await refresh({ reason: cached ? 'stale_cache' : 'empty_cache' });
if (!isFresh(cached)) {
throw new Error('verifier salt cache refresh did not produce a fresh salt');
}
return buildSaltResult(cached, 'refresh');
}
function start() {
if (timer) return;
void refresh({ reason: 'startup' }).catch(() => {});
timer = setIntervalFn(() => {
void refresh({ reason: 'prefetch' }).catch(() => {});
}, refreshIntervalMs);
timer?.unref?.();
}
function stop() {
if (!timer) return;
clearIntervalFn(timer);
timer = null;
}
function getState() {
const ageMs = cached ? now() - cached.refreshedAtMs : null;
return {
configured: true,
has_cached_salt: Boolean(cached),
fresh: isFresh(cached),
max_age_ms: maxAgeMs,
refresh_interval_ms: refreshIntervalMs,
cached_age_ms: ageMs == null ? null : Math.max(0, Math.round(ageMs)),
refreshed_reason: cached?.refreshedReason || null,
refresh_in_flight: Boolean(refreshInFlight),
last_refresh_error: lastRefreshError,
};
}
function isFresh(entry) {
if (!entry) return false;
const ageMs = now() - entry.refreshedAtMs;
return ageMs >= 0 && ageMs <= maxAgeMs;
}
function buildSaltResult(entry, source) {
return {
currentSaltHex: entry.currentSaltHex,
source,
ageMs: Math.max(0, now() - entry.refreshedAtMs),
refreshedReason: entry.refreshedReason,
};
}
function logRefreshError(error, reason) {
if (!logger?.warn) return;
const serialized = serializeSaltError(error);
const currentTimeMs = now();
const shouldLog = serialized.message !== lastRefreshError?.message
|| currentTimeMs - lastRefreshErrorLoggedAtMs >= ERROR_LOG_SUPPRESSION_MS;
if (!shouldLog) return;
lastRefreshErrorLoggedAtMs = currentTimeMs;
logger.warn('verifier_salt_refresh_failed', {
details: {
reason,
error: serialized,
},
});
}
return {
getFreshSalt,
getState,
refresh,
start,
stop,
};
}
function normalizeSalt(value) {
const salt = String(value || '').replace(/^0x/, '');
if (!/^[0-9a-fA-F]{8}$/.test(salt)) {
throw new Error('current_salt must be 4 bytes in hex');
}
return salt.toLowerCase();
}
function serializeSaltError(error) {
return {
name: error?.name || 'Error',
message: error?.message || String(error),
};
}

View file

@ -42,10 +42,12 @@ function formatTimingMs(value) {
function formatExecutionTiming(timing) { function formatExecutionTiming(timing) {
if (!timing) return null; if (!timing) return null;
const saltMs = formatTimingMs(timing.current_salt_ms);
const saltAgeMs = formatTimingMs(timing.current_salt_age_ms);
const saltSource = timing.current_salt_source ? ` ${plainCodeLabel(timing.current_salt_source).toLowerCase()}` : '';
const parts = [ const parts = [
['total', timing.executor_total_ms], ['total', timing.executor_total_ms],
['cmd age', timing.command_event_age_ms], ['cmd age', timing.command_event_age_ms],
['salt', timing.current_salt_ms],
['sign', timing.sign_ms], ['sign', timing.sign_ms],
['relay', timing.relay_response_ms], ['relay', timing.relay_response_ms],
] ]
@ -54,6 +56,9 @@ function formatExecutionTiming(timing) {
return formatted ? `${label} ${formatted}` : null; return formatted ? `${label} ${formatted}` : null;
}) })
.filter(Boolean); .filter(Boolean);
if (saltMs) {
parts.splice(2, 0, `salt ${saltMs}${saltSource}${saltAgeMs ? ` age ${saltAgeMs}` : ''}`);
}
return parts.length ? `Timing: ${parts.join(', ')}` : null; return parts.length ? `Timing: ${parts.join(', ')}` : null;
} }

View file

@ -32,6 +32,8 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
assert.match(strategySource, /formatExecutionTiming/); assert.match(strategySource, /formatExecutionTiming/);
assert.match(strategySource, /item\.execution\?\.timing/); assert.match(strategySource, /item\.execution\?\.timing/);
assert.match(strategySource, /current_salt_ms/); assert.match(strategySource, /current_salt_ms/);
assert.match(strategySource, /current_salt_source/);
assert.match(strategySource, /current_salt_age_ms/);
assert.match(strategySource, /relay_response_ms/); assert.match(strategySource, /relay_response_ms/);
assert.match(strategySource, /Timing:/); assert.match(strategySource, /Timing:/);
assert.match(strategySource, /item\.execution\?\.status === 'submitted'/); assert.match(strategySource, /item\.execution\?\.status === 'submitted'/);

View file

@ -36,3 +36,14 @@ test('trade executor records hot path timing in result payloads', () => {
assert.match(source, /executor_total_ms/); assert.match(source, /executor_total_ms/);
assert.match(source, /withExecutorTiming\(\{[\s\S]*?result_code: 'submission_failed'/); assert.match(source, /withExecutorTiming\(\{[\s\S]*?result_code: 'submission_failed'/);
}); });
test('trade executor uses bounded verifier salt cache instead of per-command salt RPC', () => {
assert.match(source, /createVerifierSaltCache/);
assert.match(source, /verifierSaltCache\.start\(\)/);
assert.match(source, /verifierSaltCache\.getFreshSalt\(\)/);
assert.match(source, /current_salt_source/);
assert.match(source, /current_salt_age_ms/);
assert.match(source, /verifier_salt_cache: verifierSaltCache\.getState\(\)/);
assert.match(source, /verifierSaltCache\.stop\(\)/);
assert.doesNotMatch(source, /await verifierClient\.currentSalt\(\);/);
});

View file

@ -0,0 +1,82 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createVerifierSaltCache } from '../src/core/verifier-salt-cache.mjs';
test('verifier salt cache reuses a fresh salt without calling the verifier hot path', async () => {
let nowMs = 1_000;
let calls = 0;
const cache = createVerifierSaltCache({
loadSalt: async () => {
calls += 1;
return '252812b3';
},
maxAgeMs: 500,
now: () => nowMs,
});
const refreshed = await cache.getFreshSalt();
nowMs += 100;
const cached = await cache.getFreshSalt();
assert.equal(calls, 1);
assert.equal(refreshed.source, 'refresh');
assert.equal(cached.source, 'cache');
assert.equal(cached.currentSaltHex, '252812b3');
assert.equal(cached.ageMs, 100);
});
test('verifier salt cache refreshes a stale salt before returning it', async () => {
let nowMs = 1_000;
const salts = ['252812b3', '252812b4'];
const cache = createVerifierSaltCache({
loadSalt: async () => salts.shift(),
maxAgeMs: 500,
now: () => nowMs,
});
const first = await cache.getFreshSalt();
nowMs += 501;
const second = await cache.getFreshSalt();
assert.equal(first.currentSaltHex, '252812b3');
assert.equal(second.currentSaltHex, '252812b4');
assert.equal(second.source, 'refresh');
assert.equal(second.ageMs, 0);
});
test('verifier salt cache fails closed when stale salt refresh fails', async () => {
let nowMs = 1_000;
let fail = false;
const cache = createVerifierSaltCache({
loadSalt: async () => {
if (fail) throw new Error('rpc unavailable');
return '252812b3';
},
maxAgeMs: 500,
now: () => nowMs,
});
await cache.getFreshSalt();
nowMs += 501;
fail = true;
await assert.rejects(
() => cache.getFreshSalt(),
/rpc unavailable/,
);
assert.equal(cache.getState().fresh, false);
assert.equal(cache.getState().last_refresh_error.message, 'rpc unavailable');
});
test('verifier salt cache rejects malformed salts before signing can use them', async () => {
const cache = createVerifierSaltCache({
loadSalt: async () => 'not-a-salt',
});
await assert.rejects(
() => cache.getFreshSalt(),
/current_salt must be 4 bytes in hex/,
);
assert.equal(cache.getState().has_cached_salt, false);
});