Skip to content

Commit 6e03ce4

Browse files
committed
asset: add new script key type for pedersen unique keys
1 parent 90ef28a commit 6e03ce4

File tree

13 files changed

+337
-34
lines changed

13 files changed

+337
-34
lines changed

address/book.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ func (b *Book) NewAddressWithKeys(ctx context.Context, addrVersion Version,
510510

511511
// We might not know the type of script key, if it was given to us
512512
// through an RPC call. So we make a guess here.
513-
keyType := scriptKey.DetermineType()
513+
keyType := scriptKey.DetermineType(fn.Ptr(assetGroup.Genesis.ID()))
514514

515515
err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, keyType)
516516
if err != nil {

asset/asset.go

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

155162
var (
@@ -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.
453475
func 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

itest/script_key_type_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package itest
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/lightninglabs/taproot-assets/asset"
8+
"github.com/lightninglabs/taproot-assets/rpcutils"
9+
"github.com/lightninglabs/taproot-assets/tappsbt"
10+
"github.com/lightninglabs/taproot-assets/taprpc"
11+
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
12+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// testScriptKeyTypePedersenUnique tests that we can declare a script key with
17+
// the Pedersen unique tweak type, which is used for assets that are sent using
18+
// the future address V2 scheme.
19+
func testScriptKeyTypePedersenUnique(t *harnessTest) {
20+
ctxb := context.Background()
21+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
22+
defer cancel()
23+
24+
rpcAssets := MintAssetsConfirmBatch(
25+
t.t, t.lndHarness.Miner().Client, t.tapd,
26+
[]*mintrpc.MintAssetRequest{
27+
simpleAssets[0],
28+
// Our "passive" asset.
29+
{
30+
Asset: &mintrpc.MintAsset{
31+
AssetType: taprpc.AssetType_NORMAL,
32+
Name: "itestbuxx-passive",
33+
AssetMeta: &taprpc.AssetMeta{
34+
Data: []byte("some metadata"),
35+
},
36+
Amount: 123,
37+
},
38+
},
39+
},
40+
)
41+
activeAsset := rpcAssets[0]
42+
passiveAsset := rpcAssets[1]
43+
44+
var (
45+
activeID, passiveID asset.ID
46+
)
47+
copy(activeID[:], activeAsset.AssetGenesis.AssetId)
48+
copy(passiveID[:], passiveAsset.AssetGenesis.AssetId)
49+
50+
// We need to derive two sets of keys, one for the new script key and
51+
// one for the internal key each.
52+
activeScriptKey, activeAnchorIntKeyDesc1 := DeriveKeys(t.t, t.tapd)
53+
activeScriptKey = declarePedersenUniqueScriptKey(
54+
t.t, t.tapd, activeScriptKey, activeID,
55+
)
56+
passiveScriptKey, _ := DeriveKeys(t.t, t.tapd)
57+
passiveScriptKey = declarePedersenUniqueScriptKey(
58+
t.t, t.tapd, passiveScriptKey, passiveID,
59+
)
60+
61+
// We create the output at anchor index 0 for the first address.
62+
outputAmounts := []uint64{300, 4700, 123}
63+
vPkt := tappsbt.ForInteractiveSend(
64+
activeID, outputAmounts[1], activeScriptKey, 0, 0, 0,
65+
activeAnchorIntKeyDesc1, asset.V0, chainParams,
66+
)
67+
68+
// We now fund the packet, so we get the passive assets as well.
69+
fundResp := fundPacket(t, t.tapd, vPkt)
70+
require.Len(t.t, fundResp.PassiveAssetPsbts, 1)
71+
72+
// We now replace the script key of the passive packet with the Pedersen
73+
// key that we declared above, then sign the packet.
74+
passiveAssetPkt, err := tappsbt.Decode(fundResp.PassiveAssetPsbts[0])
75+
require.NoError(t.t, err)
76+
require.Len(t.t, passiveAssetPkt.Outputs, 1)
77+
78+
passiveAssetPkt.Outputs[0].ScriptKey = passiveScriptKey
79+
if passiveAssetPkt.Outputs[0].Asset != nil {
80+
passiveAssetPkt.Outputs[0].Asset.ScriptKey = passiveScriptKey
81+
}
82+
passiveAssetPkt = signVirtualPacket(t.t, t.tapd, passiveAssetPkt)
83+
84+
// We now also sign the active asset packet.
85+
activeAssetPkt, err := tappsbt.Decode(fundResp.FundedPsbt)
86+
require.NoError(t.t, err)
87+
88+
activeAssetPkt = signVirtualPacket(t.t, t.tapd, activeAssetPkt)
89+
90+
activeBytes, err := tappsbt.Encode(activeAssetPkt)
91+
require.NoError(t.t, err)
92+
passiveBytes, err := tappsbt.Encode(passiveAssetPkt)
93+
require.NoError(t.t, err)
94+
95+
// Now we'll attempt to complete the transfer.
96+
sendResp, err := t.tapd.AnchorVirtualPsbts(
97+
ctxt, &wrpc.AnchorVirtualPsbtsRequest{
98+
VirtualPsbts: [][]byte{
99+
activeBytes,
100+
passiveBytes,
101+
},
102+
},
103+
)
104+
require.NoError(t.t, err)
105+
106+
ConfirmAndAssertOutboundTransferWithOutputs(
107+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp, activeID[:],
108+
outputAmounts, 0, 1, 3,
109+
)
110+
111+
AssertBalances(
112+
t.t, t.tapd, 4700, WithAssetID(activeID[:]), WithNumUtxos(1),
113+
WithScriptKeyType(asset.ScriptKeyUniquePedersen),
114+
)
115+
AssertBalances(
116+
t.t, t.tapd, 5000, WithAssetID(activeID[:]), WithNumUtxos(2),
117+
WithScriptKeyType(asset.ScriptKeyBip86),
118+
)
119+
AssertBalances(
120+
t.t, t.tapd, 123, WithAssetID(passiveID[:]), WithNumUtxos(1),
121+
WithScriptKeyType(asset.ScriptKeyUniquePedersen),
122+
)
123+
124+
aliceAssets, err := t.tapd.ListAssets(ctxb, &taprpc.ListAssetRequest{})
125+
require.NoError(t.t, err)
126+
127+
assetsJSON, err := formatProtoJSON(aliceAssets)
128+
require.NoError(t.t, err)
129+
t.Logf("Got assets: %s", assetsJSON)
130+
131+
// We should now be able to spend all the outputs, the Pedersen keys
132+
// should be signed correctly both in the active and passive assets.
133+
sendAssetAndAssert(
134+
ctxt, t, t.tapd, t.tapd, 4900, 100, activeAsset.AssetGenesis,
135+
activeAsset, 1, 2, 1,
136+
)
137+
}
138+
139+
func declarePedersenUniqueScriptKey(t *testing.T, node tapClient,
140+
sk asset.ScriptKey, assetID asset.ID) asset.ScriptKey {
141+
142+
pedersenKey, err := asset.DeriveUniqueScriptKey(
143+
*sk.RawKey.PubKey, assetID,
144+
asset.ScriptKeyDerivationUniquePedersen,
145+
)
146+
require.NoError(t, err)
147+
148+
// We need to let the wallet of Bob know that we're going to use a
149+
// script key with a custom root.
150+
ctxt, cancel := context.WithTimeout(
151+
context.Background(), defaultTimeout,
152+
)
153+
defer cancel()
154+
155+
_, err = node.DeclareScriptKey(ctxt, &wrpc.DeclareScriptKeyRequest{
156+
ScriptKey: rpcutils.MarshalScriptKey(pedersenKey),
157+
})
158+
require.NoError(t, err)
159+
160+
return pedersenKey
161+
}

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@ var testCases = []*testCase{
351351
name: "auth mailbox message store and fetch",
352352
test: testAuthMailboxStoreAndFetchMessage,
353353
},
354+
{
355+
name: "script key type pedersen unique",
356+
test: testScriptKeyTypePedersenUnique,
357+
},
354358
}
355359

356360
var optionalTestCases = []*testCase{

rpcserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8508,7 +8508,7 @@ func (r *rpcServer) DeclareScriptKey(ctx context.Context,
85088508
// 100% sure of the type, if it wasn't declared. But we can make a
85098509
// best-effort guess based on the fields the user has set. This is a
85108510
// no-op if the type is already set.
8511-
scriptKey.Type = scriptKey.DetermineType()
8511+
scriptKey.Type = scriptKey.DetermineType(nil)
85128512

85138513
// The user is declaring the key, so they should know what type it is.
85148514
// So if they didn't set it, and it wasn't an obvious one, we'll require

0 commit comments

Comments
 (0)