Skip to content
Open
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
41 changes: 38 additions & 3 deletions packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
getTransactionOutputs,
} from "../utils";

/** Most tx hashes an evaluator keeps fetched outputs for, dropping the oldest first */
const MAX_CACHED_TX_HASHES = 1000;

/**
* OfflineEvaluator implements the IEvaluator interface to provide offline evaluation of Plutus scripts.
* This class evaluates Plutus scripts contained in Cardano transactions without requiring network connectivity,
Expand Down Expand Up @@ -74,6 +77,8 @@ export class OfflineEvaluator implements IEvaluator {
private readonly network: Network;
public slotConfig: Omit<Omit<SlotConfig, "startEpoch">, "epochLength">;
public costModels: number[][];
/** Outputs fetched per tx hash. Outputs never change once created, so they are reused across evaluations */
private readonly fetchedUTxOs = new Map<string, Promise<UTxO[]>>();

/**
* Creates a new instance of OfflineEvaluator.
Expand Down Expand Up @@ -122,6 +127,8 @@ export class OfflineEvaluator implements IEvaluator {
additionalUtxos: UTxO[],
additionalTxs: string[],
): Promise<Omit<Action, "data">[]> {
// Resolved utxos are appended to a copy, leaving the caller's array untouched
additionalUtxos = [...additionalUtxos];
// Track which utxos is resolved
const foundUtxos = new Set<string>();

Expand All @@ -137,9 +144,14 @@ export class OfflineEvaluator implements IEvaluator {
const inputsToResolve = getTransactionInputs(tx).filter(
(input) => !foundUtxos.has(`${input.txHash}:${input.outputIndex}`),
);
const txHashesSet = new Set(inputsToResolve.map((input) => input.txHash));
for (const txHash of txHashesSet) {
const utxos = await this.fetcher.fetchUTxOs(txHash);
const txHashes = Array.from(
new Set(inputsToResolve.map((input) => input.txHash)),
);
const fetched = await Promise.all(
txHashes.map((txHash) => this.fetchUTxOsOnce(txHash)),
);
for (const [i, txHash] of txHashes.entries()) {
const utxos = fetched[i]!;
for (const utxo of utxos) {
if (utxo)
if (
Expand All @@ -158,6 +170,8 @@ export class OfflineEvaluator implements IEvaluator {
(input) => !foundUtxos.has(`${input.txHash}:${input.outputIndex}`),
);
if (missing.length > 0) {
// The fetcher may not have seen these outputs yet; ask again next time
missing.forEach((input) => this.fetchedUTxOs.delete(input.txHash));
const missingList = missing
.map((m) => `${m.txHash}:${m.outputIndex}`)
.join(", ");
Expand All @@ -173,4 +187,25 @@ export class OfflineEvaluator implements IEvaluator {
this.slotConfig,
);
}

/**
* Fetches the outputs of a transaction once: concurrent and later calls for the same hash
* reuse the first request, while a failed or empty response is forgotten.
*/
private fetchUTxOsOnce(txHash: string): Promise<UTxO[]> {
const cached = this.fetchedUTxOs.get(txHash);
if (cached) return cached;
const request = this.fetcher.fetchUTxOs(txHash);
this.fetchedUTxOs.set(txHash, request);
request.then(
(utxos) => {
if (!utxos || utxos.length === 0) this.fetchedUTxOs.delete(txHash);
},
() => this.fetchedUTxOs.delete(txHash),
);
if (this.fetchedUTxOs.size > MAX_CACHED_TX_HASHES) {
this.fetchedUTxOs.delete(this.fetchedUTxOs.keys().next().value!);
}
return request;
}
}
13 changes: 13 additions & 0 deletions packages/mesh-core-csl/test/offline-providers/evaluator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,21 @@ describe("Offline Evaluator", () => {
},
};
fetcher.addUTxOs([utxo_1, utxo_2, utxo_3, utxo_4, utxo_5, utxo_6, utxo_7]);
const fetchUTxOs = jest.spyOn(fetcher, "fetchUTxOs");

const res = await evaluator.evaluateTx(txHex, [], []);
// Evaluating again, as the tx builder does per coin selection round, fetches nothing new
expect(await evaluator.evaluateTx(txHex, [], [])).toStrictEqual(res);
const fetchedTxHashes = fetchUTxOs.mock.calls.map(([txHash]) => txHash);
expect(fetchedTxHashes.sort()).toStrictEqual(
Array.from(
new Set(
[utxo_1, utxo_2, utxo_3, utxo_4, utxo_5, utxo_6, utxo_7].map(
(utxo) => utxo.input.txHash,
),
),
).sort(),
);
expect(res).toStrictEqual([
{
index: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ const MESH_TAG: Record<RedeemerBudget["tag"], RedeemerTagType> = {
Proposing: "PROPOSE",
};

/** Most tx hashes an evaluator keeps fetched outputs for, dropping the oldest first */
const MAX_CACHED_TX_HASHES = 1000;

export class OfflineEvaluatorScalus implements IEvaluator {
private readonly fetcher: IFetcher;
private readonly network: Network;
public slotConfig: Omit<Omit<SlotConfig, "startEpoch">, "epochLength">;
public costModels: number[][];
/** Outputs fetched per tx hash. Outputs never change once created, so they are reused across evaluations */
private readonly fetchedUTxOs = new Map<string, Promise<UTxO[]>>();

/**
* Creates a new instance of OfflineEvaluatorScalus.
Expand Down Expand Up @@ -106,10 +111,11 @@ export class OfflineEvaluatorScalus implements IEvaluator {
queriesNeeded.add(input.txHash);
}
const fetchedUtxos: Map<string, UTxO[]> = new Map<string, UTxO[]>();
for (const txHash of queriesNeeded) {
const utxos = await this.fetcher.fetchUTxOs(txHash);
fetchedUtxos.set(txHash, utxos);
}
await Promise.all(
Array.from(queriesNeeded, async (txHash) => {
fetchedUtxos.set(txHash, await this.fetchUTxOsOnce(txHash));
}),
);

for (const input of inputsToResolve) {
const utxos = fetchedUtxos.get(input.txHash);
Expand All @@ -120,6 +126,8 @@ export class OfflineEvaluatorScalus implements IEvaluator {
}
const utxo = utxos.find((u) => u.input.outputIndex === input.outputIndex);
if (!utxo) {
// The fetcher may not have seen this output yet; ask again next time
this.fetchedUTxOs.delete(input.txHash);
throw new Error(
`UTxO not found for input: ${input.txHash}:${input.outputIndex}`,
);
Expand Down Expand Up @@ -170,4 +178,25 @@ export class OfflineEvaluatorScalus implements IEvaluator {
};
});
}

/**
* Fetches the outputs of a transaction once: concurrent and later calls for the same hash
* reuse the first request, while a failed or empty response is forgotten.
*/
private fetchUTxOsOnce(txHash: string): Promise<UTxO[]> {
const cached = this.fetchedUTxOs.get(txHash);
if (cached) return cached;
const request = this.fetcher.fetchUTxOs(txHash);
this.fetchedUTxOs.set(txHash, request);
request.then(
(utxos) => {
if (!utxos || utxos.length === 0) this.fetchedUTxOs.delete(txHash);
},
() => this.fetchedUTxOs.delete(txHash),
);
if (this.fetchedUTxOs.size > MAX_CACHED_TX_HASHES) {
this.fetchedUTxOs.delete(this.fetchedUTxOs.keys().next().value!);
}
return request;
}
}
Loading
Loading