Skip to content

Commit 6c68c9f

Browse files
committed
multi: add universe commitment fields to meta
1 parent 98f9acb commit 6c68c9f

File tree

4 files changed

+626
-4
lines changed

4 files changed

+626
-4
lines changed

proof/encoding.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ 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"
1214
"github.com/lightninglabs/taproot-assets/fn"
@@ -500,3 +502,143 @@ func DUint32Option(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
500502
}
501503
return tlv.NewTypeForDecodingErr(val, "*fn.Option[uint32]", l, l)
502504
}
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+
}

proof/meta.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"fmt"
99
"io"
1010
"math"
11+
"net/url"
1112

13+
"github.com/btcsuite/btcd/btcec/v2"
1214
"github.com/lightninglabs/taproot-assets/asset"
1315
"github.com/lightninglabs/taproot-assets/fn"
1416
"github.com/lightningnetwork/lnd/tlv"
@@ -46,6 +48,14 @@ const (
4648
// define when minting assets. Since the uint64 max value has 19 decimal
4749
// places we will allow for a max of 12 decimal places.
4850
MaxDecDisplay = uint32(12)
51+
52+
// MaxNumCanonicalUniverseURLs is the maximum number of canonical
53+
// universe URLs that can be set.
54+
MaxNumCanonicalUniverseURLs = 16
55+
56+
// MaxCanonicalUniverseURLLength is the maximum length of the canonical
57+
// universe URL.
58+
MaxCanonicalUniverseURLLength = 255
4959
)
5060

