Skip to content

Canonical transaction encoding leaves a stale script data hash; no way to build hw-cli-signable Plutus txs #585

Description

@ptrdsh

Summary

Encoding a Plutus transaction with CBOR.CANONICAL_OPTIONS produces a transaction the ledger rejects. The canonical encoding rewrites the redeemers, but the body keeps the script data hash the builder computed over the default encoding. There is currently no way to build a fully canonical transaction, which is what cardano-hw-cli requires before it will sign with a Ledger or Trezor. A CIP-21-compliant body alone isn't enough for it.

Related: #584 sorts Mint/MultiAsset/Withdrawals keys on encode. That makes the body CIP-21 compliant, which is what the hardware wallet itself signs. This issue is about the rest of the transaction.

Reproduction

A real preprod transaction built with the SDK: 3 mints, 4 PlutusV3 redeemers, 1 script withdrawal.

const tx = Transaction.fromCBORHex(rawHex, CBOR.CANONICAL_OPTIONS) // non-default options: no format cache
const canonicalHex = Transaction.toCBORHex(tx, CBOR.CANONICAL_OPTIONS)

Result:

  • Witness set: re-encoded with definite-length Plutus data. It's byte-identical to what cardano-hw-cli transaction transform produces ✅
  • Body field 11 (script data hash): still 85237670…, computed over the original indefinite-length redeemers ❌
  • Hash of the redeemers actually in canonicalHex (+ PlutusV3 language views): 4b833ae9…

Submitting canonicalHex fails the script-integrity check (PPViewHashesDontMatch).

Cause

Several hashes, plus the fee, are computed with the default options, independently of the options later used to serialize:

  • txBuilder.ts calls Redeemers.toScriptDataHash(redeemers, costModels, datums) without options, so it uses CML_DEFAULT_OPTIONS.
  • encodeDatumsTaggedSet (inside toScriptDataHash) takes no options at all.
  • calculateTransactionSize uses Transaction.toCBORBytes(transaction) with defaults, so the fee is sized for the default encoding.
  • The auxiliary data hash: AuxiliaryData.toHash gained an options parameter in fix: allow auxiliary data hashing with codec options #566, but the builder would also need to pass it.

So "serialize with canonical options" and "flip the default to canonical" are both unsafe. The second would also change the bytes of every transaction and datum the SDK emits, and drop the CML byte compatibility the defaults are named for.

Proposal

An opt-in build option, e.g. codecOptions: CBOR.CANONICAL_OPTIONS, that the builder passes through consistently to:

  1. Redeemers.toScriptDataHash (redeemers and datums),
  2. the auxiliary data hash,
  3. fee and size estimation,
  4. the final Transaction.toCBOR*, including the bytes handed to CIP-30 signTx and to providers.

Acceptance: a Plutus transaction built with the option passes cardano-hw-cli transaction validate ("valid and canonical") and is accepted by the node.

Context

The Ledger only needs a canonical body, which #584 addresses. But cardano-hw-cli transaction witness refuses any transaction with a fixable issue (validateTxBeforeWitnessing throws on containsFixable). Today, users have to run cardano-hw-cli transaction transform --protocol-params-file … so it can recompute the script data hash, and that changes the tx ID from the one the SDK built.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions