Skip to content

Commit e337256

Browse files
Returns KeyPathInformation.Index and reuse queries (#501)
1 parent e5eaf76 commit e337256

File tree

8 files changed

+193
-122
lines changed

8 files changed

+193
-122
lines changed

NBXplorer.Client/Models/KeyPathInformation.cs

+2-19
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,6 @@ namespace NBXplorer.Models
66
{
77
public class KeyPathInformation
88
{
9-
public KeyPathInformation()
10-
{
11-
12-
}
13-
14-
public KeyPathInformation(Derivation derivation, DerivationSchemeTrackedSource derivationStrategy, DerivationFeature feature, KeyPath keyPath, NBXplorerNetwork network)
15-
{
16-
ScriptPubKey = derivation.ScriptPubKey;
17-
Redeem = derivation.Redeem;
18-
TrackedSource = derivationStrategy;
19-
DerivationStrategy = derivationStrategy.DerivationStrategy;
20-
Feature = feature;
21-
KeyPath = keyPath;
22-
Address = network.CreateAddress(derivationStrategy.DerivationStrategy, keyPath, ScriptPubKey);
23-
}
249
public TrackedSource TrackedSource { get; set; }
2510
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
2611
public DerivationFeature Feature
@@ -48,9 +33,7 @@ public Script Redeem
4833
{
4934
get; set;
5035
}
51-
public int GetIndex(KeyPathTemplates keyPathTemplates)
52-
{
53-
return (int)keyPathTemplates.GetKeyPathTemplate(Feature).GetIndex(KeyPath);
54-
}
36+
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
37+
public int? Index { get; set; }
5538
}
5639
}

NBXplorer.Tests/UnitTest1.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -2643,7 +2643,7 @@ public async Task CanReserveAddress()
26432643
await Task.WhenAll(tasks.ToArray());
26442644

26452645
var paths = tasks.Select(t => t.Result).ToDictionary(c => c.KeyPath);
2646-
Assert.Equal(99, paths.Select(p => p.Value.GetIndex(KeyPathTemplates.Default)).Max());
2646+
Assert.Equal(99, paths.Select(p => p.Value.Index).Max());
26472647

26482648
tester.Client.CancelReservation(bob, new[] { new KeyPath("0/0") });
26492649
var addr = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit);
@@ -2658,6 +2658,7 @@ public async Task CanReserveAddress()
26582658
tester.Client.CancelReservation(bob, new[] { new KeyPath("0/0") });
26592659
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit);
26602660
Assert.Equal(new KeyPath("0/100"), addr2.KeyPath);
2661+
Assert.Equal(100, addr2.Index);
26612662
}
26622663
}
26632664

@@ -2854,12 +2855,15 @@ public async Task CanTrack5()
28542855

28552856
Logs.Tester.LogInformation("Let's check if direct addresses can be tracked by sending to 0");
28562857
var address = await tester.Client.GetUnusedAsync(pubkey, DerivationFeature.Direct);
2858+
Assert.Equal(0, address.Index);
28572859
Assert.Equal(DerivationFeature.Direct, address.Feature);
28582860
fundingTx = tester.SendToAddress(tester.AddressOf(key, "0"), Money.Coins(1.0m));
28592861
tester.Notifications.WaitForTransaction(pubkey, fundingTx);
28602862
utxo = tester.Client.GetUTXOs(pubkey);
28612863
Assert.Equal(address.ScriptPubKey, utxo.Unconfirmed.UTXOs[0].ScriptPubKey);
28622864
var address2 = await tester.Client.GetUnusedAsync(pubkey, DerivationFeature.Direct);
2865+
Logs.Tester.LogInformation("ADDRESS2: " + address2.Address);
2866+
Assert.Equal(1, address2.Index);
28632867
Assert.Equal(new KeyPath(1), address2.KeyPath);
28642868

