Skip to content

TxOut: map-form outputs without a script ref are re-encoded as arrays, changing the transaction id #577

Description

@solidsnakedev

Summary

An output written in the post-Alonzo map form, with no script ref, comes back from Transaction.fromCBORHex then Transaction.toCBORHex in the legacy array form. Nothing else was changed. The body bytes change, so the transaction id changes and every witness already collected no longer verifies. Transaction.addVKeyWitnessesHex and Transaction.toCBORHexWithFormat do the same.

Received Written back
a2 00 [addr] 01 [coin] 82 [addr] [coin]
bf 00 [addr] 01 [coin] ff 82 [addr] [coin]
a3 00 [addr] 01 [coin] 02 8200 5820 [hash] 83 [addr] [coin] 5820 [hash]

Outputs with an inline datum or a script ref keep the map form, and array-form outputs keep the array form.

The ledger accepts both forms for these outputs and hashes the bytes as received:

  • cardano-ledger Babbage/TxOut.hs L549-551: decodeBabbageTxOut sends definite and indefinite maps to decodeTxOut, which requires only keys 0 and 1 (L658-661). Everything else goes to the array decoder.
  • Conway/TxOut.hs L37: type TxOut ConwayEra = BabbageTxOut ConwayEra.
  • Core.hs L651: the transaction id is hashAnnotated over the original body bytes.

The ledger's own encoder writes these outputs as arrays (Babbage/TxOut.hs L505-506), and so does CML's builder, so the map form comes only from other tools that choose it.

Affected

packages/evolution/src/TxOut.ts

  • encode (L242-252): canUseShelleyFormat picks the array form whenever there is no script ref and the datum is absent or a hash
  • decode (L274 onward) reads both forms into the same value and does not record which one it read

The captured format node says map while the encoder produces an array, so the format is dropped for that output, the same shape mismatch as #574.

Fix

Let the captured format decide the form. When the format node for an output is a map, encode the map form with that node. With no captured format, keep today's choice, so freshly built transactions do not change. An edit that adds a datum or script ref still moves to the map form, as today.

Regression test

Oracle is the node; CML agrees on every case below.

  • given: a transaction with tagged inputs and one output in each form in the table above
  • before fix: the output is written as an array and the body hash changes
  • after fix: byte-identical, and addVKeyWitnessesHex leaves the body bytes unchanged
  • control: array-form outputs and map-form outputs with an inline datum are unchanged; a builder-made transaction is unchanged

Devnet, raw submission through Ogmios, output a2 00 [addr] 01 [coin]:

  • original bytes signed over their own body: accepted
  • same transaction after addVKeyWitnessesHex: rejected, 3100 invalid signatures

Must FAIL on main today and PASS after the fix.

Reference

Same class as #574 and #576: the decoder accepts two forms, and the encoder writes one without consulting the captured format.

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