Summary
The CBOR map decoder stores entries in a JS Map, so a repeated key keeps only its last value, and no error is raised. On re-encode the captured key order replays every key, so the output repeats the last value:
body a4 ... 02 00 02 0a → a4 ... 02 0a 02 0a (fee 0 lost)
label a2 00 01 00 02 → a2 00 02 00 02 ({0: 1} lost)
datum a2 01 01 01 02 → a2 01 02 01 02 ({1: 1} lost)
The ledger treats repeated keys in two different ways, so a single rule at the CBOR layer cannot be right:
| Where |
Ledger |
SDK today |
Should |
| transaction body, witness set |
reject: SparseKeyed decoders fail on a repeated field (Coders.hs L565-569, duplicateKey L634-640; Conway/TxBody.hs L183, Alonzo/TxWits.hs L609) |
accepts, rewrites |
reject on decode |
metadata labels and other Map fields |
reject from protocol version 9: decodeMap uses decodeMapLikeEnforceNoDuplicates (Decoder.hs L748-753, L785-789) |
accepts, rewrites |
reject on decode |
| metadatum map values |
keep every pair: Metadatum Map is a list of pairs (Metadata.hs L161-172) |
drops the earlier pair |
keep every pair |
| Plutus data maps |
keep every pair: Map [(Data, Data)] (plutus Data.hs L44, L277-283) |
drops the earlier pair |
tracked in #397 |
Affected
packages/evolution/src/CBOR.ts
decodeMapAt (L1973-2035): map.set(k, v) overwrites a repeated key; the captured keyOrder still records both
- map encode with a captured format (L1496-1525) replays every
keyOrder entry, each with the surviving value
Every schema built on this decoder inherits the behavior. Tested here: the transaction body, metadata labels and metadatum values. Multi-assets, withdrawals and mint go through the same ledger decodeMap, but were not tested, so the fix should add one case for each.
Fix
Decide per schema, following the ledger:
- ledger records and
Map fields: reject a repeated key when decoding, with an error that names the key
- metadatum map values: keep every pair in order, so decode then encode returns the same pairs
The Plutus data side is #397, and it needs the same answer as metadatum values. The devnet accepted {1: 1, 1: 2} as an inline datum and rejected the SDK's rewrite with 3100, so rejecting on decode would refuse valid on-chain datums.
Regression test
Oracle is the node.
- body
a4 00 [inputs] 01 [outputs] 02 00 02 0a: before fix, decodes and re-encodes as 02 0a 02 0a; after fix, Transaction.fromCBORHex fails with a duplicate key error
- metadata
a2 00 01 00 02 (label 0 twice): same, rejected after fix
- metadatum
a1 00 a2 01 01 01 02: before fix, written back as a1 00 a2 01 02 01 02; after fix, byte-identical
- control: maps without repeated keys are unchanged
Devnet, raw submission through Ogmios:
- body with the fee key twice: node rejects as malformed; the SDK decodes it
- metadata with label 0 twice: node rejects as malformed; the SDK decodes it
- metadatum
{1: 1, 1: 2} under label 0: original accepted; after addVKeyWitnessesHex, rejected with 3107 metadata hash mismatch
Must FAIL on main today and PASS after the fix.
Reference
#397 covers the Plutus data side. #531 asks for rejecting repeated keys on decode before the signing fix hashes original bytes.
Summary
The CBOR map decoder stores entries in a JS
Map, so a repeated key keeps only its last value, and no error is raised. On re-encode the captured key order replays every key, so the output repeats the last value:The ledger treats repeated keys in two different ways, so a single rule at the CBOR layer cannot be right:
SparseKeyeddecoders fail on a repeated field (Coders.hsL565-569,duplicateKeyL634-640;Conway/TxBody.hsL183,Alonzo/TxWits.hsL609)MapfieldsdecodeMapusesdecodeMapLikeEnforceNoDuplicates(Decoder.hsL748-753, L785-789)MetadatumMapis a list of pairs (Metadata.hsL161-172)Map [(Data, Data)](plutusData.hsL44, L277-283)Affected
packages/evolution/src/CBOR.ts
decodeMapAt(L1973-2035):map.set(k, v)overwrites a repeated key; the capturedkeyOrderstill records bothkeyOrderentry, each with the surviving valueEvery schema built on this decoder inherits the behavior. Tested here: the transaction body, metadata labels and metadatum values. Multi-assets, withdrawals and mint go through the same ledger
decodeMap, but were not tested, so the fix should add one case for each.Fix
Decide per schema, following the ledger:
Mapfields: reject a repeated key when decoding, with an error that names the keyThe Plutus data side is #397, and it needs the same answer as metadatum values. The devnet accepted
{1: 1, 1: 2}as an inline datum and rejected the SDK's rewrite with 3100, so rejecting on decode would refuse valid on-chain datums.Regression test
Oracle is the node.
a4 00 [inputs] 01 [outputs] 02 00 02 0a: before fix, decodes and re-encodes as02 0a 02 0a; after fix,Transaction.fromCBORHexfails with a duplicate key errora2 00 01 00 02(label 0 twice): same, rejected after fixa1 00 a2 01 01 01 02: before fix, written back asa1 00 a2 01 02 01 02; after fix, byte-identicalDevnet, raw submission through Ogmios:
{1: 1, 1: 2}under label 0: original accepted; afteraddVKeyWitnessesHex, rejected with 3107 metadata hash mismatchMust FAIL on main today and PASS after the fix.
Reference
#397 covers the Plutus data side. #531 asks for rejecting repeated keys on decode before the signing fix hashes original bytes.