Skip to content

Commit 766d074

Browse files
authored
Merge pull request #1592 from lightninglabs/fix-empty-json-meta
rpc: allow minting assets with empty metadata
2 parents 4594d84 + 1d1dac1 commit 766d074

File tree

6 files changed

+202
-29
lines changed

6 files changed

+202
-29
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ build-itest:
9090
@if [ ! -f itest/chantools/chantools ]; then \
9191
$(call print, "Building itest chantools."); \
9292
rm -rf itest/chantools; \
93-
git clone --depth 1 --branch v0.13.5 https://github.com/lightninglabs/chantools.git itest/chantools; \
93+
git clone --depth 1 --branch v0.14.0 https://github.com/lightninglabs/chantools.git itest/chantools; \
9494
cd itest/chantools && go build ./cmd/chantools; \
9595
else \
9696
$(call print, "Chantools is already installed and available in itest/chantools."); \

itest/asset_meta_test.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ func testAssetMeta(t *harnessTest) {
8484
},
8585
},
8686
},
87+
88+
// A mint request that doesn't specify asset meta at all should
89+
// be permitted.
90+
{
91+
asset: &mintrpc.MintAssetRequest{
92+
Asset: &mintrpc.MintAsset{
93+
AssetType: taprpc.AssetType_NORMAL,
94+
Name: "no meta",
95+
Amount: 5000,
96+
},
97+
},
98+
},
99+
100+
// A user should also be able to specify a decimal display, but
101+
// not actually specify an asset meta at all.
102+
{
103+
asset: &mintrpc.MintAssetRequest{
104+
Asset: &mintrpc.MintAsset{
105+
AssetType: taprpc.AssetType_NORMAL,
106+
Name: "dec display",
107+
Amount: 5000,
108+
DecimalDisplay: 6,
109+
},
110+
},
111+
},
87112
}
88113

89114
ctxb := context.Background()
@@ -172,18 +197,9 @@ func testMintAssetWithDecimalDisplayMetaField(t *harnessTest) {
172197
_, err = t.tapd.MintAsset(ctxt, secondAssetReq)
173198
require.ErrorContains(t.t, err, "decimal display does not match")
174199

175-
// Requesting a decimal display without specifying the metadata field
176-
// with at least the type should fail.
177-
secondAssetReq.Asset.DecimalDisplay = firstAsset.DecimalDisplay
178-
secondAssetReq.Asset.AssetMeta = nil
179-
180-
_, err = t.tapd.MintAsset(ctxt, secondAssetReq)
181-
require.ErrorContains(
182-
t.t, err, "decimal display requires asset metadata",
183-
)
184-
185200
// Attempting to set a different decimal display in the JSON meta data
186201
// as in the new RPC request field should give us an error as well.
202+
secondAssetReq.Asset.DecimalDisplay = firstAsset.DecimalDisplay
187203
secondAssetReq.Asset.AssetMeta = &taprpc.AssetMeta{
188204
Type: taprpc.AssetMetaType_META_TYPE_JSON,
189205
Data: []byte(`{"foo": "bar", "decimal_display": 3}`),

itest/chantools_harness.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func NewChantoolsHarness(t *testing.T) ChantoolsHarness {
6666
require.NoError(t, err, "failed to get chantools version")
6767

6868
versionOutStr := string(versionOut)
69-
if !strings.Contains(versionOutStr, "chantools version v0.13.5") {
69+
if !strings.Contains(versionOutStr, "chantools version v0.14.0") {
7070
t.Fatalf("unexpected chantools version: %v", versionOutStr)
7171
}
7272

proof/meta.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,11 @@ func IsValidMetaType[T SizableInteger](num T) (MetaType, error) {
271271
// IsValidMetaSize checks if the passed data is non-empty and below the maximum
272272
// size.
273273
func IsValidMetaSize(mBytes []byte, maxSize int) error {
274-
mSize := len(mBytes)
275-
switch {
276-
case mSize == 0:
277-
return ErrMetaDataMissing
278-
279-
case mSize > maxSize:
274+
if len(mBytes) > maxSize {
280275
return ErrMetaDataTooLarge
281-
282-
default:
283-
return nil
284276
}
277+
278+
return nil
285279
}
286280

287281
// IsValidDecDisplay checks if the decimal display value is below the maximum.
@@ -295,15 +289,13 @@ func IsValidDecDisplay(decDisplay uint32) error {
295289

296290
// DecodeMetaJSON decodes bytes as a JSON object, after checking that the bytes
297291
// could be valid metadata.
298-
//
299-
// TODO(ffranr): Add unit test for `jBytes := []byte{}`.
300292
func DecodeMetaJSON(jBytes []byte) (map[string]interface{}, error) {
301293
jMeta := make(map[string]interface{})
302294

303295
// These bytes must match our metadata size constraints.
304296
err := IsValidMetaSize(jBytes, MetaDataMaxSizeBytes)
305297
if err != nil {
306-
return nil, fmt.Errorf("%w: %s", ErrInvalidJSON, err.Error())
298+
return nil, fmt.Errorf("%w: %w", ErrInvalidJSON, err)
307299
}
308300

309301
// Unmarshal checks internally if the JSON is valid.

proof/meta_test.go

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package proof
33
import (
44
"bytes"
55
"encoding/hex"
6+
"encoding/json"
7+
"errors"
68
"net/url"
79
"os"
810
"path/filepath"
11+
"reflect"
912
"strings"
1013
"testing"
1114

@@ -15,6 +18,7 @@ import (
1518
"github.com/lightninglabs/taproot-assets/internal/test"
1619
"github.com/lightningnetwork/lnd/tlv"
1720
"github.com/stretchr/testify/require"
21+
"pgregory.net/rapid"
1822
)
1923

2024
var (
@@ -71,7 +75,6 @@ func TestValidateMetaReveal(t *testing.T) {
7175
Type: MetaOpaque,
7276
Data: nil,
7377
},
74-
expectedErr: ErrMetaDataMissing,
7578
},
7679
{
7780
name: "too much data",
@@ -466,3 +469,162 @@ func TestDecodeNewMetaReveal(t *testing.T) {
466469
require.Equal(t, fn.None[[]url.URL](), decoded.CanonicalUniverses)
467470
require.Equal(t, fn.None[btcec.PublicKey](), decoded.DelegationKey)
468471
}
472+
473+
// TestDecodeMetaJSONEmptyBytes tests DecodeMetaJSON with an empty byte slice.
474+
func TestDecodeMetaJSONEmptyBytes(t *testing.T) {
475+
t.Parallel()
476+
477+
_, err := DecodeMetaJSON([]byte{})
478+
require.ErrorIs(t, err, ErrInvalidJSON)
479+
}
480+
481+
// TestEncodeMetaJSONEmptyOrNilMap tests EncodeMetaJSON with nil and empty maps.
482+
func TestEncodeMetaJSONEmptyOrNilMap(t *testing.T) {
483+
t.Parallel()
484+
485+
t.Run("nil map", func(t *testing.T) {
486+
nilMapBytes, err := EncodeMetaJSON(nil)
487+
require.NoError(t, err)
488+
require.Equal(
489+
t, []byte{}, nilMapBytes,
490+
"encoding nil map should produce empty bytes",
491+
)
492+
})
493+
494+
t.Run("empty map", func(t *testing.T) {
495+
emptyMap := make(map[string]interface{})
496+
emptyMapBytes, err := EncodeMetaJSON(emptyMap)
497+
require.NoError(t, err)
498+
require.Equal(t, []byte("{}"), emptyMapBytes,
499+
"Encoding empty map should produce '{}'")
500+
})
501+
}
502+
503+
// simpleJSONMap is a helper struct for generating map[string]interface{}
504+
// values for property-based testing.
505+
type simpleJSONMap struct {
506+
StringEntries map[string]string
507+
FloatEntries map[string]float64
508+
BoolEntries map[string]bool
509+
}
510+
511+
// toMap converts a simpleJSONMap to a map[string]interface{}.
512+
func (s simpleJSONMap) toMap() map[string]interface{} {
513+
m := make(map[string]interface{})
514+
for k, v := range s.StringEntries {
515+
// To avoid key collisions if rapid generates identical keys for
516+
// different entry types, we could prefix them, but for
517+
// simplicity, we'll assume distinct keys or accept overwrites
518+
// if rapid generates identical string keys for different map
519+
// types (unlikely to be an issue for typical map generation).
520+
m[k] = v
521+
}
522+
for k, v := range s.FloatEntries {
523+
m[k] = v
524+
}
525+
for k, v := range s.BoolEntries {
526+
m[k] = v
527+
}
528+
return m
529+
}
530+
531+
// TestMetaJsonEncodeDecodeProperties tests the EncodeMetaJSON and
532+
// DecodeMetaJSON functions using property-based testing. It ensures that
533+
// encoding and then decoding a map yields the original map.
534+
func TestMetaJsonEncodeDecodeProperties(t *testing.T) {
535+
t.Parallel()
536+
537+
rapid.Check(t, func(rt *rapid.T) {
538+
// Generate a simpleJSONMap instance.
539+
sjm := rapid.Custom(func(t *rapid.T) simpleJSONMap {
540+
return simpleJSONMap{
541+
StringEntries: rapid.MapOf(
542+
rapid.String(), rapid.String(),
543+
).Draw(t, "StringEntries"),
544+
FloatEntries: rapid.MapOf(
545+
rapid.String(), rapid.Float64(),
546+
).Draw(t, "FloatEntries"),
547+
BoolEntries: rapid.MapOf(
548+
rapid.String(), rapid.Bool(),
549+
).Draw(t, "BoolEntries"),
550+
}
551+
}).Draw(rt, "sjm")
552+
553+
originalMap := sjm.toMap()
554+
555+
encodedBytes, err := EncodeMetaJSON(originalMap)
556+
if err != nil {
557+
// If the generated map is too large, EncodeMetaJSON
558+
// will return ErrMetaDataTooLarge. This is an expected
559+
// behavior, not a property failure.
560+
if errors.Is(err, ErrMetaDataTooLarge) {
561+
return
562+
}
563+
564+
// Any other encoding error is a failure.
565+
rt.Fatalf("EncodeMetaJSON failed for map %#v: %v",
566+
originalMap, err)
567+
}
568+
569+
decodedMap, err := DecodeMetaJSON(encodedBytes)
570+
if err != nil {
571+
rt.Fatalf("DecodeMetaJSON failed for bytes %s "+
572+
"(from map %#v): %v",
573+
string(encodedBytes), originalMap, err)
574+
}
575+
576+
if !reflect.DeepEqual(originalMap, decodedMap) {
577+
rt.Fatalf("Decoded map does not match original map.\n"+
578+
"Original: %#v\nEncoded: %s\nDecoded: %#v",
579+
originalMap, string(encodedBytes), decodedMap)
580+
}
581+
})
582+
}
583+
584+
// TestDecodeMetaJSONOversizedProperty tests that DecodeMetaJSON returns an
585+
// error when the input byte slice is larger than MetaDataMaxSizeBytes.
586+
func TestDecodeMetaJSONOversizedProperty(t *testing.T) {
587+
t.Parallel()
588+
589+
rapid.Check(t, func(rt *rapid.T) {
590+
// Generate a byte slice that is guaranteed to be too large. We
591+
// add a small delta to avoid generating slices that are
592+
// excessively large and slow down the test.
593+
oversizedBytes := rapid.SliceOfN(
594+
rapid.Byte(),
595+
MetaDataMaxSizeBytes+1,
596+
MetaDataMaxSizeBytes+10,
597+
).Draw(rt, "oversizedBytes")
598+
599+
_, err := DecodeMetaJSON(oversizedBytes)
600+
require.ErrorIs(rt, err, ErrMetaDataTooLarge)
601+
})
602+
}
603+
604+
// TestDecodeMetaJSONInvalidJSONProperty tests that DecodeMetaJSON returns an
605+
// error for byte slices that are not valid JSON but are within size limits.
606+
func TestDecodeMetaJSONInvalidJSONProperty(t *testing.T) {
607+
t.Parallel()
608+
609+
rapid.Check(t, func(rt *rapid.T) {
610+
// Generate a byte slice that is within the size limit. There's
611+
// a high chance this random byte slice won't be valid JSON.
612+
inputBytes := rapid.SliceOfN(
613+
rapid.Byte(),
614+
0, MetaDataMaxSizeBytes,
615+
).Draw(rt, "inputBytes")
616+
617+
// If, by chance, we generated valid JSON or an empty slice
618+
// (which DecodeMetaJSON handles as valid empty JSON), we skip
619+
// this iteration as we want to test invalid JSON.
620+
// DecodeMetaJSON considers empty bytes as valid (empty map).
621+
if json.Valid(inputBytes) {
622+
rt.Skip("Generated valid JSON or empty " +
623+
"slice, skipping to test invalid case")
624+
return
625+
}
626+
627+
_, err := DecodeMetaJSON(inputBytes)
628+
require.ErrorIs(rt, err, ErrInvalidJSON)
629+
})
630+
}

rpcserver.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -505,11 +505,14 @@ func (r *rpcServer) MintAsset(ctx context.Context,
505505
return nil, err
506506
}
507507

508-
// If a custom decimal display is set, we require the AssetMeta to be
509-
// set. That means the user has to at least specify the meta type.
508+
// If a custom decimal display is set, but the user didn't specify an
509+
// asset meta, then we'll assume an opaque type.
510510
if req.Asset.DecimalDisplay != 0 && req.Asset.AssetMeta == nil {
511-
return nil, fmt.Errorf("decimal display requires asset " +
512-
"metadata")
511+
rpcsLog.Infof("No asset meta specified, using opaque type "+
512+
"for decimal display %d", req.Asset.DecimalDisplay)
513+
514+
req.Asset.AssetMeta = &taprpc.AssetMeta{}
515+
513516
}
514517

515518
// Decimal display doesn't really make sense for collectibles.

0 commit comments

Comments
 (0)