You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
consttx=Transaction.fromCBORHex(rawHex,CBOR.CANONICAL_OPTIONS)// non-default options: no format cacheconstcanonicalHex=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.
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:
Redeemers.toScriptDataHash (redeemers and datums),
the auxiliary data hash,
fee and size estimation,
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.
Summary
Encoding a Plutus transaction with
CBOR.CANONICAL_OPTIONSproduces 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 whatcardano-hw-clirequires 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/Withdrawalskeys 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.
Result:
cardano-hw-cli transaction transformproduces ✅85237670…, computed over the original indefinite-length redeemers ❌canonicalHex(+ PlutusV3 language views):4b833ae9…Submitting
canonicalHexfails 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.tscallsRedeemers.toScriptDataHash(redeemers, costModels, datums)withoutoptions, so it usesCML_DEFAULT_OPTIONS.encodeDatumsTaggedSet(insidetoScriptDataHash) takes no options at all.calculateTransactionSizeusesTransaction.toCBORBytes(transaction)with defaults, so the fee is sized for the default encoding.AuxiliaryData.toHashgained 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:Redeemers.toScriptDataHash(redeemers and datums),Transaction.toCBOR*, including the bytes handed to CIP-30signTxand 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 witnessrefuses any transaction with a fixable issue (validateTxBeforeWitnessingthrows oncontainsFixable). Today, users have to runcardano-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.