Skip to content

Commit 2dc5be6

Browse files
authored
fix(cbor): handle non-shortest list headers (#1999)
* fix(cbor): handle non-shortest list headers Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io> * test: use non-fatal field assertions Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io> --------- Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent c46c80f commit 2dc5be6

11 files changed

Lines changed: 613 additions & 7 deletions

File tree

cbor/decode.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,20 +177,20 @@ func DecodeIdFromList(cborData []byte) (int, error) {
177177
if len(cborData) < 2 {
178178
return 0, errors.New("CBOR data too short for list with ID")
179179
}
180-
// If the list length is <= the max simple uint and the first list value
181-
// is <= the max simple uint, then we can extract the value straight from
182-
// the byte slice
180+
// If both the list header and first value use their canonical one-byte
181+
// forms, then we can extract the value straight from the byte slice.
182+
// A non-shortest list header puts its length at byte one instead.
183183
listLen, err := ListLength(cborData)
184184
if err != nil {
185185
return 0, err
186186
}
187187
if listLen == 0 {
188188
return 0, errors.New("cannot return first item from empty list")
189189
}
190-
if listLen < int(CborMaxUintSimple) {
191-
if cborData[1] <= CborMaxUintSimple {
192-
return int(cborData[1]), nil
193-
}
190+
if cborData[0] >= CborTypeArray &&
191+
cborData[0] <= (CborTypeArray+CborMaxUintSimple) &&
192+
cborData[1] <= CborMaxUintSimple {
193+
return int(cborData[1]), nil
194194
}
195195
// If we couldn't use the shortcut above, actually decode the list
196196
var tmp Value

cbor/dispatch_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cbor_test
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
test "github.com/blinklabs-io/gouroboros/internal/test"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestDecodeByIdAcceptsListLengthEncodings(t *testing.T) {
26+
canonical := test.DecodeHexString("8403010203")
27+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
28+
t.Run(encoding.Name, func(t *testing.T) {
29+
objects := map[int]any{
30+
1: &decodeByIdObjectA{},
31+
2: &decodeByIdObjectB{},
32+
3: &decodeByIdObjectC{},
33+
}
34+
decoded, err := cbor.DecodeById(encoding.Data, objects)
35+
require.NoError(t, err)
36+
require.Equal(
37+
t,
38+
&decodeByIdObjectC{Type: 3, Foo: 1, Bar: 2, Baz: 3},
39+
decoded,
40+
)
41+
})
42+
}
43+
}

internal/test/helpers.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,43 @@ func DecodeHexString(hexData string) []byte {
2121
return decoded
2222
}
2323

24+
// ListEncoding pairs a CBOR list encoding with a descriptive test name.
25+
type ListEncoding struct {
26+
Name string
27+
Data []byte
28+
}
29+
30+
// CanonicalAndNonShortestList returns the provided canonical short-form CBOR
31+
// list and equivalent encodings using every wider definite-length form.
32+
// Cardano decoders accept all of these forms, so tagged-union dispatch must not
33+
// assume that the first item always starts at byte one.
34+
func CanonicalAndNonShortestList(canonical []byte) []ListEncoding {
35+
if len(canonical) == 0 || canonical[0] < 0x80 || canonical[0] > 0x97 {
36+
panic("expected a canonical short-form CBOR list")
37+
}
38+
canonicalCopy := bytes.Clone(canonical)
39+
listLen := canonical[0] - 0x80
40+
withHeader := func(name string, header []byte) ListEncoding {
41+
data := make([]byte, len(header)+len(canonical)-1)
42+
copy(data, header)
43+
copy(data[len(header):], canonical[1:])
44+
return ListEncoding{Name: name, Data: data}
45+
}
46+
return []ListEncoding{
47+
{Name: "canonical", Data: canonicalCopy},
48+
withHeader("non-shortest-uint8-list-length", []byte{0x98, listLen}),
49+
withHeader("non-shortest-uint16-list-length", []byte{0x99, 0, listLen}),
50+
withHeader(
51+
"non-shortest-uint32-list-length",
52+
[]byte{0x9a, 0, 0, 0, listLen},
53+
),
54+
withHeader(
55+
"non-shortest-uint64-list-length",
56+
[]byte{0x9b, 0, 0, 0, 0, 0, 0, 0, listLen},
57+
),
58+
}
59+
}
60+
2461
// JsonStringsEqual is a helper function for tests that compares JSON strings. To account for
2562
// differences in whitespace, map key ordering, etc., we unmarshal the JSON strings into
2663
// objects and then compare the objects

ledger/babbage/dispatch_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package babbage
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
test "github.com/blinklabs-io/gouroboros/internal/test"
22+
"github.com/blinklabs-io/gouroboros/ledger/common"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestDatumOptionAcceptsListLengthEncodings(t *testing.T) {
27+
hash := common.Blake2b256{1, 2, 3}
28+
canonical, err := cbor.Encode([]any{DatumOptionTypeHash, hash})
29+
require.NoError(t, err)
30+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
31+
t.Run(encoding.Name, func(t *testing.T) {
32+
var decoded BabbageTransactionOutputDatumOption
33+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
34+
require.NotNil(t, decoded.hash)
35+
require.Equal(t, hash, *decoded.hash)
36+
})
37+
}
38+
}

ledger/byron/dispatch_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package byron_test
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
test "github.com/blinklabs-io/gouroboros/internal/test"
22+
"github.com/blinklabs-io/gouroboros/ledger/byron"
23+
"github.com/blinklabs-io/gouroboros/ledger/common"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestByronTransactionInputAcceptsListLengthEncodings(t *testing.T) {
29+
hash := common.Blake2b256{1, 2, 3}
30+
inner, err := cbor.Encode([]any{hash, uint32(7)})
31+
require.NoError(t, err)
32+
canonical, err := cbor.Encode([]any{0, inner})
33+
require.NoError(t, err)
34+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
35+
t.Run(encoding.Name, func(t *testing.T) {
36+
var decoded byron.ByronTransactionInput
37+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
38+
assert.Equal(t, hash, decoded.TxId)
39+
assert.Equal(t, uint32(7), decoded.OutputIndex)
40+
})
41+
}
42+
}

ledger/common/dispatch_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
test "github.com/blinklabs-io/gouroboros/internal/test"
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestCommonDispatchersAcceptListLengthEncodings(t *testing.T) {
27+
credential := Credential{
28+
CredType: CredentialTypeAddrKeyHash,
29+
Credential: Blake2b224{1, 2, 3},
30+
}
31+
32+
t.Run("CertificateWrapper", func(t *testing.T) {
33+
canonical, err := cbor.Encode(&StakeRegistrationCertificate{
34+
CertType: uint(CertificateTypeStakeRegistration),
35+
StakeCredential: credential,
36+
})
37+
require.NoError(t, err)
38+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
39+
t.Run(encoding.Name, func(t *testing.T) {
40+
var decoded CertificateWrapper
41+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
42+
require.IsType(t, &StakeRegistrationCertificate{}, decoded.Certificate)
43+
})
44+
}
45+
})
46+
47+
t.Run("Drep", func(t *testing.T) {
48+
canonical, err := cbor.Encode(Drep{Type: DrepTypeAbstain})
49+
require.NoError(t, err)
50+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
51+
t.Run(encoding.Name, func(t *testing.T) {
52+
var decoded Drep
53+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
54+
require.Equal(t, DrepTypeAbstain, decoded.Type)
55+
})
56+
}
57+
})
58+
59+
t.Run("PoolRelay", func(t *testing.T) {
60+
canonical, err := cbor.Encode([]any{1, uint32(3001), "relay.example"})
61+
require.NoError(t, err)
62+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
63+
t.Run(encoding.Name, func(t *testing.T) {
64+
var decoded PoolRelay
65+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
66+
assert.Equal(t, PoolRelayTypeSingleHostName, decoded.Type)
67+
require.NotNil(t, decoded.Hostname)
68+
assert.Equal(t, "relay.example", *decoded.Hostname)
69+
})
70+
}
71+
})
72+
73+
t.Run("Nonce", func(t *testing.T) {
74+
canonical, err := cbor.Encode([]any{NonceTypeNeutral})
75+
require.NoError(t, err)
76+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
77+
t.Run(encoding.Name, func(t *testing.T) {
78+
var decoded Nonce
79+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
80+
require.Equal(t, uint(NonceTypeNeutral), decoded.Type)
81+
})
82+
}
83+
})
84+
85+
t.Run("NativeScript", func(t *testing.T) {
86+
canonical, err := cbor.Encode(NativeScriptInvalidBefore{Type: 4, Slot: 5})
87+
require.NoError(t, err)
88+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
89+
t.Run(encoding.Name, func(t *testing.T) {
90+
var decoded NativeScript
91+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
92+
require.IsType(t, &NativeScriptInvalidBefore{}, decoded.Item())
93+
})
94+
}
95+
})
96+
}

ledger/conway/dispatch_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package conway
16+
17+
import (
18+
"testing"
19+
20+
test "github.com/blinklabs-io/gouroboros/internal/test"
21+
"github.com/blinklabs-io/gouroboros/ledger/common"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func TestConwayGovActionAcceptsListLengthEncodings(t *testing.T) {
26+
action := &common.InfoGovAction{Type: uint(common.GovActionTypeInfo)}
27+
canonical, err := (&ConwayGovAction{Action: action}).MarshalCBOR()
28+
require.NoError(t, err)
29+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
30+
t.Run(encoding.Name, func(t *testing.T) {
31+
var decoded ConwayGovAction
32+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
33+
require.IsType(t, &common.InfoGovAction{}, decoded.Action)
34+
})
35+
}
36+
}

ledger/dijkstra/dispatch_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dijkstra
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
test "github.com/blinklabs-io/gouroboros/internal/test"
22+
"github.com/blinklabs-io/gouroboros/ledger/common"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestDijkstraGovActionAcceptsListLengthEncodings(t *testing.T) {
27+
canonical, err := cbor.Encode(&DijkstraGovAction{
28+
Action: &common.InfoGovAction{Type: uint(common.GovActionTypeInfo)},
29+
})
30+
require.NoError(t, err)
31+
for _, encoding := range test.CanonicalAndNonShortestList(canonical) {
32+
t.Run(encoding.Name, func(t *testing.T) {
33+
var decoded DijkstraGovAction
34+
require.NoError(t, decoded.UnmarshalCBOR(encoding.Data))
35+
require.IsType(t, &common.InfoGovAction{}, decoded.Action)
36+
})
37+
}
38+
}

0 commit comments

Comments
 (0)