Skip to content

Commit 98f9acb

Browse files
committed
multi: add decimal display as new TLV field
With this commit we add the decimal display as a new TLV field on the meta reveal. We'll still encode it within the JSON meta data if the meta data is of type JSON. For all other types we can now also carry the value, to make sure we don't put any type constraints on future asset mints (in case issuers want to use a different format than JSON). Since we're using TLV, old meta reveals should still be read correctly and the default value of 0 is assumed for any meta reveal that doesn't have the explicit record set. We'll add a unit test for that in the next commit.
1 parent fc804cb commit 98f9acb

File tree

7 files changed

+225
-66
lines changed

7 files changed

+225
-66
lines changed

itest/assertions.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,22 +1852,19 @@ func AssertAssetsMinted(t *testing.T, tapClient commands.RpcClientsBundle,
18521852
confirmedAssets := GroupAssetsByName(listRespConfirmed.Assets)
18531853

18541854
for _, assetRequest := range assetRequests {
1855-
metaReveal := &proof.MetaReveal{
1856-
Data: assetRequest.Asset.AssetMeta.Data,
1857-
}
1858-
18591855
validMetaType, err := proof.IsValidMetaType(
18601856
assetRequest.Asset.AssetMeta.Type,
18611857
)
18621858
require.NoError(t, err)
18631859

1864-
metaReveal.Type = validMetaType
1865-
if metaReveal.Type == proof.MetaJson {
1866-
err := metaReveal.SetDecDisplay(
1867-
assetRequest.Asset.DecimalDisplay,
1868-
)
1869-
require.NoError(t, err)
1860+
metaReveal := &proof.MetaReveal{
1861+
Data: assetRequest.Asset.AssetMeta.Data,
1862+
Type: validMetaType,
18701863
}
1864+
err = metaReveal.SetDecDisplay(
1865+
assetRequest.Asset.DecimalDisplay,
1866+
)
1867+
require.NoError(t, err)
18711868

18721869
metaHash := metaReveal.MetaHash()
18731870

@@ -2077,10 +2074,15 @@ func assertGroups(t *testing.T, client taprpc.TaprootAssetsClient,
20772074
equalityCheck := func(a *mintrpc.MintAsset,
20782075
b *taprpc.AssetHumanReadable) {
20792076

2080-
metaHash := (&proof.MetaReveal{
2077+
assetMeta := &proof.MetaReveal{
20812078
Type: proof.MetaOpaque,
20822079
Data: a.AssetMeta.Data,
2083-
}).MetaHash()
2080+
}
2081+
2082+
err = assetMeta.SetDecDisplay(a.DecimalDisplay)
2083+
require.NoError(t, err)
2084+
2085+
metaHash := assetMeta.MetaHash()
20842086

20852087
require.Equal(t, a.AssetType, b.Type)
20862088
require.Equal(t, a.Name, b.Tag)

itest/asset_meta_test.go

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,31 +167,83 @@ func testMintAssetWithDecimalDisplayMetaField(t *harnessTest) {
167167
secondAssetReq.Asset.GroupKey = groupKey
168168
secondAssetReq.Asset.DecimalDisplay = 0
169169

170-
// Reissuance should fail if the decimal display does not match the
170+
// Re-issuance should fail if the decimal display does not match the
171171
// group anchor.
172172
_, err = t.tapd.MintAsset(ctxt, secondAssetReq)
173173
require.ErrorContains(t.t, err, "decimal display does not match")
174174

175-
// Requesting a decimal display without specifying the metadata type as
176-
// JSON should fail.
175+
// Requesting a decimal display without specifying the metadata field
176+
// with at least the type should fail.
177177
secondAssetReq.Asset.DecimalDisplay = firstAsset.DecimalDisplay
178178
secondAssetReq.Asset.AssetMeta = nil
179179

180180
_, err = t.tapd.MintAsset(ctxt, secondAssetReq)
181-
require.ErrorContains(t.t, err, "decimal display requires JSON")
181+
require.ErrorContains(
182+
t.t, err, "decimal display requires asset metadata",
183+
)
182184

183-
// If we update the decimal display to match the group anchor, minting
184-
// should succeed. We also unset the metadata to ensure that the decimal
185-
// display is set as the sole JSON object if needed.
185+
// Attempting to set a different decimal display in the JSON meta data
186+
// as in the new RPC request field should give us an error as well.
186187
secondAssetReq.Asset.AssetMeta = &taprpc.AssetMeta{
187188
Type: taprpc.AssetMetaType_META_TYPE_JSON,
189+
Data: []byte(`{"foo": "bar", "decimal_display": 3}`),
188190
}
189-
MintAssetsConfirmBatch(
191+
_, err = t.tapd.MintAsset(ctxt, secondAssetReq)
192+
require.ErrorContains(
193+
t.t, err, "decimal display in JSON asset meta does not match",
194+
)
195+
196+
// If we set a valid asset meta again, minting should succeed, using the
197+
// same decimal display as the group anchor.
198+
secondAssetReq.Asset.AssetMeta.Data = []byte(`{"foo": "bar"}`)
199+
secondAssets := MintAssetsConfirmBatch(
190200
t.t, t.lndHarness.Miner().Client, t.tapd,
191201
[]*mintrpc.MintAssetRequest{secondAssetReq},
192202
)
203+
require.Len(t.t, secondAssets, 1)
204+
require.NotNil(t.t, secondAssets[0].DecimalDisplay)
205+
require.EqualValues(
206+
t.t, 2, secondAssets[0].DecimalDisplay.DecimalDisplay,
207+
)
208+
209+
// For an asset with a JSON meta data type, we also expect the decimal
210+
// display to be encoded in the meta data JSON.
211+
metaResp, err := t.tapd.FetchAssetMeta(
212+
ctxt, &taprpc.FetchAssetMetaRequest{
213+
Asset: &taprpc.FetchAssetMetaRequest_AssetId{
214+
AssetId: secondAssets[0].AssetGenesis.AssetId,
215+
},
216+
},
217+
)
218+
require.NoError(t.t, err)
219+
require.Contains(t.t, string(metaResp.Data), `"foo":"bar"`)
220+
require.Contains(t.t, string(metaResp.Data), `"decimal_display":2`)
193221

194222
AssertGroupSizes(
195223
t.t, t.tapd, []string{hex.EncodeToString(groupKey)}, []int{2},
196224
)
225+
226+
// Now we also test minting an asset that uses the opaque meta data type
227+
// and check that the decimal display is correctly encoded as well.
228+
thirdAsset := &mintrpc.MintAsset{
229+
AssetType: taprpc.AssetType_NORMAL,
230+
Name: "test-asset-opaque-decimal-display",
231+
AssetMeta: &taprpc.AssetMeta{
232+
Type: taprpc.AssetMetaType_META_TYPE_OPAQUE,
233+
Data: []byte("some opaque data"),
234+
},
235+
Amount: 123,
236+
DecimalDisplay: 7,
237+
}
238+
thirdAssetReq := &mintrpc.MintAssetRequest{Asset: thirdAsset}
239+
thirdAssets := MintAssetsConfirmBatch(
240+
t.t, t.lndHarness.Miner().Client, t.tapd,
241+
[]*mintrpc.MintAssetRequest{thirdAssetReq},
242+
)
243+
244+
require.Len(t.t, thirdAssets, 1)
245+
require.NotNil(t.t, thirdAssets[0].DecimalDisplay)
246+
require.EqualValues(
247+
t.t, 7, thirdAssets[0].DecimalDisplay.DecimalDisplay,
248+
)
197249
}

itest/utils.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,10 @@ func FinalizeBatchUnconfirmed(t *testing.T, minerClient *rpcclient.Client,
427427
require.NoError(t, err)
428428

429429
metaReveal.Type = validMetaType
430-
if metaReveal.Type == proof.MetaJson {
431-
err := metaReveal.SetDecDisplay(
432-
assetRequest.Asset.DecimalDisplay,
433-
)
434-
require.NoError(t, err)
435-
}
430+
err = metaReveal.SetDecDisplay(
431+
assetRequest.Asset.DecimalDisplay,
432+
)
433+
require.NoError(t, err)
436434

437435
metaHash := metaReveal.MetaHash()
438436

@@ -604,6 +602,9 @@ func ManualMintSimpleAsset(t *harnessTest, lndNode *node.HarnessNode,
604602
Type: proof.MetaOpaque,
605603
Data: req.AssetMeta.Data,
606604
}
605+
err = assetMeta.SetDecDisplay(req.DecimalDisplay)
606+
require.NoError(t.t, err)
607+
607608
metaHash := assetMeta.MetaHash()
608609
metaReveals := tapgarden.AssetMetas{
609610
asset.ToSerialized(assetScriptKey.PubKey): &assetMeta,

proof/encoding.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/btcsuite/btcd/blockchain"
1010
"github.com/btcsuite/btcd/wire"
1111
"github.com/lightninglabs/taproot-assets/asset"
12+
"github.com/lightninglabs/taproot-assets/fn"
1213
"github.com/lightningnetwork/lnd/tlv"
1314
)
1415

@@ -463,3 +464,39 @@ func GenesisRevealDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
463464

464465
return tlv.NewTypeForEncodingErr(val, "GenesisReveal")
465466
}
467+
468+
// EUint32Option encodes a uint32 option. If the value is not set, we'll encode
469+
// it as a zero-length record. But that means that the type and length fields
470+
// will still be encoded, which is different from the record not being present
471+
// at all. If the distinction should be made (e.g. to not re-encode old records
472+
// that didn't have that field at all with the new zero-length record), the
473+
// caller needs to handle that by conditionally including or not including the
474+
// record.
475+
func EUint32Option(w io.Writer, val interface{}, buf *[8]byte) error {
476+
if t, ok := val.(*fn.Option[uint32]); ok {
477+
return fn.MapOptionZ(*t, func(value uint32) error {
478+
return tlv.EUint32T(w, value, buf)
479+
})
480+
}
481+
return tlv.NewTypeForEncodingErr(val, "*fn.Option[uint32]")
482+
}
483+
484+
// DUint32Option decodes a uint32 option.
485+
func DUint32Option(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
486+
if t, ok := val.(*fn.Option[uint32]); ok {
487+
if l == 0 {
488+
*t = fn.None[uint32]()
489+
return nil
490+
}
491+
492+
var newVal uint32
493+
if err := tlv.DUint32(r, &newVal, buf, l); err != nil {
494+
return err
495+
}
496+
497+
*t = fn.Some(newVal)
498+
499+
return nil
500+
}
501+
return tlv.NewTypeForDecodingErr(val, "*fn.Option[uint32]", l, l)
502+
}

proof/meta.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ type MetaReveal struct {
100100
// Data is the committed data being revealed.
101101
Data []byte
102102

103+
// DecimalDisplay is the decimal display value of the asset. This is
104+
// used to determine the number of decimal places to display when
105+
// presenting the asset amount to the user. If this field is not
106+
// explicitly encoded in the TLV, this is an older asset that didn't
107+
// have this field. New assets will always set an explicit value, even
108+
// if that is the default value of zero. If the meta type is JSON and
109+
// this value is not zero, then the decimal display is also added as a
110+
// field to the JSON object for backward compatibility.
111+
DecimalDisplay fn.Option[uint32]
112+
103113
// UnknownOddTypes is a map of unknown odd types that were encountered
104114
// during decoding. This map is used to preserve unknown types that we
105115
// don't know of yet, so we can still encode them back when serializing.
@@ -141,7 +151,8 @@ func (m *MetaReveal) Validate() error {
141151
}
142152
}
143153

144-
return nil
154+
// If the decimal display is set, it must be valid.
155+
return fn.MapOptionZ(m.DecimalDisplay, IsValidDecDisplay)
145156
}
146157

147158
// IsValidMetaType checks if the passed value is a valid meta type.
@@ -232,6 +243,12 @@ func (m *MetaReveal) GetDecDisplay() (map[string]interface{}, uint32, error) {
232243
return nil, 0, nil
233244
}
234245

246+
// If the decimal display is set as the new TLV value, we can use that
247+
// directly.
248+
if m.DecimalDisplay.IsSome() {
249+
return nil, m.DecimalDisplay.UnwrapOr(0), nil
250+
}
251+
235252
if m.Type != MetaJson {
236253
return nil, 0, ErrNotJSON
237254
}
@@ -311,9 +328,18 @@ func (m *MetaReveal) SetDecDisplay(decDisplay uint32) error {
311328
return err
312329
}
313330

314-
// If the meta type is not JSON, we can't set the decimal display value.
331+
m.DecimalDisplay = fn.Some(decDisplay)
332+
333+
// We only set the decimal display value in the JSON if it isn't the
334+
// default value of 0.
335+
if decDisplay == 0 {
336+
return nil
337+
}
338+
339+
// If the meta type is not JSON, we're done already, as the decimal
340+
// display will only be encoded in the TLV.
315341
if m.Type != MetaJson {
316-
return ErrNotJSON
342+
return nil
317343
}
318344

319345
// If the meta type is JSON, we'll also want to set the decimal display
@@ -359,6 +385,15 @@ func (m *MetaReveal) EncodeRecords() []tlv.Record {
359385
MetaRevealDataRecord(&m.Data),
360386
}
361387

388+
// To make sure we don't re-encode old assets that don't have a decimal
389+
// display value as a TLV field with a different value, we only encode
390+
// the decimal display value if it is explicitly set.
391+
if m.DecimalDisplay.IsSome() {
392+
records = append(records, MetaRevealDecimalDisplayRecord(
393+
&m.DecimalDisplay,
394+
))
395+
}
396+
362397
// Add any unknown odd types that were encountered during decoding.
363398
return asset.CombineRecords(records, m.UnknownOddTypes)
364399
}
@@ -368,6 +403,7 @@ func (m *MetaReveal) DecodeRecords() []tlv.Record {
368403
return []tlv.Record{
369404
MetaRevealTypeRecord(&m.Type),
370405
MetaRevealDataRecord(&m.Data),
406+
MetaRevealDecimalDisplayRecord(&m.DecimalDisplay),
371407
}
372408
}
373409

proof/records.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ const (
4343
TapscriptProofTapPreimage2 tlv.Type = 3
4444
TapscriptProofBip86 tlv.Type = 4
4545

46-
MetaRevealEncodingType tlv.Type = 0
47-
MetaRevealDataType tlv.Type = 2
46+
MetaRevealEncodingType tlv.Type = 0
47+
MetaRevealDataType tlv.Type = 2
48+
MetaRevealDecimalDisplay tlv.Type = 5
4849
)
4950

5051
// KnownProofTypes is a set of all known proof TLV types. This set is asserted
@@ -83,7 +84,7 @@ var KnownTapscriptProofTypes = fn.NewSet(
8384
// KnownMetaRevealTypes is a set of all known meta reveal TLV types. This set is
8485
// asserted to be complete by a check in the BIP test vector unit tests.
8586
var KnownMetaRevealTypes = fn.NewSet(
86-
MetaRevealEncodingType, MetaRevealDataType,
87+
MetaRevealEncodingType, MetaRevealDataType, MetaRevealDecimalDisplay,
8788
)
8889

8990
func VersionRecord(version *TransitionVersion) tlv.Record {
@@ -362,6 +363,24 @@ func MetaRevealDataRecord(data *[]byte) tlv.Record {
362363
)
363364
}
364365

366+
func MetaRevealDecimalDisplayRecord(
367+
decimalDisplay *fn.Option[uint32]) tlv.Record {
368+
369+
// If the option is not set, we'll encode it as a zero-length record.
370+
// But because we'll not include the record at all if it's not set at
371+
// the call site when encoding, this will not be the case for this
372+
// specific record.
373+
var size uint64
374+
if decimalDisplay != nil && decimalDisplay.IsSome() {
375+
size = 4
376+
}
377+
378+
return tlv.MakeStaticRecord(
379+
MetaRevealDecimalDisplay, decimalDisplay, size,
380+
EUint32Option, DUint32Option,
381+
)
382+
}
383+
365384
func GenesisRevealRecord(genesis **asset.Genesis) tlv.Record {
366385
recordSize := func() uint64 {
367386
var (

0 commit comments

Comments
 (0)