Skip to content

Commit

Permalink
Merge pull request #518 from lightninglabs/asset-name
Browse files Browse the repository at this point in the history
Sanitise asset name
  • Loading branch information
Roasbeef authored Sep 22, 2023
2 parents 69b737a + 7f63ef5 commit 603078b
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 15 deletions.
52 changes: 51 additions & 1 deletion asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"fmt"
"io"
"reflect"
"strings"
"unicode"
"unicode/utf8"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
Expand All @@ -18,6 +21,13 @@ import (
"github.com/lightningnetwork/lnd/tlv"
)

const (
// MaxAssetNameLength is the maximum byte length of an asset's name.
// This byte length is equivalent to character count for single-byte
// UTF-8 characters.
MaxAssetNameLength = 64
)

// SerializedKey is a type for representing a public key, serialized in the
// compressed, 33-byte form.
type SerializedKey [33]byte
Expand Down Expand Up @@ -100,7 +110,7 @@ type Genesis struct {

// Tag is a human-readable identifier for the asset. This does not need
// to be unique, but asset issuers should attempt for it to be unique if
// possible.
// possible. Users usually recognise this field as the asset's name.
//
// NOTE: This is immutable for the lifetime of the asset.
Tag string
Expand Down Expand Up @@ -1045,3 +1055,43 @@ func (a *Asset) Leaf() (*mssmt.LeafNode, error) {
}
return mssmt.NewLeafNode(buf.Bytes(), a.Amount), nil
}

// Validate ensures that an asset is valid.
func (a *Asset) Validate() error {
// TODO(ffranr): Add validation check for remaining fields.
return ValidateAssetName(a.Genesis.Tag)
}

// ValidateAssetName validates an asset name (the asset's genesis tag).
func ValidateAssetName(name string) error {
if len(name) == 0 {
return fmt.Errorf("asset name cannot be empty")
}

// Ensure the asset name is not too long.
if len(name) > MaxAssetNameLength {
return fmt.Errorf("asset name cannot exceed %d bytes",
MaxAssetNameLength)
}

// Ensure the asset name is a valid UTF-8 string.
if !utf8.ValidString(name) {
return fmt.Errorf("asset name is not a valid UTF-8 string")
}

// Ensure each character is printable.
for _, char := range name {
if !unicode.IsPrint(char) {
hexValue := fmt.Sprintf("\\x%X", char)
return fmt.Errorf("asset name cannot contain "+
"unprintable character: %s", hexValue)
}
}

// Ensure the asset name does not contain only spaces.
if len(strings.TrimSpace(name)) == 0 {
return fmt.Errorf("asset name cannot contain only spaces")
}

return nil
}
80 changes: 80 additions & 0 deletions asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,86 @@ func TestGroupKeyIsEqual(t *testing.T) {
}
}

// TestValidateAssetName tests that asset names are validated correctly.
func TestValidateAssetName(t *testing.T) {
t.Parallel()

tests := []struct {
name string
valid bool
}{
{
// A name with spaces is valid.
name: "a name with spaces",
valid: true,
},
{
// Capital letters are valid.
name: "ABC",
valid: true,
},
{
// Numbers are valid.
name: "1234",
valid: true,
},
{
// A mix of lower/upper, spaces, and numbers is valid.
name: "Name 1234",
valid: true,
},
{
// Japanese characters are valid.
name: "日本語",
valid: true,
},
{
// The "place of interest" character takes up multiple
// bytes and is valid.
name: "⌘",
valid: true,
},
{
// Exclusively whitespace is an invalid name.
name: " ",
valid: false,
},
{
// An empty name string is invalid.
name: "",
valid: false,
},
{
// A 65 character name is too long and therefore
// invalid.
name: "asdasdasdasdasdasdasdasdasdasdasdasdasdasdas" +
"dasdasdasdadasdasdada",
valid: false,
},
{
// Invalid if tab in name.
name: "tab tab",
valid: false,
},
{
// Invalid if newline in name.
name: "newline\nnewline",
valid: false,
},
}

for _, testCase := range tests {
testCase := testCase

err := ValidateAssetName(testCase.name)
if testCase.valid {
require.NoError(t, err)
} else {
require.Error(t, err)
}
}
}

// TestAssetEncoding asserts that we can properly encode and decode assets
// through their TLV serialization.
func TestAssetEncoding(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions proof/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ type HeaderVerifier func(blockHeader wire.BlockHeader, blockHeight uint32) error
func (p *Proof) Verify(ctx context.Context, prev *AssetSnapshot,
headerVerifier HeaderVerifier) (*AssetSnapshot, error) {

// Ensure proof asset is valid.
if err := p.Asset.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate proof asset: "+
"%w", err)
}

// 1. A transaction that spends the previous asset output has a valid
// merkle proof within a block in the chain.
if prev != nil && p.PrevOut != prev.OutPoint {
Expand Down
6 changes: 3 additions & 3 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,9 @@ func (r *rpcServer) MintAsset(ctx context.Context,
return nil, fmt.Errorf("asset cannot be nil")
}

// An asset name is mandatory, and cannot be the empty string.
if len(req.Asset.Name) == 0 {
return nil, fmt.Errorf("asset name cannot be empty")
err := asset.ValidateAssetName(req.Asset.Name)
if err != nil {
return nil, fmt.Errorf("invalid asset name: %w", err)
}

specificGroupKey := len(req.Asset.GroupKey) != 0
Expand Down
17 changes: 6 additions & 11 deletions tapgarden/seedling.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ var (
// TODO(roasbeef): make proper error type struct?
ErrInvalidAssetType = fmt.Errorf("invalid asset type")

// ErrNoAssetName is returned if an asset request doesn't have a valid
// name.
ErrNoAssetName = fmt.Errorf("asset name cannot be blank")

// ErrInvalidAssetAmt is returned in an asset request has an invalid
// amount.
ErrInvalidAssetAmt = fmt.Errorf("asset amt cannot be zero")
Expand Down Expand Up @@ -104,6 +100,12 @@ type Seedling struct {
// NOTE: This function does not check the group key. That check is performed in
// the validateGroupKey method.
func (c Seedling) validateFields() error {
// Validate the asset name.
err := asset.ValidateAssetName(c.AssetName)
if err != nil {
return err
}

switch {
// Only normal and collectible asset types are supported.
//
Expand All @@ -112,13 +114,6 @@ func (c Seedling) validateFields() error {
return fmt.Errorf("%v: %v", int(c.AssetType),
ErrInvalidAssetType)

// The asset name can't be blank as that's needed to generate the asset
// ID.
//
// TODO(roasbeef): also bubble up to the spec?
case c.AssetName == "":
return ErrNoAssetName

// Creating an asset with zero available supply is not allowed.
case c.Amount == 0:
return ErrInvalidAssetAmt
Expand Down

0 comments on commit 603078b

Please sign in to comment.