Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7bf3c0a
Add GetBalance to CoinView
quantumagi Jun 11, 2022
4a3d8f6
Fix
quantumagi Jun 11, 2022
8a69fc6
Fix indentation
quantumagi Jun 11, 2022
230763f
Add XML comment for GetBalance method
quantumagi Jun 12, 2022
4d7951e
More XML comment updates
quantumagi Jun 12, 2022
c4b642a
Update RocksDb and add CoinView fast-forward
quantumagi Jun 12, 2022
fbf4a9b
Refactor
quantumagi Jun 12, 2022
2dea122
Reduce changes
quantumagi Jun 12, 2022
f4c8f86
Reduce changes
quantumagi Jun 12, 2022
645d6da
Reduce changes
quantumagi Jun 12, 2022
87e83e6
Reduce changes
quantumagi Jun 12, 2022
fd363c7
Flush after rebuild
quantumagi Jun 12, 2022
4f4fda3
Index only PoS
quantumagi Jun 12, 2022
2793a15
Optimize
quantumagi Jun 12, 2022
577a032
Optimize / fix tests
quantumagi Jun 13, 2022
fe2ff6b
Refactor / fix tests
quantumagi Jun 13, 2022
a577923
Use AddressIndexerCV
quantumagi Jun 13, 2022
84bd656
Support toggling indexing mode
quantumagi Jun 13, 2022
ff88244
Add logs
quantumagi Jun 14, 2022
c36eb20
Refactor
quantumagi Jun 14, 2022
1e47951
Add sync method
quantumagi Jun 14, 2022
bed3316
Add ReadWriteBatch
quantumagi Jun 15, 2022
0ba5076
Fix tests
quantumagi Jun 15, 2022
7c38975
Bug fix
quantumagi Jun 15, 2022
8b2d80c
Handle unresolvable destinations
quantumagi Jun 16, 2022
da3a657
Fix ToDiff
quantumagi Jun 16, 2022
0d65346
Restore endianness fix still required for tests
quantumagi Jun 17, 2022
1ca8668
Merge branch 'release/1.3.1.0' into getbalance
quantumagi Jun 17, 2022
4a6bdc2
Merge release/1.4.0.0
quantumagi Jul 2, 2022
06c90b3
Fix merge
quantumagi Jul 2, 2022
9800ba0
Fix merge
quantumagi Jul 2, 2022
4443a5e
Guarantee CoindDb's IScriptAddressReader dependncy
quantumagi Jul 3, 2022
984bd23
Merge
quantumagi Jul 4, 2022
d755442
Reduce changes
quantumagi Jul 4, 2022
f83cc42
Reduce changes
quantumagi Jul 4, 2022
88bf887
Remove unused code
quantumagi Jul 4, 2022
70c0512
Refactor
quantumagi Jul 6, 2022
b93c277
Add XML comments
quantumagi Jul 7, 2022
c92a9ae
Add XML comments
quantumagi Jul 7, 2022
b1c1482
Modify XML comment
quantumagi Jul 7, 2022
daba62a
Merge
quantumagi Jul 12, 2022
0083e09
Fix merge
quantumagi Jul 12, 2022
98a3ef5
Fix merge
quantumagi Jul 12, 2022
6fe610a
Reorganize code
quantumagi Jul 12, 2022
bff166b
Split code into separate files
quantumagi Jul 12, 2022
89da0c2
Remove whitespace
quantumagi Jul 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NBitcoin;
using Stratis.Bitcoin.Builder.Feature;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Controllers.Models;
using Stratis.Bitcoin.Features.BlockStore.Models;
using Stratis.Bitcoin.Features.Consensus.CoinViews;
using Stratis.Bitcoin.Interfaces;

