Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions julc-cardano-client-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +41,15 @@
* .withSigner(signer)
* .complete();
* }</pre>
*
* <p><b>Thread-safety:</b> an instance is <b>not</b> 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). <b>Create a new {@code JulcTransactionEvaluator} per transaction / evaluation
* context (or confine one instance to a single thread); do not share an instance across threads.</b>
* 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 {

Expand All @@ -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
Expand Down Expand Up @@ -206,12 +213,15 @@ public Result<List<EvaluationResult>> evaluateTx(byte[] cbor, Set<Utxo> 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));
}

Expand Down Expand Up @@ -261,10 +271,10 @@ public Result<List<EvaluationResult>> evaluateTx(byte[] cbor, Set<Utxo> 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();

Expand Down Expand Up @@ -316,17 +326,6 @@ public Result<List<EvaluationResult>> evaluateTx(byte[] cbor, Set<Utxo> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Utxo> getPage(String address, Integer nrOfItems, Integer page,
com.bloxbean.cardano.client.api.common.OrderEnum order) {
return List.of();
}
@Override
public Optional<Utxo> getTxOutput(String txHash, int outputIndex) {
return Optional.empty();
}
};
ProtocolParamsSupplier protocolParamsSupplier = () -> {
var params = new ProtocolParams();
params.setMaxTxExMem("14000000");
params.setMaxTxExSteps("10000000000");
var raw = new LinkedHashMap<String, List<Long>>();
var list = new ArrayList<Long>(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);
Expand Down
2 changes: 1 addition & 1 deletion julc-vm-scalus/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -59,10 +59,12 @@ public void setCostModelParams(long[] costModelValues, PlutusLanguage language,
}
scala.collection.immutable.IndexedSeq<Object> 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<Object, scala.collection.immutable.IndexedSeq<Object>> map =
scala.collection.immutable.Map$.MODULE$.<Object, scala.collection.immutable.IndexedSeq<Object>>empty()
.updated(Language.PlutusV3, indexedSeq);
.updated(Language.PlutusV3.languageId(), indexedSeq);

CostModels costModels = new CostModels(map);
this.machineParams = MachineParams.fromCostModels(costModels, Language.PlutusV3, pv);
Expand Down Expand Up @@ -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);
Expand Down
Loading