From 15e7552d9f36ee9883f3dc1b2b1f4b81ff6c7075 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:43 +0100 Subject: [PATCH 1/8] multi: move GenChallengeNUMS To avoid a circular package dependency between the address and proof package, we move the GenChallengeNUMS function into the assets package. That function is the only reference the proof package has to the address package, so the move removes that dependency. --- address/address.go | 46 ---------------------------- address/address_test.go | 59 ------------------------------------ asset/witness.go | 51 ++++++++++++++++++++++++++++++- asset/witness_test.go | 66 +++++++++++++++++++++++++++++++++++++++++ proof/verifier.go | 3 +- 5 files changed, 117 insertions(+), 108 deletions(-) create mode 100644 asset/witness_test.go diff --git a/address/address.go b/address/address.go index a4d80755ce..37d73208de 100644 --- a/address/address.go +++ b/address/address.go @@ -14,7 +14,6 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -537,48 +536,3 @@ func DecodeAddress(addr string, net *ChainParams) (*Tap, error) { return &a, nil } - -// GenChallengeNUMS generates a variant of the NUMS script key that is modified -// by the provided challenge. -// -// The resulting scriptkey is: -// res := NUMS + challenge*G -func GenChallengeNUMS(challengeBytesOpt fn.Option[[32]byte]) asset.ScriptKey { - var ( - nums, g, res btcec.JacobianPoint - challenge secp256k1.ModNScalar - ) - - if challengeBytesOpt.IsNone() { - return asset.NUMSScriptKey - } - - var challengeBytes [32]byte - - challengeBytesOpt.WhenSome(func(b [32]byte) { - challengeBytes = b - }) - - // Convert the NUMS key to a Jacobian point. - asset.NUMSPubKey.AsJacobian(&nums) - - // Multiply G by 1 to get G as a Jacobian point. - secp256k1.ScalarBaseMultNonConst( - new(secp256k1.ModNScalar).SetInt(1), &g, - ) - - // Convert the challenge to a scalar. - challenge.SetByteSlice(challengeBytes[:]) - - // Calculate res = challenge * G. - secp256k1.ScalarMultNonConst(&challenge, &g, &res) - - // Calculate res = nums + res. - secp256k1.AddNonConst(&nums, &res, &res) - - res.ToAffine() - - resultPubKey := btcec.NewPublicKey(&res.X, &res.Y) - - return asset.NewScriptKey(resultPubKey) -} diff --git a/address/address_test.go b/address/address_test.go index 8b57aea523..38308c7061 100644 --- a/address/address_test.go +++ b/address/address_test.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -502,64 +501,6 @@ func TestBIPTestVectors(t *testing.T) { } } -// TestGenChallengeNUMS tests the generation of NUMS challenges. -func TestGenChallengeNUMS(t *testing.T) { - t.Parallel() - - gx, gy := secp256k1.Params().Gx, secp256k1.Params().Gy - - // addG is a helper function that adds G to the given public key. - addG := func(p *btcec.PublicKey) *btcec.PublicKey { - x, y := secp256k1.S256().Add(p.X(), p.Y(), gx, gy) - var xFieldVal, yFieldVal secp256k1.FieldVal - xFieldVal.SetByteSlice(x.Bytes()) - yFieldVal.SetByteSlice(y.Bytes()) - return btcec.NewPublicKey(&xFieldVal, &yFieldVal) - } - - testCases := []struct { - name string - challenge fn.Option[[32]byte] - expectedKey asset.ScriptKey - }{ - { - name: "no challenge", - challenge: fn.None[[32]byte](), - expectedKey: asset.NUMSScriptKey, - }, - { - name: "challenge is scalar 1", - challenge: fn.Some([32]byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - }), - expectedKey: asset.NewScriptKey(addG(asset.NUMSPubKey)), - }, - { - name: "challenge is scalar 2", - challenge: fn.Some([32]byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - }), - expectedKey: asset.NewScriptKey( - addG(addG(asset.NUMSPubKey)), - ), - }, - } - - for _, tc := range testCases { - result := GenChallengeNUMS(tc.challenge) - require.Equal( - t, tc.expectedKey.PubKey.SerializeCompressed(), - result.PubKey.SerializeCompressed(), - ) - } -} - // runBIPTestVector runs the tests in a single BIP test vector file. func runBIPTestVector(t *testing.T, testVectors *TestVectors) { for _, validCase := range testVectors.ValidTestCases { diff --git a/asset/witness.go b/asset/witness.go index f7a7eb485c..e16f50715e 100644 --- a/asset/witness.go +++ b/asset/witness.go @@ -1,6 +1,10 @@ package asset -import "github.com/btcsuite/btcd/btcec/v2" +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightninglabs/taproot-assets/fn" +) // IsSplitCommitWitness returns true if the witness is a split-commitment // witness. @@ -34,3 +38,48 @@ func IsBurnKey(scriptKey *btcec.PublicKey, witness Witness) bool { return scriptKey.IsEqual(DeriveBurnKey(prevID)) } + +// GenChallengeNUMS generates a variant of the NUMS script key that is modified +// by the provided challenge. +// +// The resulting scriptkey is: +// res := NUMS + challenge*G +func GenChallengeNUMS(challengeBytesOpt fn.Option[[32]byte]) ScriptKey { + var ( + nums, g, res btcec.JacobianPoint + challenge secp256k1.ModNScalar + ) + + if challengeBytesOpt.IsNone() { + return NUMSScriptKey + } + + var challengeBytes [32]byte + + challengeBytesOpt.WhenSome(func(b [32]byte) { + challengeBytes = b + }) + + // Convert the NUMS key to a Jacobian point. + NUMSPubKey.AsJacobian(&nums) + + // Multiply G by 1 to get G as a Jacobian point. + secp256k1.ScalarBaseMultNonConst( + new(secp256k1.ModNScalar).SetInt(1), &g, + ) + + // Convert the challenge to a scalar. + challenge.SetByteSlice(challengeBytes[:]) + + // Calculate res = challenge * G. + secp256k1.ScalarMultNonConst(&challenge, &g, &res) + + // Calculate res = nums + res. + secp256k1.AddNonConst(&nums, &res, &res) + + res.ToAffine() + + resultPubKey := btcec.NewPublicKey(&res.X, &res.Y) + + return NewScriptKey(resultPubKey) +} diff --git a/asset/witness_test.go b/asset/witness_test.go new file mode 100644 index 0000000000..2c4a982a2b --- /dev/null +++ b/asset/witness_test.go @@ -0,0 +1,66 @@ +package asset + +import ( + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/stretchr/testify/require" +) + +// TestGenChallengeNUMS tests the generation of NUMS challenges. +func TestGenChallengeNUMS(t *testing.T) { + t.Parallel() + + gx, gy := secp256k1.Params().Gx, secp256k1.Params().Gy + + // addG is a helper function that adds G to the given public key. + addG := func(p *btcec.PublicKey) *btcec.PublicKey { + x, y := secp256k1.S256().Add(p.X(), p.Y(), gx, gy) + var xFieldVal, yFieldVal secp256k1.FieldVal + xFieldVal.SetByteSlice(x.Bytes()) + yFieldVal.SetByteSlice(y.Bytes()) + return btcec.NewPublicKey(&xFieldVal, &yFieldVal) + } + + testCases := []struct { + name string + challenge fn.Option[[32]byte] + expectedKey ScriptKey + }{ + { + name: "no challenge", + challenge: fn.None[[32]byte](), + expectedKey: NUMSScriptKey, + }, + { + name: "challenge is scalar 1", + challenge: fn.Some([32]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }), + expectedKey: NewScriptKey(addG(NUMSPubKey)), + }, + { + name: "challenge is scalar 2", + challenge: fn.Some([32]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + }), + expectedKey: NewScriptKey(addG(addG(NUMSPubKey))), + }, + } + + for _, tc := range testCases { + result := GenChallengeNUMS(tc.challenge) + require.Equal( + t, tc.expectedKey.PubKey.SerializeCompressed(), + result.PubKey.SerializeCompressed(), + ) + } +} diff --git a/proof/verifier.go b/proof/verifier.go index 271e165ed4..4485ab5c68 100644 --- a/proof/verifier.go +++ b/proof/verifier.go @@ -13,7 +13,6 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" - "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -385,7 +384,7 @@ func CreateOwnershipProofAsset(ownedAsset *asset.Asset, // This is handled by CopySpendTemplate. outputAsset := ownedAsset.CopySpendTemplate() - outputAsset.ScriptKey = address.GenChallengeNUMS(challengeBytes) + outputAsset.ScriptKey = asset.GenChallengeNUMS(challengeBytes) outputAsset.PrevWitnesses = []asset.Witness{{ PrevID: &prevId, }} From bb035cf404c62e3166c51b4e5caa847781d920b8 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:44 +0100 Subject: [PATCH 2/8] multi: move address marshal/unmarshal functions To resolve another circular dependency issue in an upcoming commit, this time between the address and taprpc package, we move two more functions to another place where they fit better. In general, any RPC package should _not_ have a dependency into other packages, so this was wrong in the first place. --- address/address.go | 38 ++++++++++++++++++++++++++++++++++++++ rpcserver.go | 4 ++-- taprpc/marshal.go | 39 --------------------------------------- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/address/address.go b/address/address.go index 37d73208de..b13787c3dd 100644 --- a/address/address.go +++ b/address/address.go @@ -17,6 +17,7 @@ import ( "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightningnetwork/lnd/tlv" ) @@ -536,3 +537,40 @@ func DecodeAddress(addr string, net *ChainParams) (*Tap, error) { return &a, nil } + +// UnmarshalVersion parses an address version from the RPC variant. +func UnmarshalVersion(version taprpc.AddrVersion) (Version, error) { + // For now, we'll only support two address versions. The ones in the + // future should be reserved for future use, so we disallow unknown + // versions. + switch version { + case taprpc.AddrVersion_ADDR_VERSION_UNSPECIFIED: + return V1, nil + + case taprpc.AddrVersion_ADDR_VERSION_V0: + return V0, nil + + case taprpc.AddrVersion_ADDR_VERSION_V1: + return V1, nil + + default: + return 0, fmt.Errorf("unknown address version: %v", version) + } +} + +// MarshalVersion marshals the native address version into the RPC variant. +func MarshalVersion(version Version) (taprpc.AddrVersion, error) { + // For now, we'll only support two address versions. The ones in the + // future should be reserved for future use, so we disallow unknown + // versions. + switch version { + case V0: + return taprpc.AddrVersion_ADDR_VERSION_V0, nil + + case V1: + return taprpc.AddrVersion_ADDR_VERSION_V1, nil + + default: + return 0, fmt.Errorf("unknown address version: %v", version) + } +} diff --git a/rpcserver.go b/rpcserver.go index 00da168e4e..183e4a63d4 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1461,7 +1461,7 @@ func (r *rpcServer) NewAddr(ctx context.Context, return nil, err } - addrVersion, err := taprpc.UnmarshalAddressVersion(req.AddressVersion) + addrVersion, err := address.UnmarshalVersion(req.AddressVersion) if err != nil { return nil, err } @@ -3027,7 +3027,7 @@ func marshalAddr(addr *address.Tap, return nil, err } - addrVersion, err := taprpc.MarshalAddressVersion(addr.Version) + addrVersion, err := address.MarshalVersion(addr.Version) if err != nil { return nil, err } diff --git a/taprpc/marshal.go b/taprpc/marshal.go index 8ad92df6df..7e36d9448c 100644 --- a/taprpc/marshal.go +++ b/taprpc/marshal.go @@ -11,7 +11,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" - "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" @@ -165,44 +164,6 @@ func MarshalAssetVersion(version asset.Version) (AssetVersion, error) { } } -// UnmarshalAddressVerion parses an address version from the RPC variant. -func UnmarshalAddressVersion(version AddrVersion) (address.Version, error) { - // For now we'll only support two address versions. The ones in the - // future should be reserved for future use, so we disallow unknown - // versions. - switch version { - case AddrVersion_ADDR_VERSION_UNSPECIFIED: - return address.V1, nil - - case AddrVersion_ADDR_VERSION_V0: - return address.V0, nil - - case AddrVersion_ADDR_VERSION_V1: - return address.V1, nil - - default: - return 0, fmt.Errorf("unknown address version: %v", version) - } -} - -// MarshalAddressVerion marshals the native address version into the RPC -// variant. -func MarshalAddressVersion(version address.Version) (AddrVersion, error) { - // For now we'll only support two address versions. The ones in the - // future should be reserved for future use, so we disallow unknown - // versions. - switch version { - case address.V0: - return AddrVersion_ADDR_VERSION_V0, nil - - case address.V1: - return AddrVersion_ADDR_VERSION_V1, nil - - default: - return 0, fmt.Errorf("unknown address version: %v", version) - } -} - // MarshalGenesisInfo marshals the native asset genesis into the RPC // counterpart. func MarshalGenesisInfo(gen *asset.Genesis, assetType asset.Type) *GenesisInfo { From bbcab76e633774d9305abb76244fff762b2454c3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:45 +0100 Subject: [PATCH 3/8] proof+rpcserver: move DecDisplayOption to proof pkg We'll want to re-use the DecDisplayOption method in other places, so we move it from the RPC server to the meta reveal struct itself. --- proof/meta.go | 27 +++++++++++++++++++++++++++ rpcserver.go | 36 +++--------------------------------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/proof/meta.go b/proof/meta.go index d09509598c..f7047ab8ba 100644 --- a/proof/meta.go +++ b/proof/meta.go @@ -11,6 +11,7 @@ import ( "math" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/tlv" "golang.org/x/exp/constraints" ) @@ -277,6 +278,32 @@ func (m *MetaReveal) GetDecDisplay() (map[string]interface{}, uint32, error) { } } +// DecDisplayOption attempts to decode a decimal display value from metadata. If +// no custom decimal display value is decoded, an empty option is returned +// without error. +func (m *MetaReveal) DecDisplayOption() (fn.Option[uint32], error) { + _, decDisplay, err := m.GetDecDisplay() + switch { + // If it isn't JSON, or doesn't have a dec display, we'll just return 0 + // below. + case errors.Is(err, ErrNotJSON): + fallthrough + case errors.Is(err, ErrInvalidJSON): + fallthrough + case errors.Is(err, ErrDecDisplayMissing): + fallthrough + case errors.Is(err, ErrDecDisplayInvalidType): + // We can't determine if there is a decimal display value set. + return fn.None[uint32](), nil + + case err != nil: + return fn.None[uint32](), fmt.Errorf("unable to extract "+ + "decimal display: %v", err) + } + + return fn.Some(decDisplay), nil +} + // SetDecDisplay attempts to set the decimal display value in existing JSON // metadata. It checks that the new metadata is below the maximum metadata size. func (m *MetaReveal) SetDecDisplay(decDisplay uint32) (*MetaReveal, error) { diff --git a/rpcserver.go b/rpcserver.go index 183e4a63d4..7dc6d89a22 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1061,7 +1061,7 @@ func (r *rpcServer) MarshalChainAsset(ctx context.Context, a *asset.ChainAsset, // the database when decoding a decimal display value. switch { case meta != nil: - decDisplay, err = getDecimalDisplayNonStrict(meta) + decDisplay, err = meta.DecDisplayOption() default: decDisplay, err = r.DecDisplayForAssetID(ctx, a.ID()) } @@ -7603,43 +7603,13 @@ func encodeVirtualPackets(packets []*tappsbt.VPacket) ([][]byte, error) { func (r *rpcServer) DecDisplayForAssetID(ctx context.Context, id asset.ID) (fn.Option[uint32], error) { - meta, err := r.cfg.AssetStore.FetchAssetMetaForAsset( - ctx, id, - ) + meta, err := r.cfg.AssetStore.FetchAssetMetaForAsset(ctx, id) if err != nil { return fn.None[uint32](), fmt.Errorf("unable to fetch asset "+ "meta for asset_id=%v :%v", id, err) } - return getDecimalDisplayNonStrict(meta) -} - -// getDecimalDisplayNonStrict attempts to decode a decimal display value from -// metadata. If no custom decimal display value is decoded, the default value of -// 0 is returned without error. -func getDecimalDisplayNonStrict( - meta *proof.MetaReveal) (fn.Option[uint32], error) { - - _, decDisplay, err := meta.GetDecDisplay() - switch { - // If it isn't JSON, or doesn't have a dec display, we'll just return 0 - // below. - case errors.Is(err, proof.ErrNotJSON): - fallthrough - case errors.Is(err, proof.ErrInvalidJSON): - fallthrough - case errors.Is(err, proof.ErrDecDisplayMissing): - fallthrough - case errors.Is(err, proof.ErrDecDisplayInvalidType): - // We can't determine if there is a decimal display value set. - return fn.None[uint32](), nil - - case err != nil: - return fn.None[uint32](), fmt.Errorf("unable to extract "+ - "decimal display: %v", err) - } - - return fn.Some(decDisplay), nil + return meta.DecDisplayOption() } // rfqChannel returns the channel to use for RFQ operations. If a peer public From 9fe32aa78cd58426531d18317de8bc6b78262aaf Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:46 +0100 Subject: [PATCH 4/8] multi: move asset meta retrieval to addr book To make it possible that we can sync an asset when querying for the asset meta information, we move the two meta related queries to the address book. That way we can use the sync enabled asset meta fetch in a later commit in the channel funding controller. --- address/book.go | 68 +++++++++++++++++++++++++++++++++-- rpcserver.go | 10 +++--- tapdb/addrs.go | 73 ++++++++++++++++++++++++++++++++++++- tapdb/assets_store.go | 84 +++---------------------------------------- 4 files changed, 148 insertions(+), 87 deletions(-) diff --git a/address/book.go b/address/book.go index 00ae6cf4da..10cc88ec3c 100644 --- a/address/book.go +++ b/address/book.go @@ -15,6 +15,7 @@ import ( "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/proof" "github.com/lightningnetwork/lnd/keychain" ) @@ -23,6 +24,10 @@ var ( // This means an address can't be created until a Universe bootstrap or // manual issuance proof insertion. ErrAssetGroupUnknown = fmt.Errorf("asset group is unknown") + + // ErrAssetMetaNotFound is returned when an asset meta is not found in + // the database. + ErrAssetMetaNotFound = fmt.Errorf("asset meta not found") ) // AddrWithKeyInfo wraps a normal Taproot Asset struct with key descriptor @@ -100,6 +105,16 @@ type Storage interface { // (genesis + group key) associated with a given asset. QueryAssetGroup(context.Context, asset.ID) (*asset.AssetGroup, error) + // FetchAssetMetaByHash attempts to fetch an asset meta based on an + // asset hash. + FetchAssetMetaByHash(ctx context.Context, + metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) + + // FetchAssetMetaForAsset attempts to fetch an asset meta based on an + // asset ID. + FetchAssetMetaForAsset(ctx context.Context, + assetID asset.ID) (*proof.MetaReveal, error) + // AddrByTaprootOutput returns a single address based on its Taproot // output key or a sql.ErrNoRows error if no such address exists. AddrByTaprootOutput(ctx context.Context, @@ -218,7 +233,7 @@ func (b *Book) QueryAssetInfo(ctx context.Context, return nil, err } - log.Debugf("asset %v is unknown, attempting to bootstrap", id.String()) + log.Debugf("Asset %v is unknown, attempting to bootstrap", id.String()) // Use the AssetSyncer to query our universe federation for the asset. err = b.cfg.Syncer.SyncAssetInfo(ctx, &id) @@ -233,7 +248,7 @@ func (b *Book) QueryAssetInfo(ctx context.Context, return nil, err } - log.Debugf("bootstrap succeeded for asset %v", id.String()) + log.Debugf("Bootstrap succeeded for asset %v", id.String()) // If the asset was found after sync, and has an asset group, update our // universe sync config to ensure that we sync future issuance proofs. @@ -253,6 +268,55 @@ func (b *Book) QueryAssetInfo(ctx context.Context, return assetGroup, nil } +// FetchAssetMetaByHash attempts to fetch an asset meta based on an asset hash. +func (b *Book) FetchAssetMetaByHash(ctx context.Context, + metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { + + return b.cfg.Store.FetchAssetMetaByHash(ctx, metaHash) +} + +// FetchAssetMetaForAsset attempts to fetch an asset meta based on an asset ID. +func (b *Book) FetchAssetMetaForAsset(ctx context.Context, + assetID asset.ID) (*proof.MetaReveal, error) { + + // Check if we know of this meta hash already. + meta, err := b.cfg.Store.FetchAssetMetaForAsset(ctx, assetID) + switch { + case meta != nil: + return meta, nil + + // Asset lookup failed gracefully; continue to asset lookup using the + // AssetSyncer if enabled. + case errors.Is(err, ErrAssetMetaNotFound): + if b.cfg.Syncer == nil { + return nil, ErrAssetMetaNotFound + } + + case err != nil: + return nil, err + } + + log.Debugf("Asset %v is unknown, attempting to bootstrap", + assetID.String()) + + // Use the AssetSyncer to query our universe federation for the asset. + err = b.cfg.Syncer.SyncAssetInfo(ctx, &assetID) + if err != nil { + return nil, err + } + + // The asset meta info may have been synced from a universe server; + // query for the asset ID again. + meta, err = b.cfg.Store.FetchAssetMetaForAsset(ctx, assetID) + if err != nil { + return nil, err + } + + log.Debugf("Bootstrap succeeded for asset %v", assetID.String()) + + return meta, nil +} + // NewAddress creates a new Taproot Asset address based on the input parameters. func (b *Book) NewAddress(ctx context.Context, addrVersion Version, assetID asset.ID, amount uint64, diff --git a/rpcserver.go b/rpcserver.go index 7dc6d89a22..58f68d2750 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -4407,7 +4407,7 @@ func (r *rpcServer) FetchAssetMeta(ctx context.Context, var assetID asset.ID copy(assetID[:], req.GetAssetId()) - assetMeta, err = r.cfg.AssetStore.FetchAssetMetaForAsset( + assetMeta, err = r.cfg.AddrBook.FetchAssetMetaForAsset( ctx, assetID, ) @@ -4426,7 +4426,7 @@ func (r *rpcServer) FetchAssetMeta(ctx context.Context, var assetID asset.ID copy(assetID[:], assetIDBytes) - assetMeta, err = r.cfg.AssetStore.FetchAssetMetaForAsset( + assetMeta, err = r.cfg.AddrBook.FetchAssetMetaForAsset( ctx, assetID, ) @@ -4438,7 +4438,7 @@ func (r *rpcServer) FetchAssetMeta(ctx context.Context, var metaHash [asset.MetaHashLen]byte copy(metaHash[:], req.GetMetaHash()) - assetMeta, err = r.cfg.AssetStore.FetchAssetMetaByHash( + assetMeta, err = r.cfg.AddrBook.FetchAssetMetaByHash( ctx, metaHash, ) @@ -4457,7 +4457,7 @@ func (r *rpcServer) FetchAssetMeta(ctx context.Context, var metaHash [asset.MetaHashLen]byte copy(metaHash[:], metaHashBytes) - assetMeta, err = r.cfg.AssetStore.FetchAssetMetaByHash( + assetMeta, err = r.cfg.AddrBook.FetchAssetMetaByHash( ctx, metaHash, ) @@ -7603,7 +7603,7 @@ func encodeVirtualPackets(packets []*tappsbt.VPacket) ([][]byte, error) { func (r *rpcServer) DecDisplayForAssetID(ctx context.Context, id asset.ID) (fn.Option[uint32], error) { - meta, err := r.cfg.AssetStore.FetchAssetMetaForAsset(ctx, id) + meta, err := r.cfg.AddrBook.FetchAssetMetaForAsset(ctx, id) if err != nil { return fn.None[uint32](), fmt.Errorf("unable to fetch asset "+ "meta for asset_id=%v :%v", id, err) diff --git a/tapdb/addrs.go b/tapdb/addrs.go index 538514b496..6548214449 100644 --- a/tapdb/addrs.go +++ b/tapdb/addrs.go @@ -77,6 +77,9 @@ type ( // KeyLocator is a type alias for fetching the key locator information // for an internal key. KeyLocator = sqlc.FetchInternalKeyLocatorRow + + // AssetMeta is the metadata record for an asset. + AssetMeta = sqlc.FetchAssetMetaForAssetRow ) // AddrBook is an interface that represents the storage backed needed to create @@ -160,6 +163,14 @@ type AddrBook interface { // FetchInternalKeyLocator fetches the key locator for an internal key. FetchInternalKeyLocator(ctx context.Context, rawKey []byte) (KeyLocator, error) + + // FetchAssetMetaByHash fetches the asset meta for a given meta hash. + FetchAssetMetaByHash(ctx context.Context, + metaDataHash []byte) (sqlc.FetchAssetMetaByHashRow, error) + + // FetchAssetMetaForAsset fetches the asset meta for a given asset. + FetchAssetMetaForAsset(ctx context.Context, + assetID []byte) (AssetMeta, error) } // AddrBookTxOptions defines the set of db txn options the AddrBook @@ -1104,8 +1115,68 @@ func (t *TapAddressBook) QueryAssetGroup(ctx context.Context, return &assetGroup, nil } +// FetchAssetMetaByHash attempts to fetch an asset meta based on an asset hash. +func (t *TapAddressBook) FetchAssetMetaByHash(ctx context.Context, + metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { + + var assetMeta *proof.MetaReveal + + readOpts := NewAssetStoreReadTx() + dbErr := t.db.ExecTx(ctx, &readOpts, func(q AddrBook) error { + dbMeta, err := q.FetchAssetMetaByHash(ctx, metaHash[:]) + if err != nil { + return err + } + + assetMeta = &proof.MetaReveal{ + Data: dbMeta.MetaDataBlob, + Type: proof.MetaType(dbMeta.MetaDataType.Int16), + } + + return nil + }) + switch { + case errors.Is(dbErr, sql.ErrNoRows): + return nil, address.ErrAssetMetaNotFound + case dbErr != nil: + return nil, dbErr + } + + return assetMeta, nil +} + +// FetchAssetMetaForAsset attempts to fetch an asset meta based on an asset ID. +func (t *TapAddressBook) FetchAssetMetaForAsset(ctx context.Context, + assetID asset.ID) (*proof.MetaReveal, error) { + + var assetMeta *proof.MetaReveal + + readOpts := NewAssetStoreReadTx() + dbErr := t.db.ExecTx(ctx, &readOpts, func(q AddrBook) error { + dbMeta, err := q.FetchAssetMetaForAsset(ctx, assetID[:]) + if err != nil { + return err + } + + assetMeta = &proof.MetaReveal{ + Data: dbMeta.MetaDataBlob, + Type: proof.MetaType(dbMeta.MetaDataType.Int16), + } + + return nil + }) + switch { + case errors.Is(dbErr, sql.ErrNoRows): + return nil, address.ErrAssetMetaNotFound + case dbErr != nil: + return nil, dbErr + } + + return assetMeta, nil +} + // insertFullAssetGen inserts a new asset genesis and optional asset group -// into the database. A place holder for the asset meta inserted as well. +// into the database. A placeholder for the asset meta inserted as well. func insertFullAssetGen(ctx context.Context, gen *asset.Genesis, group *asset.GroupKey) func(AddrBook) error { diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index d38da9d7e9..7e8e28e6ff 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -216,7 +216,7 @@ type ActiveAssetsStore interface { // disk. FetchAssetProofs(ctx context.Context) ([]AssetProof, error) - // FetchAssetsProofsSizes fetches all the asset proofs lengths that are + // FetchAssetProofsSizes fetches all the asset proofs lengths that are // stored on disk. FetchAssetProofsSizes(ctx context.Context) ([]AssetProofSize, error) @@ -353,16 +353,6 @@ type ActiveAssetsStore interface { // the passed params. ReAnchorPassiveAssets(ctx context.Context, arg ReAnchorParams) error - // FetchAssetMetaByHash fetches the asset meta for a given meta hash. - // - // TODO(roasbeef): split into MetaStore? - FetchAssetMetaByHash(ctx context.Context, - metaDataHash []byte) (sqlc.FetchAssetMetaByHashRow, error) - - // FetchAssetMetaForAsset fetches the asset meta for a given asset. - FetchAssetMetaForAsset(ctx context.Context, - assetID []byte) (sqlc.FetchAssetMetaForAssetRow, error) - // InsertBurn inserts a new row to the asset burns table which // includes all important data related to the burn. InsertBurn(ctx context.Context, arg sqlc.InsertBurnParams) (int64, @@ -376,12 +366,12 @@ type ActiveAssetsStore interface { // MetaStore is a sub-set of the main sqlc.Querier interface that contains // methods related to metadata of the daemon. type MetaStore interface { - // AssetsDBSize returns the total size of the taproot assets sqlite - // database. + // AssetsDBSizeSqlite returns the total size of the taproot assets + // sqlite database. AssetsDBSizeSqlite(ctx context.Context) (int32, error) - // AssetsDBSize returns the total size of the taproot assets postgres - // database. + // AssetsDBSizePostgres returns the total size of the taproot assets + // postgres database. AssetsDBSizePostgres(ctx context.Context) (int64, error) } @@ -3419,40 +3409,6 @@ func (a *AssetStore) QueryParcels(ctx context.Context, return outboundParcels, nil } -// ErrAssetMetaNotFound is returned when an asset meta is not found in the -// database. -var ErrAssetMetaNotFound = fmt.Errorf("asset meta not found") - -// FetchAssetMetaForAsset attempts to fetch an asset meta based on an asset ID. -func (a *AssetStore) FetchAssetMetaForAsset(ctx context.Context, - assetID asset.ID) (*proof.MetaReveal, error) { - - var assetMeta *proof.MetaReveal - - readOpts := NewAssetStoreReadTx() - dbErr := a.db.ExecTx(ctx, &readOpts, func(q ActiveAssetsStore) error { - dbMeta, err := q.FetchAssetMetaForAsset(ctx, assetID[:]) - if err != nil { - return err - } - - assetMeta = &proof.MetaReveal{ - Data: dbMeta.MetaDataBlob, - Type: proof.MetaType(dbMeta.MetaDataType.Int16), - } - - return nil - }) - switch { - case errors.Is(dbErr, sql.ErrNoRows): - return nil, ErrAssetMetaNotFound - case dbErr != nil: - return nil, dbErr - } - - return assetMeta, nil -} - // AssetsDBSize returns the total size of the taproot assets database. func (a *AssetStore) AssetsDBSize(ctx context.Context) (int64, error) { var totalSize int64 @@ -3492,36 +3448,6 @@ func (a *AssetStore) AssetsDBSize(ctx context.Context) (int64, error) { return totalSize, nil } -// FetchAssetMetaByHash attempts to fetch an asset meta based on an asset hash. -func (a *AssetStore) FetchAssetMetaByHash(ctx context.Context, - metaHash [asset.MetaHashLen]byte) (*proof.MetaReveal, error) { - - var assetMeta *proof.MetaReveal - - readOpts := NewAssetStoreReadTx() - dbErr := a.db.ExecTx(ctx, &readOpts, func(q ActiveAssetsStore) error { - dbMeta, err := q.FetchAssetMetaByHash(ctx, metaHash[:]) - if err != nil { - return err - } - - assetMeta = &proof.MetaReveal{ - Data: dbMeta.MetaDataBlob, - Type: proof.MetaType(dbMeta.MetaDataType.Int16), - } - - return nil - }) - switch { - case errors.Is(dbErr, sql.ErrNoRows): - return nil, ErrAssetMetaNotFound - case dbErr != nil: - return nil, dbErr - } - - return assetMeta, nil -} - // TxHeight returns the block height of a given transaction. This will only // return the height if the transaction is known to the store, which is only // the case for assets relevant to this node. From 0be95a6b6a26ad2853620d2041f7925a85274ab9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:48 +0100 Subject: [PATCH 5/8] tapchannel: add FetchAssetMetaForAsset to AssetSyncer interface With the address book now implementing that method, we can add it to our AssetSyncer interface so we can query asset meta information during channel funding. --- tapchannel/aux_funding_controller.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index f2c27d60ed..cecc38d0ab 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -183,6 +183,11 @@ type AssetSyncer interface { // for issuance proofs. QueryAssetInfo(ctx context.Context, id asset.ID) (*asset.AssetGroup, error) + + // FetchAssetMetaForAsset attempts to fetch an asset meta based on an + // asset ID. + FetchAssetMetaForAsset(ctx context.Context, + assetID asset.ID) (*proof.MetaReveal, error) } // FundingControllerCfg is a configuration struct that houses the necessary From 7869f871efd5c15e4e30f165f4508f785f4802aa Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:49 +0100 Subject: [PATCH 6/8] tapfreighter+tapsend: check input uniqueness In this commit, we check for input asset uniqueness for parcels in the PreBroadcast state, before being converted to a Transfer. This prevents invalid transfers from being published and logged via RPC. --- tapfreighter/parcel.go | 5 +++++ tapsend/send.go | 32 +++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/tapfreighter/parcel.go b/tapfreighter/parcel.go index 72366754b7..efe5f9157e 100644 --- a/tapfreighter/parcel.go +++ b/tapfreighter/parcel.go @@ -529,6 +529,11 @@ func ConvertToTransfer(currentHeight uint32, activeTransfers []*tappsbt.VPacket, PassiveAssetsAnchor: passiveAssetAnchor, } + allPackets := append(activeTransfers, passiveAssets...) + if err := tapsend.AssertInputsUnique(allPackets); err != nil { + return nil, fmt.Errorf("unable to convert to transfer: %w", err) + } + for pIdx := range activeTransfers { vPkt := activeTransfers[pIdx] diff --git a/tapsend/send.go b/tapsend/send.go index 773fdb823f..f5e3ea2e92 100644 --- a/tapsend/send.go +++ b/tapsend/send.go @@ -27,6 +27,7 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/tapscript" + lfn "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "golang.org/x/exp/maps" @@ -1030,9 +1031,10 @@ func addKeyTweaks(unknowns []*psbt.Unknown, desc *lndclient.SignDescriptor) { func CreateOutputCommitments( packets []*tappsbt.VPacket) (tappsbt.OutputCommitments, error) { - // We create an empty output commitment map, keyed by the anchor output - // index. - outputCommitments := make(tappsbt.OutputCommitments) + // Inputs must be unique. + if err := AssertInputsUnique(packets); err != nil { + return nil, err + } // We require all outputs that reference the same anchor output to be // identical, otherwise some assumptions in the code below don't hold. @@ -1052,6 +1054,10 @@ func CreateOutputCommitments( return nil, err } + // We create an empty output commitment map, keyed by the anchor output + // index. + outputCommitments := make(tappsbt.OutputCommitments) + // And now we commit each packet to the respective anchor output // commitments. for _, vPkt := range packets { @@ -1544,6 +1550,26 @@ func AssertInputAnchorsEqual(packets []*tappsbt.VPacket) error { return nil } +// AssertInputsUnique makes sure that every input across all virtual packets is +// referencing a unique input asset, which is identified by the input PrevID. +func AssertInputsUnique(packets []*tappsbt.VPacket) error { + // PrevIDs are comparable enough to serve as a map key without hashing. + inputs := make(lfn.Set[asset.PrevID]) + + for _, vPkt := range packets { + for _, vIn := range vPkt.Inputs { + if inputs.Contains(vIn.PrevID) { + return fmt.Errorf("input %v is duplicated", + vIn.PrevID) + } + + inputs.Add(vIn.PrevID) + } + } + + return nil +} + // ExtractUnSpendable extracts all tombstones and burns from the active input // commitment. func ExtractUnSpendable(c *commitment.TapCommitment) []*asset.Asset { From 63e7997ecae6a4125a58765a771747741f53f4c5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:50 +0100 Subject: [PATCH 7/8] tapchannel: determine decimal display of funding assets --- tapchannel/aux_funding_controller.go | 81 ++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index cecc38d0ab..ec23347233 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -532,8 +532,8 @@ func newCommitBlobAndLeaves(pendingFunding *pendingAssetFunding, // desc. This is the final step in the modified funding process, as after this, // both sides are able to construct the funding output, and will be able to // store the appropriate funding blobs. -func (p *pendingAssetFunding) toAuxFundingDesc( - req *bindFundingReq) (*lnwallet.AuxFundingDesc, error) { +func (p *pendingAssetFunding) toAuxFundingDesc(req *bindFundingReq, + decimalDisplay uint8) (*lnwallet.AuxFundingDesc, error) { // First, we'll map all the assets into asset outputs that'll be stored // in the open channel struct on the lnd side. @@ -1732,13 +1732,28 @@ func (f *FundingController) chanFunder() { continue } - fundingDesc, err := fundingFlow.toAuxFundingDesc(req) + // We'll want to store the decimal display of the asset + // in the funding blob, so let's determine it now. + decimalDisplay, err := f.fundingAssetDecimalDisplay( + ctxc, fundingFlow.assetOutputs(), + ) + if err != nil { + fErr := fmt.Errorf("unable to determine "+ + "decimal display: %w", err) + f.cfg.ErrReporter.ReportError( + ctxc, fundingFlow.peerPub, pid, fErr, + ) + continue + } + + fundingDesc, err := fundingFlow.toAuxFundingDesc( + req, decimalDisplay, + ) if err != nil { fErr := fmt.Errorf("unable to create aux "+ "funding desc: %w", err) f.cfg.ErrReporter.ReportError( - ctxc, fundingFlow.peerPub, pid, - fErr, + ctxc, fundingFlow.peerPub, pid, fErr, ) continue } @@ -1771,6 +1786,62 @@ func (f *FundingController) chanFunder() { } } +// fundingAssetDecimalDisplay determines the decimal display of the funding +// asset(s). If no specific decimal display value was chosen for the asset, then +// the default value of 0 is returned. +func (f *FundingController) fundingAssetDecimalDisplay(ctx context.Context, + assetOutputs []*cmsg.AssetOutput) (uint8, error) { + + // We now check the decimal display of each funding asset, to make sure + // we know the meta information for each asset. And we also verify that + // each asset tranche has the same decimal display (which should've been + // verified during the minting process already). + var decimalDisplay uint8 + for idx, a := range assetOutputs { + meta, err := f.cfg.AssetSyncer.FetchAssetMetaForAsset( + ctx, a.AssetID.Val, + ) + if err != nil { + return 0, fmt.Errorf("unable to fetch asset meta: %w", + err) + } + + decDisplayOpt, err := meta.DecDisplayOption() + if err != nil { + return 0, fmt.Errorf("unable to get decimal display "+ + "option: %w", err) + } + + var thisAssetDecDisplay uint8 + decDisplayOpt.WhenSome(func(decDisplay uint32) { + // We limit the decimal display value to a maximum of + // 12, so it should easily fit into an uint8. + thisAssetDecDisplay = uint8(decDisplay) + }) + + // If this is the first asset we're looking at, we just use the + // decimal display. Every other asset should have the same + // decimal display. The value of 0 is a valid decimal display, + // and we use that if the meta information didn't contain a + // specific decimal display value, assuming it's either a + // non-JSON meta information or the value just wasn't set. + if idx == 0 { + decimalDisplay = thisAssetDecDisplay + continue + } + + // Make sure every subsequent asset has the same decimal display + // as the first asset. + if decimalDisplay != thisAssetDecDisplay { + return 0, fmt.Errorf("decimal display mismatch: "+ + "expected %v, got %v", decimalDisplay, + thisAssetDecDisplay) + } + } + + return decimalDisplay, nil +} + // channelAcceptor is a callback that's called by the lnd client when a new // channel is proposed. This function is responsible for deciding whether to // accept the channel based on the channel parameters, and to also set some From 565ee3cf8d22900e71442aa8b9e653aa2b15c20a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 6 Dec 2024 12:17:51 +0100 Subject: [PATCH 8/8] multi: add decimal display to OpenChannel struct --- rfqmsg/custom_channel_data.go | 9 +++++---- tapchannel/aux_funding_controller.go | 2 +- tapchannelmsg/custom_channel_data.go | 1 + tapchannelmsg/records.go | 15 ++++++++++++++- tapchannelmsg/records_test.go | 4 ++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/rfqmsg/custom_channel_data.go b/rfqmsg/custom_channel_data.go index 7b10fc891a..f252f378cf 100644 --- a/rfqmsg/custom_channel_data.go +++ b/rfqmsg/custom_channel_data.go @@ -21,10 +21,11 @@ type JsonAssetGenesis struct { // JsonAssetUtxo is a struct that represents the UTXO information of an asset // within a channel. type JsonAssetUtxo struct { - Version int64 `json:"version"` - AssetGenesis JsonAssetGenesis `json:"asset_genesis"` - Amount uint64 `json:"amount"` - ScriptKey string `json:"script_key"` + Version int64 `json:"version"` + AssetGenesis JsonAssetGenesis `json:"asset_genesis"` + Amount uint64 `json:"amount"` + ScriptKey string `json:"script_key"` + DecimalDisplay uint8 `json:"decimal_display"` } // JsonAssetChanInfo is a struct that represents the channel information of a diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index ec23347233..3a298501d2 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -541,7 +541,7 @@ func (p *pendingAssetFunding) toAuxFundingDesc(req *bindFundingReq, // With all the outputs assembled, we'll now map that to the open // channel wrapper that'll go in the set of TLV blobs. - openChanDesc := cmsg.NewOpenChannel(assetOutputs) + openChanDesc := cmsg.NewOpenChannel(assetOutputs, decimalDisplay) // Now we'll encode the 3 TLV blobs that lnd will store: the main one // for the funding details, and then the blobs for the local and remote diff --git a/tapchannelmsg/custom_channel_data.go b/tapchannelmsg/custom_channel_data.go index 69fe2428ec..8a930ca503 100644 --- a/tapchannelmsg/custom_channel_data.go +++ b/tapchannelmsg/custom_channel_data.go @@ -81,6 +81,7 @@ func (c *ChannelCustomData) AsJson() ([]byte, error) { ScriptKey: hex.EncodeToString( a.ScriptKey.PubKey.SerializeCompressed(), ), + DecimalDisplay: c.OpenChan.DecimalDisplay.Val, } resp.Assets = append(resp.Assets, rfqmsg.JsonAssetChanInfo{ AssetInfo: utxo, diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index 12964693d4..118001b743 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -86,16 +86,28 @@ type OpenChannel struct { // FundedAssets is a list of asset outputs that was committed to the // funding output of a commitment. FundedAssets tlv.RecordT[tlv.TlvType0, AssetOutputListRecord] + + // DecimalDisplay is the asset's unit precision. We place this value on + // the channel directly and not into each funding asset balance struct + // since even for a channel with multiple tranches of fungible assets, + // this value needs to be the same for all assets. Otherwise, they would + // not be fungible. + DecimalDisplay tlv.RecordT[tlv.TlvType1, uint8] } // NewOpenChannel creates a new OpenChannel record with the given funded assets. -func NewOpenChannel(fundedAssets []*AssetOutput) *OpenChannel { +func NewOpenChannel(fundedAssets []*AssetOutput, + decimalDisplay uint8) *OpenChannel { + return &OpenChannel{ FundedAssets: tlv.NewRecordT[tlv.TlvType0]( AssetOutputListRecord{ Outputs: fundedAssets, }, ), + DecimalDisplay: tlv.NewPrimitiveRecord[tlv.TlvType1]( + decimalDisplay, + ), } } @@ -109,6 +121,7 @@ func (o *OpenChannel) Assets() []*AssetOutput { func (o *OpenChannel) records() []tlv.Record { return []tlv.Record{ o.FundedAssets.Record(), + o.DecimalDisplay.Record(), } } diff --git a/tapchannelmsg/records_test.go b/tapchannelmsg/records_test.go index cdd6473158..024ac356c0 100644 --- a/tapchannelmsg/records_test.go +++ b/tapchannelmsg/records_test.go @@ -76,14 +76,14 @@ func TestOpenChannel(t *testing.T) { name: "channel with funded asset", channel: NewOpenChannel([]*AssetOutput{ NewAssetOutput([32]byte{1}, 1000, *randProof), - }), + }, 0), }, { name: "channel with multiple funded assets", channel: NewOpenChannel([]*AssetOutput{ NewAssetOutput([32]byte{1}, 1000, *randProof), NewAssetOutput([32]byte{2}, 2000, *randProof), - }), + }, 11), }, }