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.
Summary
An output written in the post-Alonzo map form, with no script ref, comes back from
Transaction.fromCBORHexthenTransaction.toCBORHexin 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.addVKeyWitnessesHexandTransaction.toCBORHexWithFormatdo the same.a2 00 [addr] 01 [coin]82 [addr] [coin]bf 00 [addr] 01 [coin] ff82 [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:
Babbage/TxOut.hsL549-551:decodeBabbageTxOutsends definite and indefinite maps todecodeTxOut, which requires only keys 0 and 1 (L658-661). Everything else goes to the array decoder.Conway/TxOut.hsL37:type TxOut ConwayEra = BabbageTxOut ConwayEra.Core.hsL651: the transaction id ishashAnnotatedover the original body bytes.The ledger's own encoder writes these outputs as arrays (
Babbage/TxOut.hsL505-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
canUseShelleyFormatpicks the array form whenever there is no script ref and the datum is absent or a hashThe 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.
addVKeyWitnessesHexleaves the body bytes unchangedDevnet, raw submission through Ogmios, output
a2 00 [addr] 01 [coin]:addVKeyWitnessesHex: rejected, 3100 invalid signaturesMust 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.