Skip to content

feat(tx-builder): evaluate with Scalus by default, through IEvaluator - #866

Merged
lisicky merged 5 commits into
feat/tx-builder-fetch-efficiencyfrom
feat/default-scalus-evaluator
Nov 1, 2026
Merged

lisicky merged 5 commits into
feat/tx-builder-fetch-efficiencyfrom
feat/default-scalus-evaluator

Conversation

@lisicky

@lisicky lisicky commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #862 (its commits are included unchanged, on the same base) and replaces its evaluator: "scalus" option with a plain IEvaluator default. Opened as an alternative to #862's API; the rest of #862 (network option, declared key inputs, known UTxOs for a local evaluator) is kept.

Why

evaluator?: IEvaluator | "scalus" makes the builder recognise one evaluator by identity: a private scalusEvaluator field, clone bookkeeping to keep that identity, and a validity-interval check that lives in the builder although it is about the evaluator's slot config. Mesh's default evaluator should be an ordinary IEvaluator instance, and whatever the builder knows that an evaluator could use should reach it through the interface, so any evaluator can take it.

What changes

  • MeshTxBuilderOptions.evaluator is IEvaluator | null. Left out, with a fetcher, the builder evaluates locally with OfflineEvaluatorScalus, the default Mesh chooses (Scalus today). null turns evaluation off: redeemers keep the units declared on them. Without a fetcher there is no default, as there is nothing to resolve inputs with. MeshTxBuilderV2 inherits all of this.
  • IEvaluator gains two optional members. acceptsKnownUtxos: a local evaluator sets it to receive every UTxO the builder holds (held inputs, declared key inputs), whether or not they are on chain yet. configure(context): takes an EvaluationContext with the network and the cost models the builder learned while completing. Remote providers implement neither and behave as before, with one improvement: an input given for evaluation is passed as its fetched copy when the builder has one, since a given copy may lack script bytes (utxosForEvaluation).
  • OfflineEvaluatorScalus takes an optional network, implements configure, keeps a slot config given to its constructor whatever network it is told, and checks the validity interval of the serialized transaction itself, before any fetch. getTransactionValidityInterval is added to core-cst's transaction utils.
  • MeshTxBuilder loses scalusEvaluator, configureScalusEvaluator and the "scalus" constructor branch. evaluateRedeemers calls evaluator.configure?.() and passes known UTxOs when acceptsKnownUtxos is set (utxosForScalusEvaluator is now utxosForLocalEvaluator). clone() passes evaluator ?? null, so a clone never makes a default of its own.

Behaviour changes to be aware of

  • Local evaluation is on by default. Any new MeshTxBuilder({ fetcher }) that builds a transaction with a Plutus script witness now evaluates it with Scalus. Scalus loads on first evaluation (an ESM chunk, through a dynamic import()), and inputs the builder does not hold are fetched once per complete(). Under Jest this needs --experimental-vm-modules, which mesh-transaction, mesh-contract and mesh-scalus-emulator already run with. Five mesh-transaction tests that build Plutus transactions over an OfflineFetcher got evaluator: null to keep testing with declared units.
  • The unknown-network error is now thrown by the evaluator, so it surfaces as Tx evaluation failed: The transaction has a validity interval, but the network it is for is unknown ..., like other evaluation errors. It is checked on the serialized transaction: invalidBefore(0), which the serializer leaves out, no longer needs a known network (feat(tx-builder): opt-in Scalus evaluation with evaluator: "scalus" #862's test for it is changed accordingly).
  • A Scalus evaluator passed in is configured too, and gets the builder's UTxOs. One made for preprod but used with a mainnet change address and no network option follows the builder to mainnet; a slot config given to the constructor is never replaced. It no longer fetches inputs the builder already holds.
  • mesh-x402's SpendContext doc comment is updated; its code already passes an evaluator explicitly.

Tests

  • scalus-evaluator.test.ts (mesh-transaction): the default is Scalus in both builders, none without a fetcher, shared with clones, off with evaluator: null; a given evaluator is left alone, or configured if it takes a context, or given every held UTxO if it is local; the evaluator is kept across reset(); network inference and the unknown-network errors; slot 0.
  • offline-evaluator-scalus.test.ts (core-cst): configure with a network, a pinned slot config and cost models version by version; the unknown-network error before any fetch; the TxEvaluator kept when nothing new is configured.
  • test/utils/transaction.test.ts (core-cst): getTransactionValidityInterval, including a zero start.
  • Two existing tests were adjusted because evaluation is now on by default: balance.test.ts "should balance even when outputs don't have lovelaces" (its fixture stores bare scripts in scriptRef, which Scalus cannot decode; it tests balancing, so evaluator: null) and provider-calls.test.ts "offline evaluator fetches each transaction once across evaluations" (a given OfflineEvaluatorScalus now gets the builder's UTxOs and fetches nothing itself, which the test now asserts, with the builder's fetcher asked once per transaction).
  • provider-calls.test.ts "fetches missing script bytes for a locally known Plutus reference UTxO" fails on the base branch and on feat(tx-builder): opt-in Scalus evaluation with evaluator: "scalus" #862 (Script not found); utxosForEvaluation above fixes it.

Verification

Run locally after npm run build:mesh:

package tests
mesh-core-cst 182 passed, plus 16 under the Scalus (ESM) config
mesh-transaction 197 passed
mesh-scalus-emulator, mesh-contract, mesh-x402, mesh-wallet 24, 1, 98, 34 passed

git merge-tree with #861: no conflicts. CI does not run for PRs into this branch.

twwu123 and others added 5 commits October 30, 2026 11:53
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.
@lisicky
lisicky merged commit 29a4702 into feat/tx-builder-fetch-efficiency Nov 1, 2026
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.
@lisicky
lisicky deleted the feat/default-scalus-evaluator branch November 1, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants