Skip to content

Commit bfb5f24

Browse files
committed
multi: make forward-compatible by parsing unknown odd types
This commit makes it possible for the protocol to add new odd (optional) types in the future without old clients breaking. By parsing and keeping any left-over unknown odd types, then re-encoding them when calculating the commitment leaf, old software can still correctly use and validate an asset, even if it doesn't fully understand all of its (optional) new fields.
1 parent fa6516e commit bfb5f24

File tree

17 files changed

+694
-206
lines changed

17 files changed

+694
-206
lines changed

address/address.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,9 @@ func (a *Tap) Decode(r io.Reader) error {
415415
return err
416416
}
417417

418-
return asset.TlvStrictDecodeP2P(stream, r, KnownAddressTypes)
418+
// TODO(guggero): Store unknown types (next commits).
419+
_, err = asset.TlvStrictDecodeP2P(stream, r, KnownAddressTypes)
420+
return err
419421
}
420422

421423
// EncodeAddress returns a bech32m string encoding of a Taproot Asset address.

asset/asset.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,16 @@ type Asset struct {
14701470
// together across distinct asset IDs, allowing further issuance of the
14711471
// asset to be made possible.
14721472
GroupKey *GroupKey
1473+
1474+
// UnknownOddTypes is a map of unknown odd types that were encountered
1475+
// during decoding. This map is used to preserve unknown types that we
1476+
// don't know of yet, so we can still encode them back when serializing
1477+
// as a leaf to arrive at the same byte representation and with that
1478+
// same commitment root hash. This enables forward compatibility with
1479+
// future versions of the protocol as it allows new odd (optional) types
1480+
// to be added without breaking old clients that don't yet fully
1481+
// understand them.
1482+
UnknownOddTypes tlv.TypeMap
14731483
}
14741484

14751485
// IsUnknownVersion returns true if an asset has a version that is not
@@ -1924,7 +1934,9 @@ func (a *Asset) encodeRecords(encodeType EncodeType) []tlv.Record {
19241934
if a.GroupKey != nil {
19251935
records = append(records, NewLeafGroupKeyRecord(&a.GroupKey))
19261936
}
1927-
return records
1937+
1938+
// Add any unknown odd types that were encountered during decoding.
1939+
return CombineRecords(records, a.UnknownOddTypes)
19281940
}
19291941

19301942
// EncodeRecords determines the non-nil records to include when encoding an
@@ -2001,7 +2013,14 @@ func (a *Asset) Decode(r io.Reader) error {
20012013
return err
20022014
}
20032015

2004-
return TlvStrictDecode(stream, r, KnownAssetLeafTypes)
2016+
unknownOddTypes, err := TlvStrictDecode(stream, r, KnownAssetLeafTypes)
2017+
if err != nil {
2018+
return err
2019+
}
2020+
2021+
a.UnknownOddTypes = unknownOddTypes
2022+
2023+
return nil
20052024
}
20062025

20072026
// Leaf returns the asset encoded as a MS-SMT leaf node.

asset/asset_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/lightninglabs/taproot-assets/mssmt"
1717
"github.com/lightningnetwork/lnd/input"
1818
"github.com/lightningnetwork/lnd/keychain"
19+
"github.com/lightningnetwork/lnd/tlv"
1920
"github.com/stretchr/testify/require"
2021
)
2122

@@ -496,6 +497,18 @@ func TestAssetEncoding(t *testing.T) {
496497
ScriptKey: NewScriptKey(pubKey),
497498
})
498499

500+
assertAssetEncoding("minimal asset with unknown odd type", &Asset{
501+
Genesis: Genesis{
502+
MetaHash: [MetaHashLen]byte{},
503+
},
504+
ScriptKey: NewScriptKey(pubKey),
505+
UnknownOddTypes: tlv.TypeMap{
506+
test.TestVectorAllowedUnknownType: []byte(
507+
"the great unknown",
508+
),
509+
},
510+
})
511+
499512
// Write test vectors to file. This is a no-op if the "gen_test_vectors"
500513
// build tag is not set.
501514
test.WriteTestVectors(t, generatedTestVectorName, testVectors)
@@ -1073,6 +1086,14 @@ func runBIPTestVector(t *testing.T, testVectors *TestVectors) {
10731086
// yet. If the following check fails, you need to update
10741087
// the KnownAssetLeafTypes set.
10751088
for _, record := range a.encodeRecords(EncodeNormal) {
1089+
// Test vectors may contain this one type to
1090+
// demonstrate that it is not rejected.
1091+
if record.Type() ==
1092+
test.TestVectorAllowedUnknownType {
1093+
1094+
continue
1095+
}
1096+
10761097
require.Contains(
10771098
tt, KnownAssetLeafTypes, record.Type(),
10781099
)

asset/mock.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lightninglabs/taproot-assets/mssmt"
1818
"github.com/lightningnetwork/lnd/input"
1919
"github.com/lightningnetwork/lnd/keychain"
20+
"github.com/lightningnetwork/lnd/tlv"
2021
"github.com/stretchr/testify/require"
2122
)
2223

@@ -552,6 +553,7 @@ func NewTestFromAsset(t testing.TB, a *Asset) *TestAsset {
552553
RelativeLockTime: a.RelativeLockTime,
553554
ScriptVersion: uint16(a.ScriptVersion),
554555
ScriptKey: test.HexPubKey(a.ScriptKey.PubKey),
556+
UnknownOddTypes: a.UnknownOddTypes,
555557
}
556558

557559
for _, w := range a.PrevWitnesses {
@@ -588,6 +590,7 @@ type TestAsset struct {
588590
ScriptVersion uint16 `json:"script_version"`
589591
ScriptKey string `json:"script_key"`
590592
GroupKey *TestGroupKey `json:"group_key"`
593+
UnknownOddTypes tlv.TypeMap `json:"unknown_odd_types"`
591594
}
592595

593596
func (ta *TestAsset) ToAsset(t testing.TB) *Asset {
@@ -635,6 +638,7 @@ func (ta *TestAsset) ToAsset(t testing.TB) *Asset {
635638
ScriptKey: ScriptKey{
636639
PubKey: test.ParsePubKey(t, ta.ScriptKey),
637640
},
641+
UnknownOddTypes: ta.UnknownOddTypes,
638642
}
639643

640644
for _, tw := range ta.PrevWitnesses {

asset/records.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,32 +84,75 @@ func AssertNoUnknownEvenTypes(parsedTypes tlv.TypeMap,
8484
return nil
8585
}
8686

87+
// FilterUnknownTypes filters out all types that are unknown from the given
88+
// parsed types. The known types are specified as a set.
89+
func FilterUnknownTypes(parsedTypes tlv.TypeMap,
90+
knownTypes fn.Set[tlv.Type]) tlv.TypeMap {
91+
92+
result := make(tlv.TypeMap, len(parsedTypes))
93+
for t, v := range parsedTypes {
94+
if !knownTypes.Contains(t) {
95+
result[t] = v
96+
}
97+
}
98+
99+
// Avoid failures due to comparisons with nil vs. empty map.
100+
if len(result) == 0 {
101+
return nil
102+
}
103+
104+
return result
105+
}
106+
87107
// TlvStrictDecode attempts to decode the passed buffer into the TLV stream. It
88108
// takes the set of known types for a given stream, and returns an error if the
89109
// buffer includes any unknown even types.
90110
func TlvStrictDecode(stream *tlv.Stream, r io.Reader,
91-
knownTypes fn.Set[tlv.Type]) error {
111+
knownTypes fn.Set[tlv.Type]) (tlv.TypeMap, error) {
92112

93113
parsedTypes, err := stream.DecodeWithParsedTypes(r)
94114
if err != nil {
95-
return err
115+
return nil, err
96116
}
97117

98-
return AssertNoUnknownEvenTypes(parsedTypes, knownTypes)
118+
err = AssertNoUnknownEvenTypes(parsedTypes, knownTypes)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
return FilterUnknownTypes(parsedTypes, knownTypes), nil
99124
}
100125

101126
// TlvStrictDecodeP2P is identical to TlvStrictDecode except that the record
102127
// size is capped at 65535. This should only be called from a p2p setting where
103128
// untrusted input is being deserialized.
104129
func TlvStrictDecodeP2P(stream *tlv.Stream, r io.Reader,
105-
knownTypes fn.Set[tlv.Type]) error {
130+
knownTypes fn.Set[tlv.Type]) (tlv.TypeMap, error) {
106131

107132
parsedTypes, err := stream.DecodeWithParsedTypesP2P(r)
108133
if err != nil {
109-
return err
134+
return nil, err
135+
}
136+
137+
err = AssertNoUnknownEvenTypes(parsedTypes, knownTypes)
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
return FilterUnknownTypes(parsedTypes, knownTypes), nil
143+
}
144+
145+
// CombineRecords returns a new slice of records that combines the given records
146+
// with the unparsed types converted to static records.
147+
func CombineRecords(records []tlv.Record, unparsed tlv.TypeMap) []tlv.Record {
148+
stubRecords := make([]tlv.Record, 0, len(unparsed))
149+
for k, v := range unparsed {
150+
stubRecords = append(stubRecords, tlv.MakeStaticRecord(
151+
k, nil, uint64(len(v)), tlv.StubEncoder(v), nil,
152+
))
110153
}
111154

112-
return AssertNoUnknownEvenTypes(parsedTypes, knownTypes)
155+
return append(records, stubRecords...)
113156
}
114157

115158
// WitnessTlvType represents the different TLV types for Asset Witness TLV

asset/records_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package asset
22

33
import (
4+
"bytes"
45
"testing"
56

67
"github.com/lightninglabs/taproot-assets/fn"
8+
"github.com/lightninglabs/taproot-assets/internal/test"
9+
"github.com/lightningnetwork/lnd/keychain"
710
"github.com/lightningnetwork/lnd/tlv"
811
"github.com/stretchr/testify/require"
912
)
@@ -60,3 +63,109 @@ func TestTlvStrictDecode(t *testing.T) {
6063
))
6164
}
6265
}
66+
67+
// TestFilterUnknownTypes tests that the filtering of unknown TLV records works
68+
// as expected.
69+
func TestFilterUnknownTypes(t *testing.T) {
70+
t.Parallel()
71+
72+
testCases := []struct {
73+
parsedTypes tlv.TypeMap
74+
knownTypes fn.Set[tlv.Type]
75+
result tlv.TypeMap
76+
}{
77+
// No unknown types.
78+
{
79+
parsedTypes: tlv.TypeMap{
80+
0: []byte{},
81+
2: []byte{},
82+
},
83+
knownTypes: fn.NewSet[tlv.Type](0, 2),
84+
result: nil,
85+
},
86+
87+
// Unknown type, but odd.
88+
{
89+
parsedTypes: tlv.TypeMap{
90+
0: []byte{},
91+
2: []byte{},
92+
3: []byte{},
93+
},
94+
knownTypes: fn.NewSet[tlv.Type](0, 2),
95+
result: tlv.TypeMap{
96+
3: []byte{},
97+
},
98+
},
99+
100+
// Multiple unknown types, both odd and even.
101+
{
102+
parsedTypes: tlv.TypeMap{
103+
0: []byte{},
104+
2: []byte{},
105+
3: []byte{},
106+
4: []byte{},
107+
},
108+
knownTypes: fn.NewSet[tlv.Type](0, 2),
109+
result: tlv.TypeMap{
110+
3: []byte{},
111+
4: []byte{},
112+
},
113+
},
114+
}
115+
116+
for _, testCase := range testCases {
117+
require.Equal(t, testCase.result, FilterUnknownTypes(
118+
testCase.parsedTypes, testCase.knownTypes,
119+
))
120+
}
121+
}
122+
123+
// TestAssetUnknownOddType tests that an unknown odd type is allowed in an asset
124+
// and that we can still arrive at the correct leaf hash with it.
125+
func TestAssetUnknownOddType(t *testing.T) {
126+
knownAsset := RandAsset(t, Normal)
127+
knownAssetLeaf, err := knownAsset.Leaf()
128+
require.NoError(t, err)
129+
130+
test.RunUnknownOddTypeTest(
131+
t, knownAsset, &ErrUnknownType{},
132+
func(buf *bytes.Buffer, asset *Asset) error {
133+
return asset.Encode(buf)
134+
},
135+
func(buf *bytes.Buffer) (*Asset, error) {
136+
var asset Asset
137+
return &asset, asset.Decode(buf)
138+
},
139+
func(parsedAsset *Asset, unknownTypes tlv.TypeMap) {
140+
// The unknown types should be reported correctly.
141+
require.Equal(
142+
t, unknownTypes, parsedAsset.UnknownOddTypes,
143+
)
144+
145+
// The leaf should've changed, to make sure the unknown
146+
// value was taken into account when creating the
147+
// serialized leaf.
148+
parsedAssetLeaf, err := parsedAsset.Leaf()
149+
require.NoError(t, err)
150+
151+
require.Equal(
152+
t, knownAssetLeaf.NodeSum(),
153+
parsedAssetLeaf.NodeSum(),
154+
)
155+
require.NotEqual(
156+
t, knownAssetLeaf.NodeHash(),
157+
parsedAssetLeaf.NodeHash(),
158+
)
159+
160+
parsedAsset.UnknownOddTypes = nil
161+
162+
// The group key's raw key and witness aren't
163+
// serialized, so we need to clear them out before
164+
// comparing.
165+
knownAsset.GroupKey.RawKey = keychain.KeyDescriptor{}
166+
knownAsset.GroupKey.Witness = nil
167+
168+
require.Equal(t, knownAsset, parsedAsset)
169+
},
170+
)
171+
}

asset/testdata/asset_tlv_encoding_generated.json

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"script_key": "02a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
5454
"group_key": {
5555
"group_key": "03a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f"
56-
}
56+
},
57+
"unknown_odd_types": null
5758
}
5859
}
5960
}
@@ -63,7 +64,8 @@
6364
"script_key": "02a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
6465
"group_key": {
6566
"group_key": "03a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f"
66-
}
67+
},
68+
"unknown_odd_types": null
6769
},
6870
"expected": "000101024f010101010101010101010101010101010101010101010101010101010101010100000001056173736574010203000000000000000000000000000000000000000000000000000000000000000001010401010601010703fd05390901060bfd020501fd02010165010101010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010101010103a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f05fd01964a000197efb64d447880bacc7070f428a1310b2592d155b752da382934d4bd0fbb419a000000000000000affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7ffd0148000101024f010101010101010101010101010101010101010101010101010101010101010100000001056173736574010203000000000000000000000000000000000000000000000000000000000000000001010401010601010703fd05390901060b70016e0165020202020202020202020202020202020202020202020202020202020202020200000002020202020202020202020202020202020202020202020202020202020202020203a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f030502010201020d28010101010101010101010101010101010101010101010101010101010101010100000000000005390e020001102102a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f112103a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f0e020001102102a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f112103a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
6971
"comment": "random split asset with root asset"
@@ -110,7 +112,8 @@
110112
"split_commitment_root": null,
111113
"script_version": 2,
112114
"script_key": "02a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
113-
"group_key": null
115+
"group_key": null,
116+
"unknown_odd_types": null
114117
},
115118
"expected": "000102024f020202020202020202020202020202020202020202020202020202020202020200000002056173736574010203000000000000000000000000000000000000000000000000000000000000000002020401020601020703fd05390901060bd9030067016500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006e0165020202020202020202020202020202020202020202020202020202020202020200000002020202020202020202020202020202020202020202020202020202020202020203a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f030502010201020e020002102102a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
116119
"comment": "random asset with multiple previous witnesses"
@@ -130,10 +133,34 @@
130133
"split_commitment_root": null,
131134
"script_version": 0,
132135
"script_key": "02a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
133-
"group_key": null
136+
"group_key": null,
137+
"unknown_odd_types": null
134138
},
135139
"expected": "000100024a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000401000601000e020000102102a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
136140
"comment": "minimal asset"
141+
},
142+
{
143+
"asset": {
144+
"version": 0,
145+
"genesis_first_prev_out": "0000000000000000000000000000000000000000000000000000000000000000:0",
146+
"genesis_tag": "",
147+
"genesis_meta_hash": "0000000000000000000000000000000000000000000000000000000000000000",
148+
"genesis_output_index": 0,
149+
"genesis_type": 0,
150+
"amount": 0,
151+
"lock_time": 0,
152+
"relative_lock_time": 0,
153+
"prev_witnesses": null,
154+
"split_commitment_root": null,
155+
"script_version": 0,
156+
"script_key": "02a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078f",
157+
"group_key": null,
158+
"unknown_odd_types": {
159+
"31337": "dGhlIGdyZWF0IHVua25vd24="
160+
}
161+
},
162+
"expected": "000100024a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000401000601000e020000102102a0afeb165f0ec36880b68e0baabd9ad9c62fd1a69aa998bc30e9a346202e078ffd7a691174686520677265617420756e6b6e6f776e",
163+
"comment": "minimal asset with unknown odd type"
137164
}
138165
],
139166
"error_test_cases": null

0 commit comments

Comments
 (0)