Skip to content

CRITICAL: PlutusV3 Script Hash Mismatch in Conway Era (Permanent Fund Lock) #763

@muhammadtakdir

Description

@muhammadtakdir

Summary

Summary

MeshJS produces an incorrect script hash for PlutusV3 scripts, causing a permanent lock of funds in UTxOs created with MeshJS on Cardano Conway era (Preview/Mainnet).

The Problem

When using resolveScriptHash() or building transactions with PlutusV3 scripts, MeshJS:

  1. Unwraps the CBOR ByteString before hashing.
  2. Hashes the raw UPLC bytes instead of the CBOR-wrapped bytes.
  3. Produces a different hash than what Aiken/Cardano-CLI produces.
  4. When spending, puts raw bytes (not CBOR) into the witness set.

Root Cause Analysis

Correct Cardano Specification (CIP-0057, Conway Era):
script_hash = blake2b_224(0x03 || cbor_wrapped_script_bytes)
(Where cbor_wrapped_script_bytes includes the 59 01 b0 header).

MeshJS Behavior:
It appears MeshJS unwraps the CBOR (extracts inner bytes) and hashes 0x03 || unwrapped, which is incorrect for V3.

Suggested Fix

Option 1: Fix Hash Calculation
In resolveScriptHash for V3:

function resolveScriptHash(scriptCbor: string, version: "V3"): string {
  const scriptBytes = Buffer.from(scriptCbor, "hex");
  // DO NOT unwrap! Hash the CBOR as-is
  return blake2b_224(Buffer.concat([Buffer.from([0x03]), scriptBytes]));
}

### Steps to reproduce the bug

1. **Create Aiken PlutusV3 Script**
   Run `aiken build`. This produces a `plutus.json` with a specific hash (e.g., `47be01b4...`).

2. **Use MeshJS to Create Escrow**
   ```typescript
   import { resolveScriptHash, MeshTxBuilder } from "@meshsdk/core";
   import contract from "./plutus.json";

   const scriptCbor = contract.validators[0].compiledCode;
   // Aiken Hash: 47be01b4...
   
   // Bug: MeshJS calculates wrong hash
   const meshHash = resolveScriptHash(scriptCbor, "V3"); 
   console.log(meshHash); // Output: d80cc51a... (DIFFERENT!)

   // Transaction locks funds at the WRONG address (derived from d80cc51a...)
   const tx = new MeshTxBuilder({...});
   await tx
     .txOut(scriptAddress, [{unit: "lovelace", quantity: "20000000"}])
     .txOutInlineDatumValue(datum)
     .complete();

### Actual Result

tx.spendingPlutusScriptV3()
  .txIn(utxo.txHash, utxo.outputIndex, ...)
  .txInScript(scriptCbor)
  .txInRedeemerValue(redeemer)
  .txInInlineDatumPresent();

await tx.complete(); // Fails on submission

The transaction fails with a `MalformedScriptWitnesses` error because the node expects the hash derived from the Raw Bytes (since that's what created the address), but the witness provided is likely malformed or rejected due to header mismatch.

**Hash Comparison:**
- **Aiken/CLI Hash (Correct):** `47be01b4e6535eff63f75202295fc81afb1b80635e0b3c55d110ec61`
- **MeshJS Hash (Incorrect):** `d80cc51a64e0157b1663fd1008fe3d06237e270924d223b57feb6a12`

**Error Log:**
```json
{
  "error": "ConwayUtxowFailure (MalformedScriptWitnesses (fromList [ScriptHash \"d80cc51a64e0157b1663fd1008fe3d06237e270924d223b57feb6a12\"]))"
}

### Expected Result

1. `resolveScriptHash(scriptCbor, "V3")` should return the same hash as Aiken's `plutus.json` and Cardano-CLI.
2. Transaction witnesses should contain **CBOR-wrapped** script bytes.
3. Users should be able to spend UTxOs locked with PlutusV3 scripts without `MalformedScriptWitnesses` errors.

### SDK version

@meshsdk/core (latest / Nov 2025)

### Environment type

- [x] Node.js
- [x] Browser
- [x] Browser Extension
- [ ] Other

### Environment details

- **Network**: Cardano Preview Testnet (Conway Era)
- **Smart Contract**: Aiken Plutus V3 validator
- **Wallet**: Eternl (Browser Extension)
- **OS**: Windows/Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions