Start putting together componenets
MODIFY
MagicalCryptoWallet.Tests/ClientSideFilter/StoreTest.cs
MagicalCryptoWallet.Tests/ClientSideFilter/StoreTest.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using MagicalCryptoWallet.Backend;
using NBitcoin;
+using NBitcoin.DataEncoders;
using Xunit;
namespace MagicalCryptoWallet.Tests

MODIFY
MagicalCryptoWallet.Tests/RegTests.cs
MagicalCryptoWallet.Tests/RegTests.cs
@@ -336,6 +336,8 @@ namespace MagicalCryptoWallet.Tests
{
await AssertFiltersInitializedAsync();
+ _filterReorgTestAsync_ReorgCount = 0;
+
var node = Fixture.BackendRegTestNode;
var indexFilePath = Path.Combine(SharedFixture.DataDir, nameof(FilterDownloaderTestAsync), $"Index{Global.RpcClient.Network}.dat");
@@ -344,6 +346,8 @@ namespace MagicalCryptoWallet.Tests
{
downloader.Synchronize(requestInterval: TimeSpan.FromSeconds(1));
+ downloader.Reorged += FilterReorgTestAsync_Downloader_Reorged;
+
// Test initial syncronization.
var times = 0;
@@ -360,11 +364,15 @@ namespace MagicalCryptoWallet.Tests
// Test syncronization after fork.
var tip = await Global.RpcClient.GetBestBlockHashAsync();
- await Global.RpcClient.InvalidateBlockAsync(tip);
+ await Global.RpcClient.InvalidateBlockAsync(tip); // Reorg 1
+ tip = await Global.RpcClient.GetBestBlockHashAsync();
+ await Global.RpcClient.InvalidateBlockAsync(tip); // Reorg 2
+
await Global.RpcClient.GenerateAsync(5);
+ var bestHeight = await Global.RpcClient.GetBlockCountAsync();
times = 0;
- while ((filterCount = downloader.GetFiltersIncluding(new Height(0)).Count()) < 116)
+ while ((filterCount = downloader.GetFiltersIncluding(new Height(0)).Count()) < bestHeight)
{
if (times > 2400) // 4 min
{
@@ -376,17 +384,25 @@ namespace MagicalCryptoWallet.Tests
// Test filter block hashes are correct after fork.
var filters = downloader.GetFiltersIncluding(Network.RegTest.GenesisHash).ToArray();
- for (int i = 0; i < 116; i++)
+ for (int i = 0; i <= bestHeight; i++)
{
var expectedHash = await Global.RpcClient.GetBlockHashAsync(i);
var filter = filters[i];
Assert.Equal(i, filter.BlockHeight.Value);
Assert.Equal(expectedHash, filter.BlockHash);
+ if (i < 101) // Later other tests may fill the filter.
+ {
Assert.Null(filter.Filter);
}
}
+
+ // Assert reorg happened exactly as many times as we reorged.
+ Assert.Equal(2, Interlocked.Read(ref _filterReorgTestAsync_ReorgCount));
+ }
finally
{
+ downloader.Reorged -= FilterReorgTestAsync_Downloader_Reorged;
+
if (downloader != null)
{
await downloader.StopAsync();
@@ -394,6 +410,13 @@ namespace MagicalCryptoWallet.Tests
}
}
+ private long _filterReorgTestAsync_ReorgCount;
+ private void FilterReorgTestAsync_Downloader_Reorged(object sender, uint256 e)
+ {
+ Assert.NotNull(e);
+ Interlocked.Increment(ref _filterReorgTestAsync_ReorgCount);
+ }
+
#endregion
#region ClientTests
@@ -404,6 +427,8 @@ namespace MagicalCryptoWallet.Tests
// Make sure fitlers are created on the server side.
await AssertFiltersInitializedAsync();
+ var network = Global.RpcClient.Network;
+
// Create the services.
// 1. Create connection service.
var nodes = new NodesGroup(Global.Config.Network,
@@ -434,13 +459,50 @@ namespace MagicalCryptoWallet.Tests
// 6. Create wallet service.
var wallet = new WalletService(keyManager, blockDownloader, indexDownloader);
+ // Get some money, make it confirm.
+ var key = keyManager.GenerateNewKey("", KeyState.Clean, isInternal: false);
+ await Global.RpcClient.SendToAddressAsync(key.GetP2wpkhAddress(network), new Money(1, MoneyUnit.BTC));
+ await Global.RpcClient.GenerateAsync(1);
+
try
{
nodes.Connect(); // Start connection service.
blockDownloader.Synchronize(); // Start block downloader service.
node.VersionHandshake(); // Start mempool service.
indexDownloader.Synchronize(requestInterval: TimeSpan.FromSeconds(3)); // Start index downloader service.
+
+ // Wait until the filter our previous transaction is present.
+ var times = 0;
+ while (1 != indexDownloader.GetFiltersIncluding(indexDownloader.StartingFilter.BlockHash).Where(x => x.Filter != null).Count())
+ {
+ if (times > 300) // 30 seconds
+ {
+ throw new TimeoutException($"{nameof(IndexDownloader)} test timed out.");
+ }
+ await Task.Delay(100);
+ times++;
+ }
+ Assert.Equal(1, blockDownloader.NumberOfBlocksToDownload + blockDownloader.NumberOfDownloadedBlocks);
+
+ wallet.Initialize(); // Initialize wallet service.
wallet.Synchronize(); // Start wallet service.
+
+ // Get some money, make it confirm.
+ key = keyManager.GenerateNewKey("", KeyState.Clean, isInternal: false);
+ await Global.RpcClient.SendToAddressAsync(key.GetP2wpkhAddress(network), new Money(1, MoneyUnit.BTC));
+ await Global.RpcClient.GenerateAsync(1);
+
+ times = 0;
+ while (2 != indexDownloader.GetFiltersIncluding(indexDownloader.StartingFilter.BlockHash).Where(x => x.Filter != null).Count())
+ {
+ if (times > 300) // 30 seconds
+ {
+ throw new TimeoutException($"{nameof(IndexDownloader)} test timed out.");
+ }
+ await Task.Delay(100);
+ times++;
+ }
+ Assert.Equal(2, blockDownloader.NumberOfBlocksToDownload + blockDownloader.NumberOfDownloadedBlocks);
}
finally
{

MODIFY
MagicalCryptoWallet/Backend/Models/FilterModel.cs
MagicalCryptoWallet/Backend/Models/FilterModel.cs
@@ -14,6 +14,11 @@ namespace MagicalCryptoWallet.Backend.Models
public uint256 BlockHash { get; set; }
public GolombRiceFilter Filter { get; set; }
+ // https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki
+ // The parameter k MUST be set to the first 16 bytes of the hash of the block for which the filter
+ // is constructed.This ensures the key is deterministic while still varying from block to block.
+ public byte[] FilterKey => BlockHash.ToBytes().Take(16).ToArray();
+
public string ToLine()
{
var builder = new StringBuilder();

MODIFY
MagicalCryptoWallet/KeyManagement/KeyManager.cs
MagicalCryptoWallet/KeyManagement/KeyManager.cs
@@ -169,5 +169,30 @@ namespace MagicalCryptoWallet.KeyManagement
return hdPubKey;
}
}
+
+ public IEnumerable<HdPubKey> GetKeys(KeyState? keyState = null, bool? isInternal = null)
+ {
+ // BIP44-ish derivation scheme
+ // m / purpose' / coin_type' / account' / change / address_index
+ lock (HdPubKeysLock)
+ {
+ if (keyState == null && isInternal == null)
+ {
+ return HdPubKeys;
+ }
+ else if (keyState != null && isInternal == null)
+ {
+ return HdPubKeys.Where(x => x.KeyState == keyState);
+ }
+ else if (keyState == null && isInternal != null)
+ {
+ return HdPubKeys.Where(x => x.IsInternal() == isInternal);
+ }
+ else // Neither of them null.
+ {
+ return HdPubKeys.Where(x => x.KeyState == keyState && x.IsInternal() == isInternal);
+ }
+ }
+ }
}
}

MODIFY
MagicalCryptoWallet/Services/BlockDownloader.cs
MagicalCryptoWallet/Services/BlockDownloader.cs
@@ -198,8 +198,11 @@ namespace MagicalCryptoWallet.Services
}
public async Task StopAsync()
+ {
+ if (IsRunning)
{
Interlocked.Exchange(ref _running, 2);
+ }
while (IsStopping)
{
await Task.Delay(50);

MODIFY
MagicalCryptoWallet/Services/IndexBuilderService.cs
MagicalCryptoWallet/Services/IndexBuilderService.cs
@@ -357,8 +357,11 @@ namespace MagicalCryptoWallet.Services
}
public async Task StopAsync()
+ {
+ if (IsRunning)
{
Interlocked.Exchange(ref _running, 2);
+ }
while (IsStopping)
{
await Task.Delay(50);

MODIFY
MagicalCryptoWallet/Services/IndexDownloader.cs
MagicalCryptoWallet/Services/IndexDownloader.cs
@@ -30,6 +30,12 @@ namespace MagicalCryptoWallet.Services
public static Height GetStartingHeight(Network network) => IndexBuilderService.GetStartingHeight(network);
public Height StartingHeight => GetStartingHeight(Network);
+ public event EventHandler<uint256> Reorged;
+ private void OnReorg(uint256 invalidBlockHash) => Reorged?.Invoke(this, invalidBlockHash);
+
+ public event EventHandler<FilterModel> NewFilter;
+ private void OnNewFilter(FilterModel filter) => NewFilter?.Invoke(this, filter);
+
public static FilterModel GetStartingFilter(Network network)
{
if (network == Network.Main)
@@ -135,6 +141,7 @@ namespace MagicalCryptoWallet.Services
var filterModel = FilterModel.FromLine(filters[i], new Height(bestKnownFilter.BlockHeight.Value + i + 1));
Index.Add(filterModel);
+ OnNewFilter(filterModel);
}
if (filters.Count == 1) // minor optimization
@@ -154,13 +161,16 @@ namespace MagicalCryptoWallet.Services
else if (response.StatusCode == HttpStatusCode.NotFound)
{
// Reorg happened
- Logger.LogInfo<IndexDownloader>($"REORG Invalid Block: {bestKnownFilter.BlockHash}");
+ var reorgedHash = bestKnownFilter.BlockHash;
+ Logger.LogInfo<IndexDownloader>($"REORG Invalid Block: {reorgedHash}");
// 1. Rollback index
using (await IndexLock.LockAsync())
{
Index.RemoveAt(Index.Count - 1);
}
+ OnReorg(reorgedHash);
+
// 2. Serialize Index. (Remove last line.)
var lines = File.ReadAllLines(IndexFilePath);
File.WriteAllLines(IndexFilePath, lines.Take(lines.Length - 1).ToArray());
@@ -195,14 +205,25 @@ namespace MagicalCryptoWallet.Services
}
public async Task StopAsync()
+ {
+ if (IsRunning)
{
Interlocked.Exchange(ref _running, 2);
+ }
while (IsStopping)
{
await Task.Delay(50);
}
}
+ public FilterModel GetBestFilter()
+ {
+ using (IndexLock.Lock())
+ {
+ return Index.Last();
+ }
+ }
+
public IEnumerable<FilterModel> GetFiltersIncluding(uint256 blockHash)
{
using (IndexLock.Lock())

Start putting together componenets