Repository navigation
perf: cache provider calls and reduce fetcher and evaluator calls in MeshTxBuilder - #849
Merged
Merged
Conversation
lisicky
force-pushed
the
feat/tx-builder-fetch-efficiency
branch
from
October 12, 2026 14:58
4003da6 to
a9875e2
Compare
lisicky
force-pushed
the
feat/tx-builder-fetch-efficiency
branch
from
October 12, 2026 15:04
a9875e2 to
6845f09
Compare
…ency perf: stop wallets and contracts refetching UTxOs they already have
…ange to a fixed point Coin selection prices redeemers on a skeleton with a 1 000 ADA placeholder fee and a change output that still holds the fee, so a script that reads the fee or the change can need other units on chain than the ones declared. complete() now evaluates the final body, recomputes the fee, moves the difference into the change output with the most lovelace and repeats until one more pass changes nothing (at most 20 passes, then it throws). With a change output, the first pass sets the minimum fee; after that the fee only rises, so a script that gets cheaper with a higher fee cannot flip between two fees. The collateral is then checked against the settled fee. Without an evaluator or Plutus scripts nothing changes; a manual fee is kept.
…ted fee The largest-first selector priced every pass with the 1 000 ADA placeholder fee and dropped the redeemers it evaluated, so complete() always had to re-evaluate the final transaction at least twice. Passes after the first now use the fee already computed, whose change already pays it, and the selector returns its redeemers. When that pass priced the final transaction, the final check costs one evaluation.
Switch OfflineEvaluatorScalus from a per-call evalPlutusScripts to one Scalus TxEvaluator with its own ScriptCache. The evaluator is built on first use and rebuilt only when slotConfig or costModels change; the script cache survives rebuilds. UTxOs are passed as the [input, output] pairs TransactionUnspentOutput already encodes, so the hand-built CBOR map goes. A runtime that cannot import() Scalus (Jest without --experimental-vm-modules) gets an error that says so. MeshTxBuilder evaluates on every coin-selection round, so reuse pays off: on recorded Midgard requests, TxEvaluator with a ScriptCache takes 0.98x the time of Aiken WASM, and the cache alone saves about 17-25%.
`evaluator: "scalus"` makes MeshTxBuilder (and MeshTxBuilderV2) evaluate scripts locally with OfflineEvaluatorScalus, so a script transaction carries the execution units it really needs. It needs a fetcher. Without an evaluator the builder behaves as before: no evaluation and no extra fetches. Before each evaluation the builder gives the Scalus evaluator the network (the `network` option or setNetwork, else mainnet for an addr1 change address; a validity interval without known slot times is an error), the fetched cost models, and the UTxOs it holds for the transaction. Key inputs given only by address and value are fetched once per complete() for their chain data; their declared data is used only when the fetcher reports them not found. clone() passes `network` and keeps track of the Scalus evaluator, since coin selection evaluates on clones. OfflineEvaluatorScalus suggests another evaluator when Scalus cannot be imported.
Drop the `"scalus"` string variant of the `evaluator` option. A builder given a fetcher and no evaluator now evaluates locally with OfflineEvaluatorScalus, Mesh's default; `evaluator: null` turns evaluation off, and a clone never makes its own default. What the builder learns while completing a transaction (the network, the cost models it fetched) reaches any evaluator through the new optional `IEvaluator.configure`, and an evaluator that runs locally says so with `acceptsKnownUtxos` to get every UTxO the builder holds. The builder no longer treats one evaluator specially. OfflineEvaluatorScalus takes an optional network, is configured from a context, keeps a slot config given to its constructor, and checks the validity interval of the serialized transaction itself, before any fetch: a zero start the serializer leaves out no longer needs a network.
… evaluation inputs Evaluators that do not take known UTxOs get the fetched copy of an input given for evaluation when the builder has one, since a given copy may lack script bytes. This fixes the provider-calls test "fetches missing script bytes for a locally known Plutus reference UTxO", which failed on the base branch. Two tests met the new default: the balance fixture holds bare scripts in `scriptRef`, which Scalus cannot decode, so it opts out with `evaluator: null`; an OfflineEvaluatorScalus given to the builder now gets the builder's UTxOs and fetches nothing itself, which its test asserts. New tests: a given local evaluator gets every held UTxO; a given evaluator gets the fetched cost models; the evaluator is kept across reset(); configure() with nothing new keeps the TxEvaluator; getTransactionValidityInterval. Comments trimmed.
fix(tx-builder): evaluate the final transaction and settle fee and change to a fixed point
feat(tx-builder): opt-in Scalus evaluation with evaluator: "scalus"
feat(tx-builder): evaluate with Scalus by default, through IEvaluator
twwu123
approved these changes
Nov 1, 2026
Resolves the conflicts with #860 (reusable Scalus TxEvaluator, which this branch carried as a copy) and with the Scalus 1.4.1 test fixes on main: - offline-evaluator-scalus.ts and its test keep this branch's version, which already holds #860's content plus the default-evaluator work (#862, #866). - offline-evaluator-scalus-cases.test.ts keeps its Scalus expectations. Main's edits were to the CSL evaluator test it was copied from, so that test (mesh-core-csl/test/offline-providers/evaluator.test.ts) is restored from main rather than staying deleted. - script-tx-fail / script-tx-fetcher tests import MeshTxBuilder from @meshsdk/transaction, as on main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Caches provider answers that cannot change, cuts the calls
MeshTxBuilder.complete()makes, and fixes two builds that could not complete at all.Contents
mesh-common:CachedFetcher: anIFetcherwrapper that caches only answers that cannot change.fetchUTxOs(hash): a tx's outputs are fixed by its body, so they are fetched once per hash (whole tx, whateverindexis asked for). LRU, 1000 txs by default.fetchProtocolParameters/fetchCostModels: the latest values are kept until the current epoch ends. Values for an explicit past epoch are kept for good. Within 5 minutes after a boundary, values are kept only until those 5 minutes are up, while providers catch up.fetchAddressUTxOs,fetchAccountInfo,fetchTxInfo,fetchBlockInfo, asset and governance queries) reflects current chain state. Answers are kept for 10 s by default, half of Cardano's 20 s average block time (1 s slots, active slot coefficient 0.05), so a burst of identical queries costs one request. Configurable throughvolatileTtlMs(0 disables it).clearVolatile()drops them, andMeshTxBuilder.submitTxcalls it after submitting.get(url)passes straight through.CachedFetcher.wrap(fetcher)returns one shared cache per fetcher instance.resolveEpochEnd(network, ms): the time the current epoch ends, computed from the genesis epoch and slot lengths inSLOT_CONFIG_NETWORK. These change only by hard fork. They were checked against current chain tips: mainnet epoch 661, preprod 318 and preview 1448 all match epoch number and epoch slot.CachedFetcheruses the earliest end among mainnet, preprod and preview."testnet"(custom network) disables epoch caching.mesh-transaction(MeshTxBuilder):CachedFetcher.wrap(fetcher). Builders and evaluators sharing a provider share answers: cost models are fetched once per epoch instead of once percomplete(), and reference-script / input txs once per process instead of once per builder.evaluateRedeemersskipsevaluateTxand the serialization before it when nothing is witnessed by a Plutus script. Before, it ran once per coin-selection cost estimate on every transaction, including plain ADA sends.selectUtxosFromUTxOs,inputForEvaluationUTxOs andchainTxoutputs.fetchUTxOsis called only for hashes still unknown.complete()retries it instead of failing with "Couldn't find value information".fetchCostModelsruns alongside the UTxO fetches instead of before them.scriptSize. Before,simpleScriptTxInReferencewithout a hash and size crashed withTypeError: Cannot read properties of undefined (reading 'find'). Native-script inputs given by reference only are now completed too.scriptRefwhen the fetcher does not returnscriptHash(e.g.ScalusEmulator).clone()shares the fetched UTxO cache instead of deep-copying it for every coin-selection callback.TxParserfetches missing txs in parallel, through the same cache.mesh-core-cst(OfflineEvaluatorScalus) andmesh-core-csl(OfflineEvaluator):CachedFetcher.wrap(fetcher), in parallel. Each coin-selection round used to refetch every input, collateral and reference input. If the builder already fetched a tx through the same provider, the evaluator reuses it.OfflineEvaluatorno longer appends resolved UTxOs to the caller'sadditionalUtxosarray.Provider calls, before → after
Measured with
ScalusEmulatorbehind a call-recording proxy (provider-calls.test.ts):evaluatorsetevaluateTx×1 (×1+k with extra selection rounds)fetchCostModels×2,fetchUTxOs×2 per tx hashchainTxoutput given as hash/indexfetchUTxOsfor an unsubmitted hash, then "Couldn't find value information"OfflineEvaluatorScalusfetchUTxOs×4OfflineEvaluator, same tx evaluated twicefetchUTxOs×10No breaking API changes. The protected
getUTxOInfo/queryAllTxInfokeep their signatures.Validation
mesh-common: 200 tests pass, includingcached-fetcher.test.tsand theresolveEpochEndcases.mesh-transaction: 160 tests pass.script-tx-fetcherandscript-tx-failfail to load under--experimental-vm-modulesonmaintoo, because they importdistthroughrequire. Both pass without the flag.mesh-core-cst(179 tests),mesh-scalus-emulator(24) andmesh-wallet(32) pass.mesh-core-csl: the new fetch assertions pass. The same 4 tests inoffline-providers/evaluator.test.tsfail onmain: expectedstepsdiffer after the cost-model update (e.g. 164980381 vs 164973765).