diff --git a/julc-cardano-client-lib/build.gradle b/julc-cardano-client-lib/build.gradle index 5b28999..339da22 100644 --- a/julc-cardano-client-lib/build.gradle +++ b/julc-cardano-client-lib/build.gradle @@ -10,11 +10,11 @@ dependencies { implementation project(':julc-ledger-api') implementation project(':julc-stdlib') implementation project(':julc-vm') - implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.1' + implementation 'com.bloxbean.cardano:cardano-client-lib:0.7.2' testImplementation project(':julc-vm-java') testRuntimeOnly project(':julc-vm-scalus') - testImplementation 'org.scalus:scalus-bloxbean-cardano-client-lib_3:0.16.0' + testImplementation 'org.scalus:scalus-bloxbean-cardano-client-lib_3:0.17.0' } publishing { diff --git a/julc-cardano-client-lib/src/main/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluator.java b/julc-cardano-client-lib/src/main/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluator.java index a63b007..f9327d6 100644 --- a/julc-cardano-client-lib/src/main/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluator.java +++ b/julc-cardano-client-lib/src/main/java/com/bloxbean/cardano/julc/clientlib/eval/JulcTransactionEvaluator.java @@ -19,7 +19,6 @@ import com.bloxbean.cardano.julc.ledger.*; import com.bloxbean.cardano.julc.vm.EvalResult; import com.bloxbean.cardano.julc.vm.ExBudget; -import com.bloxbean.cardano.julc.vm.JulcVm; import com.bloxbean.cardano.julc.vm.PlutusLanguage; import com.bloxbean.cardano.julc.vm.trace.ExecutionTraceEntry; import com.bloxbean.cardano.julc.vm.trace.FailureReportBuilder; @@ -42,6 +41,15 @@ * .withSigner(signer) * .complete(); * } + * + *

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 indexedSeq = builder.result(); - // Build CostModels map: Language.PlutusV3 -> indexedSeq + // Build CostModels map: the key MUST be the integer languageId (Language.PlutusV3.languageId() == 2, + // which equals the enum ordinal). MachineParams.fromCostModels() looks the model up via + // boxToInteger(language.ordinal()), so keying by the Language enum object caused "key not found: 2". scala.collection.immutable.Map> map = scala.collection.immutable.Map$.MODULE$.>empty() - .updated(Language.PlutusV3, indexedSeq); + .updated(Language.PlutusV3.languageId(), indexedSeq); CostModels costModels = new CostModels(map); this.machineParams = MachineParams.fromCostModels(costModels, Language.PlutusV3, pv); @@ -128,8 +130,10 @@ private EvalResult evaluateScalusTerm(scalus.uplc.Term scalusTerm, PlutusLanguag // Evaluate using evaluateDeBruijnedTerm (general evaluation, // does not enforce script return-value semantics like evaluateScriptDebug) + // Scalus 0.17.0 added a 4th 'tracing' boolean (default false); pass false to preserve + // the prior 3-arg behaviour (no script return-value enforcement / tracing). scalus.uplc.Term scalusResult = vm.evaluateDeBruijnedTerm( - scalusTerm, budgetSpender, logger); + scalusTerm, budgetSpender, logger, false); // Convert result Term resultTerm = TermConverter.fromScalus(scalusResult);