28652869
Logs.Tester.LogInformation("Let's check see if an unconf tx can be conf then unconf again");
@@ -3680,7 +3684,7 @@ public async Task CanScanUTXOSet()
36803684
// Nothing has been tracked because it is way out of bound and the first address is always unused
36813685
var transactions = tester.Client.GetTransactions(pubkey);
36823686
Assert.Empty(transactions.ConfirmedTransactions.Transactions);
3683-
Assert.Equal(0, tester.Client.GetUnused(pubkey, DerivationFeature.Deposit).GetIndex(KeyPathTemplates.Default));
3687+
Assert.Equal(0, tester.Client.GetUnused(pubkey, DerivationFeature.Deposit).Index);
36843688

36853689
// W00t! let's scan and see if it now appear in the UTXO
36863690
tester.Client.ScanUTXOSet(pubkey, batchsize, gaplimit);
@@ -3703,7 +3707,7 @@ public async Task CanScanUTXOSet()
37033707
#pragma warning restore CS0618 // Type or member is obsolete
37043708

37053709
Logs.Tester.LogInformation($"Check that the address pool has been emptied: 0/51 should be the next unused address");
3706-
Assert.Equal(51, tester.Client.GetUnused(pubkey, DerivationFeature.Deposit).GetIndex(KeyPathTemplates.Default));
3710+
Assert.Equal(51, tester.Client.GetUnused(pubkey, DerivationFeature.Deposit).Index);
37073711
utxo = tester.Client.GetUTXOs(pubkey);
37083712
Assert.Equal(txId, utxo.Confirmed.UTXOs[0].TransactionHash);
37093713

NBXplorer/Backend/Repository.cs

+89-38
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Text.RegularExpressions;
1919
using Npgsql;
2020
using static NBXplorer.Backend.DbConnectionHelper;
21+
using Microsoft.AspNetCore.DataProtection.KeyManagement;
2122

2223

