diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3bbedd4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +JULC compile code : ../julc + +Yaci Devkit admin API documentation: http://localhost:10000/v3/api-docs +Use Yaci DevKit admin's reset endpoint to reset the devnet before running test + +Cardano Foundation's cardano-template-and-ecosystem-monitoring repository: https://github.com/cardano-foundation/cardano-template-and-ecosystem-monitoring.git + +For writing JuLC validator + - Always prefer to use high-level typed apis + - Try not to use low level builtins or PlutusData directly unless necessary. + - Follow julc-best-practices guide in docs folder for writing clean and maintainable code. diff --git a/build.gradle b/build.gradle index ac0e7aa..f202d1d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,14 @@ plugins { id 'java' id 'application' + id 'com.bloxbean.cardano.julc' version '0.1.0-pre15' } group = 'com.example' version = '0.1.0' ext { - julcVersion = '0.1.0-pre12' + julcVersion = '0.1.0-pre15' cardanoClientLibVersion = '0.8.0-pre4' } @@ -68,7 +69,8 @@ dependencies { testImplementation "com.bloxbean.cardano:julc-compiler:${julcVersion}" testImplementation "com.bloxbean.cardano:julc-stdlib:${julcVersion}" testRuntimeOnly "com.bloxbean.cardano:julc-vm-java:${julcVersion}" - testImplementation "com.bloxbean.cardano:julc-vm-truffle:${julcVersion}" // For JulcDebugger API +// testImplementation "com.bloxbean.cardano:julc-vm-truffle:${julcVersion}" // For JulcDebugger API +// testImplementation "com.bloxbean.cardano:julc-vm-scalus:${julcVersion}" // Scalus VM backend — cross-VM budget comparison testImplementation 'net.jqwik:jqwik:1.9.2' testImplementation platform('org.junit:junit-bom:5.11.4') testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/docs/julc-best-practices.md b/docs/julc-best-practices.md index 2645a5f..de794a7 100644 --- a/docs/julc-best-practices.md +++ b/docs/julc-best-practices.md @@ -204,6 +204,7 @@ list.take(n) // first n elements list.drop(n) // skip first n elements JulcList.empty() // empty list JulcList.of(a, b, c) // list from elements +JulcList.of(a, b, c).toPlutusData() // typed list -> ListData ``` ### 3.6 JulcMap<K, V> — On-Chain Associative Map @@ -218,6 +219,7 @@ map.insert(key, value) // new map with entry added map.delete(key) // new map without key map.keys() // all keys as JulcList map.values() // all values as JulcList +map.toPlutusData() // typed map -> MapData map.size() // number of entries map.isEmpty() // check if empty JulcMap.empty() // empty map @@ -266,6 +268,7 @@ import com.bloxbean.cardano.julc.stdlib.lib.ByteStringLib; // Basic operations ByteStringLib.append(a, b) // concatenate two byte arrays +Builtins.concat(a, b, c, ...) // concatenate 2+ arrays ByteStringLib.empty() // empty byte array ByteStringLib.cons(byteVal, bs) // prepend a byte (0-255) ByteStringLib.length(bs) // byte length @@ -303,6 +306,8 @@ ValuesLib.assetOf(value, policyId, tokenName) // quantity of specific ValuesLib.containsPolicy(value, policyId) // check if policy exists ValuesLib.countTokensWithQty(mint, policyId, qty) // count tokens with exact qty ValuesLib.findTokenName(mint, policyId, qty) // find token name with exact qty +ValuesLib.refBytes(seedRef) // txId ++ 2-byte BE index +ValuesLib.uniqueTokenName(seedRef) // blake2b_256(refBytes) ValuesLib.flatten(value) // flatten to list of (policy, name, amount) triples // Construction @@ -940,13 +945,17 @@ if (a.equals(BigInteger.ZERO)) if (a.compareTo(BigInteger.ZERO) > 0) ``` -### 10.3 JVM vs UPLC Behavior Difference +### 10.3 `integerToByteString` Bounds -`Builtins.integerToByteString(true, 0, 0)`: -- **JVM**: Returns `[0]` (1 byte) — BigInteger.ZERO.toByteArray() bug -- **UPLC**: Returns `[]` (empty) +The JVM and UPLC implementations now share the same contract: -**Fix in tests**: Manually handle zero: `if (n.signum() == 0) return new byte[0];` +- width must be between 0 and 8192 bytes; +- a positive width fails if the integer does not fit; +- width 0 uses the minimal representation and encodes zero as an empty byte string; +- even with width 0, the result cannot exceed 8192 bytes. + +For deterministic `TxOutRef` seeds, prefer `ValuesLib.refBytes(ref)`, which uses +a fixed two-byte big-endian output index and fails if the index exceeds 65535. ### 10.4 `contains()` on ByteString Lists diff --git a/src/main/java/com/example/cftemplates/auction/offchain/AuctionDemo.java b/src/main/java/com/example/cftemplates/auction/offchain/AuctionDemo.java index 6ec55fc..4c237ad 100644 --- a/src/main/java/com/example/cftemplates/auction/offchain/AuctionDemo.java +++ b/src/main/java/com/example/cftemplates/auction/offchain/AuctionDemo.java @@ -81,7 +81,6 @@ public static void main(String[] args) throws Exception { .collateralPayer(seller.baseAddress()) .withRequiredSigners(sellerPkh) .validTo(currentSlot + 10) // before expiration - .ignoreScriptCostEvaluationError(true) .withTxEvaluator(YaciHelper.julcEvaluator(backend)) .complete(); diff --git a/src/main/java/com/example/cftemplates/bet/offchain/BetDemo.java b/src/main/java/com/example/cftemplates/bet/offchain/BetDemo.java index a101b06..493fbdd 100644 --- a/src/main/java/com/example/cftemplates/bet/offchain/BetDemo.java +++ b/src/main/java/com/example/cftemplates/bet/offchain/BetDemo.java @@ -89,7 +89,6 @@ public static void main(String[] args) throws Exception { .collateralPayer(player1.baseAddress()) .withRequiredSigners(player1Pkh) .validTo(currentSlot + 10) // upper bound before expiration - .ignoreScriptCostEvaluationError(true) .withTxEvaluator(YaciHelper.julcEvaluator(backend)) .complete(); diff --git a/src/main/java/com/example/offchain/MintingDemo.java b/src/main/java/com/example/offchain/MintingDemo.java index cf9f685..d7544c2 100644 --- a/src/main/java/com/example/offchain/MintingDemo.java +++ b/src/main/java/com/example/offchain/MintingDemo.java @@ -63,7 +63,7 @@ public static void main(String[] args) throws Exception { .withRequiredSigners(authorizerPkh) .feePayer(authorizerAddr) .collateralPayer(authorizerAddr) - .withTxEvaluator(YaciHelper.julcEvaluator(backend)) +// .withTxEvaluator(YaciHelper.julcEvaluator(backend)) .complete(); if (!mintResult.isSuccessful()) { diff --git a/src/main/java/com/example/offchain/OneShotMintDemo.java b/src/main/java/com/example/offchain/OneShotMintDemo.java index 93709c4..43c8884 100644 --- a/src/main/java/com/example/offchain/OneShotMintDemo.java +++ b/src/main/java/com/example/offchain/OneShotMintDemo.java @@ -4,6 +4,7 @@ import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData; import com.bloxbean.cardano.client.plutus.spec.BytesPlutusData; @@ -12,6 +13,9 @@ import com.bloxbean.cardano.client.transaction.spec.Asset; import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.julc.clientlib.JulcScriptLoader; +import com.bloxbean.cardano.julc.ledger.TxId; +import com.bloxbean.cardano.julc.ledger.TxOutRef; +import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib; import com.example.validators.OneShotMintPolicy; import java.math.BigInteger; @@ -60,9 +64,17 @@ public static void main(String[] args) throws Exception { BigIntPlutusData.of(utxoIndex)); System.out.println("Policy loaded (parameterized)"); - // 4. Mint 1 UniqueNFT token - System.out.println("\n--- Minting 1 UniqueNFT ---"); - var asset = new Asset("UniqueNFT", BigInteger.ONE); + // 4. Derive the canonical 32-byte token name from the consumed seed. + // ValuesLib.refBytes is JVM/UPLC-identical; Blake2bUtil is the off-chain + // equivalent of the policy's ValuesLib.uniqueTokenName(seedRef). + var seedRef = new TxOutRef(new TxId(utxoTxId), utxoIndex); + byte[] tokenName = Blake2bUtil.blake2bHash256(ValuesLib.refBytes(seedRef)); + String tokenNameHex = HexUtil.encodeHexString(tokenName); + System.out.println("Canonical token name: " + tokenNameHex); + + // 5. Mint exactly one token with that canonical name. + System.out.println("\n--- Minting canonical one-shot NFT ---"); + var asset = new Asset("0x" + tokenNameHex, BigInteger.ONE); var redeemer = BigIntPlutusData.of(0); // unused redeemer var mintTx = new ScriptTx() diff --git a/src/main/java/com/example/uverify/onchain/UVerifyFeePot.java b/src/main/java/com/example/uverify/onchain/UVerifyFeePot.java index 4a61615..0481a12 100644 --- a/src/main/java/com/example/uverify/onchain/UVerifyFeePot.java +++ b/src/main/java/com/example/uverify/onchain/UVerifyFeePot.java @@ -3,6 +3,7 @@ import com.bloxbean.cardano.julc.core.PlutusData; import com.bloxbean.cardano.julc.core.types.JulcList; import com.bloxbean.cardano.julc.ledger.*; +import com.bloxbean.cardano.julc.stdlib.Builtins; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.MultiValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Param; @@ -111,13 +112,8 @@ static boolean handleOnBehalf(TxInfo txInfo, TxOutRef utxo, OnBehalf ob) { static byte[] buildExpectedMessage(byte[] signerPkhHex, byte[] submitterKeyHashHex, BigInteger ttl) { byte[] colon = ByteStringLib.cons(58, ByteStringLib.empty()); // ':' byte[] ttlStr = ByteStringLib.intToDecimalString(ttl); - return ByteStringLib.append( - ByteStringLib.append( - ByteStringLib.append( - ByteStringLib.append(signerPkhHex, colon), - submitterKeyHashHex), - colon), - ttlStr); + return Builtins.concat( + signerPkhHex, colon, submitterKeyHashHex, colon, ttlStr); } static BigInteger sumLovelaceAtWhitelistedScripts(JulcList outputs) { diff --git a/src/main/java/com/example/uverify/onchain/UVerifyV1.java b/src/main/java/com/example/uverify/onchain/UVerifyV1.java index 5069d8b..748baa0 100644 --- a/src/main/java/com/example/uverify/onchain/UVerifyV1.java +++ b/src/main/java/com/example/uverify/onchain/UVerifyV1.java @@ -453,8 +453,7 @@ static boolean certificateIsValid(UVerifyCertificate cert, JulcList } static byte[] certificateToByteArray(UVerifyCertificate cert) { - byte[] base = ByteStringLib.append( - ByteStringLib.append(cert.hash(), cert.algorithm()), cert.issuer()); + byte[] base = Builtins.concat(cert.hash(), cert.algorithm(), cert.issuer()); byte[] result = base; for (var extra : cert.extra()) { result = ByteStringLib.append(result, extra); @@ -467,4 +466,4 @@ static boolean oneOfAdminKeysSigned(JulcList signatories) { sig.hash().equals(adminKey1) || sig.hash().equals(adminKey2)); } -} \ No newline at end of file +} diff --git a/src/main/java/com/example/validators/AuctionValidator.java b/src/main/java/com/example/validators/AuctionValidator.java index ed5aed7..4a17c9b 100644 --- a/src/main/java/com/example/validators/AuctionValidator.java +++ b/src/main/java/com/example/validators/AuctionValidator.java @@ -1,7 +1,6 @@ package com.example.validators; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; -import com.bloxbean.cardano.julc.stdlib.annotation.Validator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; diff --git a/src/main/java/com/example/validators/EscrowValidator.java b/src/main/java/com/example/validators/EscrowValidator.java index 4d09311..3832275 100644 --- a/src/main/java/com/example/validators/EscrowValidator.java +++ b/src/main/java/com/example/validators/EscrowValidator.java @@ -1,7 +1,6 @@ package com.example.validators; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; -import com.bloxbean.cardano.julc.stdlib.annotation.Validator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; diff --git a/src/main/java/com/example/validators/MultiSigMinting.java b/src/main/java/com/example/validators/MultiSigMinting.java index cee55e9..abdb5e3 100644 --- a/src/main/java/com/example/validators/MultiSigMinting.java +++ b/src/main/java/com/example/validators/MultiSigMinting.java @@ -1,6 +1,5 @@ package com.example.validators; -import com.bloxbean.cardano.julc.stdlib.annotation.MintingPolicy; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.stdlib.annotation.MintingValidator; import com.bloxbean.cardano.julc.ledger.ScriptContext; diff --git a/src/main/java/com/example/validators/OneShotMintPolicy.java b/src/main/java/com/example/validators/OneShotMintPolicy.java index ebdfe66..b5824d8 100644 --- a/src/main/java/com/example/validators/OneShotMintPolicy.java +++ b/src/main/java/com/example/validators/OneShotMintPolicy.java @@ -4,9 +4,12 @@ import com.bloxbean.cardano.julc.stdlib.annotation.MintingValidator; import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.ledger.ScriptContext; +import com.bloxbean.cardano.julc.ledger.ScriptInfo; import com.bloxbean.cardano.julc.ledger.TxInfo; import com.bloxbean.cardano.julc.ledger.TxOutRef; +import com.bloxbean.cardano.julc.ledger.Value; import com.bloxbean.cardano.julc.stdlib.lib.ContextsLib; +import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib; import com.bloxbean.cardano.julc.stdlib.Builtins; import com.bloxbean.cardano.julc.core.PlutusData; @@ -24,16 +27,58 @@ public class OneShotMintPolicy { @Entrypoint public static boolean validate(PlutusData redeemer, ScriptContext ctx) { TxInfo txInfo = ctx.txInfo(); + ScriptInfo.MintingScript minting = (ScriptInfo.MintingScript) ctx.scriptInfo(); + byte[] ownPolicyId = Builtins.toByteString(minting.policyId()); + ContextsLib.trace("Checking UTXO input"); - boolean found = false; + boolean validMint = false; for (var input : txInfo.inputs()) { TxOutRef ref = input.outRef(); byte[] refTxIdBytes = Builtins.toByteString(ref.txId()); - found = Builtins.equalsByteString(refTxIdBytes, utxoTxId) && ref.index().compareTo(utxoIndex) == 0; - if (found) { + boolean consumesSeed = Builtins.equalsByteString(refTxIdBytes, utxoTxId) + && ref.index().compareTo(utxoIndex) == 0; + if (consumesSeed) { + byte[] expectedTokenName = ValuesLib.uniqueTokenName(ref); + validMint = mintsOnlyCanonicalToken( + txInfo.mint(), ownPolicyId, expectedTokenName); break; // ← break is separate from assignment } } - return found; + return validMint; + } + + /** + * Require exactly one asset under this policy: the canonical token derived + * from the consumed seed reference, with quantity one. + */ + static boolean mintsOnlyCanonicalToken( + Value mint, byte[] ownPolicyId, byte[] expectedTokenName) { + PlutusData policies = Builtins.unMapData(mint); + PlutusData ownPolicyData = Builtins.bData(ownPolicyId); + boolean validMint = false; + + while (!Builtins.nullList(policies)) { + var policy = Builtins.headList(policies); + if (Builtins.equalsData(Builtins.fstPair(policy), ownPolicyData)) { + PlutusData assets = Builtins.unMapData( + (PlutusData.MapData) Builtins.sndPair(policy)); + + if (!Builtins.nullList(assets)) { + var asset = Builtins.headList(assets); + boolean exactlyOneAsset = + Builtins.nullList(Builtins.tailList(assets)); + boolean expectedName = Builtins.equalsData( + Builtins.fstPair(asset), Builtins.bData(expectedTokenName)); + boolean expectedQuantity = Builtins.unIData( + Builtins.sndPair(asset)).compareTo(BigInteger.ONE) == 0; + + validMint = exactlyOneAsset && expectedName && expectedQuantity; + } + break; + } + policies = Builtins.tailList(policies); + } + + return validMint; } } diff --git a/src/main/java/com/example/validators/VestingValidator.java b/src/main/java/com/example/validators/VestingValidator.java index 9ea5bf8..5c58618 100644 --- a/src/main/java/com/example/validators/VestingValidator.java +++ b/src/main/java/com/example/validators/VestingValidator.java @@ -2,13 +2,11 @@ import com.bloxbean.cardano.julc.stdlib.annotation.Param; import com.bloxbean.cardano.julc.stdlib.annotation.SpendingValidator; -import com.bloxbean.cardano.julc.stdlib.annotation.Validator; import com.bloxbean.cardano.julc.stdlib.annotation.Entrypoint; import com.bloxbean.cardano.julc.ledger.ScriptContext; import com.bloxbean.cardano.julc.ledger.TxInfo; import com.bloxbean.cardano.julc.ledger.PubKeyHash; import com.bloxbean.cardano.julc.stdlib.lib.ContextsLib; -//import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib; import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib; import com.example.util.SumTest; diff --git a/src/test/java/com/example/nft/Cip68NftIntegrationTest.java b/src/test/java/com/example/nft/Cip68NftIntegrationTest.java index 426790b..4841ce5 100644 --- a/src/test/java/com/example/nft/Cip68NftIntegrationTest.java +++ b/src/test/java/com/example/nft/Cip68NftIntegrationTest.java @@ -3,6 +3,7 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.function.helper.SignerProviders; @@ -21,6 +22,7 @@ import org.junit.jupiter.api.*; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -46,13 +48,15 @@ class Cip68NftIntegrationTest { static byte[] refTokenName; static byte[] userTokenName; static String mintTxHash; + static String updateTxHash; @BeforeAll static void setup() throws Exception { yaciAvailable = YaciHelper.isYaciReachable(); if (!yaciAvailable) return; - byte[] assetName = "TestNFT".getBytes(); + byte[] assetName = ("TestNFT" + Long.toHexString(System.currentTimeMillis())) + .getBytes(StandardCharsets.UTF_8); refTokenName = concat(REF_PREFIX, assetName); userTokenName = concat(USER_PREFIX, assetName); @@ -86,11 +90,17 @@ void step1_mintCip68Nft() throws Exception { var mintRedeemer = ConstrPlutusData.of(0); // MintNft - // Mint both tokens in a single mintAsset call — multiple mintAsset calls on the - // same policy don't merge correctly. Both tokens go to the script address with - // inline datum; the validator only requires the ref token to be there. + var refUnit = policyId + HexUtil.encodeHexString(refTokenName); + var userUnit = policyId + HexUtil.encodeHexString(userTokenName); + + // Mint both tokens in a single mintAsset call. The reference token is locked + // at the script with inline metadata; the user token stays at the minter. var mintTx = new ScriptTx() - .mintAsset(script, List.of(refAsset, userAsset), mintRedeemer, scriptAddr, metadataDatum); + .mintAsset(script, List.of(refAsset, userAsset), mintRedeemer) + .payToContract(scriptAddr, + List.of(Amount.ada(2), new Amount(refUnit, BigInteger.ONE)), + metadataDatum) + .payToAddress(minter.baseAddress(), List.of(new Amount(userUnit, BigInteger.ONE))); var result = quickTx.compose(mintTx) .withSigner(SignerProviders.signerFrom(minter)) @@ -148,21 +158,20 @@ void step3_updateMetadata() throws Exception { .complete(); assertTrue(result.isSuccessful(), "Update tx should succeed: " + result); - YaciHelper.waitForConfirmation(backend, result.getValue()); - System.out.println("Step 3 OK: Updated metadata, tx=" + result.getValue()); + updateTxHash = result.getValue(); + YaciHelper.waitForConfirmation(backend, updateTxHash); + System.out.println("Step 3 OK: Updated metadata, tx=" + updateTxHash); } @Test @Order(4) void step4_burnBothTokens() throws Exception { assumeTrue(yaciAvailable, "Yaci DevKit not available"); + assertNotNull(updateTxHash, "Step 3 must complete first"); - // Find ref UTxO at script address (most recent from step3) - var utxoResult = backend.getUtxoService().getUtxos(scriptAddr, 100, 1); - assertTrue(utxoResult.isSuccessful() && !utxoResult.getValue().isEmpty(), - "Should find ref UTxO at script address"); - var allUtxos = utxoResult.getValue(); - var refUtxo = allUtxos.get(allUtxos.size() - 1); + var refUtxo = YaciHelper.findUtxo(backend, scriptAddr, updateTxHash); + var userUtxo = findUtxoWithAsset(backend, minter.baseAddress(), + policyId + HexUtil.encodeHexString(userTokenName)); var negOne = BigInteger.ONE.negate(); var refAsset = new Asset("0x" + HexUtil.encodeHexString(refTokenName), negOne); @@ -172,15 +181,14 @@ void step4_burnBothTokens() throws Exception { var burnTx = new ScriptTx() .collectFrom(refUtxo, burnRedeemer) + .collectFrom(userUtxo) .mintAsset(script, List.of(refAsset, userAsset), burnRedeemer) - .payToAddress(minter.baseAddress(), Amount.ada(2)) .attachSpendingValidator(script); var result = quickTx.compose(burnTx) .withSigner(SignerProviders.signerFrom(minter)) .feePayer(minter.baseAddress()) .collateralPayer(minter.baseAddress()) - .ignoreScriptCostEvaluationError(true) .complete(); assertTrue(result.isSuccessful(), "Burn tx should succeed: " + result); @@ -188,6 +196,23 @@ void step4_burnBothTokens() throws Exception { System.out.println("Step 4 OK: Burned both tokens, tx=" + result.getValue()); } + private static Utxo findUtxoWithAsset(BackendService backend, String address, String unit) throws Exception { + for (int attempt = 0; attempt < 5; attempt++) { + var utxoResult = backend.getUtxoService().getUtxos(address, 100, 1); + if (utxoResult.isSuccessful() && utxoResult.getValue() != null) { + var match = utxoResult.getValue().stream() + .filter(utxo -> utxo.getAmount() != null + && utxo.getAmount().stream().anyMatch(amount -> + unit.equals(amount.getUnit()) + && amount.getQuantity().compareTo(BigInteger.ZERO) > 0)) + .findFirst(); + if (match.isPresent()) return match.get(); + } + Thread.sleep(2000); + } + throw new RuntimeException("UTXO not found at " + address + " for asset " + unit); + } + private static byte[] concat(byte[] a, byte[] b) { byte[] result = new byte[a.length + b.length]; System.arraycopy(a, 0, result, 0, a.length); diff --git a/src/test/java/com/example/validators/OneShotMintPolicyTest.java b/src/test/java/com/example/validators/OneShotMintPolicyTest.java index 77234e8..3a52ab5 100644 --- a/src/test/java/com/example/validators/OneShotMintPolicyTest.java +++ b/src/test/java/com/example/validators/OneShotMintPolicyTest.java @@ -4,6 +4,7 @@ import com.bloxbean.cardano.julc.core.types.JulcList; import com.bloxbean.cardano.julc.core.types.JulcMap; import com.bloxbean.cardano.julc.ledger.*; +import com.bloxbean.cardano.julc.stdlib.lib.ValuesLib; import com.bloxbean.cardano.julc.testkit.*; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; @@ -27,22 +28,32 @@ class OneShotMintPolicyTest extends ContractTest { 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68}; + static final byte[] POLICY_ID = new byte[28]; @BeforeAll static void setup() { initCrypto(); } + static byte[] canonicalTokenName(byte[] txId, BigInteger index) { + return ValuesLib.uniqueTokenName(new TxOutRef(new TxId(txId), index)); + } + + static Value mintValue(byte[] tokenName) { + return Value.singleton( + new PolicyId(POLICY_ID), new TokenName(tokenName), BigInteger.ONE); + } + // ---- Mode A: Direct Java tests ---- @Nested class DirectJavaTests { - private ScriptContext buildCtx(TxInInfo... inputs) { + private ScriptContext buildCtx(Value mint, TxInInfo... inputs) { var txInfo = new TxInfo( JulcList.of(inputs), JulcList.of(), JulcList.of(), BigInteger.valueOf(200_000), - Value.zero(), + mint, JulcList.of(), JulcMap.empty(), Interval.always(), JulcList.of(), @@ -51,7 +62,7 @@ private ScriptContext buildCtx(TxInInfo... inputs) { JulcMap.empty(), JulcList.of(), Optional.empty(), Optional.empty()); return new ScriptContext(txInfo, PlutusData.UNIT, - new ScriptInfo.MintingScript(new PolicyId(new byte[28]))); + new ScriptInfo.MintingScript(new PolicyId(POLICY_ID))); } private TxInInfo makeInput(byte[] txId, BigInteger index) { @@ -66,12 +77,12 @@ private TxInInfo makeInput(byte[] txId, BigInteger index) { @Test void matchingUtxoInput_passes() { - // Set @Param — SAME byte[] reference for txId OneShotMintPolicy.utxoTxId = TX_ID; OneShotMintPolicy.utxoIndex = BigInteger.ZERO; - var input = makeInput(TX_ID, BigInteger.ZERO); // same reference - var ctx = buildCtx(input); + var input = makeInput(TX_ID.clone(), BigInteger.ZERO); + var ctx = buildCtx( + mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO)), input); boolean result = OneShotMintPolicy.validate(PlutusData.UNIT, ctx); assertTrue(result, "Matching UTXO input should pass"); @@ -82,12 +93,24 @@ void noMatchingInput_fails() { OneShotMintPolicy.utxoTxId = TX_ID; OneShotMintPolicy.utxoIndex = BigInteger.ZERO; - var input = makeInput(OTHER_TX_ID, BigInteger.ZERO); // different txId - var ctx = buildCtx(input); + var input = makeInput(OTHER_TX_ID, BigInteger.ZERO); + var ctx = buildCtx( + mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO)), input); boolean result = OneShotMintPolicy.validate(PlutusData.UNIT, ctx); assertFalse(result, "Non-matching UTXO should fail"); } + + @Test + void wrongTokenName_fails() { + OneShotMintPolicy.utxoTxId = TX_ID; + OneShotMintPolicy.utxoIndex = BigInteger.ZERO; + + var input = makeInput(TX_ID, BigInteger.ZERO); + var ctx = buildCtx(mintValue("wrong-token".getBytes()), input); + + assertFalse(OneShotMintPolicy.validate(PlutusData.UNIT, ctx)); + } } // ---- Mode B: UPLC compilation tests ---- @@ -95,7 +118,8 @@ void noMatchingInput_fails() { @Nested class UplcTests { - private PlutusData buildCtxWithInput(PlutusData redeemer, byte[] txId, int index) { + private PlutusData buildCtxWithInput( + PlutusData redeemer, byte[] txId, int index, Value mint) { var ref = new com.bloxbean.cardano.julc.ledger.TxOutRef( new TxId(txId), BigInteger.valueOf(index)); var address = TestDataBuilder.pubKeyAddress(TestDataBuilder.randomPubKeyHash_typed()); @@ -103,10 +127,11 @@ private PlutusData buildCtxWithInput(PlutusData redeemer, byte[] txId, int index com.bloxbean.cardano.julc.ledger.Value.lovelace(BigInteger.valueOf(5_000_000))); var txIn = TestDataBuilder.txIn(ref, txOut); - var policyId = new com.bloxbean.cardano.julc.ledger.PolicyId(new byte[28]); + var policyId = new com.bloxbean.cardano.julc.ledger.PolicyId(POLICY_ID); return mintingContext(policyId) .redeemer(redeemer) .input(txIn) + .mint(mint) .buildPlutusData(); } @@ -120,7 +145,11 @@ void compilesWithParams_andEvaluates() throws Exception { PlutusData.integer(0)); var redeemer = PlutusData.integer(0); - var ctx = buildCtxWithInput(redeemer, TX_ID, 0); + var ctx = buildCtxWithInput( + redeemer, + TX_ID, + 0, + mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO))); var result = evaluate(concrete, ctx); assertSuccess(result); @@ -136,13 +165,47 @@ void rejectsWrongUtxo() throws Exception { PlutusData.integer(0)); var redeemer = PlutusData.integer(0); - var ctx = buildCtxWithInput(redeemer, OTHER_TX_ID, 0); // wrong txId + var ctx = buildCtxWithInput( + redeemer, + OTHER_TX_ID, + 0, + mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO))); var result = evaluate(concrete, ctx); assertFailure(result); logBudget("rejectsWrongUtxo", result); } + @Test + void rejectsWrongTokenName() throws Exception { + var program = compileValidator(OneShotMintPolicy.class).program(); + var concrete = program.applyParams( + PlutusData.bytes(TX_ID), + PlutusData.integer(0)); + + var ctx = buildCtxWithInput( + PlutusData.integer(0), + TX_ID, + 0, + mintValue("wrong-token".getBytes())); + + assertFailure(evaluate(concrete, ctx)); + } + + @Test + void rejectsAdditionalAssetUnderOwnPolicy() throws Exception { + var program = compileValidator(OneShotMintPolicy.class).program(); + var concrete = program.applyParams( + PlutusData.bytes(TX_ID), + PlutusData.integer(0)); + + Value mint = mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO)) + .merge(mintValue("extra-token".getBytes())); + var ctx = buildCtxWithInput(PlutusData.integer(0), TX_ID, 0, mint); + + assertFailure(evaluate(concrete, ctx)); + } + @Test void tracesAppear() throws Exception { var program = compileValidator(OneShotMintPolicy.class).program(); @@ -152,7 +215,11 @@ void tracesAppear() throws Exception { PlutusData.integer(0)); var redeemer = PlutusData.integer(0); - var ctx = buildCtxWithInput(redeemer, TX_ID, 0); + var ctx = buildCtxWithInput( + redeemer, + TX_ID, + 0, + mintValue(canonicalTokenName(TX_ID, BigInteger.ZERO))); var result = evaluate(concrete, ctx); assertSuccess(result); diff --git a/src/test/java/com/example/validators/TruffleDebuggerDemoTest.java b/src/test/java/com/example/validators/TruffleDebuggerDemoTest.java index d7f9728..65908b8 100644 --- a/src/test/java/com/example/validators/TruffleDebuggerDemoTest.java +++ b/src/test/java/com/example/validators/TruffleDebuggerDemoTest.java @@ -10,7 +10,7 @@ import com.bloxbean.cardano.julc.testkit.ContractTest; import com.bloxbean.cardano.julc.testkit.TestDataBuilder; import com.bloxbean.cardano.julc.vm.EvalResult; -import com.bloxbean.cardano.julc.vm.truffle.debug.JulcDebugger; +//import com.bloxbean.cardano.julc.vm.truffle.debug.JulcDebugger; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -33,272 +33,272 @@ */ class TruffleDebuggerDemoTest extends ContractTest { - @BeforeAll - static void setup() { - initCrypto(); - } - - // ---- JIT: verify Truffle VM is active ---- - - @Test - void truffleVmIsActive() { - String provider = vm().providerName(); - System.out.println("VM provider: " + provider); - assertEquals("Truffle", provider, - "Truffle VM should be auto-selected (highest priority)"); - } - - // ---- JIT: existing validator works identically under Truffle ---- - - @Test - void escrowValidator_passingPath_underTruffle() { - CompileResult compiled = compileValidator(EscrowValidator.class); - EvalResult result = evaluate(compiled.program(), passingCompleteCtx()); - assertSuccess(result); - - System.out.println("EscrowValidator (complete, pass) — Truffle VM"); - System.out.println(" CPU: " + result.budgetConsumed().cpuSteps()); - System.out.println(" Mem: " + result.budgetConsumed().memoryUnits()); - } - - @Test - void escrowValidator_failingPath_underTruffle() { - CompileResult compiled = compileValidator(EscrowValidator.class); - EvalResult result = evaluate(compiled.program(), failingRefundCtx()); - assertFailure(result); - - System.out.println("EscrowValidator (refund, fail) — Truffle VM"); - System.out.println(" CPU: " + result.budgetConsumed().cpuSteps()); - } - - // ---- JIT: warmup benchmark ---- - - @Test - void jitWarmup_escrowValidator() { - CompileResult compiled = compileValidator(EscrowValidator.class); - PlutusData ctx = passingCompleteCtx(); - - // Cold run (interpreted) - long coldStart = System.nanoTime(); - for (int i = 0; i < 10; i++) { - evaluate(compiled.program(), ctx); - } - long coldMs = (System.nanoTime() - coldStart) / 1_000_000; - - // Warmup — Truffle JIT compiles hot CallTargets after ~1000 iterations - for (int i = 0; i < 2000; i++) { - evaluate(compiled.program(), ctx); - } - - // Hot run (JIT-compiled if GraalVM compiler is available) - long hotStart = System.nanoTime(); - for (int i = 0; i < 10; i++) { - evaluate(compiled.program(), ctx); - } - long hotMs = (System.nanoTime() - hotStart) / 1_000_000; - - System.out.println(); - System.out.println("=== JIT WARMUP BENCHMARK: EscrowValidator ==="); - System.out.println(" Cold (first 10): " + coldMs + " ms"); - System.out.println(" Warmup: 2000 iterations"); - System.out.println(" Hot (next 10): " + hotMs + " ms"); - if (coldMs > 0) { - System.out.println(" Speedup: " + - String.format("%.1fx", (double) coldMs / Math.max(hotMs, 1))); - } - System.out.println(); - } - - // ---- Debugger: step through every statement ---- - - @Test - void stepThrough_escrowValidator_completePath() { - CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); - Term term = applyArg(compiled.program().term(), passingCompleteCtx()); - - var steps = new ArrayList(); - - try (var debugger = JulcDebugger.create()) { - debugger.sourceMap(compiled.sourceMap()); - EvalResult result = debugger.stepThrough(term, event -> { - steps.add(String.format("%-30s line %3d CPU=%,d Mem=%,d", - event.fileName(), event.line(), - event.cpuConsumed(), event.memConsumed())); - event.stepOver(); - }); - - assertTrue(result.isSuccess(), - "Expected success but got: " + describeResult(result)); - } - - System.out.println(); - System.out.println("=== DEBUGGER STEP-THROUGH: EscrowValidator (complete path) ==="); - steps.forEach(s -> System.out.println(" " + s)); - System.out.println(" Total steps: " + steps.size()); - System.out.println(); - - assertFalse(steps.isEmpty(), "Should have step events with source map"); - } - - // ---- Debugger: step through failing path ---- - - @Test - void stepThrough_escrowValidator_failingRefund() { - CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); - Term term = applyArg(compiled.program().term(), failingRefundCtx()); - - var steps = new ArrayList(); - - try (var debugger = JulcDebugger.create()) { - debugger.sourceMap(compiled.sourceMap()); - EvalResult result = debugger.stepThrough(term, event -> { - steps.add(String.format("%-30s line %3d CPU=%,d", - event.fileName(), event.line(), event.cpuConsumed())); - event.stepOver(); - }); - - // Refund path fails (deadline not passed) - assertFalse(result.isSuccess()); - } - - System.out.println(); - System.out.println("=== DEBUGGER STEP-THROUGH: EscrowValidator (refund path, FAILS) ==="); - steps.forEach(s -> System.out.println(" " + s)); - System.out.println(" Total steps: " + steps.size()); - System.out.println(); - } - - // ---- Debugger: breakpoints ---- - - @Test - void breakAt_escrowValidator() { - CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); - Term term = applyArg(compiled.program().term(), passingCompleteCtx()); - - var hits = new ArrayList(); - - try (var debugger = JulcDebugger.create()) { - debugger.sourceMap(compiled.sourceMap()) - .breakAt("EscrowValidator.java", 24) // if (redeemer.action()...) - .breakAt("EscrowValidator.java", 34); // checkComplete: buyerSigned - - EvalResult result = debugger.run(term, event -> { - hits.add(String.format("BREAK %s:%d CPU=%,d Mem=%,d", - event.fileName(), event.line(), - event.cpuConsumed(), event.memConsumed())); - event.resume(); - }); - - assertTrue(result.isSuccess(), - "Expected success but got: " + describeResult(result)); - } - - System.out.println(); - System.out.println("=== BREAKPOINT HITS: EscrowValidator ==="); - hits.forEach(h -> System.out.println(" " + h)); - System.out.println(); - } - - // ---- Debugger: budget parity with non-debug evaluation ---- - - @Test - void budgetParity_debugVsNormal() { - CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); - PlutusData ctx = passingCompleteCtx(); - - // Normal evaluation (via TruffleVmProvider) - EvalResult normalResult = evaluate(compiled.program(), ctx); - assertSuccess(normalResult); - - // Debug evaluation (via JulcDebugger with Truffle Debugger API) - Term term = applyArg(compiled.program().term(), ctx); - try (var debugger = JulcDebugger.create()) { - debugger.sourceMap(compiled.sourceMap()); - EvalResult debugResult = debugger.stepThrough(term, event -> event.stepOver()); - - assertTrue(debugResult.isSuccess(), - "Debug eval should succeed: " + describeResult(debugResult)); - - System.out.println(); - System.out.println("=== BUDGET PARITY ==="); - System.out.println(" Normal: CPU=" + normalResult.budgetConsumed().cpuSteps() - + " Mem=" + normalResult.budgetConsumed().memoryUnits()); - System.out.println(" Debug: CPU=" + debugResult.budgetConsumed().cpuSteps() - + " Mem=" + debugResult.budgetConsumed().memoryUnits()); - - assertEquals(normalResult.budgetConsumed().cpuSteps(), - debugResult.budgetConsumed().cpuSteps(), - "CPU budget must match between debug and normal evaluation"); - assertEquals(normalResult.budgetConsumed().memoryUnits(), - debugResult.budgetConsumed().memoryUnits(), - "Memory budget must match between debug and normal evaluation"); - } - } - - // ---- Debugger: kill stops execution ---- - - @Test - void kill_abortsExecution() { - CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); - Term term = applyArg(compiled.program().term(), passingCompleteCtx()); - - int[] stepCount = {0}; - - try (var debugger = JulcDebugger.create()) { - debugger.sourceMap(compiled.sourceMap()); - EvalResult result = debugger.stepThrough(term, event -> { - stepCount[0]++; - if (stepCount[0] >= 3) { - event.kill(); // Kill after 3 steps - } else { - event.stepOver(); - } - }); - - assertFalse(result.isSuccess(), "Kill should abort execution"); - System.out.println("Killed after " + stepCount[0] + " steps"); - } - } - - // --- helpers --- - - private static Term applyArg(Term program, PlutusData arg) { - return new Term.Apply(program, Term.const_(Constant.data(arg))); - } - - private static String describeResult(EvalResult result) { - return switch (result) { - case EvalResult.Success s -> "Success"; - case EvalResult.Failure f -> "Failure: " + f.error(); - case EvalResult.BudgetExhausted b -> "BudgetExhausted"; - }; - } - - private PlutusData escrowDatum() { - return PlutusData.constr(0, - PlutusData.bytes(new byte[28]), - PlutusData.bytes(new byte[28]), - PlutusData.integer(1000), - PlutusData.integer(10_000_000)); - } - - private PlutusData passingCompleteCtx() { - var ref = TestDataBuilder.randomTxOutRef_typed(); - return spendingContext(ref, escrowDatum()) - .redeemer(PlutusData.constr(0, PlutusData.integer(0))) - .signer(new byte[28]) - .output(TestDataBuilder.txOut( - TestDataBuilder.pubKeyAddress(new PubKeyHash(new byte[28])), - Value.lovelace(BigInteger.valueOf(10_000_000)))) - .buildPlutusData(); - } - - private PlutusData failingRefundCtx() { - var ref = TestDataBuilder.randomTxOutRef_typed(); - return spendingContext(ref, escrowDatum()) - .redeemer(PlutusData.constr(0, PlutusData.integer(1))) - .signer(new byte[28]) - .validRange(com.bloxbean.cardano.julc.ledger.Interval.before(BigInteger.valueOf(500))) - .buildPlutusData(); - } +// @BeforeAll +// static void setup() { +// initCrypto(); +// } +// +// // ---- JIT: verify Truffle VM is active ---- +// +// @Test +// void truffleVmIsActive() { +// String provider = vm().providerName(); +// System.out.println("VM provider: " + provider); +// assertEquals("Truffle", provider, +// "Truffle VM should be auto-selected (highest priority)"); +// } +// +// // ---- JIT: existing validator works identically under Truffle ---- +// +// @Test +// void escrowValidator_passingPath_underTruffle() { +// CompileResult compiled = compileValidator(EscrowValidator.class); +// EvalResult result = evaluate(compiled.program(), passingCompleteCtx()); +// assertSuccess(result); +// +// System.out.println("EscrowValidator (complete, pass) — Truffle VM"); +// System.out.println(" CPU: " + result.budgetConsumed().cpuSteps()); +// System.out.println(" Mem: " + result.budgetConsumed().memoryUnits()); +// } +// +// @Test +// void escrowValidator_failingPath_underTruffle() { +// CompileResult compiled = compileValidator(EscrowValidator.class); +// EvalResult result = evaluate(compiled.program(), failingRefundCtx()); +// assertFailure(result); +// +// System.out.println("EscrowValidator (refund, fail) — Truffle VM"); +// System.out.println(" CPU: " + result.budgetConsumed().cpuSteps()); +// } +// +// // ---- JIT: warmup benchmark ---- +// +// @Test +// void jitWarmup_escrowValidator() { +// CompileResult compiled = compileValidator(EscrowValidator.class); +// PlutusData ctx = passingCompleteCtx(); +// +// // Cold run (interpreted) +// long coldStart = System.nanoTime(); +// for (int i = 0; i < 10; i++) { +// evaluate(compiled.program(), ctx); +// } +// long coldMs = (System.nanoTime() - coldStart) / 1_000_000; +// +// // Warmup — Truffle JIT compiles hot CallTargets after ~1000 iterations +// for (int i = 0; i < 2000; i++) { +// evaluate(compiled.program(), ctx); +// } +// +// // Hot run (JIT-compiled if GraalVM compiler is available) +// long hotStart = System.nanoTime(); +// for (int i = 0; i < 10; i++) { +// evaluate(compiled.program(), ctx); +// } +// long hotMs = (System.nanoTime() - hotStart) / 1_000_000; +// +// System.out.println(); +// System.out.println("=== JIT WARMUP BENCHMARK: EscrowValidator ==="); +// System.out.println(" Cold (first 10): " + coldMs + " ms"); +// System.out.println(" Warmup: 2000 iterations"); +// System.out.println(" Hot (next 10): " + hotMs + " ms"); +// if (coldMs > 0) { +// System.out.println(" Speedup: " + +// String.format("%.1fx", (double) coldMs / Math.max(hotMs, 1))); +// } +// System.out.println(); +// } +// +// // ---- Debugger: step through every statement ---- +// +// @Test +// void stepThrough_escrowValidator_completePath() { +// CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); +// Term term = applyArg(compiled.program().term(), passingCompleteCtx()); +// +// var steps = new ArrayList(); +// +// try (var debugger = JulcDebugger.create()) { +// debugger.sourceMap(compiled.sourceMap()); +// EvalResult result = debugger.stepThrough(term, event -> { +// steps.add(String.format("%-30s line %3d CPU=%,d Mem=%,d", +// event.fileName(), event.line(), +// event.cpuConsumed(), event.memConsumed())); +// event.stepOver(); +// }); +// +// assertTrue(result.isSuccess(), +// "Expected success but got: " + describeResult(result)); +// } +// +// System.out.println(); +// System.out.println("=== DEBUGGER STEP-THROUGH: EscrowValidator (complete path) ==="); +// steps.forEach(s -> System.out.println(" " + s)); +// System.out.println(" Total steps: " + steps.size()); +// System.out.println(); +// +// assertFalse(steps.isEmpty(), "Should have step events with source map"); +// } +// +// // ---- Debugger: step through failing path ---- +// +// @Test +// void stepThrough_escrowValidator_failingRefund() { +// CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); +// Term term = applyArg(compiled.program().term(), failingRefundCtx()); +// +// var steps = new ArrayList(); +// +// try (var debugger = JulcDebugger.create()) { +// debugger.sourceMap(compiled.sourceMap()); +// EvalResult result = debugger.stepThrough(term, event -> { +// steps.add(String.format("%-30s line %3d CPU=%,d", +// event.fileName(), event.line(), event.cpuConsumed())); +// event.stepOver(); +// }); +// +// // Refund path fails (deadline not passed) +// assertFalse(result.isSuccess()); +// } +// +// System.out.println(); +// System.out.println("=== DEBUGGER STEP-THROUGH: EscrowValidator (refund path, FAILS) ==="); +// steps.forEach(s -> System.out.println(" " + s)); +// System.out.println(" Total steps: " + steps.size()); +// System.out.println(); +// } +// +// // ---- Debugger: breakpoints ---- +// +// @Test +// void breakAt_escrowValidator() { +// CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); +// Term term = applyArg(compiled.program().term(), passingCompleteCtx()); +// +// var hits = new ArrayList(); +// +// try (var debugger = JulcDebugger.create()) { +// debugger.sourceMap(compiled.sourceMap()) +// .breakAt("EscrowValidator.java", 24) // if (redeemer.action()...) +// .breakAt("EscrowValidator.java", 34); // checkComplete: buyerSigned +// +// EvalResult result = debugger.run(term, event -> { +// hits.add(String.format("BREAK %s:%d CPU=%,d Mem=%,d", +// event.fileName(), event.line(), +// event.cpuConsumed(), event.memConsumed())); +// event.resume(); +// }); +// +// assertTrue(result.isSuccess(), +// "Expected success but got: " + describeResult(result)); +// } +// +// System.out.println(); +// System.out.println("=== BREAKPOINT HITS: EscrowValidator ==="); +// hits.forEach(h -> System.out.println(" " + h)); +// System.out.println(); +// } +// +// // ---- Debugger: budget parity with non-debug evaluation ---- +// +// @Test +// void budgetParity_debugVsNormal() { +// CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); +// PlutusData ctx = passingCompleteCtx(); +// +// // Normal evaluation (via TruffleVmProvider) +// EvalResult normalResult = evaluate(compiled.program(), ctx); +// assertSuccess(normalResult); +// +// // Debug evaluation (via JulcDebugger with Truffle Debugger API) +// Term term = applyArg(compiled.program().term(), ctx); +// try (var debugger = JulcDebugger.create()) { +// debugger.sourceMap(compiled.sourceMap()); +// EvalResult debugResult = debugger.stepThrough(term, event -> event.stepOver()); +// +// assertTrue(debugResult.isSuccess(), +// "Debug eval should succeed: " + describeResult(debugResult)); +// +// System.out.println(); +// System.out.println("=== BUDGET PARITY ==="); +// System.out.println(" Normal: CPU=" + normalResult.budgetConsumed().cpuSteps() +// + " Mem=" + normalResult.budgetConsumed().memoryUnits()); +// System.out.println(" Debug: CPU=" + debugResult.budgetConsumed().cpuSteps() +// + " Mem=" + debugResult.budgetConsumed().memoryUnits()); +// +// assertEquals(normalResult.budgetConsumed().cpuSteps(), +// debugResult.budgetConsumed().cpuSteps(), +// "CPU budget must match between debug and normal evaluation"); +// assertEquals(normalResult.budgetConsumed().memoryUnits(), +// debugResult.budgetConsumed().memoryUnits(), +// "Memory budget must match between debug and normal evaluation"); +// } +// } +// +// // ---- Debugger: kill stops execution ---- +// +// @Test +// void kill_abortsExecution() { +// CompileResult compiled = compileValidatorWithSourceMap(EscrowValidator.class); +// Term term = applyArg(compiled.program().term(), passingCompleteCtx()); +// +// int[] stepCount = {0}; +// +// try (var debugger = JulcDebugger.create()) { +// debugger.sourceMap(compiled.sourceMap()); +// EvalResult result = debugger.stepThrough(term, event -> { +// stepCount[0]++; +// if (stepCount[0] >= 3) { +// event.kill(); // Kill after 3 steps +// } else { +// event.stepOver(); +// } +// }); +// +// assertFalse(result.isSuccess(), "Kill should abort execution"); +// System.out.println("Killed after " + stepCount[0] + " steps"); +// } +// } +// +// // --- helpers --- +// +// private static Term applyArg(Term program, PlutusData arg) { +// return new Term.Apply(program, Term.const_(Constant.data(arg))); +// } +// +// private static String describeResult(EvalResult result) { +// return switch (result) { +// case EvalResult.Success s -> "Success"; +// case EvalResult.Failure f -> "Failure: " + f.error(); +// case EvalResult.BudgetExhausted b -> "BudgetExhausted"; +// }; +// } +// +// private PlutusData escrowDatum() { +// return PlutusData.constr(0, +// PlutusData.bytes(new byte[28]), +// PlutusData.bytes(new byte[28]), +// PlutusData.integer(1000), +// PlutusData.integer(10_000_000)); +// } +// +// private PlutusData passingCompleteCtx() { +// var ref = TestDataBuilder.randomTxOutRef_typed(); +// return spendingContext(ref, escrowDatum()) +// .redeemer(PlutusData.constr(0, PlutusData.integer(0))) +// .signer(new byte[28]) +// .output(TestDataBuilder.txOut( +// TestDataBuilder.pubKeyAddress(new PubKeyHash(new byte[28])), +// Value.lovelace(BigInteger.valueOf(10_000_000)))) +// .buildPlutusData(); +// } +// +// private PlutusData failingRefundCtx() { +// var ref = TestDataBuilder.randomTxOutRef_typed(); +// return spendingContext(ref, escrowDatum()) +// .redeemer(PlutusData.constr(0, PlutusData.integer(1))) +// .signer(new byte[28]) +// .validRange(com.bloxbean.cardano.julc.ledger.Interval.before(BigInteger.valueOf(500))) +// .buildPlutusData(); +// } }