Skip to content

Commit a62b68d

Browse files
authored
Merge pull request #1621 from lightninglabs/pedersen-keys-refactor
[group key addrs 5/6]: Add Pedersen unique script key type
2 parents e97827a + 7436356 commit a62b68d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1613
-246
lines changed

address/book.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ type Storage interface {
127127
AddrByTaprootOutput(ctx context.Context,
128128
key *btcec.PublicKey) (*AddrWithKeyInfo, error)
129129

130+
// AddrByScriptKeyAndVersion returns a single address based on its
131+
// script key and version or a sql.ErrNoRows error if no such address
132+
// exists.
133+
AddrByScriptKeyAndVersion(context.Context, *btcec.PublicKey,
134+
Version) (*AddrWithKeyInfo, error)
135+
130136
// SetAddrManaged sets an address as being managed by the internal
131137
// wallet.
132138
SetAddrManaged(ctx context.Context, addr *AddrWithKeyInfo,
@@ -504,7 +510,7 @@ func (b *Book) NewAddressWithKeys(ctx context.Context, addrVersion Version,
504510

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

509515
err = b.cfg.Store.InsertScriptKey(ctx, scriptKey, keyType)
510516
if err != nil {
@@ -603,6 +609,14 @@ func (b *Book) AddrByTaprootOutput(ctx context.Context,
603609
return b.cfg.Store.AddrByTaprootOutput(ctx, key)
604610
}
605611

612+
// AddrByScriptKeyAndVersion returns a single address based on its script key
613+
// and version or a sql.ErrNoRows error if no such address exists.
614+
func (b *Book) AddrByScriptKeyAndVersion(ctx context.Context,
615+
scriptKey *btcec.PublicKey, version Version) (*AddrWithKeyInfo, error) {
616+
617+
return b.cfg.Store.AddrByScriptKeyAndVersion(ctx, scriptKey, version)
618+
}
619+
606620
// SetAddrManaged sets an address as being managed by the internal
607621
// wallet.
608622
func (b *Book) SetAddrManaged(ctx context.Context, addr *AddrWithKeyInfo,

address/book_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,13 @@ type MockStorage struct {
175175
mock.Mock
176176
}
177177

178+
func (m *MockStorage) AddrByScriptKeyAndVersion(ctx context.Context,
179+
key *btcec.PublicKey, version Version) (*AddrWithKeyInfo, error) {
180+
181+
args := m.Called(ctx, key, version)
182+
return args.Get(0).(*AddrWithKeyInfo), args.Error(1)
183+
}
184+
178185
func (m *MockStorage) GetOrCreateEvent(ctx context.Context, status Status,
179186
addr *AddrWithKeyInfo, walletTx *lndclient.Transaction,
180187
outputIdx uint32) (*Event, error) {

asset/asset.go

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,85 @@ 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
160+
)
161+
162+
var (
163+
// AllScriptKeyTypes is a slice of all known script key types.
164+
AllScriptKeyTypes = []ScriptKeyType{
165+
ScriptKeyUnknown,
166+
ScriptKeyBip86,
167+
ScriptKeyScriptPathExternal,
168+
ScriptKeyBurn,
169+
ScriptKeyTombstone,
170+
ScriptKeyScriptPathChannel,
171+
ScriptKeyUniquePedersen,
172+
}
173+
174+
// ScriptKeyTypesNoChannel is a slice of all known script key types
175+
// that are not related to channels. This is used to filter out channel
176+
// related script keys when querying for assets that are not related to
177+
// channels.
178+
ScriptKeyTypesNoChannel = []ScriptKeyType{
179+
ScriptKeyUnknown,
180+
ScriptKeyBip86,
181+
ScriptKeyScriptPathExternal,
182+
ScriptKeyBurn,
183+
ScriptKeyTombstone,
184+
ScriptKeyUniquePedersen,
185+
}
153186
)
154187

188+
// ScriptKeyTypeForDatabaseQuery returns a slice of script key types that should
189+
// be used when querying the database for assets. The returned slice will either
190+
// contain all script key types or only those that are not related to channels,
191+
// depending on the `excludeChannelRelated` parameter. Unless the user specifies
192+
// a specific script key type, in which case the returned slice will only
193+
// contain that specific script key type.
194+
func ScriptKeyTypeForDatabaseQuery(excludeChannelRelated bool,
195+
userSpecified fn.Option[ScriptKeyType]) []ScriptKeyType {
196+
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+
216+
// For some queries, we want to get all the assets with all possible
217+
// script key types. For those, we use the full set of script key types.
218+
dbTypes := fn.CopySlice(AllScriptKeyTypes)
219+
220+
// For some RPCs (mostly balance related), we exclude the assets that
221+
// are specifically used for funding custom channels by default. The
222+
// balance of those assets is reported through lnd channel balance.
223+
// Those assets are identified by the specific script key type for
224+
// channel keys. We exclude them unless explicitly queried for.
225+
if excludeChannelRelated {
226+
dbTypes = fn.CopySlice(ScriptKeyTypesNoChannel)
227+
}
228+
229+
return dbTypes
230+
}
231+
155232
var (
156233
// ZeroPrevID is the blank prev ID used for genesis assets and also
157234
// asset split leaves.
@@ -392,7 +469,7 @@ func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier {
392469
}
393470
}
394471

395-
// NewExlusiveSpecifier creates a specifier that may only include one of asset
472+
// NewExclusiveSpecifier creates a specifier that may only include one of asset
396473
// ID or group key. If both are set then a specifier over the group key is
397474
// created.
398475
func NewExclusiveSpecifier(id *ID,
@@ -1043,6 +1120,65 @@ func EqualKeyDescriptors(a, o keychain.KeyDescriptor) bool {
10431120
return a.PubKey.IsEqual(o.PubKey)
10441121
}
10451122

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+
10461182
// TweakedScriptKey is an embedded struct which is primarily used by wallets to
10471183
// be able to keep track of the tweak of a script key alongside the raw key
10481184
// derivation information.
@@ -1142,14 +1278,16 @@ func (s *ScriptKey) HasScriptPath() bool {
11421278
}
11431279

11441280
// DetermineType attempts to determine the type of the script key based on the
1145-
// information available. This method will only return ScriptKeyUnknown if the
1146-
// following condition is met:
1281+
// information available. This method will only return ScriptKeyUnknown if one
1282+
// of the following conditions is met:
11471283
// - The script key doesn't have a script path, but the final Taproot output
11481284
// key doesn't match a BIP-0086 key derived from the internal key. This will
11491285
// be the case for "foreign" script keys we import from proofs, where we set
11501286
// the internal key to the same key as the tweaked script key (because we
11511287
// don't know the internal key, as it's not part of the proof encoding).
1152-
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 {
11531291
// If we have an explicit script key type set, we can return that.
11541292
if s.TweakedScriptKey != nil &&
11551293
s.TweakedScriptKey.Type != ScriptKeyUnknown {
@@ -1178,6 +1316,24 @@ func (s *ScriptKey) DetermineType() ScriptKeyType {
11781316
if bip86.PubKey.IsEqual(s.PubKey) {
11791317
return ScriptKeyBip86
11801318
}
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+
}
11811337
}
11821338

11831339
return ScriptKeyUnknown

authmailbox/client_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"net/url"
78
"os"
89
"testing"
910
"time"
@@ -236,6 +237,40 @@ func TestServerClientAuthAndRestart(t *testing.T) {
236237
client2.stop(t)
237238
})
238239

240+
// We also add a multi-subscription to the same two keys, so we can make
241+
// sure we can receive messages from multiple clients at once.
242+
multiSub := NewMultiSubscription(*clientCfg)
243+
err := multiSub.Subscribe(
244+
ctx, url.URL{Host: clientCfg.ServerAddress}, clientKey1, filter,
245+
)
246+
require.NoError(t, err)
247+
err = multiSub.Subscribe(
248+
ctx, url.URL{Host: clientCfg.ServerAddress}, clientKey2, filter,
249+
)
250+
require.NoError(t, err)
251+
t.Cleanup(func() {
252+
require.NoError(t, multiSub.Stop())
253+
})
254+
msgChan := multiSub.MessageChan()
255+
readMultiSub := func(targetID ...uint64) {
256+
t.Helper()
257+
select {
258+
case inboundMsgs := <-msgChan:
259+
receivedIDs := fn.Map(
260+
inboundMsgs.Messages,
261+
func(msg *mboxrpc.MailboxMessage) uint64 {
262+
return msg.MessageId
263+
},
264+
)
265+
for _, target := range targetID {
266+
require.Contains(t, receivedIDs, target)
267+
}
268+
case <-time.After(testTimeout):
269+
t.Fatalf("timeout waiting for message with ID %v",
270+
targetID)
271+
}
272+
}
273+
239274
// Send a message to all clients.
240275
msg1 := &Message{
241276
ID: 1000,
@@ -244,14 +279,15 @@ func TestServerClientAuthAndRestart(t *testing.T) {
244279
}
245280

246281
// We also store the message in the store, so we can retrieve it later.
247-
_, err := harness.mockMsgStore.StoreMessage(ctx, randOp, msg1)
282+
_, err = harness.mockMsgStore.StoreMessage(ctx, randOp, msg1)
248283
require.NoError(t, err)
249284

250285
harness.srv.publishMessage(msg1)
251286

252287
// We should be able to receive that message.
253288
client1.readMessages(t, msg1.ID)
254289
client2.readMessages(t, msg1.ID)
290+
readMultiSub(msg1.ID)
255291

256292
// We now stop the server and assert that the subscription is no longer
257293
// active.
@@ -282,6 +318,7 @@ func TestServerClientAuthAndRestart(t *testing.T) {
282318
// We should be able to receive that message.
283319
client1.readMessages(t, msg2.ID)
284320
client2.readMessages(t, msg2.ID)
321+
readMultiSub(msg2.ID)
285322

286323
// If we now start a third client, we should be able to receive all
287324
// three messages, given we are using the same key and specify the
@@ -314,6 +351,23 @@ func TestServerClientAuthAndRestart(t *testing.T) {
314351
harness.srv.publishMessage(msg3)
315352
client4.expectNoMessage(t)
316353
client1.readMessages(t, msg3.ID)
354+
client2.readMessages(t, msg3.ID)
355+
client3.readMessages(t, msg3.ID)
356+
readMultiSub(msg3.ID)
357+
358+
// Let's make sure that a message sent to the second key is only
359+
// received by the fourth client and the multi-subscription.
360+
msg4 := &Message{
361+
ID: 1001,
362+
ReceiverKey: *clientKey2.PubKey,
363+
ArrivalTimestamp: time.Now(),
364+
}
365+
harness.srv.publishMessage(msg4)
366+
client1.expectNoMessage(t)
367+
client2.expectNoMessage(t)
368+
client3.expectNoMessage(t)
369+
client4.readMessages(t, msg4.ID)
370+
readMultiSub(msg4.ID)
317371
}
318372

319373
// TestSendMessage tests the SendMessage RPC of the server and its ability to

authmailbox/mock.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,8 @@ func (s *MockMsgStore) QueryMessages(_ context.Context,
8787
}
8888

8989
func (s *MockMsgStore) NumMessages(context.Context) uint64 {
90+
s.mu.Lock()
91+
defer s.mu.Unlock()
92+
9093
return uint64(len(s.messages))
9194
}

0 commit comments

Comments
 (0)