Summary
Six encodings of Plutus data that the ledger accepts come back from Transaction.fromCBORHex then Transaction.toCBORHex in different bytes, with nothing changed:
| Received |
Written back |
Value |
d8799fff |
d87980 |
constructor 0, empty indefinite list |
bfff |
a0 |
empty indefinite map |
d8799800 |
d87980 |
empty list, 2-byte length header |
c24101 |
01 |
1 as a bignum |
5803cccccc |
43cccccc |
3 bytes, 2-byte length header |
5f4201024103ff |
43010203 |
bytes in chunks |
The ledger keeps the original bytes of every datum and redeemer and hashes those:
- plutus
Data.hs L215-230 and L245-283 decode all six forms: indefinite and definite lists and maps, bignums through decodeBoundedBigInteger, and chunked bytes through decodeBoundedBytesIndef.
- cardano-ledger
Plutus/Data.hs L95: newtype Data era = MkData (MemoBytes (PlutusData era)).
Alonzo/Tx.hs L318-321: the script integrity hash is taken over originalBytes of the redeemers and datums.
So a changed witness datum or redeemer fails with PPViewHashesDontMatch, and a changed inline datum changes the body and the transaction id.
Affected
packages/evolution/src/CBOR.ts
encodeArraySync (L1314) and encodeMapSync (L1371): the empty fast paths return 80 and a0 before the captured format is read
- the
BoundedBytes branch (L962-968) calls encodeBoundedBytesSync with no format, so header width and chunking are dropped
encodeUintSync (L983-995) emits tag 2 only above 2^64-1 and honours only a uint format, so a captured tag 2 or 3 node is ignored
Fix
Give each of these paths the captured format, and use it only when it still fits the value:
- read the format before the empty fast paths
- pass the format to the bounded bytes encoder: keep the header width, and keep the chunk layout only when the chunk sizes add up to the value's length and none is over 64 bytes
- when the captured node is tag 2 or 3, write the value as that bignum
Land this with or after #575 (stale chunk sizes). Passing the format into the bounded bytes encoder without that guard would carry the stale-chunk corruption into datums.
Regression test
Oracle is the node; CML agrees on every case below.
- given: each encoding above as a witness datum (
a1 04 d90102 81 [datum]) and as an inline datum ([1, #6.24(bytes)])
- before fix: the bytes change in both places; for the inline datum the body hash changes
- after fix: byte-identical in both places
- control:
d8799f01ff and a correctly chunked 65-byte value already round-trip and must stay unchanged; a builder-made datum is unchanged
Devnet, raw submission through Ogmios:
- all six as inline datums: original accepted; after
addVKeyWitnessesHex, rejected with 3100 invalid signatures
d8799fff as a witness datum, with a datum-hash output and the script data hash in the body: original accepted; after addVKeyWitnessesHex, rejected with 3113 script integrity hash mismatch
Must FAIL on main today and PASS after the fix.
Reference
#235 (fixed in #236) covered non-empty indefinite lists in redeemers; these are the cases it did not reach. Related: #530 (bignum chunking above 64 bytes), #395 (Data map default), #397 (duplicate keys in Data maps).
Summary
Six encodings of Plutus data that the ledger accepts come back from
Transaction.fromCBORHexthenTransaction.toCBORHexin different bytes, with nothing changed:d8799fffd87980bfffa0d8799800d87980c24101015803cccccc43cccccc5f4201024103ff43010203The ledger keeps the original bytes of every datum and redeemer and hashes those:
Data.hsL215-230 and L245-283 decode all six forms: indefinite and definite lists and maps, bignums throughdecodeBoundedBigInteger, and chunked bytes throughdecodeBoundedBytesIndef.Plutus/Data.hsL95:newtype Data era = MkData (MemoBytes (PlutusData era)).Alonzo/Tx.hsL318-321: the script integrity hash is taken overoriginalBytesof the redeemers and datums.So a changed witness datum or redeemer fails with
PPViewHashesDontMatch, and a changed inline datum changes the body and the transaction id.Affected
packages/evolution/src/CBOR.ts
encodeArraySync(L1314) andencodeMapSync(L1371): the empty fast paths return80anda0before the captured format is readBoundedBytesbranch (L962-968) callsencodeBoundedBytesSyncwith no format, so header width and chunking are droppedencodeUintSync(L983-995) emits tag 2 only above 2^64-1 and honours only auintformat, so a captured tag 2 or 3 node is ignoredFix
Give each of these paths the captured format, and use it only when it still fits the value:
Land this with or after #575 (stale chunk sizes). Passing the format into the bounded bytes encoder without that guard would carry the stale-chunk corruption into datums.
Regression test
Oracle is the node; CML agrees on every case below.
a1 04 d90102 81 [datum]) and as an inline datum ([1, #6.24(bytes)])d8799f01ffand a correctly chunked 65-byte value already round-trip and must stay unchanged; a builder-made datum is unchangedDevnet, raw submission through Ogmios:
addVKeyWitnessesHex, rejected with 3100 invalid signaturesd8799fffas a witness datum, with a datum-hash output and the script data hash in the body: original accepted; afteraddVKeyWitnessesHex, rejected with 3113 script integrity hash mismatchMust FAIL on main today and PASS after the fix.
Reference
#235 (fixed in #236) covered non-empty indefinite lists in redeemers; these are the cases it did not reach. Related: #530 (bignum chunking above 64 bytes), #395 (Data map default), #397 (duplicate keys in Data maps).