Skip to content

Commit 46eeec4

Browse files
authored
fix(ledger): tolerate tag-258 set duplicates in Conway witness fields (#1855)
Fixes #1853. Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 0885f39 commit 46eeec4

2 files changed

Lines changed: 105 additions & 7 deletions

File tree

ledger/conway/conway.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -438,17 +438,22 @@ func (w *ConwayTransactionWitnessSet) UnmarshalCBOR(cborData []byte) error {
438438
if _, err := cbor.Decode(cborData, &tmp); err != nil {
439439
return err
440440
}
441-
// Reject duplicate members in any tag-258 witness set field.
442-
// Untagged array fields are left unchecked so pre-Conway encodings remain valid.
441+
// Conway (protocol versions 9-11) tolerates duplicate members in the
442+
// witness-set sets that cardano-ledger decodes via Set/Map.fromList: vkey
443+
// witnesses, bootstrap witnesses, native scripts, and plutus data all
444+
// silently deduplicate at decode and only begin rejecting duplicates at
445+
// protocol version 12 (Dijkstra, handled by DijkstraTransactionWitnessSet).
446+
// Plutus script sets, by contrast, reject duplicates from version 9
447+
// (cardano-ledger scriptDecoderV9 / decodeMapLikeEnforceNoDuplicates), like
448+
// the tx-body sets. Only enforce the fields cardano-ledger enforces in
449+
// Conway; enforcing the tolerated fields rejects valid historical blocks at
450+
// decode (issue #1853). Untagged array fields are left unchecked so
451+
// pre-Conway encodings remain valid.
443452
type duplicateChecker interface {
444453
CheckForDuplicates() error
445454
}
446455
for _, c := range []duplicateChecker{
447-
&tmp.VkeyWitnesses,
448-
&tmp.WsNativeScripts,
449-
&tmp.BootstrapWitnesses,
450456
&tmp.WsPlutusV1Scripts,
451-
&tmp.WsPlutusData,
452457
&tmp.WsPlutusV2Scripts,
453458
&tmp.WsPlutusV3Scripts,
454459
} {

ledger/conway/conway_test.go

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,11 @@ func TestConwayTransactionBodyRejectsDuplicateTaggedSetFields(t *testing.T) {
306306
}
307307
}
308308

309-
func TestConwayWitnessSetRejectsDuplicateTaggedVkeyWitness(t *testing.T) {
309+
// Conway (protocol versions 9-11) tolerates duplicate vkey witnesses:
310+
// cardano-ledger decodes them via Set.fromList and only begins rejecting
311+
// duplicates at protocol version 12 (Dijkstra). Rejecting at decode in Conway
312+
// wrongly rejects valid historical blocks (issue #1853).
313+
func TestConwayWitnessSetToleratesDuplicateTaggedVkeyWitness(t *testing.T) {
310314
dupCbor := []byte{
311315
0xa1, // map(1)
312316
0x00, // key: 0 (VkeyWitnesses field)
@@ -316,11 +320,100 @@ func TestConwayWitnessSetRejectsDuplicateTaggedVkeyWitness(t *testing.T) {
316320
0x82, 0x41, 0x01, 0x41, 0x02, // duplicate
317321
}
318322

323+
var ws ConwayTransactionWitnessSet
324+
err := ws.UnmarshalCBOR(dupCbor)
325+
assert.NoError(t, err,
326+
"Conway must tolerate duplicate vkey witnesses (dedup at decode, matching cardano-ledger pv 9-11)")
327+
}
328+
329+
func TestConwayWitnessSetToleratesDuplicateTaggedWitnessSetFields(t *testing.T) {
330+
tests := []struct {
331+
name string
332+
field byte
333+
member []byte
334+
}{
335+
{
336+
name: "bootstrap witnesses",
337+
field: 0x02,
338+
member: []byte{
339+
0x84, // BootstrapWitness
340+
0x41, 0x01, 0x41, 0x02, // public key, signature
341+
0x41, 0x03, 0x41, 0x04, // chain code, attributes
342+
},
343+
},
344+
{
345+
name: "native scripts",
346+
field: 0x01,
347+
member: []byte{0x82, 0x00, 0x41, 0x01}, // pubkey script
348+
},
349+
{
350+
name: "plutus data",
351+
field: 0x04,
352+
member: []byte{0x00}, // integer datum
353+
},
354+
}
355+
for _, tt := range tests {
356+
t.Run(tt.name, func(t *testing.T) {
357+
var ws ConwayTransactionWitnessSet
358+
err := ws.UnmarshalCBOR(
359+
duplicateTaggedWitnessSetFieldCbor(tt.field, tt.member),
360+
)
361+
assert.NoError(t, err)
362+
})
363+
}
364+
}
365+
366+
// Plutus script sets reject duplicates from protocol version 9 (cardano-ledger
367+
// scriptDecoderV9), so Conway must still reject them at decode.
368+
func TestConwayWitnessSetRejectsDuplicateTaggedPlutusV1Script(t *testing.T) {
369+
dupCbor := []byte{
370+
0xa1, // map(1)
371+
0x03, // key: 3 (WsPlutusV1Scripts field)
372+
0xd9, 0x01, 0x02, // tag(258) - CBOR set
373+
0x82, // array(2)
374+
0x41, 0x01, // PlutusV1Script [0x01]
375+
0x41, 0x01, // duplicate
376+
}
377+
319378
var ws ConwayTransactionWitnessSet
320379
err := ws.UnmarshalCBOR(dupCbor)
321380
assert.ErrorContains(t, err, "duplicate member in set")
322381
}
323382

383+
func TestConwayWitnessSetRejectsDuplicateTaggedPlutusV2AndV3Scripts(t *testing.T) {
384+
tests := []struct {
385+
name string
386+
field byte
387+
}{
388+
{name: "Plutus V2", field: 0x06},
389+
{name: "Plutus V3", field: 0x07},
390+
}
391+
for _, tt := range tests {
392+
t.Run(tt.name, func(t *testing.T) {
393+
var ws ConwayTransactionWitnessSet
394+
err := ws.UnmarshalCBOR(
395+
duplicateTaggedWitnessSetFieldCbor(
396+
tt.field,
397+
[]byte{0x41, 0x01},
398+
),
399+
)
400+
assert.ErrorContains(t, err, "duplicate member in set")
401+
})
402+
}
403+
}
404+
405+
func duplicateTaggedWitnessSetFieldCbor(field byte, member []byte) []byte {
406+
ret := []byte{
407+
0xa1, // map(1)
408+
field, // witness set field key
409+
0xd9, 0x01, 0x02, // tag(258) - CBOR set
410+
0x82, // array(2)
411+
}
412+
ret = append(ret, member...)
413+
ret = append(ret, member...)
414+
return ret
415+
}
416+
324417
func testConwayShelleyInput() shelley.ShelleyTransactionInput {
325418
var txId common.Blake2b256
326419
txId[0] = 1

0 commit comments

Comments
 (0)