/// <inheritdoc /> /// <remarks>This is currently not in use but will be required for exchange integration.</remarks> public AddressBalancesResult GetAddressBalances(string[] addresses, int minConfirmations = 1) { var(isQueryable, reason) = this.IsQueryable(); if (!isQueryable) { return(AddressBalancesResult.RequestFailed(reason)); } var result = new AddressBalancesResult(); lock (this.lockObject) { foreach (var address in addresses) { AddressIndexerData indexData = this.addressIndexRepository.GetOrCreateAddress(address); int maxAllowedHeight = this.consensusManager.Tip.Height - minConfirmations + 1; long balance = indexData.BalanceChanges.Where(x => x.BalanceChangedHeight <= maxAllowedHeight).CalculateBalance(); this.logger.LogDebug("Address: {0}, balance: {1}.", address, balance); result.Balances.Add(new AddressBalanceResult(address, new Money(balance))); } return(result); } }
/// <inheritdoc /> public VerboseAddressBalancesResult GetAddressIndexerState(string[] addresses) { var result = new VerboseAddressBalancesResult(this.consensusManager.Tip.Height); if (addresses.Length == 0) { return(result); } var(isQueryable, reason) = IsQueryable(); if (!isQueryable) { return(VerboseAddressBalancesResult.RequestFailed(reason)); } lock (this.lockObject) { foreach (var address in addresses) { var indexData = this.addressIndexRepository.GetOrCreateAddress(address); var copy = new AddressIndexerData { Address = indexData.Address, BalanceChanges = new List <AddressBalanceChange>(indexData.BalanceChanges) }; result.BalancesData.Add(copy); } } return(result); }
/// <summary>Adds a new balance change entry to to the <see cref="addressIndexRepository"/>.</summary> /// <param name="height">The height of the block this being processed.</param> /// <param name="address">The address receiving the funds.</param> /// <param name="amount">The amount being received.</param> /// <param name="deposited"><c>false</c> if this is an output being spent, <c>true</c> otherwise.</param> /// <remarks>Should be protected by <see cref="lockObject"/>.</remarks> private void ProcessBalanceChangeLocked(int height, string address, Money amount, bool deposited) { AddressIndexerData indexData = this.addressIndexRepository.GetOrCreateAddress(address); // Record new balance change into the address index data. indexData.BalanceChanges.Add(new AddressBalanceChange() { BalanceChangedHeight = height, Satoshi = amount.Satoshi, Deposited = deposited }); // Anything less than that should be compacted. int heightThreshold = this.consensusManager.Tip.Height - this.compactionTriggerDistance; bool compact = (indexData.BalanceChanges.Count > CompactingThreshold) && (indexData.BalanceChanges[1].BalanceChangedHeight < heightThreshold); if (!compact) { this.logger.LogTrace("(-)[TOO_FEW_CHANGE_RECORDS]"); return; } var compacted = new List <AddressBalanceChange>(CompactingThreshold / 2) { new AddressBalanceChange() { BalanceChangedHeight = 0, Satoshi = 0, Deposited = true } }; foreach (AddressBalanceChange change in indexData.BalanceChanges) { if (change.BalanceChangedHeight < heightThreshold) { this.logger.LogDebug("Balance change: {0} was selected for compaction. Compacted balance now: {1}.", change, compacted[0].Satoshi); if (change.Deposited) { compacted[0].Satoshi += change.Satoshi; } else { compacted[0].Satoshi -= change.Satoshi; } this.logger.LogDebug("New compacted balance: {0}.", compacted[0].Satoshi); } else { compacted.Add(change); } } indexData.BalanceChanges = compacted; this.addressIndexRepository.AddOrUpdate(indexData.Address, indexData, indexData.BalanceChanges.Count + 1); }
private void RewindAndSave(ChainedHeader rewindToHeader) { lock (this.lockObject) { // The cache doesn't really lend itself to handling a reorg very well. // Therefore, we leverage LiteDb's indexing capabilities to tell us // which records are for the affected blocks. List <string> affectedAddresses = this.addressIndexRepository.GetAddressesHigherThanHeight(rewindToHeader.Height); foreach (string address in affectedAddresses) { AddressIndexerData indexData = this.addressIndexRepository.GetOrCreateAddress(address); indexData.BalanceChanges.RemoveAll(x => x.BalanceChangedHeight > rewindToHeader.Height); } this.logger.LogDebug("Rewinding changes for {0} addresses.", affectedAddresses.Count); // Rewind all the way back to the fork point. this.outpointsRepository.RewindDataAboveHeight(rewindToHeader.Height); this.IndexerTip = rewindToHeader; this.SaveAll(); } }
public void AddressCacheCanRetrieveExisting() { const string CollectionName = "DummyCollection"; var dataFolder = new DataFolder(TestBase.CreateTestDir(this)); string dbPath = Path.Combine(dataFolder.RootPath, CollectionName); var database = new LiteDatabase(new ConnectionString() { Filename = dbPath, Upgrade = true }); var cache = new AddressIndexRepository(database, new ExtendedLoggerFactory()); string address = "xyz"; var balanceChanges = new List <AddressBalanceChange>(); balanceChanges.Add(new AddressBalanceChange() { BalanceChangedHeight = 1, Deposited = true, Satoshi = 1 }); var data = new AddressIndexerData() { Address = address, BalanceChanges = balanceChanges }; cache.AddOrUpdate(data.Address, data, data.BalanceChanges.Count + 1); AddressIndexerData retrieved = cache.GetOrCreateAddress("xyz"); Assert.NotNull(retrieved); Assert.Equal("xyz", retrieved.Address); Assert.Equal(1, retrieved.BalanceChanges.First().BalanceChangedHeight); Assert.True(retrieved.BalanceChanges.First().Deposited); Assert.Equal(1, retrieved.BalanceChanges.First().Satoshi); }
/// <inheritdoc /> public bool CheckCollateral(IFederationMember federationMember, int heightToCheckAt) { if (!this.collateralUpdated) { this.logger.LogTrace("(-)[NOT_INITIALIZED]"); throw new Exception("Component is not initialized!"); } var member = federationMember as CollateralFederationMember; if (member == null) { this.logger.LogTrace("(-)[WRONG_TYPE]"); throw new ArgumentException($"{nameof(federationMember)} should be of type: {nameof(CollateralFederationMember)}."); } lock (this.locker) { if (heightToCheckAt > this.counterChainConsensusTipHeight - this.maxReorgLength) { this.logger.LogTrace("(-)[INVALID_CHECK_HEIGHT]:false"); return(false); } } if ((member.CollateralAmount == null) || (member.CollateralAmount == 0)) { this.logger.LogTrace("(-)[NO_COLLATERAL_REQUIREMENT]:true"); return(true); } lock (this.locker) { AddressIndexerData balanceData = this.balancesDataByAddress[member.CollateralMainchainAddress]; if (balanceData == null) { // No data. Assume collateral is 0. It's ok if there is no collateral set for that fed member. this.logger.LogTrace("(-)[NO_DATA]"); return(0 >= member.CollateralAmount); } long balance = balanceData.BalanceChanges.Where(x => x.BalanceChangedHeight <= heightToCheckAt).CalculateBalance(); this.logger.LogDebug("Calculated balance at {0} is {1}, collateral requirement is {2}.", heightToCheckAt, balance, member.CollateralAmount); return(balance >= member.CollateralAmount.Satoshi); } }
/// <inheritdoc /> public bool CheckCollateral(IFederationMember federationMember, int heightToCheckAt) { if (!this.collateralUpdated) { this.logger.Debug("(-)[NOT_INITIALIZED]"); throw new Exception("Component is not initialized!"); } var member = federationMember as CollateralFederationMember; if (member == null) { this.logger.Debug("(-)[WRONG_TYPE]"); throw new ArgumentException($"{nameof(federationMember)} should be of type: {nameof(CollateralFederationMember)}."); } lock (this.locker) { if (heightToCheckAt > this.counterChainConsensusTipHeight - this.maxReorgLength) { this.logger.Debug("(-)[HEIGHTTOCHECK_HIGHER_THAN_COUNTER_TIP_LESS_MAXREORG]:{0}={1}, {2}={3}:false", nameof(heightToCheckAt), heightToCheckAt, nameof(this.counterChainConsensusTipHeight), this.counterChainConsensusTipHeight); return(false); } } if ((member.CollateralAmount == null) || (member.CollateralAmount == 0)) { this.logger.Debug("(-)[NO_COLLATERAL_REQUIREMENT]:true"); return(true); } lock (this.locker) { AddressIndexerData balanceData = this.balancesDataByAddress[member.CollateralMainchainAddress]; if (balanceData == null) { // No data. Assume collateral is 0. It's ok if there is no collateral set for that fed member. this.logger.Debug("(-)[NO_DATA]:{0}={1}", nameof(this.balancesDataByAddress.Count), this.balancesDataByAddress.Count); return(0 >= member.CollateralAmount); } long balance = balanceData.BalanceChanges.Where(x => x.BalanceChangedHeight <= heightToCheckAt).CalculateBalance(); this.logger.Info("Calculated balance for '{0}' at {1} is {2}, collateral requirement is {3}.", member.CollateralMainchainAddress, heightToCheckAt, Money.Satoshis(balance).ToUnit(MoneyUnit.BTC), member.CollateralAmount); return(balance >= member.CollateralAmount.Satoshi); } }
public void AddressCacheRetrievesBlankRecordForNonexistent() { const string CollectionName = "DummyCollection"; var dataFolder = new DataFolder(TestBase.CreateTestDir(this)); string dbPath = Path.Combine(dataFolder.RootPath, CollectionName); var database = new LiteDatabase(new ConnectionString() { Filename = dbPath, Upgrade = true }); var cache = new AddressIndexRepository(database, new ExtendedLoggerFactory()); AddressIndexerData retrieved = cache.GetOrCreateAddress("xyz"); // A record will be returned with no balance changes associated, if it is new. Assert.NotNull(retrieved); Assert.Equal("xyz", retrieved.Address); Assert.Empty(retrieved.BalanceChanges); }
public void AddressCacheRetrievesBlankRecordForNonexistent() { const string CollectionName = "DummyCollection"; var dataFolder = new DataFolder(TestBase.CreateTestDir(this)); string dbPath = Path.Combine(dataFolder.RootPath, CollectionName); FileMode fileMode = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? FileMode.Exclusive : FileMode.Shared; var database = new LiteDatabase(new ConnectionString() { Filename = dbPath, Mode = fileMode }); var cache = new AddressIndexRepository(database); AddressIndexerData retrieved = cache.GetOrCreateAddress("xyz"); // A record will be returned with no balance changes associated, if it is new. Assert.NotNull(retrieved); Assert.Equal("xyz", retrieved.Address); Assert.Empty(retrieved.BalanceChanges); }
public void AddressCacheEvicts() { const string CollectionName = "AddrData"; var dataFolder = new DataFolder(TestBase.CreateTestDir(this)); string dbPath = Path.Combine(dataFolder.RootPath, CollectionName); FileMode fileMode = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? FileMode.Exclusive : FileMode.Shared; var database = new LiteDatabase(new ConnectionString() { Filename = dbPath, Mode = fileMode }); var cache = new AddressIndexRepository(database, new ExtendedLoggerFactory(), 4); // Recall, each index entry counts as 1 and each balance change associated with it is an additional 1. Assert.Equal(0, database.GetCollection <AddressIndexerData>(CollectionName).Count()); string address1 = "xyz"; var balanceChanges1 = new List <AddressBalanceChange>(); balanceChanges1.Add(new AddressBalanceChange() { BalanceChangedHeight = 1, Deposited = true, Satoshi = 1 }); var data1 = new AddressIndexerData() { Address = address1, BalanceChanges = balanceChanges1 }; cache.AddOrUpdate(data1.Address, data1, data1.BalanceChanges.Count + 1); Assert.Equal(0, database.GetCollection <AddressIndexerData>(CollectionName).Count()); string address2 = "abc"; var balanceChanges2 = new List <AddressBalanceChange>(); balanceChanges2.Add(new AddressBalanceChange() { BalanceChangedHeight = 2, Deposited = false, Satoshi = 2 }); cache.AddOrUpdate(address2, new AddressIndexerData() { Address = address2, BalanceChanges = balanceChanges2 }, balanceChanges2.Count + 1); Assert.Equal(0, database.GetCollection <AddressIndexerData>(CollectionName).Count()); string address3 = "def"; var balanceChanges3 = new List <AddressBalanceChange>(); balanceChanges3.Add(new AddressBalanceChange() { BalanceChangedHeight = 3, Deposited = true, Satoshi = 3 }); cache.AddOrUpdate(address3, new AddressIndexerData() { Address = address3, BalanceChanges = balanceChanges3 }, balanceChanges3.Count + 1); // One of the cache items should have been evicted, and will therefore be persisted on disk. Assert.Equal(1, database.GetCollection <AddressIndexerData>(CollectionName).Count()); // The evicted item should be data1. Assert.Equal(data1.Address, database.GetCollection <AddressIndexerData>(CollectionName).FindAll().First().Address); Assert.Equal(1, database.GetCollection <AddressIndexerData>(CollectionName).FindAll().First().BalanceChanges.First().BalanceChangedHeight); Assert.True(database.GetCollection <AddressIndexerData>(CollectionName).FindAll().First().BalanceChanges.First().Deposited); Assert.Equal(1, database.GetCollection <AddressIndexerData>(CollectionName).FindAll().First().BalanceChanges.First().Satoshi); // Check that the first address can still be retrieved, it should come from disk in this case. AddressIndexerData retrieved = cache.GetOrCreateAddress("xyz"); Assert.NotNull(retrieved); Assert.Equal("xyz", retrieved.Address); Assert.Equal(1, retrieved.BalanceChanges.First().BalanceChangedHeight); Assert.True(retrieved.BalanceChanges.First().Deposited); Assert.Equal(1, retrieved.BalanceChanges.First().Satoshi); }
public async Task CanInitializeAndCheckCollateralAsync() { var blockStoreClientMock = new Mock <IBlockStoreClient>(); var collateralData = new VerboseAddressBalancesResult(this.collateralCheckHeight + 1000) { BalancesData = new List <AddressIndexerData>() { new AddressIndexerData() { Address = this.collateralFederationMembers[0].CollateralMainchainAddress, BalanceChanges = new List <AddressBalanceChange>() { new AddressBalanceChange() { BalanceChangedHeight = 0, Deposited = true, Satoshi = this.collateralFederationMembers[0].CollateralAmount } } }, new AddressIndexerData() { Address = this.collateralFederationMembers[1].CollateralMainchainAddress, BalanceChanges = new List <AddressBalanceChange>() { new AddressBalanceChange() { BalanceChangedHeight = 0, Deposited = true, Satoshi = this.collateralFederationMembers[1].CollateralAmount + 10 } } }, new AddressIndexerData() { Address = this.collateralFederationMembers[2].CollateralMainchainAddress, BalanceChanges = new List <AddressBalanceChange>() { new AddressBalanceChange() { BalanceChangedHeight = 0, Deposited = true, Satoshi = this.collateralFederationMembers[2].CollateralAmount - 10 } } } } }; blockStoreClientMock.Setup(x => x.GetVerboseAddressesBalancesDataAsync(It.IsAny <IEnumerable <string> >(), It.IsAny <CancellationToken>())).ReturnsAsync(collateralData); this.collateralChecker.SetPrivateVariableValue("blockStoreClient", blockStoreClientMock.Object); await this.collateralChecker.InitializeAsync(); Assert.True(this.collateralChecker.CheckCollateral(this.collateralFederationMembers[0], this.collateralCheckHeight)); Assert.True(this.collateralChecker.CheckCollateral(this.collateralFederationMembers[1], this.collateralCheckHeight)); Assert.False(this.collateralChecker.CheckCollateral(this.collateralFederationMembers[2], this.collateralCheckHeight)); // Now change what the client returns and make sure collateral check fails after update. AddressIndexerData updated = collateralData.BalancesData.First(b => b.Address == this.collateralFederationMembers[0].CollateralMainchainAddress); updated.BalanceChanges.First().Satoshi = this.collateralFederationMembers[0].CollateralAmount - 1; // Wait CollateralUpdateIntervalSeconds + 1 seconds await Task.Delay(21_000); Assert.False(this.collateralChecker.CheckCollateral(this.collateralFederationMembers[0], this.collateralCheckHeight)); this.collateralChecker.Dispose(); }
public GetUTXOsResponseModel GetUTXOsForAddress([FromQuery] string address) { VerboseAddressBalancesResult balancesResult = this.addressIndexer.GetAddressIndexerState(new[] { address }); if (balancesResult.BalancesData == null || balancesResult.BalancesData.Count != 1) { this.logger.LogWarning("No balances found for address {0}, Reason: {1}", address, balancesResult.Reason); return(new GetUTXOsResponseModel() { Reason = balancesResult.Reason }); } BitcoinAddress bitcoinAddress = this.network.CreateBitcoinAddress(address); AddressIndexerData addressBalances = balancesResult.BalancesData.First(); List <AddressBalanceChange> deposits = addressBalances.BalanceChanges.Where(x => x.Deposited).ToList(); long totalDeposited = deposits.Sum(x => x.Satoshi); long totalWithdrawn = addressBalances.BalanceChanges.Where(x => !x.Deposited).Sum(x => x.Satoshi); long balanceSat = totalDeposited - totalWithdrawn; List <int> heights = deposits.Select(x => x.BalanceChangedHeight).Distinct().ToList(); HashSet <uint256> blocksToRequest = new HashSet <uint256>(heights.Count); foreach (int height in heights) { uint256 blockHash = this.chainState.ConsensusTip.GetAncestor(height).Header.GetHash(); blocksToRequest.Add(blockHash); } List <Block> blocks = this.blockStore.GetBlocks(blocksToRequest.ToList()); List <OutPoint> collectedOutPoints = new List <OutPoint>(deposits.Count); foreach (List <Transaction> txList in blocks.Select(x => x.Transactions)) { foreach (Transaction transaction in txList.Where(x => !x.IsCoinBase && !x.IsCoinStake)) { for (int i = 0; i < transaction.Outputs.Count; i++) { if (!transaction.Outputs[i].IsTo(bitcoinAddress)) { continue; } collectedOutPoints.Add(new OutPoint(transaction, i)); } } } FetchCoinsResponse fetchCoinsResponse = this.coinView.FetchCoins(collectedOutPoints.ToArray()); GetUTXOsResponseModel response = new GetUTXOsResponseModel() { BalanceSat = balanceSat, UTXOs = new List <UTXOModel>() }; foreach (KeyValuePair <OutPoint, UnspentOutput> unspentOutput in fetchCoinsResponse.UnspentOutputs) { if (unspentOutput.Value.Coins == null) { continue; // spent } OutPoint outPoint = unspentOutput.Key; Money value = unspentOutput.Value.Coins.TxOut.Value; response.UTXOs.Add(new UTXOModel(outPoint, value)); } return(response); }