5161
var (
@@ -86,6 +96,35 @@ var (
8696
// ErrDecDisplayMissing is returned if the decimal display key is
8797
// not present in a JSON object.
8898
ErrDecDisplayMissing = errors.New("decimal display field missing")
99+
100+
// ErrCanonicalUniverseInvalid is returned if the canonical universe
101+
// URL is invalid.
102+
ErrCanonicalUniverseInvalid = errors.New(
103+
"canonical universe URL invalid",
104+
)
105+
106+
// ErrTooManyCanonicalUniverseURLs is returned if the number of
107+
// canonical universe URLs exceeds the maximum.
108+
ErrTooManyCanonicalUniverseURLs = fmt.Errorf(
109+
"too many canonical universe URLs, max %d",
110+
MaxNumCanonicalUniverseURLs,
111+
)
112+
113+
// ErrCanonicalUniverseURLTooLong is returned if the canonical universe
114+
// URL is too long.
115+
ErrCanonicalUniverseURLTooLong = fmt.Errorf(
116+
"canonical universe URL too long, max %d characters",
117+
MaxCanonicalUniverseURLLength,
118+
)
119+
120+
// ErrDelegationKeyEmpty is returned if the delegation key is empty.
121+
ErrDelegationKeyEmpty = errors.New("delegation key is empty")
122+
123+
// ErrDelegationKeyNotOnCurve is returned if the delegation key is not
124+
// on the curve.
125+
ErrDelegationKeyNotOnCurve = errors.New(
126+
"delegation key is not on curve",
127+
)
89128
)
90129

91130
// MetaReveal is an optional TLV type that can be added to the proof of a
@@ -110,6 +149,22 @@ type MetaReveal struct {
110149
// field to the JSON object for backward compatibility.
111150
DecimalDisplay fn.Option[uint32]
112151

152+
// UniverseCommitments indicates that the asset group this asset belongs
153+
// to will create and push universe commitments to the canonical
154+
// universe. A universe commitment is a "proof of inventory" that
155+
// commits the issuer's current total asset balance (sum of all mints
156+
// minus burns or ignored assets) on-chain.
157+
UniverseCommitments bool
158+
159+
// CanonicalUniverses is a list of URLs of the canonical (approved,
160+
// authoritative) universe where the asset minting and universe
161+
// commitment proofs will be pushed to.
162+
CanonicalUniverses fn.Option[[]url.URL]
163+
164+
// DelegationKey is the public key that is used to verify universe
165+
// commitment related on-chain outputs and proofs.
166+
DelegationKey fn.Option[btcec.PublicKey]
167+
113168
// UnknownOddTypes is a map of unknown odd types that were encountered
114169
// during decoding. This map is used to preserve unknown types that we
115170
// don't know of yet, so we can still encode them back when serializing.
@@ -152,7 +207,44 @@ func (m *MetaReveal) Validate() error {
152207
}
153208

154209
// If the decimal display is set, it must be valid.
155-
return fn.MapOptionZ(m.DecimalDisplay, IsValidDecDisplay)
210+
err = fn.MapOptionZ(m.DecimalDisplay, IsValidDecDisplay)
211+
if err != nil {
212+
return err
213+
}
214+
215+
err = fn.MapOptionZ(m.CanonicalUniverses, func(urls []url.URL) error {
216+
// If the option is set, the slice must not be empty.
217+
if len(urls) == 0 {
218+
return ErrCanonicalUniverseInvalid
219+
}
220+
221+
if len(urls) > MaxNumCanonicalUniverseURLs {
222+
return ErrTooManyCanonicalUniverseURLs
223+
}
224+
225+
for _, u := range urls {
226+
if len(u.String()) > MaxCanonicalUniverseURLLength {
227+
return ErrCanonicalUniverseURLTooLong
228+
}
229+
}
230+
231+
return nil
232+
})
233+
if err != nil {
234+
return err
235+
}
236+
237+
return fn.MapOptionZ(m.DelegationKey, func(key btcec.PublicKey) error {
238+
if key == emptyKey {
239+
return ErrDelegationKeyEmpty
240+
}
241+
242+
if !key.IsOnCurve() {
243+
return ErrDelegationKeyNotOnCurve
244+
}
245+
246+
return nil
247+
})
156248
}
157249

158250
// IsValidMetaType checks if the passed value is a valid meta type.
@@ -385,6 +477,15 @@ func (m *MetaReveal) EncodeRecords() []tlv.Record {
385477
MetaRevealDataRecord(&m.Data),
386478
}
387479

480+
// In order not to change the encoding of existing records if
481+
// we de-serialize and re-serialize them, we only encode this boolean
482+
// value if it's actually true.
483+
if m.UniverseCommitments {
484+
records = append(records, MetaRevealUniverseCommitmentsRecord(
485+
&m.UniverseCommitments,
486+
))
487+
}
488+
388489
// To make sure we don't re-encode old assets that don't have a decimal
389490
// display value as a TLV field with a different value, we only encode
390491
// the decimal display value if it is explicitly set.
@@ -394,6 +495,18 @@ func (m *MetaReveal) EncodeRecords() []tlv.Record {
394495
))
395496
}
396497

498+
if m.CanonicalUniverses.IsSome() {
499+
records = append(records, MetaRevealCanonicalUniversesRecord(
500+
&m.CanonicalUniverses,
501+
))
502+
}
503+
504+
if m.DelegationKey.IsSome() {
505+
records = append(records, MetaRevealDelegationKeyRecord(
506+
&m.DelegationKey,
507+
))
508+
}
509+
397510
// Add any unknown odd types that were encountered during decoding.
398511
return asset.CombineRecords(records, m.UnknownOddTypes)
399512
}
@@ -404,6 +517,9 @@ func (m *MetaReveal) DecodeRecords() []tlv.Record {
404517
MetaRevealTypeRecord(&m.Type),
405518
MetaRevealDataRecord(&m.Data),
406519
MetaRevealDecimalDisplayRecord(&m.DecimalDisplay),
520+
MetaRevealUniverseCommitmentsRecord(&m.UniverseCommitments),
521+
MetaRevealCanonicalUniversesRecord(&m.CanonicalUniverses),
522+
MetaRevealDelegationKeyRecord(&m.DelegationKey),
407523
}
408524
}
409525

0 commit comments

Comments
 (0)