@@ -150,6 +150,13 @@ const (
150150 // Keys related to channels are not shown in asset balances (unless
151151 // specifically requested) and are _never_ used for coin selection.
152152 ScriptKeyScriptPathChannel ScriptKeyType = 5
153+
154+ // ScriptKeyUniquePedersen is the script key type used for assets that
155+ // use a unique script key, tweaked with a Pedersen commitment key in a
156+ // single Tapscript leaf. This is used to avoid collisions in the
157+ // universe when there are multiple grouped asset UTXOs within the same
158+ // on-chain output.
159+ ScriptKeyUniquePedersen ScriptKeyType = 6
153160)
154161
155162var (
@@ -161,6 +168,7 @@ var (
161168 ScriptKeyBurn ,
162169 ScriptKeyTombstone ,
163170 ScriptKeyScriptPathChannel ,
171+ ScriptKeyUniquePedersen ,
164172 }
165173
166174 // ScriptKeyTypesNoChannel is a slice of all known script key types
@@ -173,18 +181,38 @@ var (
173181 ScriptKeyScriptPathExternal ,
174182 ScriptKeyBurn ,
175183 ScriptKeyTombstone ,
184+ ScriptKeyUniquePedersen ,
176185 }
177186)
178187
179188// ScriptKeyTypeForDatabaseQuery returns a slice of script key types that should
180189// be used when querying the database for assets. The returned slice will either
181190// contain all script key types or only those that are not related to channels,
182- // depending on the `filterChannelRelated ` parameter. Unless the user specifies
191+ // depending on the `excludeChannelRelated ` parameter. Unless the user specifies
183192// a specific script key type, in which case the returned slice will only
184193// contain that specific script key type.
185- func ScriptKeyTypeForDatabaseQuery (filterChannelRelated bool ,
194+ func ScriptKeyTypeForDatabaseQuery (excludeChannelRelated bool ,
186195 userSpecified fn.Option [ScriptKeyType ]) []ScriptKeyType {
187196
197+ // If the user specified a script key type, we use that directly to
198+ // filter the results.
199+ if userSpecified .IsSome () {
200+ specifiedType := userSpecified .UnwrapOr (ScriptKeyUnknown )
201+ dbTypes := []ScriptKeyType {
202+ specifiedType ,
203+ }
204+
205+ // If the user specifically requested BIP-86 script keys, we
206+ // also include the Pedersen unique script key type, because
207+ // those can be spent the same way as BIP-86 script keys, and
208+ // they should be treated the same way as BIP-86 script keys.
209+ if specifiedType == ScriptKeyBip86 {
210+ dbTypes = append (dbTypes , ScriptKeyUniquePedersen )
211+ }
212+
213+ return dbTypes
214+ }
215+
188216 // For some queries, we want to get all the assets with all possible
189217 // script key types. For those, we use the full set of script key types.
190218 dbTypes := fn .CopySlice (AllScriptKeyTypes )
@@ -194,16 +222,10 @@ func ScriptKeyTypeForDatabaseQuery(filterChannelRelated bool,
194222 // balance of those assets is reported through lnd channel balance.
195223 // Those assets are identified by the specific script key type for
196224 // channel keys. We exclude them unless explicitly queried for.
197- if filterChannelRelated {
225+ if excludeChannelRelated {
198226 dbTypes = fn .CopySlice (ScriptKeyTypesNoChannel )
199227 }
200228
201- // If the user specified a script key type, we use that to filter the
202- // results.
203- userSpecified .WhenSome (func (t ScriptKeyType ) {
204- dbTypes = []ScriptKeyType {t }
205- })
206-
207229 return dbTypes
208230}
209231
@@ -447,7 +469,7 @@ func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier {
447469 }
448470}
449471
450- // NewExlusiveSpecifier creates a specifier that may only include one of asset
472+ // NewExclusiveSpecifier creates a specifier that may only include one of asset
451473// ID or group key. If both are set then a specifier over the group key is
452474// created.
453475func NewExclusiveSpecifier (id * ID ,
@@ -1098,6 +1120,65 @@ func EqualKeyDescriptors(a, o keychain.KeyDescriptor) bool {
10981120 return a .PubKey .IsEqual (o .PubKey )
10991121}
11001122
1123+ // ScriptKeyDerivationMethod is the method used to derive the script key of an
1124+ // asset send output from the recipient's internal key and the asset ID of
1125+ // the output. This is used to ensure that the script keys are unique for each
1126+ // asset ID, so that proofs can be fetched from the universe without collisions.
1127+ type ScriptKeyDerivationMethod uint8
1128+
1129+ const (
1130+ // ScriptKeyDerivationUniquePedersen means the script key is derived
1131+ // using the address's recipient ID key and a single leaf that contains
1132+ // an un-spendable Pedersen commitment key
1133+ // (OP_CHECKSIG <NUMS_key + asset_id * G>). This can be used to
1134+ // create unique script keys for each virtual packet in the fragment,
1135+ // to avoid proof collisions in the universe, where the script keys
1136+ // should be spendable by a hardware wallet that only supports
1137+ // miniscript policies for signing P2TR outputs.
1138+ ScriptKeyDerivationUniquePedersen ScriptKeyDerivationMethod = 0
1139+ )
1140+
1141+ // DeriveUniqueScriptKey derives a unique script key for the given asset ID
1142+ // using the recipient's internal key and the specified derivation method.
1143+ func DeriveUniqueScriptKey (internalKey btcec.PublicKey , assetID ID ,
1144+ method ScriptKeyDerivationMethod ) (ScriptKey , error ) {
1145+
1146+ switch method {
1147+ // For the unique Pedersen method, we derive the script key using the
1148+ // internal key and the asset ID using a Pedersen commitment key in a
1149+ // single OP_CHECKSIG leaf.
1150+ case ScriptKeyDerivationUniquePedersen :
1151+ leaf , err := NewNonSpendableScriptLeaf (
1152+ PedersenVersion , assetID [:],
1153+ )
1154+ if err != nil {
1155+ return ScriptKey {}, fmt .Errorf ("unable to create " +
1156+ "non-spendable leaf: %w" , err )
1157+ }
1158+
1159+ rootHash := leaf .TapHash ()
1160+ scriptPubKey , _ := schnorr .ParsePubKey (schnorr .SerializePubKey (
1161+ txscript .ComputeTaprootOutputKey (
1162+ & internalKey , rootHash [:],
1163+ ),
1164+ ))
1165+ return ScriptKey {
1166+ PubKey : scriptPubKey ,
1167+ TweakedScriptKey : & TweakedScriptKey {
1168+ RawKey : keychain.KeyDescriptor {
1169+ PubKey : & internalKey ,
1170+ },
1171+ Tweak : rootHash [:],
1172+ Type : ScriptKeyUniquePedersen ,
1173+ },
1174+ }, nil
1175+
1176+ default :
1177+ return ScriptKey {}, fmt .Errorf ("unknown script key derivation " +
1178+ "method: %d" , method )
1179+ }
1180+ }
1181+
11011182// TweakedScriptKey is an embedded struct which is primarily used by wallets to
11021183// be able to keep track of the tweak of a script key alongside the raw key
11031184// derivation information.
@@ -1197,14 +1278,16 @@ func (s *ScriptKey) HasScriptPath() bool {
11971278}
11981279
11991280// DetermineType attempts to determine the type of the script key based on the
1200- // information available. This method will only return ScriptKeyUnknown if the
1201- // following condition is met:
1281+ // information available. This method will only return ScriptKeyUnknown if one
1282+ // of the following conditions is met:
12021283// - The script key doesn't have a script path, but the final Taproot output
12031284// key doesn't match a BIP-0086 key derived from the internal key. This will
12041285// be the case for "foreign" script keys we import from proofs, where we set
12051286// the internal key to the same key as the tweaked script key (because we
12061287// don't know the internal key, as it's not part of the proof encoding).
1207- func (s * ScriptKey ) DetermineType () ScriptKeyType {
1288+ // - No asset ID was provided (because it is unavailable in the given
1289+ // context), and the script key is a unique Pedersen-based key.
1290+ func (s * ScriptKey ) DetermineType (id * ID ) ScriptKeyType {
12081291 // If we have an explicit script key type set, we can return that.
12091292 if s .TweakedScriptKey != nil &&
12101293 s .TweakedScriptKey .Type != ScriptKeyUnknown {
@@ -1233,6 +1316,24 @@ func (s *ScriptKey) DetermineType() ScriptKeyType {
12331316 if bip86 .PubKey .IsEqual (s .PubKey ) {
12341317 return ScriptKeyBip86
12351318 }
1319+
1320+ // If we have the asset's ID, we can check whether this is a
1321+ // Pedersen-based key. If we don't have the ID, then we can't
1322+ // determine the type, so we'll end up in the default return
1323+ // below.
1324+ if id != nil {
1325+ scriptKey , err := DeriveUniqueScriptKey (
1326+ * s .TweakedScriptKey .RawKey .PubKey , * id ,
1327+ ScriptKeyDerivationUniquePedersen ,
1328+ )
1329+ if err != nil {
1330+ return ScriptKeyUnknown
1331+ }
1332+
1333+ if scriptKey .PubKey .IsEqual (s .PubKey ) {
1334+ return ScriptKeyUniquePedersen
1335+ }
1336+ }
12361337 }
12371338
12381339 return ScriptKeyUnknown
0 commit comments