Repository navigation
feat(tx-builder): opt-in Scalus evaluation with evaluator: "scalus" - #862
Merged
lisicky merged 3 commits intoNov 1, 2026
Merged
Conversation
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%.
nau
force-pushed
the
pr-c/default-evaluator
branch
from
October 30, 2026 10:55
c4ead22 to
ec4d7d8
Compare
`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.
nau
force-pushed
the
pr-c/default-evaluator
branch
from
October 30, 2026 14:44
ec4d7d8 to
5b2c645
Compare
This was referenced Nov 1, 2026
lisicky
approved these changes
Nov 1, 2026
lisicky
left a comment
Contributor
There was a problem hiding this comment.
Thanks for this one too. Merging it as is, then #866 goes on top and changes the shape a bit, so you're not surprised:
- No
evaluator: "scalus". The option staysIEvaluator | null; with afetcherand no evaluator the builder now defaults toOfflineEvaluatorScalus,nullturns evaluation off. - What the builder learns goes through the interface: optional
IEvaluator.configure(context)(network, cost models) andacceptsKnownUtxos(a local evaluator gets every UTxO the builder holds). The builder no longer special-cases its Scalus instance, soscalusEvaluator/configureScalusEvaluatorare gone. - The validity-interval / network check moved into
OfflineEvaluatorScalusand runs on the serialized tx, before any fetch.invalidBefore(0)no longer needs a network, the serializer drops it anyway. - Non-local evaluators get the fetched copy of an
inputForEvaluationwhen the builder has one. That fixed the provider-calls "missing script bytes" test that was failing on the base branch. - Your tests are kept, adapted to the new API, plus a few more. All suites green locally with #861 included.
Details in #866.
lisicky
added a commit
that referenced
this pull request
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.
Summary
new MeshTxBuilder({ fetcher, evaluator: "scalus" })evaluates scripts locally withOfflineEvaluatorScalus, set up by the builder. Script transactions then carry the execution units they really need, without a round trip to a provider.MeshTxBuilderV2takes the same option.As agreed with the maintainers, this is opt-in for Mesh 1.x. Scalus evaluation is planned to become the default in Mesh 2.x. A builder without an evaluator behaves exactly as before: no evaluation, and no fetches beyond what #849 makes.
This builds on #849 (provider-call caching in
MeshTxBuilder) and on #860 (OfflineEvaluatorScalusreuses one ScalusTxEvaluatorandScriptCache). #849's branch predates b42a31d onmain(Scalus 1.4.1, ESM test runner, governance tests), so this PR carries a copy of b42a31d and of the #860 commit. Both drop out when #849 and #860 land. Only the last commit is new here.Changes
MeshTxBuilderOptions.evaluatoris nowIEvaluator | "scalus":"scalus"– anOfflineEvaluatorScalusthat the builder configures. It needs afetcher; without one, the constructor throws an error that namesfetcher.networkoption orsetNetwork(...)(asmesh-contractand the deprecatedTransactioncall it), else mainnet for anaddr1change address. Both are kept in a new publicnetworkfield onMeshTxBuilder. The builder does not readCachedFetcher.network, because perf: cache provider calls and reduce fetcher and evaluator calls in MeshTxBuilder #849'scachedFetcher()writes itsaddr1guess there for every builder using that fetcher. If the transaction has a validity interval and the network is unknown, has no known slot times ("testnet", whose slot config is all zeros) or is not a network name at all, evaluation throws. The error suggestsnetwork(orsetNetwork) or another evaluator built for that network. Without a validity interval the slot arithmetic is never used, so evaluation goes ahead.completeCostModels). A version the provider sent no model for keeps the default.selectUtxosFromandinputForEvaluationUTxOs and chained outputs. Key inputs given only by address and value (txIn(hash, i, amount, address, 0)) carry no datum. Their chain UTxOs are fetched once percomplete(), alongside the builder's own queries. The declared data is used only for an output the fetcher does not have yet, e.g. a wallet UTxO an indexer has not seen. "Does not have" means an empty result (Kupo, UTxO RPC, the Scalus emulator), an HTTP 404 (Blockfrost, Maestro), or a "not found" error (OfflineFetcher). Any other failure is thrown, includingparseHttpError's"{}", which is what every axios-based provider throws for a non-HTTP error. Key inputs that may hold a reference script (scriptSizenot 0), and Plutus script inputs, are never taken from declared data. Anything else is left for the evaluator to fetch.OfflineEvaluatorScalusdefault.An evaluator you pass yourself still gets only
inputForEvaluationUTxOs: a remote one (Blockfrost, Ogmios) may reject additional UTxOs that also exist on chain.clone()also passesnetwork, and keeps track of the Scalus evaluator.clone()runs on every coin-selection round, and evaluation happens on those clones. A clone of an opted-in builder stays opted in; a clone of a builder without an evaluator stays without one.OfflineEvaluatorScalus: the error feat(core-cst): evaluate with a reusable Scalus TxEvaluator #860 gives when Scalus cannot beimport()ed (Jest without--experimental-vm-modules) now also suggests givingMeshTxBuilderanother evaluator.Notes
KupoProvider(itstoUTxOkeeps only address and value), gives a script a smaller context than the ledger will. The evaluated units can then fall short, and the ledger rejects the transaction. One probe evaluated 86.85M steps where the ledger spent 89.39M. Use another fetcher, or another evaluator, with such providers.KoiosProvider.fetchUTxOsthrows"{}"for an unknown transaction (it readsdata[0].outputsfrom an empty answer). So with Koios, an input the indexer has not seen yet failscomplete()rather than falling back to its declared data. The fix, returning[]whendata[0]is undefined, belongs in the provider package, which is not in this repository.complete()there as well.networkorsetNetwork(...).mesh-contractalready callssetNetwork. Unlike the body, the network set withsetNetworkis kept acrossreset(). Byron (Ae2,DdzFF) mainnet change addresses do not imply mainnet; passnetworkwith them.@meshsdk/transactionnow always referencesOfflineEvaluatorScalus, so bundlers always emit the lazily loaded Scalus chunk. Scalus is still only loaded when an opted-in builder evaluates a script transaction, and the CommonJS build still loads without it.cachedFetcher()writesnetwork = "mainnet"into theCachedFetchershared by every builder using the same fetcher. This PR does not rely on that value.Tests
scalus-evaluator.test.ts(26 tests), againstScalusEmulator:{ mem: 7, steps: 7 }gets evaluated units withevaluator: "scalus", inMeshTxBuilderand inMeshTxBuilderV2, and the emulator accepts the signed transaction.{ mem: 7, steps: 7 }stays through coin selection. The builder fetches only what perf: cache provider calls and reduce fetcher and evaluator calls in MeshTxBuilder #849 fetches, and a fetcher failing with"{}"on the collateral does not affect it.evaluator: "scalus"without afetcherthrows, for both builders.inputForEvaluationUTxOs, and the builder fetches nothing extra for it.OfflineFetcher's "No UTxOs found": evaluated as declared;"{}":complete()rejects with that error;setNetwork("preprod")with a validity interval (themesh-contractflow), and from a mainnet change address. With a validity interval, even one from slot 0, an unknown network,"testnet"or a misspelt name throws. A"mainnet"guess left in the shared cache is not used. Without a validity interval, evaluation works."scalus","scalus"accepted without a fetcher, the evaluator ornetworknot passed inclone(), the clone's Scalus evaluator not marked, prefetching without an evaluator, slot 0 not counted as a validity interval, not-found not recognised (404,OfflineFetcher's message), every error treated as not-found, chain data not prefetched, prefetching for a user's evaluator, held UTxOs not used, declared data before chain data, noscriptSizeor Plutus-input guard,setNetworknot recorded, zero slot length accepted, network read from the shared cache, empty cost models not replaced. Every one fails a test. Two guards have no test, because breaking them only costs a fetch: prefetching for a transaction without Plutus scripts, and prefetching afterbuilder.evaluatoris reassigned.Results, before and after this commit (both on #849 + #860):
The one failure is #849's
provider-calls"fetches missing script bytes for a locally known Plutus reference UTxO". It fails the same way with Scalus 1.3.0. That test passes the emulator as an explicit evaluator. The builder fetches the reference script's bytes, but it still hands the emulator the local copy without them, given throughinputForEvaluation. The emulator lets additional UTxOs override its own, so it reports "Script not found". The Scalus evaluator avoids this, because it prefers fetched UTxOs. The fix for explicit evaluators belongs in #849.