Skip to content

CBOR: repeated map keys are silently collapsed, where the ledger either rejects them or keeps every pair #578

Description

@solidsnakedev

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.

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