2324
namespace NBXplorer.Backend
@@ -368,32 +369,94 @@ public async Task<MultiValueDictionary<Script, KeyPathInformation>> GetKeyInform
368369
await using var connection = await connectionFactory.CreateConnection();
369370
return await GetKeyInformations(connection, scripts);
370371
}
371-
async Task<MultiValueDictionary<Script, KeyPathInformation>> GetKeyInformations(DbConnection connection, IList<Script> scripts)
372+
abstract class GetKeyInformationsQuery
372373
{
373-
scripts = scripts.Distinct().ToArray();
374-
MultiValueDictionary<Script, KeyPathInformation> result = new MultiValueDictionary<Script, KeyPathInformation>();
375-
foreach (var s in scripts)
376-
result.AddRange(s, Array.Empty<KeyPathInformation>());
377-
var command = connection.CreateCommand();
378-
if (scripts.Count == 0)
374+
public static GetKeyInformationsQuery ByScripts(IList<Script> scripts) => new ByScriptsQuery(scripts);
375+
public static GetKeyInformationsQuery ByUnused(DerivationStrategyBase strategy, DerivationFeature derivationFeature, int skip, NBXplorerNetwork network) => new ByUnusedQuery(strategy, derivationFeature, skip, network);
376+
public abstract string GetScriptsQuery();
377+
public virtual string GetKeyPathInfoPredicate() => string.Empty;
378+
public abstract void AddParameters(DynamicParameters parameters);
379+
public virtual bool IsEmpty => false;
380+
public virtual MultiValueDictionary<Script, KeyPathInformation> CreateResult() => new();
381+
class ByUnusedQuery : GetKeyInformationsQuery
382+
{
383+
private int n;
384+
private readonly string descriptorId;
385+
386+
public ByUnusedQuery(DerivationStrategyBase strategy, DerivationFeature derivationFeature, int n, NBXplorerNetwork network)
387+
{
388+
this.n = n;
389+
descriptorId = DBUtils.nbxv1_get_descriptor_id(network.CryptoCode, strategy.ToString(), derivationFeature.ToString());
390+
}
391+
392+
public override void AddParameters(DynamicParameters parameters)
393+
{
394+
parameters.Add("skip", n);
395+
parameters.Add("descriptor", descriptorId);
396+
}
397+
public override string GetScriptsQuery()
398+
{
399+
return $@"
400+
(SELECT script FROM descriptors_scripts_unused
401+
WHERE code=@code AND descriptor=@descriptor
402+
ORDER BY idx
403+
LIMIT 1 OFFSET @skip) AS r (script)
404+
";
405+
}
406+
public override string GetKeyPathInfoPredicate() => "AND ki.descriptor=@descriptor";
407+
}
408+
class ByScriptsQuery : GetKeyInformationsQuery
409+
{
410+
private readonly Script[] scripts;
411+
412+
public override bool IsEmpty => scripts.Length is 0;
413+
414+
public ByScriptsQuery(IList<Script> scripts)
415+
{
416+
this.scripts = scripts.Distinct().ToArray();
417+
}
418+
public override string GetScriptsQuery() => "unnest(@records) AS r (script)";
419+
public override void AddParameters(DynamicParameters parameters)
420+
{
421+
parameters.Add("records", scripts.Select(s => s.ToHex()).ToArray());
422+
}
423+
public override MultiValueDictionary<Script, KeyPathInformation> CreateResult()
424+
{
425+
MultiValueDictionary<Script, KeyPathInformation> result = new MultiValueDictionary<Script, KeyPathInformation>();
426+
foreach (var s in scripts)
427+
result.AddRange(s, Array.Empty<KeyPathInformation>());
428+
return result;
429+
}
430+
}
431+
}
432+
Task<MultiValueDictionary<Script, KeyPathInformation>> GetKeyInformations(DbConnection connection, IList<Script> scripts)
433+
=> GetKeyInformations(connection, GetKeyInformationsQuery.ByScripts(scripts));
434+
async Task<MultiValueDictionary<Script, KeyPathInformation>> GetKeyInformations(DbConnection connection, GetKeyInformationsQuery query)
435+
{
436+
var result = query.CreateResult();
437+
if (query.IsEmpty)
379438
return result;
439+
440+
DynamicParameters parameters = new DynamicParameters();
441+
parameters.Add("code", Network.CryptoCode);
442+
query.AddParameters(parameters);
380443
string additionalColumn = Network.IsElement ? ", ts.blinded_addr" : "";
381444
var rows = await connection.QueryAsync($@"
382-
SELECT ts.code, ts.script, ts.addr, ts.derivation, ts.keypath, ts.redeem{additionalColumn},
445+
SELECT ts.code, ts.script, ts.addr, ts.derivation, ts.keypath, ts.idx, ts.feature, ts.redeem{additionalColumn},
383446
ts.wallet_id,
384447
w.metadata AS wallet_metadata
385-
FROM unnest(@records) AS r (script),
448+
FROM {query.GetScriptsQuery()},
386449
LATERAL (
387450
SELECT code, script, wallet_id, addr, descriptor_metadata->>'derivation' derivation,
388-
keypath, descriptors_scripts_metadata->>'redeem' redeem,
451+
keypath, ki.idx, descriptors_scripts_metadata->>'redeem' redeem,
389452
descriptors_scripts_metadata->>'blindedAddress' blinded_addr,
390453
descriptors_scripts_metadata->>'blindingKey' blindingKey,
391-
descriptor_metadata->>'descriptor' descriptor
392-
FROM nbxv1_keypath_info ki
393-
WHERE ki.code=@code AND ki.script=r.script
454+
descriptor_metadata->>'descriptor' descriptor,
455+
descriptor_metadata->>'feature' feature
456+
FROM nbxv1_keypath_info ki
457+
WHERE ki.code=@code AND ki.script=r.script {query.GetKeyPathInfoPredicate()}
394458
) ts
395-
JOIN wallets w USING(wallet_id)",
396-
new { code = Network.CryptoCode, records = scripts.Select(s => s.ToHex()).ToArray() });
459+
JOIN wallets w USING(wallet_id)", parameters);
397460
foreach (var r in rows)
398461
{
399462
// This might be the case for a derivation added by a different indexer
@@ -425,7 +488,12 @@ JOIN wallets w USING(wallet_id)",
425488
ki.KeyPath = keypath;
426489
ki.ScriptPubKey = script;
427490
ki.TrackedSource = trackedSource;
428-
ki.Feature = keypath is null ? DerivationFeature.Deposit : KeyPathTemplates.GetDerivationFeature(keypath);
491+
ki.Feature = DerivationFeature.Deposit;
492+
if (keypath is not null)
493+
{
494+
ki.Feature = Enum.Parse<DerivationFeature>(r.feature, true);
495+
ki.Index = (int)r.idx;
496+
}
429497
ki.Redeem = redeem is null ? null : Script.FromHex(redeem);
430498
result.Add(script, ki);
431499
}
@@ -783,14 +851,8 @@ public async Task<KeyPathInformation> GetUnused(DerivationStrategyBase strategy,
783851
{
784852
await using var helper = await connectionFactory.CreateConnectionHelper(Network);
785853
var connection = helper.Connection;
786-
var key = GetDescriptorKey(strategy, derivationFeature);
787-
string additionalColumn = Network.IsElement ? ", ds_metadata->>'blindedAddress' blinded_addr" : string.Empty;
788854
retry:
789-
var unused = await connection.QueryFirstOrDefaultAsync(
790-
$"SELECT script, addr, nbxv1_get_keypath(d_metadata, idx) keypath, ds_metadata->>'redeem' redeem {additionalColumn} FROM descriptors_scripts_unused " +
791-
"WHERE code=@code AND descriptor=@descriptor " +
792-
"ORDER BY idx " +
793-
"LIMIT 1 OFFSET @skip", new { key.code, key.descriptor, skip = n });
855+
var unused = (await GetKeyInformations(connection, GetKeyInformationsQuery.ByUnused(strategy, derivationFeature, n, Network))).FirstOrDefault().Value?.FirstOrDefault();
794856
if (unused is null)
795857
{
796858
// If we don't find unused address, then either:
@@ -809,23 +871,12 @@ public async Task<KeyPathInformation> GetUnused(DerivationStrategyBase strategy,
809871
}
810872
if (reserve)
811873
{
812-
var updated = await connection.ExecuteAsync("UPDATE descriptors_scripts SET used='t' WHERE code=@code AND script=@script AND descriptor=@descriptor AND used='f'", new { key.code, unused.script, key.descriptor });
874+
var updated = await connection.ExecuteAsync("UPDATE descriptors_scripts SET used='t' WHERE code=@code AND script=@script AND descriptor=@descriptor AND used='f'", new { code = Network.CryptoCode, script = unused.ScriptPubKey.ToHex(), descriptor = GetDescriptorKey(strategy, derivationFeature).descriptor });
813875
if (updated == 0)
814876
goto retry;
815877
}
816-
var keypath = KeyPath.Parse(unused.keypath);
817-
var keyInfo = new KeyPathInformation()
818-
{
819-
Address = GetAddress(unused),
820-
DerivationStrategy = strategy,
821-
KeyPath = keypath,
822-
ScriptPubKey = Script.FromHex(unused.script),
823-
TrackedSource = new DerivationSchemeTrackedSource(strategy),
824-
Feature = KeyPathTemplates.GetDerivationFeature(keypath),
825-
Redeem = unused.redeem is string s ? Script.FromHex(s) : null
826-
};
827-
await ImportAddressToRPC(helper, keyInfo.TrackedSource, keyInfo.Address, keyInfo.KeyPath);
828-
return keyInfo;
878+
await ImportAddressToRPC(helper, unused.TrackedSource, unused.Address, unused.KeyPath);
879+
return unused;
829880
}
830881

831882
record SingleAddressInsert(string code, string script, string address, string walletid);
@@ -863,7 +914,7 @@ internal async Task SaveKeyInformations(DbConnection connection, KeyPathInformat
863914
{
864915
descriptorInsert.Add(new DescriptorScriptInsert(
865916
descriptorKey.descriptor,
866-
ki.GetIndex(KeyPathTemplates),
917+
ki.Index.Value,
867918
ki.ScriptPubKey.ToHex(),
868919
metadata?.ToString(Formatting.None),
869920
addr.ToString(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CREATE OR REPLACE VIEW nbxv1_keypath_info AS
2+
SELECT ws.code,
3+
ws.script,
4+
s.addr,
5+
d.metadata AS descriptor_metadata,
6+
nbxv1_get_keypath(d.metadata, ds.idx) AS keypath,
7+
ds.metadata AS descriptors_scripts_metadata,
8+
ws.wallet_id,
9+
ds.idx,
10+
ds.used,
11+
d.descriptor
12+
FROM ((wallets_scripts ws
13+
JOIN scripts s ON (((s.code = ws.code) AND (s.script = ws.script))))
14+
LEFT JOIN ((wallets_descriptors wd
15+
JOIN descriptors_scripts ds ON (((ds.code = wd.code) AND (ds.descriptor = wd.descriptor))))
16+
JOIN descriptors d ON (((d.code = ds.code) AND (d.descriptor = ds.descriptor)))) ON (((wd.wallet_id = ws.wallet_id) AND (wd.code = ws.code) AND (ds.script = ws.script))));

NBXplorer/DBScripts/FullSchema.sql

+5-1
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,10 @@ CREATE VIEW nbxv1_keypath_info AS
959959
d.metadata AS descriptor_metadata,
960960
nbxv1_get_keypath(d.metadata, ds.idx) AS keypath,
961961
ds.metadata AS descriptors_scripts_metadata,
962-
ws.wallet_id
962+
ws.wallet_id,
963+
ds.idx,
964+
ds.used,
965+
d.descriptor
963966
FROM ((wallets_scripts ws
964967
JOIN scripts s ON (((s.code = ws.code) AND (s.script = ws.script))))
965968
LEFT JOIN ((wallets_descriptors wd
@@ -1367,6 +1370,7 @@ INSERT INTO nbxv1_migrations VALUES ('019.FixDoubleSpendDetection2');
13671370
INSERT INTO nbxv1_migrations VALUES ('020.ReplacingShouldBeIdempotent');
13681371
INSERT INTO nbxv1_migrations VALUES ('021.KeyPathInfoReturnsWalletId');
13691372
INSERT INTO nbxv1_migrations VALUES ('022.WalletsWalletsParentIdIndex');
1373+
INSERT INTO nbxv1_migrations VALUES ('023.KeyPathInfoReturnsIndex');
13701374

13711375
ALTER TABLE ONLY nbxv1_migrations
13721376
ADD CONSTRAINT nbxv1_migrations_pkey PRIMARY KEY (script_name);

NBXplorer/NBXplorer.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<None Remove="DBScripts\018.FastWalletRecent.sql" />
2828
<None Remove="DBScripts\020.ReplacingShouldBeIdempotent.sql" />
2929
<None Remove="DBScripts\022.WalletsWalletsParentIdIndex.sql" />
30+
<None Remove="DBScripts\023.KeyPathInfoReturnsIndex.sql" />
3031
</ItemGroup>
3132
<ItemGroup>
3233
<EmbeddedResource Include="DBScripts\*.sql" />
@@ -60,4 +61,5 @@
6061
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
6162
</EmbeddedResource>
6263
</ItemGroup>
64+
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4api_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/refs/tags/3.1.0/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
6365
</Project>

NBXplorer/ScanUTXOSetService.cs

+12-2
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,19 @@ private ScannedItems GetScannedItems(ScanUTXOWorkItem workItem, ScanUTXOProgress
308308
Enumerable.Range(progress.From, progress.Count)
309309
.Select(index =>
310310
{
311+
var keyPath = keyPathTemplate.GetKeyPath(index, false);
311312
var derivation = lineDerivation.Derive((uint)index);
312-
var info = new KeyPathInformation(derivation, derivationStrategy, feature,
313-
keyPathTemplate.GetKeyPath(index, false), network);
313+
var info = new KeyPathInformation()
314+
{
315+
ScriptPubKey = derivation.ScriptPubKey,
316+
DerivationStrategy = derivationStrategy.DerivationStrategy,
317+
Feature = feature,
318+
KeyPath = keyPath,
319+
Redeem = derivation.Redeem,
320+
TrackedSource = derivationStrategy,
321+
Address = network.CreateAddress(derivationStrategy.DerivationStrategy, keyPath, derivation.ScriptPubKey),
322+
Index = index
323+
};
314324
items.Descriptors.Add(OutputDescriptor.NewRaw(info.ScriptPubKey, network.NBitcoinNetwork));
315325
items.KeyPathInformations.TryAdd(info.ScriptPubKey, info);
316326
return info;

0 commit comments

Comments
 (0)