public void Synchronize() { Task.Run(async() => { try { if (Interlocked.Read(ref _runner) >= 2) { return; } Interlocked.Increment(ref _runner); while (Interlocked.Read(ref _runner) != 1) { await Task.Delay(100); } if (Interlocked.Read(ref _running) >= 2) { return; } try { Interlocked.Exchange(ref _running, 1); var isImmature = false; // The last 100 blocks are reorgable. (Assume it is mature at first.) SyncInfo syncInfo = null; while (IsRunning) { try { // If we did not yet initialized syncInfo, do so. if (syncInfo is null) { syncInfo = await GetSyncInfoAsync(); } uint heightToRequest = StartingHeight; uint256 currentHash = null; using (await IndexLock.LockAsync()) { if (Index.Count != 0) { var lastIndex = Index.Last(); heightToRequest = lastIndex.Header.Height + 1; currentHash = lastIndex.Header.BlockHash; } } // If not synchronized or already 5 min passed since last update, get the latest blockchain info. if (!syncInfo.IsCoreSynchornized || DateTimeOffset.UtcNow - syncInfo.BlockchainInfoUpdated > TimeSpan.FromMinutes(5)) { syncInfo = await GetSyncInfoAsync(); } if (syncInfo.BlockCount - heightToRequest <= 100) { // Both Wasabi and our Core node is in sync. Start doing stuff through P2P from now on. if (syncInfo.IsCoreSynchornized && syncInfo.BlockCount == heightToRequest - 1) { syncInfo = await GetSyncInfoAsync(); // Double it to make sure not to accidentally miss any notification. if (syncInfo.IsCoreSynchornized && syncInfo.BlockCount == heightToRequest - 1) { // Mark the process notstarted, so it can be started again and finally block can mark it is stopped. Interlocked.Exchange(ref _running, 0); return; } } // Mark the synchronizing process is working with immature blocks from now on. isImmature = true; } Block block = await RpcClient.GetBlockAsync(heightToRequest); // Reorg check, except if we're requesting the starting height, because then the "currentHash" wouldn't exist. if (heightToRequest != StartingHeight && currentHash != block.Header.HashPrevBlock) { // Reorg can happen only when immature. (If it'd not be immature, that'd be a huge issue.) if (isImmature) { await ReorgOneAsync(); } else { Logger.LogCritical("This is something serious! Over 100 block reorg is noticed! We cannot handle that!"); } // Skip the current block. continue; } if (isImmature) { PrepareBech32UtxoSetHistory(); } var scripts = new HashSet <Script>(); foreach (var tx in block.Transactions) { // If stop was requested return. // Because this tx iteration can take even minutes // It does not need to be accessed with a thread safe fashion with Interlocked through IsRunning, this may have some performance benefit if (_running != 1) { return; } for (int i = 0; i < tx.Outputs.Count; i++) { var output = tx.Outputs[i]; if (output.ScriptPubKey.IsScriptType(ScriptType.P2WPKH)) { var outpoint = new OutPoint(tx.GetHash(), i); var utxoEntry = new UtxoEntry(outpoint, output.ScriptPubKey); Bech32UtxoSet.Add(outpoint, utxoEntry); if (isImmature) { Bech32UtxoSetHistory.Last().StoreAction(Operation.Add, outpoint, output.ScriptPubKey); } scripts.Add(output.ScriptPubKey); } } foreach (var input in tx.Inputs) { OutPoint prevOut = input.PrevOut; if (Bech32UtxoSet.TryGetValue(prevOut, out UtxoEntry foundUtxoEntry)) { var foundScript = foundUtxoEntry.Script; Bech32UtxoSet.Remove(prevOut); if (isImmature) { Bech32UtxoSetHistory.Last().StoreAction(Operation.Remove, prevOut, foundScript); } scripts.Add(foundScript); } } } GolombRiceFilter filter; if (scripts.Any()) { filter = new GolombRiceFilterBuilder() .SetKey(block.GetHash()) .SetP(20) .SetM(1 << 20) .AddEntries(scripts.Select(x => x.ToCompressedBytes())) .Build(); } else { // We cannot have empty filters, because there was a bug in GolombRiceFilterBuilder that evaluates empty filters to true. // And this must be fixed in a backwards compatible way, so we create a fake filter with a random scp instead. filter = CreateDummyEmptyFilter(block.GetHash()); } var smartHeader = new SmartHeader(block.GetHash(), block.Header.HashPrevBlock, heightToRequest, block.Header.BlockTime); var filterModel = new FilterModel(smartHeader, filter); await File.AppendAllLinesAsync(IndexFilePath, new[] { filterModel.ToLine() }); using (await IndexLock.LockAsync()) { Index.Add(filterModel); } if (File.Exists(Bech32UtxoSetFilePath)) { File.Delete(Bech32UtxoSetFilePath); } var bech32UtxoSetLines = Bech32UtxoSet.Select(entry => entry.Value.Line); // Keep it sync unless you fix the performance issue with async. File.WriteAllLines(Bech32UtxoSetFilePath, bech32UtxoSetLines); // If not close to the tip, just log debug. // Use height.Value instead of simply height, because it cannot be negative height. if (syncInfo.BlockCount - heightToRequest <= 3 || heightToRequest % 100 == 0) { Logger.LogInfo($"Created filter for block: {heightToRequest}."); } else { Logger.LogDebug($"Created filter for block: {heightToRequest}."); } } catch (Exception ex) { Logger.LogDebug(ex); } } } finally { Interlocked.CompareExchange(ref _running, 3, 2); // If IsStopping, make it stopped. Interlocked.Decrement(ref _runner); } } catch (Exception ex) { Logger.LogError($"Synchronization attempt failed to start: {ex}"); } }); }
public void Synchronize() { Interlocked.Exchange(ref _running, 1); Task.Run(async() => { try { var blockCount = await RpcClient.GetBlockCountAsync(); var isIIB = true; // Initial Index Building phase while (IsRunning) { try { // If stop was requested return. if (IsRunning == false) { return; } var height = StartingHeight; uint256 prevHash = null; using (await IndexLock.LockAsync()) { if (Index.Count != 0) { var lastIndex = Index.Last(); height = lastIndex.BlockHeight + 1; prevHash = lastIndex.BlockHash; } } if (blockCount - (int)height <= 100) { isIIB = false; } Block block = null; try { block = await RpcClient.GetBlockAsync(height); } catch (RPCException) // if the block didn't come yet { await Task.Delay(1000); continue; } if (prevHash != null) { // In case of reorg: if (prevHash != block.Header.HashPrevBlock && !isIIB) // There is no reorg in IIB { Logger.LogInfo <IndexBuilderService>($"REORG Invalid Block: {prevHash}"); // 1. Rollback index using (await IndexLock.LockAsync()) { Index.RemoveLast(); } // 2. Serialize Index. (Remove last line.) var lines = File.ReadAllLines(IndexFilePath); File.WriteAllLines(IndexFilePath, lines.Take(lines.Length - 1).ToArray()); // 3. Rollback Bech32UtxoSet if (Bech32UtxoSetHistory.Count != 0) { Bech32UtxoSetHistory.Last().Rollback(Bech32UtxoSet); // The Bech32UtxoSet MUST be recovered to its previous state. Bech32UtxoSetHistory.RemoveLast(); // 4. Serialize Bech32UtxoSet. await File.WriteAllLinesAsync(Bech32UtxoSetFilePath, Bech32UtxoSet .Select(entry => entry.Key.Hash + ":" + entry.Key.N + ":" + ByteHelpers.ToHex(entry.Value.ToCompressedBytes()))); } // 5. Skip the current block. continue; } } if (!isIIB) { if (Bech32UtxoSetHistory.Count >= 100) { Bech32UtxoSetHistory.RemoveFirst(); } Bech32UtxoSetHistory.Add(new ActionHistoryHelper()); } var scripts = new HashSet <Script>(); foreach (var tx in block.Transactions) { for (int i = 0; i < tx.Outputs.Count; i++) { var output = tx.Outputs[i]; if (!output.ScriptPubKey.IsPayToScriptHash && output.ScriptPubKey.IsWitness) { var outpoint = new OutPoint(tx.GetHash(), i); Bech32UtxoSet.Add(outpoint, output.ScriptPubKey); if (!isIIB) { Bech32UtxoSetHistory.Last().StoreAction(ActionHistoryHelper.Operation.Add, outpoint, output.ScriptPubKey); } scripts.Add(output.ScriptPubKey); } } foreach (var input in tx.Inputs) { var found = Bech32UtxoSet.SingleOrDefault(x => x.Key == input.PrevOut); if (found.Key != default) { Script val = Bech32UtxoSet[input.PrevOut]; Bech32UtxoSet.Remove(input.PrevOut); if (!isIIB) { Bech32UtxoSetHistory.Last().StoreAction(ActionHistoryHelper.Operation.Remove, input.PrevOut, val); } scripts.Add(found.Value); } } } // 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. var key = block.GetHash().ToBytes().Take(16).ToArray(); GolombRiceFilter filter = null; if (scripts.Count != 0) { filter = GolombRiceFilter.Build(key, scripts.Select(x => x.ToCompressedBytes())); } var filterModel = new FilterModel { BlockHash = block.GetHash(), BlockHeight = height, Filter = filter }; await File.AppendAllLinesAsync(IndexFilePath, new[] { filterModel.ToLine() }); using (await IndexLock.LockAsync()) { Index.Add(filterModel); } if (File.Exists(Bech32UtxoSetFilePath)) { File.Delete(Bech32UtxoSetFilePath); } await File.WriteAllLinesAsync(Bech32UtxoSetFilePath, Bech32UtxoSet .Select(entry => entry.Key.Hash + ":" + entry.Key.N + ":" + ByteHelpers.ToHex(entry.Value.ToCompressedBytes()))); if (blockCount - height <= 3 || height % 100 == 0) // If not close to the tip, just log debug. { Logger.LogInfo <IndexBuilderService>($"Created filter for block: {height}."); } else { Logger.LogDebug <IndexBuilderService>($"Created filter for block: {height}."); } } catch (Exception ex) { Logger.LogDebug <IndexBuilderService>(ex); } } } finally { if (IsStopping) { Interlocked.Exchange(ref _running, 3); } } }); }
/// <summary> /// There was a bug when the server was sending wrong filters to the users for 24h. This function detects if the software tries to process the first wrong filter. /// </summary> private bool IsWrongFilter(FilterModel filter) => Network == Network.Main && filter.Header.Height == 627278 && filter.ToLine() == "627278:00000000000000000002edea14cfbbcde1cdb6a14275d9ad36491aa5d8862747:fd4b018270de28c44316c049e4e4050d8a7de5746c7bb31d093831c56196e5f5464956c9ab7142b998a93cb80149b09353cb5d46dfeb44ecb0c8255fb0eb64247a405ab2305713e3418707be4fe87286b467ac86603749aeeac6c182022f0105b6c547b22b89608d0b57eaee2767150bff2354e4cdecef069d1a7f9356e5972ac7323c907b2e42775d032b4a12cc45e96eaa86d232e14808beca91f21c09003734bf77005d2dbfdfeaf19108e971f99b91046db0a021a128bb17b91c83766c65e924bb48af50c473f80e8e8569fd68aaba856b9e4f60efba08519d4ca0f1c0453e60f50a86398b11c860607f049e2bc5e1b6201470f5383601fcfbbea766f510768bac3145dc33443131e50d41bdfb92f5b3d9affc0bbaa85a4c40be2d9e582c3ca0c82251d191ec83dbd197cc1a9f070e6754d84c8ca1c0258d21264619cb523a9bda5556aded4f82e9a8955180c8c8772304bb5f2a5498d15f28b3f0d5d0b22aba14a18be7c8a100bb35b73385ce2b410126ac614f2260557444c3279b73dab148cd14e8800815a1248fa802901a4430817b59d936ceb3e1594d002c1b8d88ec8641f2b2d9827a42948c61c888fc32f070eeba19dda8f773c6cef04485575652f3e929507a9e24dcee53bdc548a317f1e019fbc7ac87c5314548cca6c0b85ebbca79bed2685ed7024a21349189d9a6c92b05aa53a7e241b6885575a19bd737040c263ac05b9920d2e31568afff3c545a827338e103096fbd8fb60ef317e55146b74260577064627bba812c7ca06c39b45d291d7bb9142c338012ccb97330873a0e256ca8aaff778348085e1c9e9942cd10a8444f0c708a798c1d701b4e1879d78ee51f3044ee0012e9929c6e5bfddba40ed04872065373af111ebe53a832f5563078ef274cd39a6b77c8155d8996b6a5617c2ff447dcf4a37a84bfbd1ab34b8a4012f0ccb82c8085668a52e722f8a59a63a07420d2fc67a4da39209fc0cdcd335b2b4670817218f92aee62c8d0e3e895d7aa0f3c69ba36687c9559cf38adfef8ef0ec90128d1efc3b69006ed2c026a1a904bdd1bc0aa1924c74e05b4fdd8316a4cc400d9ced30eaf0ed01f82a6ab59bdf1fbd7a7c6f7186e33411140b57673a0075946902c5890e5647df67183a84f5b2001be152a23741582b529116e2d3bd9964968b40080173e5339018edf609199f25021c757ff3b8d1add3731002784c4da7176cd8b201e3931c61272d17e4e58a2487666510889935d054f0b72817700:000000000000000000028dbd5c398fa064b00e07805af0a8806e6f3b6ffce2c0:1587639149";
public void Synchronize() { Task.Run(async() => { try { if (Interlocked.Read(ref _workerCount) >= 2) { return; } Interlocked.Increment(ref _workerCount); while (Interlocked.Read(ref _workerCount) != 1) { await Task.Delay(100); } if (IsStopping) { return; } try { Interlocked.Exchange(ref _serviceStatus, Running); SyncInfo syncInfo = null; while (IsRunning) { try { // If we did not yet initialized syncInfo, do so. syncInfo ??= await GetSyncInfoAsync().ConfigureAwait(false); uint currentHeight = 0; uint256 currentHash = null; using (await IndexLock.LockAsync()) { if (Index.Count != 0) { var lastIndex = Index[^ 1]; currentHeight = lastIndex.Header.Height; currentHash = lastIndex.Header.BlockHash; } else { currentHash = StartingHeight == 0 ? uint256.Zero : await RpcClient.GetBlockHashAsync((int)StartingHeight - 1); currentHeight = StartingHeight - 1; } } var coreNotSynced = !syncInfo.IsCoreSynchornized; var tipReached = syncInfo.BlockCount == currentHeight; var isTimeToRefresh = DateTimeOffset.UtcNow - syncInfo.BlockchainInfoUpdated > TimeSpan.FromMinutes(5); if (coreNotSynced || tipReached || isTimeToRefresh) { syncInfo = await GetSyncInfoAsync(); } // If wasabi filter height is the same as core we may be done. if (syncInfo.BlockCount == currentHeight) { // Check that core is fully synced if (syncInfo.IsCoreSynchornized && !syncInfo.InitialBlockDownload) { // Mark the process notstarted, so it can be started again // and finally block can mark it as stopped. Interlocked.Exchange(ref _serviceStatus, NotStarted); return; } else { // Knots is catching up give it a 10 seconds await Task.Delay(10000); continue; } } uint nextHeight = currentHeight + 1; uint256 blockHash = await RpcClient.GetBlockHashAsync((int)nextHeight); VerboseBlockInfo block = await RpcClient.GetVerboseBlockAsync(blockHash); // Check if we are still on the best chain, // if not rewind filters till we find the fork. if (currentHash != block.PrevBlockHash) { Logger.LogWarning("Reorg observed on the network."); await ReorgOneAsync(); // Skip the current block. continue; } var scripts = FetchScripts(block); GolombRiceFilter filter; if (scripts.Any()) { filter = new GolombRiceFilterBuilder() .SetKey(block.Hash) .SetP(20) .SetM(1 << 20) .AddEntries(scripts.Select(x => x.ToCompressedBytes())) .Build(); } else { // We cannot have empty filters, because there was a bug in GolombRiceFilterBuilder that evaluates empty filters to true. // And this must be fixed in a backwards compatible way, so we create a fake filter with a random scp instead. filter = CreateDummyEmptyFilter(block.Hash); } var smartHeader = new SmartHeader(block.Hash, block.PrevBlockHash, nextHeight, block.BlockTime); var filterModel = new FilterModel(smartHeader, filter); await File.AppendAllLinesAsync(IndexFilePath, new[] { filterModel.ToLine() }); using (await IndexLock.LockAsync()) { Index.Add(filterModel); } // If not close to the tip, just log debug. if (syncInfo.BlockCount - nextHeight <= 3 || nextHeight % 100 == 0) { Logger.LogInfo($"Created filter for block: {nextHeight}."); } else { Logger.LogDebug($"Created filter for block: {nextHeight}."); } LastFilterBuildTime = DateTimeOffset.UtcNow; }
/// <inheritdoc /> public override void WriteJson(JsonWriter writer, FilterModel?value, JsonSerializer serializer) { var filterModel = value?.ToLine() ?? throw new ArgumentNullException(nameof(value)); writer.WriteValue(filterModel); }
public void Synchronize() { Interlocked.Exchange(ref _running, 1); Task.Run(async() => { try { var blockCount = await RpcClient.GetBlockCountAsync(); var isIIB = true; // Initial Index Building phase while (IsRunning) { try { // If stop was requested return. if (IsRunning == false) { return; } Height height = StartingHeight; uint256 prevHash = null; using (await IndexLock.LockAsync()) { if (Index.Count != 0) { var lastIndex = Index.Last(); height = lastIndex.BlockHeight + 1; prevHash = lastIndex.BlockHash; } } if (blockCount - height <= 100) { isIIB = false; } Block block = null; try { block = await RpcClient.GetBlockAsync(height); } catch (RPCException) // if the block didn't come yet { await Task.Delay(1000); continue; } if (blockCount - height <= 2) { NewBlock?.Invoke(this, block); } if (prevHash != null) { // In case of reorg: if (prevHash != block.Header.HashPrevBlock && !isIIB) // There is no reorg in IIB { Logger.LogInfo <IndexBuilderService>($"REORG Invalid Block: {prevHash}"); // 1. Rollback index using (await IndexLock.LockAsync()) { Index.RemoveLast(); } // 2. Serialize Index. (Remove last line.) var lines = File.ReadAllLines(IndexFilePath); File.WriteAllLines(IndexFilePath, lines.Take(lines.Length - 1).ToArray()); // 3. Rollback Bech32UtxoSet if (Bech32UtxoSetHistory.Count != 0) { Bech32UtxoSetHistory.Last().Rollback(Bech32UtxoSet); // The Bech32UtxoSet MUST be recovered to its previous state. Bech32UtxoSetHistory.RemoveLast(); // 4. Serialize Bech32UtxoSet. await File.WriteAllLinesAsync(Bech32UtxoSetFilePath, Bech32UtxoSet .Select(entry => entry.Key.Hash + ":" + entry.Key.N + ":" + ByteHelpers.ToHex(entry.Value.ToCompressedBytes()))); } // 5. Skip the current block. continue; } } if (!isIIB) { if (Bech32UtxoSetHistory.Count >= 100) { Bech32UtxoSetHistory.RemoveFirst(); } Bech32UtxoSetHistory.Add(new ActionHistoryHelper()); } var scripts = new HashSet <Script>(); foreach (var tx in block.Transactions) { // If stop was requested return. // Because this tx iteration can take even minutes // It doesn't need to be accessed with a thread safe fasion with Interlocked through IsRunning, this may have some performance benefit if (_running != 1) { return; } for (int i = 0; i < tx.Outputs.Count; i++) { var output = tx.Outputs[i]; if (!output.ScriptPubKey.IsPayToScriptHash && output.ScriptPubKey.IsWitness) { var outpoint = new OutPoint(tx.GetHash(), i); Bech32UtxoSet.Add(outpoint, output.ScriptPubKey); if (!isIIB) { Bech32UtxoSetHistory.Last().StoreAction(ActionHistoryHelper.Operation.Add, outpoint, output.ScriptPubKey); } scripts.Add(output.ScriptPubKey); } } foreach (var input in tx.Inputs) { OutPoint prevOut = input.PrevOut; if (Bech32UtxoSet.TryGetValue(prevOut, out Script foundScript)) { Bech32UtxoSet.Remove(prevOut); if (!isIIB) { Bech32UtxoSetHistory.Last().StoreAction(ActionHistoryHelper.Operation.Remove, prevOut, foundScript); } scripts.Add(foundScript); } } } GolombRiceFilter filter = null; if (scripts.Count != 0) { filter = new GolombRiceFilterBuilder() .SetKey(block.GetHash()) .SetP(20) .SetM(1 << 20) .AddEntries(scripts.Select(x => x.ToCompressedBytes())) .Build(); } var filterModel = new FilterModel { BlockHash = block.GetHash(), BlockHeight = height, Filter = filter }; await File.AppendAllLinesAsync(IndexFilePath, new[] { filterModel.ToLine() }); using (await IndexLock.LockAsync()) { Index.Add(filterModel); } if (File.Exists(Bech32UtxoSetFilePath)) { File.Delete(Bech32UtxoSetFilePath); } await File.WriteAllLinesAsync(Bech32UtxoSetFilePath, Bech32UtxoSet .Select(entry => entry.Key.Hash + ":" + entry.Key.N + ":" + ByteHelpers.ToHex(entry.Value.ToCompressedBytes()))); // If not close to the tip, just log debug. // Use height.Value instead of simply height, because it cannot be negative height. if (blockCount - height.Value <= 3 || height % 100 == 0) { Logger.LogInfo <IndexBuilderService>($"Created filter for block: {height}."); } else { Logger.LogDebug <IndexBuilderService>($"Created filter for block: {height}."); } } catch (Exception ex) { Logger.LogDebug <IndexBuilderService>(ex); } } } finally { if (IsStopping) { Interlocked.Exchange(ref _running, 3); } } }); }
public void Synchronize() { Task.Run(async() => { try { if (Interlocked.Read(ref _workerCount) >= 2) { return; } Interlocked.Increment(ref _workerCount); while (Interlocked.Read(ref _workerCount) != 1) { await Task.Delay(100).ConfigureAwait(false); } if (IsStopping) { return; } try { Interlocked.Exchange(ref _serviceStatus, Running); while (IsRunning) { try { SyncInfo syncInfo = await GetSyncInfoAsync().ConfigureAwait(false); uint currentHeight; uint256?currentHash = null; using (await IndexLock.LockAsync()) { if (Index.Count != 0) { var lastIndex = Index[^ 1]; currentHeight = lastIndex.Header.Height; currentHash = lastIndex.Header.BlockHash; } else { currentHash = StartingHeight == 0 ? uint256.Zero : await RpcClient.GetBlockHashAsync((int)StartingHeight - 1).ConfigureAwait(false); currentHeight = StartingHeight - 1; } } var coreNotSynced = !syncInfo.IsCoreSynchronized; var tipReached = syncInfo.BlockCount == currentHeight; var isTimeToRefresh = DateTimeOffset.UtcNow - syncInfo.BlockchainInfoUpdated > TimeSpan.FromMinutes(5); if (coreNotSynced || tipReached || isTimeToRefresh) { syncInfo = await GetSyncInfoAsync().ConfigureAwait(false); } // If wasabi filter height is the same as core we may be done. if (syncInfo.BlockCount == currentHeight) { // Check that core is fully synced if (syncInfo.IsCoreSynchronized && !syncInfo.InitialBlockDownload) { // Mark the process notstarted, so it can be started again // and finally block can mark it as stopped. Interlocked.Exchange(ref _serviceStatus, NotStarted); return; } else { // Knots is catching up give it a 10 seconds await Task.Delay(10000).ConfigureAwait(false); continue; } } uint nextHeight = currentHeight + 1; uint256 blockHash = await RpcClient.GetBlockHashAsync((int)nextHeight).ConfigureAwait(false); VerboseBlockInfo block = await RpcClient.GetVerboseBlockAsync(blockHash).ConfigureAwait(false); // Check if we are still on the best chain, // if not rewind filters till we find the fork. if (currentHash != block.PrevBlockHash) { Logger.LogWarning("Reorg observed on the network."); await ReorgOneAsync().ConfigureAwait(false); // Skip the current block. continue; } var filter = BuildFilterForBlock(block); var smartHeader = new SmartHeader(block.Hash, block.PrevBlockHash, nextHeight, block.BlockTime); var filterModel = new FilterModel(smartHeader, filter); await File.AppendAllLinesAsync(IndexFilePath, new[] { filterModel.ToLine() }).ConfigureAwait(false); using (await IndexLock.LockAsync()) { Index.Add(filterModel); } // If not close to the tip, just log debug. if (syncInfo.BlockCount - nextHeight <= 3 || nextHeight % 100 == 0) { Logger.LogInfo($"Created filter for block: {nextHeight}."); } else { Logger.LogDebug($"Created filter for block: {nextHeight}."); } LastFilterBuildTime = DateTimeOffset.UtcNow; }