namespace Stratis.Bitcoin.Features.BlockStore.AddressIndexing
{
public class AddressIndexerCV : IAddressIndexer
{
private readonly Network network;
private readonly ICoinView coinView;
private readonly ChainIndexer chainIndexer;
private readonly IScriptAddressReader scriptAddressReader;

public ChainedHeader IndexerTip => GetTip();

public IFullNodeFeature InitializingFeature { set; private get; }

public AddressIndexerCV(Network network, ChainIndexer chainIndexer, IScriptAddressReader scriptAddressReader, ICoinView coinView)
{
this.network = network;
this.coinView = coinView;
this.chainIndexer = chainIndexer;
this.scriptAddressReader = scriptAddressReader;
}

private ChainedHeader GetTip()
{
this.coinView.Sync(this.chainIndexer);

return this.chainIndexer[this.coinView.GetTipHash().Hash];
}

public void Initialize()
{
}

private TxDestination AddressToDestination(string address)
{
var bitcoinAddress = BitcoinAddress.Create(address, this.network);
return this.scriptAddressReader.GetDestinationFromScriptPubKey(this.network, bitcoinAddress.ScriptPubKey).Single();
}

public AddressBalancesResult GetAddressBalances(string[] addresses, int minConfirmations = 0)
{
return new AddressBalancesResult()
{
Balances = addresses
.Select(address => (address, destination: AddressToDestination(address)))
.Select(t => new AddressBalanceResult()
{
Address = t.address,
Balance = (t.destination == null) ? 0 : new Money(this.coinView.GetBalance(t.destination).First().satoshis),

}).ToList()
};
}

public LastBalanceDecreaseTransactionModel GetLastBalanceDecreaseTransaction(string address)
{
throw new NotImplementedException();
}

private IEnumerable<AddressBalanceChange> ToDiff(List<AddressBalanceChange> addressBalanceChanges)
{
for (int i = addressBalanceChanges.Count - 1; i > 0; i--)
{
yield return new AddressBalanceChange() {
BalanceChangedHeight = addressBalanceChanges[i - 1].BalanceChangedHeight,
Deposited = addressBalanceChanges[i - 1].Satoshi < addressBalanceChanges[i].Satoshi,
Satoshi = Math.Abs(addressBalanceChanges[i - 1].Satoshi - addressBalanceChanges[i].Satoshi)
};
}
}

/// <inheritdoc/>
public VerboseAddressBalancesResult GetAddressIndexerState(string[] addresses)
{
// If the containing feature is not initialized then wait a bit.
this.InitializingFeature?.WaitInitialized();

return new VerboseAddressBalancesResult(this.IndexerTip.Height)
{
BalancesData = addresses
.Select(address => (address, destination: AddressToDestination(address)))
.Select(t => new AddressIndexerData()
{
Address = t.address,
BalanceChanges = (t.destination == null) ? new List<AddressBalanceChange>() : ToDiff(this.coinView.GetBalance(t.destination).Select(b => new AddressBalanceChange()
{
BalanceChangedHeight = (int)b.height,
Deposited = b.satoshis >= 0,
Satoshi = Math.Abs(b.satoshis)
}).ToList()).ToList()
}).ToList()
};
}

public void Dispose()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public static IFullNodeBuilder UseBlockStore(this IFullNodeBuilder fullNodeBuild

services.AddSingleton<StoreSettings>();
services.AddSingleton<IBlockStoreQueueFlushCondition, BlockStoreQueueFlushCondition>();
services.AddSingleton<IAddressIndexer, AddressIndexer>();
services.AddSingleton<IAddressIndexer, AddressIndexerCV>();
services.AddSingleton<IUtxoIndexer, UtxoIndexer>();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public CoinviewTests()
this.nodeStats = new NodeStats(this.dateTimeProvider, NodeSettings.Default(this.network), new Mock<IVersionProvider>().Object);

this.coindb = new DBreezeCoindb(this.network, this.dataFolder, this.dateTimeProvider, this.loggerFactory, this.nodeStats, new DBreezeSerializer(this.network.Consensus.ConsensusFactory));
this.coindb.Initialize(new ChainedHeader(this.network.GetGenesis().Header, this.network.GenesisHash, 0));
this.coindb.Initialize(false);

this.chainIndexer = new ChainIndexer(this.network);
this.stakeChainStore = new StakeChainStore(this.network, this.chainIndexer, (IStakedb)this.coindb, this.loggerFactory);
Expand Down
89 changes: 82 additions & 7 deletions src/Stratis.Bitcoin.Features.Consensus/CoinViews/CachedCoinView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using System.Threading;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Configuration.Settings;
using Stratis.Bitcoin.Consensus;
using Stratis.Bitcoin.Consensus.Rules;
using Stratis.Bitcoin.Features.Consensus.ProvenBlockHeaders;
using Stratis.Bitcoin.Features.Consensus.Rules.CommonRules;
using Stratis.Bitcoin.Interfaces;
Expand Down Expand Up @@ -114,6 +116,10 @@ public long GetScriptSize
/// <remarks>All access to this object has to be protected by <see cref="lockobj"/>.</remarks>
private readonly Dictionary<OutPoint, CacheItem> cachedUtxoItems;

/// <summary>Tracks pending balance updates for dirty cache entries.</summary>
/// <remarks>All access to this object has to be protected by <see cref="lockobj"/>.</remarks>
private readonly Dictionary<TxDestination, Dictionary<uint, long>> cacheBalancesByDestination;

/// <summary>Number of items in the cache.</summary>
/// <remarks>The getter violates the lock contract on <see cref="cachedUtxoItems"/>, but the lock here is unnecessary as the <see cref="cachedUtxoItems"/> is marked as readonly.</remarks>
private int cacheCount => this.cachedUtxoItems.Count;
Expand All @@ -127,35 +133,38 @@ public long GetScriptSize
private long rewindDataSizeBytes;
private DateTime lastCacheFlushTime;
private readonly Network network;
private readonly ICheckpoints checkpoints;
private readonly IDateTimeProvider dateTimeProvider;
private readonly IBlockStore blockStore;
private readonly CancellationTokenSource cancellationToken;
private readonly ConsensusSettings consensusSettings;
private readonly bool addressIndexingEnabled;
private CachePerformanceSnapshot latestPerformanceSnapShot;
private IScriptAddressReader scriptAddressReader;

private readonly Random random;

public CachedCoinView(Network network, ICheckpoints checkpoints, ICoindb coindb, IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, INodeStats nodeStats, ConsensusSettings consensusSettings,
StakeChainStore stakeChainStore = null, IRewindDataIndexCache rewindDataIndexCache = null, IBlockStore blockStore = null, INodeLifetime nodeLifetime = null)
public CachedCoinView(Network network, ICheckpoints checkpoints, ICoindb coindb, IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, INodeStats nodeStats, ConsensusSettings consensusSettings,
StakeChainStore stakeChainStore = null, IRewindDataIndexCache rewindDataIndexCache = null, IScriptAddressReader scriptAddressReader = null, IBlockStore blockStore = null, INodeLifetime nodeLifetime = null, NodeSettings nodeSettings = null)
{
Guard.NotNull(coindb, nameof(CachedCoinView.coindb));

this.coindb = coindb;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.network = network;
this.checkpoints = checkpoints;
this.dateTimeProvider = dateTimeProvider;
this.consensusSettings = consensusSettings;
this.stakeChainStore = stakeChainStore;
this.rewindDataIndexCache = rewindDataIndexCache;
this.blockStore = blockStore;
this.cancellationToken = (nodeLifetime == null) ? new CancellationTokenSource() : CancellationTokenSource.CreateLinkedTokenSource(nodeLifetime.ApplicationStopping);
this.cancellationToken = (nodeLifetime == null) ? new CancellationTokenSource(): CancellationTokenSource.CreateLinkedTokenSource(nodeLifetime.ApplicationStopping);
this.lockobj = new object();
this.cachedUtxoItems = new Dictionary<OutPoint, CacheItem>();
this.cacheBalancesByDestination = new Dictionary<TxDestination, Dictionary<uint, long>>();
this.performanceCounter = new CachePerformanceCounter(this.dateTimeProvider);
this.lastCacheFlushTime = this.dateTimeProvider.GetUtcNow();
this.cachedRewindData = new Dictionary<int, RewindData>();
this.scriptAddressReader = scriptAddressReader;
this.addressIndexingEnabled = nodeSettings?.ConfigReader.GetOrDefault("addressindex", false) ?? false;
this.random = new Random();

this.MaxCacheSizeBytes = consensusSettings.MaxCoindbCacheInMB * 1024 * 1024;
Expand Down Expand Up @@ -200,7 +209,7 @@ public void Sync(ChainIndexer chainIndexer)

public void Initialize(ChainedHeader chainTip, ChainIndexer chainIndexer, IConsensusRuleEngine consensusRuleEngine)
{
this.coindb.Initialize(chainTip);
this.coindb.Initialize(this.addressIndexingEnabled);

Sync(chainIndexer);

Expand Down Expand Up @@ -469,10 +478,11 @@ public void Flush(bool force = true)

this.logger.LogDebug("Flushing {0} items.", modify.Count);

this.coindb.SaveChanges(modify, this.innerBlockHash, this.blockHash, this.cachedRewindData.Select(c => c.Value).ToList());
this.coindb.SaveChanges(modify, this.cacheBalancesByDestination, this.innerBlockHash, this.blockHash, this.cachedRewindData.Select(c => c.Value).ToList());

// All the cached utxos are now on disk so we can clear the cached entry list.
this.cachedUtxoItems.Clear();
this.cacheBalancesByDestination.Clear();
this.cacheSizeBytes = 0;

this.cachedRewindData.Clear();
Expand Down Expand Up @@ -564,6 +574,10 @@ public void SaveChanges(IList<UnspentOutput> outputs, HashHeightPair oldBlockHas
{
// DELETE COINS

// Record the UTXO as having been spent at this height.
if (cacheItem.Coins != null)
this.RecordBalanceChange(cacheItem.Coins.TxOut.ScriptPubKey, -cacheItem.Coins.TxOut.Value, (uint)nextBlockHash.Height);

// In cases of an output spent in the same block
// it wont exist in cash or in disk so its safe to remove it
if (cacheItem.Coins == null)
Expand Down Expand Up @@ -612,6 +626,9 @@ public void SaveChanges(IList<UnspentOutput> outputs, HashHeightPair oldBlockHas
{
// ADD COINS

// Update the balance.
this.RecordBalanceChange(output.Coins.TxOut.ScriptPubKey, output.Coins.TxOut.Value, output.Coins.Height);

if (cacheItem.Coins != null)
{
// Allow overrides.
Expand Down Expand Up @@ -693,6 +710,7 @@ public HashHeightPair Rewind(HashHeightPair target = null)

// All the cached utxos are now on disk so we can clear the cached entry list.
this.cachedUtxoItems.Clear();
this.cacheBalancesByDestination.Clear();
this.cacheSizeBytes = 0;
this.dirtyCacheCount = 0;

Expand Down Expand Up @@ -749,5 +767,62 @@ private void AddBenchStats(StringBuilder log)

this.latestPerformanceSnapShot = snapShot;
}

private void RecordBalanceChange(Script scriptPubKey, long satoshis, uint height)
{
if (!this.coindb.BalanceIndexingEnabled || scriptPubKey.Length == 0 || satoshis == 0)
return;

foreach (TxDestination txDestination in this.scriptAddressReader.GetDestinationFromScriptPubKey(this.network, scriptPubKey))
{
if (!this.cacheBalancesByDestination.TryGetValue(txDestination, out Dictionary<uint, long> value))
{
value = new Dictionary<uint, long>();
this.cacheBalancesByDestination[txDestination] = value;
}

if (!value.TryGetValue(height, out long balance))
balance = 0;

balance += satoshis;

value[height] = balance;
}
}

public IEnumerable<(uint, long)> GetBalance(TxDestination txDestination)
{
IEnumerable<(uint, long)> CachedBalances()
{
if (this.cacheBalancesByDestination.TryGetValue(txDestination, out Dictionary<uint, long> itemsByHeight))
{
long balance = 0;

foreach (uint height in itemsByHeight.Keys.OrderBy(k => k))
{
balance += itemsByHeight[height];
yield return (height, balance);
}
}
}

bool first = true;
foreach ((uint height, long satoshis) in this.coindb.GetBalance(txDestination))
{
if (first)
{
first = false;

foreach ((uint height2, long satoshis2) in CachedBalances().Reverse())
yield return (height2, satoshis2 + satoshis);
}

yield return (height, satoshis);
}

if (first)
foreach ((uint height2, long satoshis2) in CachedBalances().Reverse())
yield return (height2, satoshis2);
}
}
}
10 changes: 10 additions & 0 deletions src/Stratis.Bitcoin.Features.Consensus/CoinViews/CoinView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,15 @@ public interface ICoinView
/// </summary>
/// <param name="height">The height of the block.</param>
RewindData GetRewindData(int height);

/// <summary>
/// Returns a combination of (height, satoshis) values with the cumulative balance up to the corresponding height.
/// </summary>
/// <param name="txDestination">The destination value derived from the address being queried.</param>
/// <returns>A combination of (height, satoshis) values with the cumulative balance up to the corresponding height.</returns>
/// <remarks>Balance updates (even when nett 0) are delivered for every height at which transactions for the address
/// had been recorded and as such the returned heights can be used in conjunction with the block store to discover
/// all related transactions.</remarks>
IEnumerable<(uint height, long satoshis)> GetBalance(TxDestination txDestination);
}
}
Loading