diff --git a/address/address.go b/address/address.go index a4d80755c..b13787c3d 100644 --- a/address/address.go +++ b/address/address.go @@ -14,10 +14,10 @@ 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" + "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightningnetwork/lnd/tlv" ) @@ -538,47 +538,39 @@ 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, - ) +// 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 - // Convert the challenge to a scalar. - challenge.SetByteSlice(challengeBytes[:]) + case taprpc.AddrVersion_ADDR_VERSION_V0: + return V0, nil - // Calculate res = challenge * G. - secp256k1.ScalarMultNonConst(&challenge, &g, &res) + case taprpc.AddrVersion_ADDR_VERSION_V1: + return V1, nil - // Calculate res = nums + res. - secp256k1.AddNonConst(&nums, &res, &res) + default: + return 0, fmt.Errorf("unknown address version: %v", version) + } +} - res.ToAffine() +// 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 - resultPubKey := btcec.NewPublicKey(&res.X, &res.Y) + case V1: + return taprpc.AddrVersion_ADDR_VERSION_V1, nil - return asset.NewScriptKey(resultPubKey) + default: + return 0, fmt.Errorf("unknown address version: %v", version) + } } diff --git a/address/address_test.go b/address/address_test.go index 8b57aea52..38308c706 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/address/book.go b/address/book.go index 00ae6cf4d..10cc88ec3 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/asset/witness.go b/asset/witness.go index f7a7eb485..e16f50715 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 000000000..2c4a982a2 --- /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/meta.go b/proof/meta.go index d09509598..f7047ab8b 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/proof/verifier.go b/proof/verifier.go index 271e165ed..4485ab5c6 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, }} diff --git a/rfqmsg/custom_channel_data.go b/rfqmsg/custom_channel_data.go index 7b10fc891..f252f378c 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/rpcserver.go b/rpcserver.go index 00da168e4..58f68d275 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()) } @@ -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 } @@ -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,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.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) } - 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 diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index f2c27d60e..3a298501d 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 @@ -527,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. @@ -536,7 +541,7 @@ func (p *pendingAssetFunding) toAuxFundingDesc( // 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 @@ -1727,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 } @@ -1766,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 diff --git a/tapchannelmsg/custom_channel_data.go b/tapchannelmsg/custom_channel_data.go index 69fe2428e..8a930ca50 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 12964693d..118001b74 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 cdd647315..024ac356c 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), }, } diff --git a/tapdb/addrs.go b/tapdb/addrs.go index 538514b49..654821444 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 d38da9d7e..7e8e28e6f 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. diff --git a/tapfreighter/parcel.go b/tapfreighter/parcel.go index 72366754b..efe5f9157 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/taprpc/marshal.go b/taprpc/marshal.go index 8ad92df6d..7e36d9448 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 { diff --git a/tapsend/send.go b/tapsend/send.go index 773fdb823..f5e3ea2e9 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 {