Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions src/oracle/oracle-key-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ const RAINFALL_OR_TEMPERATURE_KEY_RE =
/^(?:rainfall|temperature):(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?):20\d{2}-(?:0[1-9]|1[0-2])$/;
const FLIGHT_KEY_RE = /^flight:[A-Z0-9]+:20\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;

/** Maximum oracle key length to prevent DB bloat and oversized-key DoS (#726). */
export const MAX_ORACLE_KEY_LENGTH = 128;

/**
* Range-checks the lat/lng embedded in a rainfall/temperature key in addition
* to the format regex, which only checks that they're signed decimals — same
* reasoning as IsValidOracleKeyCoordinates in
* src/policy/dto/oracle-key-coordinates.validator.ts.
*/
export function isValidOracleKeyFormat(key: string): boolean {
// Reject oversized keys before regex matching (#726).
if (typeof key !== 'string' || key.length > MAX_ORACLE_KEY_LENGTH) {
return false;
}
const coordMatch = RAINFALL_OR_TEMPERATURE_KEY_RE.exec(key);
if (coordMatch) {
const lat = parseFloat(coordMatch[1]);
Expand Down
36 changes: 27 additions & 9 deletions src/oracle/oracle.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,17 @@ export class OracleService {
}

/** Get the latest reading for a given oracle key from the database. */
// #727 — cache recent reads to avoid repeated identical queries.
private readonly readingCache = new Map<string, { value: OracleReading | null; expiresAt: number }>();
private static readonly READING_CACHE_TTL_MS = 30_000;

async getLatestReading(key: string): Promise<OracleReading | null> {
// Check cache first (#727)
const cached = this.readingCache.get(key);
if (cached && cached.expiresAt > Date.now()) {
return cached.value;
}

// #265 — submittedAt alone has no guaranteed tiebreak on ties (plausible
// under bulk/backfill writes or fast successive submissions), and this
// result directly drives claim payout decisions. id is a stable secondary
Expand All @@ -356,16 +366,24 @@ export class OracleService {
orderBy: [{ submittedAt: "desc" }, { id: "desc" }],
});

if (!record) return null;
const result = record
? {
dataType: record.dataType,
key: record.key,
value: record.value.toString(),
confidence: record.confidence,
timestamp: Math.floor(record.submittedAt.getTime() / 1000),
source: record.source,
}
: null;

return {
dataType: record.dataType,
key: record.key,
value: record.value.toString(),
confidence: record.confidence,
timestamp: Math.floor(record.submittedAt.getTime() / 1000),
source: record.source,
};
// Cache the result (#727)
this.readingCache.set(key, {
value: result,
expiresAt: Date.now() + OracleService.READING_CACHE_TTL_MS,
});

return result;
}

private async executeWithRateLimitBackoff<T>(
Expand Down
30 changes: 30 additions & 0 deletions src/stellar/stellar.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ export class StellarService {
private readonly _horizonUrl: string;
private readonly _rpcUrl: string;

// #728 — per-account transaction serialization. Concurrent invocations from
// the same account would race on sequence numbers and fail with
// TRANSACTION_BAD_SEQ. A simple promise-chain mutex serializes them.
private readonly accountLocks = new Map<string, Promise<void>>();

private async withAccountLock<T>(publicKey: string, fn: () => Promise<T>): Promise<T> {
const prev = this.accountLocks.get(publicKey) ?? Promise.resolve();
let release: () => void;
const gate = new Promise<void>((resolve) => { release = resolve; });
this.accountLocks.set(publicKey, prev.then(() => gate));
await prev;
try {
return await fn();
} finally {
release!();
}
}

constructor(private readonly config: ConfigService) {
const rpcUrl =
config.get<string>("STELLAR_RPC_URL") ??
Expand Down Expand Up @@ -137,6 +155,18 @@ export class StellarService {
);
}
const signer = signerKeypair ?? this.keeperKeypair;
// #728 — serialize per-account to prevent sequence number conflicts.
return this.withAccountLock(signer.publicKey(), () =>
this.doInvokeContract(contractId, method, args, signer),
);
}

private async doInvokeContract(
contractId: string,
method: string,
args: xdr.ScVal[],
signer: Keypair,
): Promise<string> {
const contract = new Contract(contractId);
const MAX_ATTEMPTS = 3;
let lastError: Error | null = null;
Expand Down
Loading