Skip to content

Commit c35215a

Browse files
authored
Merge pull request #794 from lightninglabs/json-meta
multi: introduce new meta type for json strings
2 parents 58f9854 + 22e63bc commit c35215a

File tree

10 files changed

+362
-160
lines changed

10 files changed

+362
-160
lines changed

cmd/tapcli/assets.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package main
22

33
import (
44
"encoding/hex"
5+
"encoding/json"
56
"fmt"
67
"math"
78
"os"
9+
"strconv"
810

911
taprootassets "github.com/lightninglabs/taproot-assets"
1012
"github.com/lightninglabs/taproot-assets/tapcfg"
@@ -88,9 +90,11 @@ var mintAssetCommand = cli.Command{
8890
Usage: "a path to a file on disk that should be read " +
8991
"and used as the asset meta",
9092
},
91-
cli.IntFlag{
92-
Name: assetMetaTypeName,
93-
Usage: "the type of the meta data for the asset",
93+
cli.StringFlag{
94+
Name: assetMetaTypeName,
95+
Usage: "the type of the meta data for the asset, must " +
96+
"be either: opaque or json",
97+
Value: "opaque",
9498
},
9599
cli.BoolFlag{
96100
Name: assetNewGroupedAssetName,
@@ -142,6 +146,36 @@ func parseAssetType(ctx *cli.Context) (taprpc.AssetType, error) {
142146
}
143147
}
144148

149+
func parseMetaType(metaType string,
150+
metaBytes []byte) (taprpc.AssetMetaType, error) {
151+
152+
switch metaType {
153+
case "opaque":
154+
fallthrough
155+
case "blob":
156+
return taprpc.AssetMetaType_META_TYPE_OPAQUE, nil
157+
158+
case "json":
159+
// If JSON is selected, the bytes must be valid.
160+
if !json.Valid(metaBytes) {
161+
return 0, fmt.Errorf("invalid JSON for meta: %s",
162+
metaBytes)
163+
}
164+
165+
return taprpc.AssetMetaType_META_TYPE_JSON, nil
166+
167+
// Otherwise, this is a custom meta type, we may not understand it, but
168+
// we want to support specifying arbitrary meta types.
169+
default:
170+
intType, err := strconv.Atoi(metaType)
171+
if err != nil {
172+
return 0, fmt.Errorf("invalid meta type: %s", metaType)
173+
}
174+
175+
return taprpc.AssetMetaType(intType), nil
176+
}
177+
}
178+
145179
func mintAsset(ctx *cli.Context) error {
146180
switch {
147181
case ctx.String(assetTagName) == "":
@@ -163,6 +197,8 @@ func mintAsset(ctx *cli.Context) error {
163197
}
164198
}
165199

200+
metaTypeStr := ctx.String(assetMetaTypeName)
201+
166202
// Both the meta bytes and the meta path can be set.
167203
var assetMeta *taprpc.AssetMeta
168204
switch {
@@ -174,7 +210,11 @@ func mintAsset(ctx *cli.Context) error {
174210
case ctx.String(assetMetaBytesName) != "":
175211
assetMeta = &taprpc.AssetMeta{
176212
Data: []byte(ctx.String(assetMetaBytesName)),
177-
Type: taprpc.AssetMetaType(ctx.Int(assetMetaTypeName)),
213+
}
214+
215+
assetMeta.Type, err = parseMetaType(metaTypeStr, assetMeta.Data)
216+
if err != nil {
217+
return fmt.Errorf("unable to parse meta type: %w", err)
178218
}
179219

180220
case ctx.String(assetMetaFilePathName) != "":
@@ -188,7 +228,11 @@ func mintAsset(ctx *cli.Context) error {
188228

189229
assetMeta = &taprpc.AssetMeta{
190230
Data: metaFileBytes,
191-
Type: taprpc.AssetMetaType(ctx.Int(assetMetaTypeName)),
231+
}
232+
233+
assetMeta.Type, err = parseMetaType(metaTypeStr, assetMeta.Data)
234+
if err != nil {
235+
return fmt.Errorf("unable to parse meta type: %w", err)
192236
}
193237
}
194238

itest/asset_meta_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package itest
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/lightninglabs/taproot-assets/taprpc"
8+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// testAssetMeta tests the validation+parsing logic for asset meta data.
13+
func testAssetMeta(t *harnessTest) {
14+
// In this test, we'll attempt to issue the following assets with
15+
// distinct meta type. Within each test case, negative failure
16+
// scenarios may exist.
17+
jsonType := taprpc.AssetMetaType_META_TYPE_JSON
18+
testCases := []struct {
19+
asset *mintrpc.MintAssetRequest
20+
errString string
21+
}{
22+
// Existing opaque meta data option, should succeed.
23+
{
24+
asset: &mintrpc.MintAssetRequest{
25+
Asset: &mintrpc.MintAsset{
26+
AssetType: taprpc.AssetType_NORMAL,
27+
Name: "opaque asset",
28+
AssetMeta: &taprpc.AssetMeta{
29+
Data: []byte("some metadata"),
30+
},
31+
Amount: 5000,
32+
},
33+
},
34+
},
35+
36+
// Existing JSON meta data option, with valid JSON should
37+
// succeed.
38+
{
39+
asset: &mintrpc.MintAssetRequest{
40+
Asset: &mintrpc.MintAsset{
41+
AssetType: taprpc.AssetType_NORMAL,
42+
Name: "json asset",
43+
AssetMeta: &taprpc.AssetMeta{
44+
Data: []byte(
45+
`{"key": "value"}`,
46+
),
47+
Type: jsonType,
48+
},
49+
Amount: 5000,
50+
},
51+
},
52+
},
53+
54+
// Existing JSON meta data option, with invalid JSON should
55+
// fail.
56+
{
57+
asset: &mintrpc.MintAssetRequest{
58+
Asset: &mintrpc.MintAsset{
59+
AssetType: taprpc.AssetType_NORMAL,
60+
Name: "invalid json",
61+
AssetMeta: &taprpc.AssetMeta{
62+
Data: []byte("not json"),
63+
Type: jsonType,
64+
},
65+
Amount: 5000,
66+
},
67+
},
68+
errString: "invalid asset meta: invalid JSON",
69+
},
70+
71+
// Custom meta data type, with valid data should succeed.
72+
{
73+
asset: &mintrpc.MintAssetRequest{
74+
Asset: &mintrpc.MintAsset{
75+
AssetType: taprpc.AssetType_NORMAL,
76+
Name: "custom meta type",
77+
AssetMeta: &taprpc.AssetMeta{
78+
Data: []byte("custom stuff"),
79+
Type: 99,
80+
},
81+
Amount: 5000,
82+
},
83+
},
84+
},
85+
}
86+
87+
ctxb := context.Background()
88+
for _, tc := range testCases {
89+
t.t.Run(tc.asset.Asset.Name, func(tt *testing.T) {
90+
_, err := t.tapd.MintAsset(ctxb, tc.asset)
91+
if err != nil {
92+
if tc.errString == "" {
93+
tt.Fatalf("unexpected error: %v", err)
94+
}
95+
require.ErrorContains(tt, err, tc.errString)
96+
}
97+
})
98+
}
99+
100+
// We only test validation here, so we'll cancel the pending batch.
101+
_, err := t.tapd.CancelBatch(ctxb, &mintrpc.CancelBatchRequest{})
102+
require.NoError(t.t, err)
103+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ var testCases = []*testCase{
99
name: "mint assets",
1010
test: testMintAssets,
1111
},
12+
{
13+
name: "asset meta validation",
14+
test: testAssetMeta,
15+
},
1216
{
1317
name: "asset name collision raises mint error",
1418
test: testMintAssetNameCollisionError,

proof/meta.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package proof
33
import (
44
"bytes"
55
"crypto/sha256"
6+
"encoding/json"
67
"errors"
78
"io"
89

@@ -18,6 +19,11 @@ const (
1819
// bytes without any specific interpretation.
1920
MetaOpaque MetaType = 0
2021

22+
// MetaJson signals that the meta data is a JSON object.
23+
MetaJson MetaType = 1
24+
)
25+
26+
const (
2127
// MetaDataMaxSizeBytes is the maximum length of the meta data. We limit
2228
// this to 1MiB for now. This should be of sufficient size to commit to
2329
// any JSON data or even medium resolution images. If there is need to
@@ -35,6 +41,9 @@ var (
3541

3642
// ErrMetaDataTooLarge signals that the meta data is too large.
3743
ErrMetaDataTooLarge = errors.New("meta data too large")
44+
45+
// ErrInvalidJSON signals that the meta data is not a valid JSON.
46+
ErrInvalidJSON = errors.New("invalid JSON")
3847
)
3948

4049
// MetaReveal is an optional TLV type that can be added to the proof of a
@@ -66,6 +75,13 @@ func (m *MetaReveal) Validate() error {
6675
return ErrMetaDataTooLarge
6776
}
6877

78+
// If the type is JSON, then it should be parseable as a JSON string.
79+
if m.Type == MetaJson {
80+
if !json.Valid(m.Data) {
81+
return ErrInvalidJSON
82+
}
83+
}
84+
6985
return nil
7086
}
7187

proof/meta_test.go

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,53 @@ func TestValidateMetaReveal(t *testing.T) {
1313
name string
1414
reveal *MetaReveal
1515
expectedErr error
16-
}{{
17-
name: "nil reveal",
18-
reveal: nil,
19-
expectedErr: nil,
20-
}, {
21-
name: "valid reveal",
22-
reveal: &MetaReveal{
23-
Type: MetaOpaque,
24-
Data: []byte("data"),
16+
}{
17+
{
18+
name: "nil reveal",
19+
reveal: nil,
20+
expectedErr: nil,
2521
},
26-
expectedErr: nil,
27-
}, {
28-
name: "missing data",
29-
reveal: &MetaReveal{
30-
Type: MetaOpaque,
31-
Data: nil,
22+
{
23+
name: "valid reveal",
24+
reveal: &MetaReveal{
25+
Type: MetaOpaque,
26+
Data: []byte("data"),
27+
},
28+
expectedErr: nil,
3229
},
33-
expectedErr: ErrMetaDataMissing,
34-
}, {
35-
name: "too much data",
36-
reveal: &MetaReveal{
37-
Type: MetaOpaque,
38-
Data: make([]byte, MetaDataMaxSizeBytes+1),
30+
{
31+
name: "missing data",
32+
reveal: &MetaReveal{
33+
Type: MetaOpaque,
34+
Data: nil,
35+
},
36+
expectedErr: ErrMetaDataMissing,
3937
},
40-
expectedErr: ErrMetaDataTooLarge,
41-
}}
38+
{
39+
name: "too much data",
40+
reveal: &MetaReveal{
41+
Type: MetaOpaque,
42+
Data: make([]byte, MetaDataMaxSizeBytes+1),
43+
},
44+
expectedErr: ErrMetaDataTooLarge,
45+
},
46+
{
47+
name: "invalid JSON",
48+
reveal: &MetaReveal{
49+
Type: MetaJson,
50+
Data: []byte("invalid"),
51+
},
52+
expectedErr: ErrInvalidJSON,
53+
},
54+
{
55+
name: "valid JSON",
56+
reveal: &MetaReveal{
57+
Type: MetaJson,
58+
Data: []byte(`{"key": "value"}`),
59+
},
60+
expectedErr: nil,
61+
},
62+
}
4263

4364
for _, tc := range testCases {
4465
tc := tc

rpcserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ func (r *rpcServer) MintAsset(ctx context.Context,
455455
// If the asset meta field was specified, then the data inside
456456
// must be valid. Let's check that now.
457457
if err := seedling.Meta.Validate(); err != nil {
458-
return nil, err
458+
return nil, fmt.Errorf("invalid asset meta: %v", err)
459459
}
460460
}
461461

taprpc/mintrpc/mint.swagger.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,11 @@
400400
"taprpcAssetMetaType": {
401401
"type": "string",
402402
"enum": [
403-
"META_TYPE_OPAQUE"
403+
"META_TYPE_OPAQUE",
404+
"META_TYPE_JSON"
404405
],
405406
"default": "META_TYPE_OPAQUE",
406-
"description": " - META_TYPE_OPAQUE: Opaque is used for asset meta blobs that have no true structure and instead\nshould be interpreted as opaque blobs."
407+
"description": " - META_TYPE_OPAQUE: Opaque is used for asset meta blobs that have no true structure and instead\nshould be interpreted as opaque blobs.\n - META_TYPE_JSON: JSON is used for asset meta blobs that are to be interpreted as valid JSON\nstrings."
407408
},
408409
"taprpcAssetType": {
409410
"type": "string",

0 commit comments

Comments
 (0)