Skip to content

Commit bf9fbad

Browse files
author
ffranr
authored
Merge pull request #1335 from guggero/tlv-decimal-display
proof: add new universe commitment TLV fields to asset meta reveal
2 parents fc804cb + b25f8df commit bf9fbad

26 files changed

+1666
-149
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: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"fmt"
66
"io"
77
"math"
8+
"net/url"
89

910
"github.com/btcsuite/btcd/blockchain"
11+
"github.com/btcsuite/btcd/btcec/v2"
1012
"github.com/btcsuite/btcd/wire"
1113
"github.com/lightninglabs/taproot-assets/asset"
14+
"github.com/lightninglabs/taproot-assets/fn"
1215
"github.com/lightningnetwork/lnd/tlv"
1316
)
1417

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

464467
return tlv.NewTypeForEncodingErr(val, "GenesisReveal")
465468
}
469+
470+
// EUint32Option encodes a uint32 option. If the value is not set, we'll encode
471+
// it as a zero-length record. But that means that the type and length fields
472+
// will still be encoded, which is different from the record not being present
473+
// at all. If the distinction should be made (e.g. to not re-encode old records
474+
// that didn't have that field at all with the new zero-length record), the
475+
// caller needs to handle that by conditionally including or not including the
476+
// record.
477+
func EUint32Option(w io.Writer, val interface{}, buf *[8]byte) error {
478+
if t, ok := val.(*fn.Option[uint32]); ok {
479+
return fn.MapOptionZ(*t, func(value uint32) error {
480+
return tlv.EUint32T(w, value, buf)
481+
})
482+
}
483+
return tlv.NewTypeForEncodingErr(val, "*fn.Option[uint32]")
484+
}
485+
486+
// DUint32Option decodes a uint32 option.
487+
func DUint32Option(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
488+
if t, ok := val.(*fn.Option[uint32]); ok {
489+
if l == 0 {
490+
*t = fn.None[uint32]()
491+
return nil
492+
}
493+
494+
var newVal uint32
495+
if err := tlv.DUint32(r, &newVal, buf, l); err != nil {
496+
return err
497+
}
498+
499+
*t = fn.Some(newVal)
500+
501+
return nil
502+
}
503+
return tlv.NewTypeForDecodingErr(val, "*fn.Option[uint32]", l, l)
504+
}
505+
506+
// UrlSliceOptionEncoder encodes a URL option. If the value is not set, or the
507+
// slice is empty, we'll encode it as a zero-length record. But that means that
508+
// the type and length fields will still be encoded, which is different from the
509+
// record not being present at all. If the distinction should be made (e.g. to
510+
// not re-encode old records that didn't have that field at all with the new
511+
// zero-length record), the caller needs to handle that by conditionally
512+
// including or not including the record.
513+
func UrlSliceOptionEncoder(w io.Writer, val any, buf *[8]byte) error {
514+
if t, ok := val.(*fn.Option[[]url.URL]); ok {
515+
return fn.MapOptionZ(*t, func(value []url.URL) error {
516+
numValues := uint64(len(value))
517+
if numValues == 0 {
518+
return nil
519+
}
520+
521+
err := tlv.WriteVarInt(w, numValues, buf)
522+
if err != nil {
523+
return err
524+
}
525+
526+
for _, addr := range value {
527+
addrBytes := []byte(addr.String())
528+
err := asset.InlineVarBytesEncoder(
529+
w, &addrBytes, buf,
530+
)
531+
if err != nil {
532+
return err
533+
}
534+
}
535+
536+
return nil
537+
})
538+
}
539+
return tlv.NewTypeForEncodingErr(val, "*fn.Option[[]url.URL]")
540+
}
541+
542+
// UrlSliceOptionDecoder decodes a URL option.
543+
func UrlSliceOptionDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
544+
if l > MaxNumCanonicalUniverseURLs*MaxCanonicalUniverseURLLength {
545+
return tlv.ErrRecordTooLarge
546+
}
547+
548+
if t, ok := val.(*fn.Option[[]url.URL]); ok {
549+
if l == 0 {
550+
*t = fn.None[[]url.URL]()
551+
552+
return nil
553+
}
554+
555+
numValues, err := tlv.ReadVarInt(r, buf)
556+
if err != nil {
557+
return err
558+
}
559+
560+
if numValues > MaxNumCanonicalUniverseURLs {
561+
return tlv.ErrRecordTooLarge
562+
}
563+
564+
urls := make([]url.URL, 0, numValues)
565+
for i := uint64(0); i < numValues; i++ {
566+
var urlBytes []byte
567+
err := asset.InlineVarBytesDecoder(
568+
r, &urlBytes, buf,
569+
MaxCanonicalUniverseURLLength,
570+
)
571+
if err != nil {
572+
return err
573+
}
574+
575+
addr, err := url.ParseRequestURI(string(urlBytes))
576+
if err != nil {
577+
return err
578+
}
579+
urls = append(urls, *addr)
580+
}
581+
582+
*t = fn.Some(urls)
583+
584+
return nil
585+
}
586+
return tlv.NewTypeForDecodingErr(val, "*fn.Option[[]url.URL]", l, l)
587+
}
588+
589+
// PublicKeyOptionEncoder encodes a public key option. If the value is not set,
590+
// we'll encode it as a zero-length record. But that means that the type and
591+
// length fields will still be encoded, which is different from the record not
592+
// being present at all. If the distinction should be made (e.g. to not
593+
// re-encode old records that didn't have that field at all with the new
594+
// zero-length record), the caller needs to handle that by conditionally
595+
// including or not including the record.
596+
func PublicKeyOptionEncoder(w io.Writer, val any, buf *[8]byte) error {
597+
if t, ok := val.(*fn.Option[btcec.PublicKey]); ok {
598+
return fn.MapOptionZ(*t, func(value btcec.PublicKey) error {
599+
if value == emptyKey {
600+
return nil
601+
}
602+
603+
ptr := &value
604+
return asset.CompressedPubKeyEncoder(w, &ptr, buf)
605+
})
606+
}
607+
return tlv.NewTypeForEncodingErr(val, "*fn.Option[btcec.PublicKey]")
608+
}
609+
610+
// PublicKeyOptionDecoder decodes a public key option.
611+
func PublicKeyOptionDecoder(r io.Reader, val any, buf *[8]byte,
612+
l uint64) error {
613+
614+
if l > btcec.PubKeyBytesLenCompressed {
615+
return tlv.ErrRecordTooLarge
616+
}
617+
618+
if t, ok := val.(*fn.Option[btcec.PublicKey]); ok {
619+
if l == 0 {
620+
*t = fn.None[btcec.PublicKey]()
621+
622+
return nil
623+
}
624+
625+
var result *btcec.PublicKey
626+
err := asset.CompressedPubKeyDecoder(r, &result, buf, l)
627+
if err != nil {
628+
return err
629+
}
630+
631+
if result == nil || *result == emptyKey {
632+
*t = fn.None[btcec.PublicKey]()
633+
634+
return nil
635+
}
636+
637+
*t = fn.Some(*result)
638+
639+
return nil
640+
}
641+
return tlv.NewTypeForDecodingErr(
642+
val, "*fn.Option[btcec.PublicKey]", l, l,
643+
)
644+
}

0 commit comments

Comments
 (0)