Пример #1
0
        /// <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);
            }
        }
Пример #2
0
        /// <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);
        }
Пример #3
0
        /// <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);
        }
Пример #4
0
        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();
            }
        }
Пример #5
0
        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);
            }
        }
Пример #7
0
        /// <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);
            }
        }
Пример #8
0
        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);
        }
Пример #10
0
        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();
        }
Пример #12
0
        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);
        }