@@ -599,3 +599,45 @@ func TestConwayTx_WithReferenceScripts_CborRoundTrip(t *testing.T) {
599599 "CBOR round-trip should be byte-identical" ,
600600 )
601601}
602+
603+ // TestConwayRedeemersDuplicateKeyLenient verifies that a Redeemers map carrying
604+ // a duplicate (tag, index) key — which cardano-node accepts and strict decoding
605+ // rejects — is decoded leniently (last-wins) instead of failing the block.
606+ // See gouroboros #1860.
607+ func TestConwayRedeemersDuplicateKeyLenient (t * testing.T ) {
608+ // Redeemers map with two entries under the same key [Spend(0), index 0]:
609+ // { [0,0]: [0,[1,2]], [0,0]: [0,[3,4]] }
610+ // Plutus data is the bare integer 0; the value's second element is
611+ // ExUnits [mem, steps].
612+ dupCbor := []byte {
613+ 0xA2 , // map(2)
614+ 0x82 , 0x00 , 0x00 , // key [Spend, 0]
615+ 0x82 , 0x00 , 0x82 , 0x01 , 0x02 , // value [datum=0, exUnits=[1,2]]
616+ 0x82 , 0x00 , 0x00 , // key [Spend, 0] (duplicate)
617+ 0x82 , 0x00 , 0x82 , 0x03 , 0x04 , // value [datum=0, exUnits=[3,4]]
618+ }
619+ // Strict decode rejects the duplicate key (the pre-fix behavior that
620+ // wedged preview sync).
621+ var strict map [common.RedeemerKey ]common.RedeemerValue
622+ _ , strictErr := cbor .Decode (dupCbor , & strict )
623+ assert .Error (t , strictErr )
624+ assert .True (
625+ t ,
626+ cbor .IsDuplicateMapKeyError (strictErr ),
627+ "expected a duplicate-map-key error, got %v" ,
628+ strictErr ,
629+ )
630+ // ConwayRedeemers decodes it, keeping the last value for the duplicate key.
631+ var r ConwayRedeemers
632+ if err := r .UnmarshalCBOR (dupCbor ); err != nil {
633+ t .Fatalf ("lenient decode of duplicate-key redeemers failed: %v" , err )
634+ }
635+ key := common.RedeemerKey {Tag : common .RedeemerTagSpend , Index : 0 }
636+ assert .Len (t , r .Redeemers , 1 )
637+ assert .Equal (
638+ t ,
639+ common.ExUnits {Memory : 3 , Steps : 4 },
640+ r .Redeemers [key ].ExUnits ,
641+ "duplicate key should resolve last-wins" ,
642+ )
643+ }
0 commit comments