Thread-safety: an instance is not thread-safe. Each {@link #evaluateTx} call
+ * (re)configures the cost model on a cached VM provider and then evaluates on it; the registered
+ * source maps/programs and trace flags are also mutable instance state. Concurrent use of a single
+ * instance can therefore race (e.g. one call reconfiguring the provider's cost model while another
+ * is evaluating). Create a new {@code JulcTransactionEvaluator} per transaction / evaluation
+ * context (or confine one instance to a single thread); do not share an instance across threads.
+ * Note: this warning is about this evaluator's per-call reconfiguration and mutable state.
+ * Do not rely on a cached provider inside this evaluator for concurrent evaluateTx calls.
*/
public class JulcTransactionEvaluator implements TransactionEvaluator {
@@ -50,7 +58,6 @@ public class JulcTransactionEvaluator implements TransactionEvaluator {
private final ScriptSupplier scriptSupplier;
private final SlotConfig slotConfig;
- private volatile JulcVm vm;
private volatile com.bloxbean.cardano.julc.vm.JulcVmProvider provider;
// Source map support
@@ -206,12 +213,15 @@ public Result> evaluateTx(byte[] cbor, Set inputUtx
Long.parseLong(params.getMaxTxExSteps()),
Long.parseLong(params.getMaxTxExMem()));
- // 6. Create VM (lazy, cached) and configure cost models for all language versions
- JulcVm julcVm = getOrCreateVm();
+ // 6. Resolve the VM provider (lazy, cached) and configure cost models for all language
+ // versions. The SAME provider instance must be used for setCostModelParams AND
+ // evaluation: cost-model params are stored per provider instance, so configuring one
+ // instance and evaluating on another would silently fall back to the built-in defaults.
+ var vmProvider = getProvider();
int pvMinor = params.getProtocolMinorVer() != null ? params.getProtocolMinorVer() : 0;
for (var lang : List.of(Language.PLUTUS_V1, Language.PLUTUS_V2, Language.PLUTUS_V3)) {
CostModelUtil.getCostModelFromProtocolParams(params, lang)
- .ifPresent(cm -> julcVm.setCostModelParams(
+ .ifPresent(cm -> vmProvider.setCostModelParams(
cm.getCosts(), toPlutusLanguage(lang), pvMajor, pvMinor));
}
@@ -261,10 +271,10 @@ public Result> evaluateTx(byte[] cbor, Set inputUtx
Program evalProgram = scriptPrograms.containsKey(scriptHash)
? scriptPrograms.get(scriptHash) : resolved.program();
- // e. Evaluate — traces are captured in the EvalResult
- var langVm = JulcVm.withProvider(getProvider(), resolved.language());
- EvalResult evalResult = langVm.evaluateWithArgs(
- evalProgram, args, maxBudget, evalOptions);
+ // e. Evaluate on the SAME provider configured above (not a fresh lookup), passing
+ // the resolved script's language explicitly.
+ EvalResult evalResult = vmProvider.evaluateWithArgs(
+ evalProgram, resolved.language(), args, maxBudget, evalOptions);
var executionTrace = evalResult.executionTrace();
var builtinTrace = evalResult.builtinTrace();
@@ -316,17 +326,6 @@ public Result> evaluateTx(byte[] cbor, Set inputUtx
}
}
- private JulcVm getOrCreateVm() {
- if (vm == null) {
- synchronized (this) {
- if (vm == null) {
- vm = JulcVm.create();
- }
- }
- }
- return vm;
- }
-
private com.bloxbean.cardano.julc.vm.JulcVmProvider getProvider() {
if (provider == null) {
synchronized (this) {
diff --git a/julc-cardano-client-lib/src/test/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluatorTest.java b/julc-cardano-client-lib/src/test/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluatorTest.java
index 457057b..6057a9c 100644
--- a/julc-cardano-client-lib/src/test/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluatorTest.java
+++ b/julc-cardano-client-lib/src/test/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluatorTest.java
@@ -16,6 +16,7 @@
import com.bloxbean.cardano.julc.vm.EvalResult;
import com.bloxbean.cardano.julc.vm.JulcVm;
import com.bloxbean.cardano.julc.vm.PlutusLanguage;
+import com.bloxbean.cardano.julc.vm.java.cost.CostModelParser;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -162,6 +163,94 @@ void evaluateTx_alwaysTrue_returnsNonZeroBudget() throws Exception {
"Expected non-zero ExUnits");
}
+ @Test
+ void evaluateTx_usesSuppliedCostModel_notBuiltInDefault() throws Exception {
+ // Regression for the provider-instance bug: the cost model was applied to one provider
+ // instance (getOrCreateVm/JulcVm.create) but evaluation ran on another (getProvider), so the
+ // chain cost model from ProtocolParams was silently ignored and the VM's built-in default was
+ // used. We supply two PlutusV3 cost models that differ (one with every cost doubled) via
+ // ProtocolParams.costModelsRaw and assert the evaluated CPU budget changes. Pre-fix, both runs
+ // would have produced the same (built-in default) budget and this test would FAIL.
+ String scriptAddr = buildScriptAddress(alwaysTrueHash);
+ String txHash = "ab".repeat(32);
+
+ Utxo inputUtxo = Utxo.builder()
+ .txHash(txHash)
+ .outputIndex(0)
+ .address(scriptAddr)
+ .amount(List.of(Amount.lovelace(BigInteger.valueOf(5_000_000))))
+ .build();
+
+ var redeemer = Redeemer.builder()
+ .tag(RedeemerTag.Spend)
+ .index(BigInteger.ZERO)
+ .data(new BigIntPlutusData(BigInteger.ZERO))
+ .exUnits(ExUnits.builder().mem(BigInteger.ZERO).steps(BigInteger.ZERO).build())
+ .build();
+
+ var tx = Transaction.builder()
+ .body(TransactionBody.builder()
+ .inputs(List.of(new TransactionInput(txHash, 0)))
+ .outputs(List.of())
+ .fee(BigInteger.valueOf(200_000))
+ .build())
+ .witnessSet(TransactionWitnessSet.builder()
+ .redeemers(List.of(redeemer))
+ .plutusV3Scripts(List.of(alwaysTrueScript))
+ .build())
+ .build();
+ byte[] cbor = tx.serialize();
+
+ // A valid PV10 PlutusV3 cost model (297 entries; pvMajor defaults to 10 in the evaluator),
+ // and a copy with every cost doubled.
+ long[] baseCosts = CostModelParser.defaultToFlatArray(10);
+ long[] scaledCosts = new long[baseCosts.length];
+ for (int i = 0; i < baseCosts.length; i++) {
+ scaledCosts[i] = baseCosts[i] * 2;
+ }
+
+ long baseCpu = evaluateV3SpendCpu(cbor, inputUtxo, baseCosts);
+ long scaledCpu = evaluateV3SpendCpu(cbor, inputUtxo, scaledCosts);
+
+ assertTrue(baseCpu > 0, "expected a non-zero CPU budget");
+ assertNotEquals(baseCpu, scaledCpu,
+ "supplied cost model must drive the budget; equal values mean the evaluator is "
+ + "ignoring setCostModelParams and using the built-in default (the bug)");
+ assertTrue(scaledCpu > baseCpu, "doubling all costs should increase the CPU budget");
+ }
+
+ /** Evaluate the single spend redeemer with a supplied PlutusV3 cost model; return CPU steps. */
+ private long evaluateV3SpendCpu(byte[] cbor, Utxo inputUtxo, long[] v3Costs) throws Exception {
+ UtxoSupplier utxoSupplier = new UtxoSupplier() {
+ @Override
+ public List getPage(String address, Integer nrOfItems, Integer page,
+ com.bloxbean.cardano.client.api.common.OrderEnum order) {
+ return List.of();
+ }
+ @Override
+ public Optional getTxOutput(String txHash, int outputIndex) {
+ return Optional.empty();
+ }
+ };
+ ProtocolParamsSupplier protocolParamsSupplier = () -> {
+ var params = new ProtocolParams();
+ params.setMaxTxExMem("14000000");
+ params.setMaxTxExSteps("10000000000");
+ var raw = new LinkedHashMap>();
+ var list = new ArrayList(v3Costs.length);
+ for (long c : v3Costs) {
+ list.add(c);
+ }
+ raw.put("PlutusV3", list);
+ params.setCostModelsRaw(raw);
+ return params;
+ };
+ var evaluator = new JulcTransactionEvaluator(utxoSupplier, protocolParamsSupplier, null);
+ var result = evaluator.evaluateTx(cbor, Set.of(inputUtxo));
+ assertTrue(result.isSuccessful(), "eval should succeed: " + result.getResponse());
+ return result.getValue().getFirst().getExUnits().getSteps().longValue();
+ }
+
@Test
void evaluateTx_alwaysFalse_returnsError() throws Exception {
String scriptAddr = buildScriptAddress(alwaysFalseHash);
diff --git a/julc-vm-scalus/build.gradle b/julc-vm-scalus/build.gradle
index 97cdc31..74631c8 100644
--- a/julc-vm-scalus/build.gradle
+++ b/julc-vm-scalus/build.gradle
@@ -15,7 +15,7 @@ sourceSets {
dependencies {
api project(':julc-vm')
- implementation 'org.scalus:scalus_3:0.16.0'
+ implementation 'org.scalus:scalus_3:0.17.0'
}
publishing {
diff --git a/julc-vm-scalus/src/main/java/com/bloxbean/cardano/julc/vm/scalus/ScalusVmProvider.java b/julc-vm-scalus/src/main/java/com/bloxbean/cardano/julc/vm/scalus/ScalusVmProvider.java
index d207704..c94be4d 100644
--- a/julc-vm-scalus/src/main/java/com/bloxbean/cardano/julc/vm/scalus/ScalusVmProvider.java
+++ b/julc-vm-scalus/src/main/java/com/bloxbean/cardano/julc/vm/scalus/ScalusVmProvider.java
@@ -1,6 +1,5 @@
package com.bloxbean.cardano.julc.vm.scalus;
-import com.bloxbean.cardano.julc.core.Constant;
import com.bloxbean.cardano.julc.core.PlutusData;
import com.bloxbean.cardano.julc.core.Program;
import com.bloxbean.cardano.julc.core.Term;
@@ -41,10 +40,11 @@ public class ScalusVmProvider implements JulcVmProvider {
@Override
public void setCostModelParams(long[] costModelValues, PlutusLanguage language,
int protocolMajorVersion, int protocolMinorVersion) {
- // Map protocol major version to Scalus MajorProtocolVersion
- MajorProtocolVersion pv = protocolMajorVersion >= 10
- ? MajorProtocolVersion.plominPV()
- : MajorProtocolVersion.changPV();
+ // Map the chain's protocol major version DIRECTLY to Scalus's MajorProtocolVersion
+ // (9 -> changPV, 10 -> plominPV, 11 -> vanRossemPV, and future versions). Do NOT collapse
+ // 11+ to plominPV: PlutusVM gates PV11 semantics (e.g. case-on-builtins) on
+ // protocolVersion >= vanRossemPV, so feeding the wrong version silently disables them.
+ MajorProtocolVersion pv = new MajorProtocolVersion(protocolMajorVersion);
this.protocolVersion = pv;
// Only V3 supports custom MachineParams in Scalus; V1/V2 use built-in defaults
@@ -59,10 +59,12 @@ public void setCostModelParams(long[] costModelValues, PlutusLanguage language,
}
scala.collection.immutable.IndexedSeq