diff --git a/src/Stratis.Bitcoin.Features.BlockStore.Tests/AddressIndexerTests.cs b/src/Stratis.Bitcoin.Features.BlockStore.Tests/AddressIndexerTests.cs index 8000b5427b..8bb9f0a27f 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore.Tests/AddressIndexerTests.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore.Tests/AddressIndexerTests.cs @@ -1,15 +1,24 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using LiteDB; using Microsoft.Extensions.DependencyInjection; using Moq; using NBitcoin; using Stratis.Bitcoin.Configuration; using Stratis.Bitcoin.Consensus; +using Stratis.Bitcoin.Consensus.Rules; using Stratis.Bitcoin.Controllers.Models; +using Stratis.Bitcoin.Database; using Stratis.Bitcoin.Features.BlockStore.AddressIndexing; +using Stratis.Bitcoin.Features.BlockStore.Repositories; +using Stratis.Bitcoin.Features.Consensus.CoinViews; +using Stratis.Bitcoin.Features.Consensus.Rules; +using Stratis.Bitcoin.Features.Consensus.Rules.CommonRules; +using Stratis.Bitcoin.Interfaces; using Stratis.Bitcoin.Networks; using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Tests.Common; @@ -26,22 +35,33 @@ public class AddressIndexerTests private readonly Mock consensusManagerMock; + private readonly ChainIndexer chainIndexer; + private readonly Network network; + private readonly IConsensusRuleEngine consensusRuleEngine; + private readonly ChainedHeader genesisHeader; public AddressIndexerTests() { this.network = new StraxMain(); + this.chainIndexer = new ChainIndexer(this.network); + var nodeSettings = new NodeSettings(this.network, args: new[] { "-addressindex", "-txindex" }); + var mockingServices = new ServiceCollection() .AddSingleton(this.network) - .AddSingleton(new StoreSettings(NodeSettings.Default(this.network)) - { - AddressIndex = true, - TxIndex = true - }) + .AddSingleton(nodeSettings) + .AddSingleton(nodeSettings.LoggerFactory) .AddSingleton(new DataFolder(TestBase.CreateTestDir(this))) - .AddSingleton(new ChainIndexer(this.network)) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(typeof(BlockRepository).GetConstructors().First(c => c.GetParameters().Any(p => p.ParameterType == typeof(DataFolder)))) + .AddSingleton() + .AddSingleton(typeof(Coindb).GetConstructors().First(c => c.GetParameters().Any(p => p.ParameterType == typeof(DataFolder)))) + .AddSingleton() + .AddSingleton(this.chainIndexer) .AddSingleton() .AddSingleton(); @@ -49,7 +69,15 @@ public AddressIndexerTests() this.addressIndexer = mockingContext.GetService(); this.genesisHeader = mockingContext.GetService().GetHeader(0); + + var rulesContainer = mockingContext.GetService(); + rulesContainer.FullValidationRules.Add(Activator.CreateInstance(typeof(LoadCoinviewRule)) as FullValidationConsensusRule); + rulesContainer.FullValidationRules.Add(Activator.CreateInstance(typeof(SaveCoinviewRule)) as FullValidationConsensusRule); + rulesContainer.FullValidationRules.Add(Activator.CreateInstance(typeof(StraxCoinviewRule)) as FullValidationConsensusRule); + rulesContainer.FullValidationRules.Add(Activator.CreateInstance(typeof(SetActivationDeploymentsFullValidationRule)) as FullValidationConsensusRule); + this.consensusManagerMock = mockingContext.GetService>(); + this.consensusRuleEngine = mockingContext.GetService(); } [Fact] @@ -64,7 +92,7 @@ public void CanInitializeAndDispose() [Fact] public void CanIndexAddresses() { - List headers = ChainedHeadersHelper.CreateConsecutiveHeaders(100, null, false, null, this.network); + List headers = ChainedHeadersHelper.CreateConsecutiveHeaders(100, null, false, null, this.network, chainIndexer: this.chainIndexer); this.consensusManagerMock.Setup(x => x.Tip).Returns(() => headers.Last()); Script p2pk1 = this.GetRandomP2PKScript(out string address1); @@ -106,7 +134,7 @@ public void CanIndexAddresses() tx.Inputs.Add(new TxIn(new OutPoint(block5.Transactions.First().GetHash(), 0))); var block10 = new Block() { Transactions = new List() { tx } }; - this.consensusManagerMock.Setup(x => x.GetBlockData(It.IsAny())).Returns((uint256 hash) => + ChainedHeaderBlock GetChainedHeaderBlock(uint256 hash) { ChainedHeader header = headers.SingleOrDefault(x => x.HashBlock == hash); @@ -123,8 +151,26 @@ public void CanIndexAddresses() } return new ChainedHeaderBlock(new Block(), header); + } + + this.consensusManagerMock.Setup(x => x.GetBlockData(It.IsAny())).Returns((uint256 hash) => + { + return GetChainedHeaderBlock(hash); }); + this.consensusManagerMock.Setup(x => x.GetBlockData(It.IsAny>())).Returns((List hashes) => + { + return hashes.Select(h => GetChainedHeaderBlock(h)).ToArray(); + }); + + this.consensusManagerMock.Setup(x => x.GetBlocksAfterBlock(It.IsAny(), It.IsAny(), It.IsAny())).Returns((ChainedHeader header, int size, CancellationTokenSource token) => + { + return headers.Where(h => h.Height > header.Height).Select(h => GetChainedHeaderBlock(h.HashBlock)).ToArray(); + }); + + this.consensusManagerMock.Setup(x => x.ConsensusRules).Returns(this.consensusRuleEngine); + + this.consensusRuleEngine.Initialize(headers.Last(), this.consensusManagerMock.Object); this.addressIndexer.Initialize(); TestBase.WaitLoop(() => this.addressIndexer.IndexerTip == headers.Last()); @@ -137,7 +183,7 @@ public void CanIndexAddresses() // Now trigger rewind to see if indexer can handle reorgs. ChainedHeader forkPoint = headers.Single(x => x.Height == 8); - List headersFork = ChainedHeadersHelper.CreateConsecutiveHeaders(100, forkPoint, false, null, this.network); + List headersFork = ChainedHeadersHelper.CreateConsecutiveHeaders(100, forkPoint, false, null, this.network, chainIndexer: this.chainIndexer); this.consensusManagerMock.Setup(x => x.GetBlockData(It.IsAny())).Returns((uint256 hash) => { @@ -146,6 +192,11 @@ public void CanIndexAddresses() return new ChainedHeaderBlock(new Block(), headerFork); }); + this.consensusManagerMock.Setup(x => x.GetBlocksAfterBlock(It.IsAny(), It.IsAny(), It.IsAny())).Returns((ChainedHeader header, int size, CancellationTokenSource token) => + { + return headersFork.Where(h => h.Height > header.Height).Select(h => new ChainedHeaderBlock(new Block(), h)).ToArray(); + }); + this.consensusManagerMock.Setup(x => x.Tip).Returns(() => headersFork.Last()); TestBase.WaitLoop(() => this.addressIndexer.IndexerTip == headersFork.Last()); diff --git a/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs b/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs index bd05214170..192c0a3085 100644 --- a/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs +++ b/src/Stratis.Bitcoin.Features.BlockStore/AddressIndexing/AddressIndexer.cs @@ -291,7 +291,7 @@ private async Task IndexAddressesContinuouslyAsync() if (prefetchedBlock != null && prefetchedBlock.ChainedHeader == nextHeader) blockToProcess = prefetchedBlock.Block; else - blockToProcess = this.consensusManager.GetBlockData(nextHeader.HashBlock).Block; + blockToProcess = this.consensusManager.GetBlockData(nextHeader.HashBlock)?.Block; if (blockToProcess == null) { diff --git a/src/Stratis.Bitcoin.Features.Consensus/CoinViews/CachedCoinView.cs b/src/Stratis.Bitcoin.Features.Consensus/CoinViews/CachedCoinView.cs index fcc3459ef6..d61572e039 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/CoinViews/CachedCoinView.cs +++ b/src/Stratis.Bitcoin.Features.Consensus/CoinViews/CachedCoinView.cs @@ -5,11 +5,12 @@ 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.Features.Consensus.ProvenBlockHeaders; using Stratis.Bitcoin.Features.Consensus.Rules.CommonRules; -using Stratis.Bitcoin.Interfaces; +using Stratis.Bitcoin.Primitives; using Stratis.Bitcoin.Utilities; using Stratis.Bitcoin.Utilities.Extensions; using TracerAttributes; @@ -128,16 +129,16 @@ public long GetScriptSize private DateTime lastCacheFlushTime; private readonly Network network; private readonly IDateTimeProvider dateTimeProvider; - private readonly IBlockStore blockStore; private readonly CancellationTokenSource cancellationToken; + private IConsensusManager consensusManager; private readonly ConsensusSettings consensusSettings; private readonly ChainIndexer chainIndexer; private CachePerformanceSnapshot latestPerformanceSnapShot; private readonly Random random; - public CachedCoinView(Network network, ICoindb coindb, IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, INodeStats nodeStats, ConsensusSettings consensusSettings, ChainIndexer chainIndexer, - StakeChainStore stakeChainStore = null, IRewindDataIndexCache rewindDataIndexCache = null, IBlockStore blockStore = null, INodeLifetime nodeLifetime = null) + public CachedCoinView(Network network, ICoindb coindb, IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, INodeStats nodeStats, ConsensusSettings consensusSettings, ChainIndexer chainIndexer, + StakeChainStore stakeChainStore = null, IRewindDataIndexCache rewindDataIndexCache = null, INodeLifetime nodeLifetime = null, NodeSettings nodeSettings = null) { Guard.NotNull(coindb, nameof(CachedCoinView.coindb)); @@ -149,7 +150,6 @@ public CachedCoinView(Network network, ICoindb coindb, IDateTimeProvider dateTim this.chainIndexer = chainIndexer; this.stakeChainStore = stakeChainStore; this.rewindDataIndexCache = rewindDataIndexCache; - this.blockStore = blockStore; this.cancellationToken = (nodeLifetime == null) ? new CancellationTokenSource() : CancellationTokenSource.CreateLinkedTokenSource(nodeLifetime.ApplicationStopping); this.lockobj = new object(); this.cachedUtxoItems = new Dictionary(); @@ -178,34 +178,34 @@ public void Sync() Flush(); - ChainedHeader fork = this.chainIndexer[coinViewTip.Hash]; - if (fork == null) + if (coinViewTip.Height > this.chainIndexer.Height || this.chainIndexer[coinViewTip.Hash] == null) { - // Determine the last usable height. - int height = BinarySearch.BinaryFindFirst(h => (h > this.chainIndexer.Height) || this.GetRewindData(h).PreviousBlockHash.Hash != this.chainIndexer[h].Previous.HashBlock, 0, coinViewTip.Height + 1) - 1; - fork = this.chainIndexer[(height < 0) ? coinViewTip.Height : height]; - } + // The coinview tip is above the chain height or on a fork. + // Determine the first unusable height by finding the first rewind data that is not on the consensus chain. + int unusableHeight = BinarySearch.BinaryFindFirst(h => (h > this.chainIndexer.Height) || (this.GetRewindData(h)?.PreviousBlockHash.Hash != this.chainIndexer[h].Previous.HashBlock), 2, coinViewTip.Height - 1); + ChainedHeader fork = this.chainIndexer[unusableHeight - 2]; - while (coinViewTip.Height != fork.Height) - { - if ((coinViewTip.Height % 100) == 0) - this.logger.LogInformation("Rewinding coin view from '{0}' to {1}.", coinViewTip, fork); + while (coinViewTip.Height != fork.Height) + { + if ((coinViewTip.Height % 100) == 0) + this.logger.LogInformation("Rewinding coin view from '{0}' to {1}.", coinViewTip, fork); + + // If the block store was initialized behind the coin view's tip, rewind it to on or before it's tip. + // The node will complete loading before connecting to peers so the chain will never know that a reorg happened. + coinViewTip = this.coindb.Rewind(new HashHeightPair(fork)); + }; + + this.blockHash = coinViewTip; + this.innerBlockHash = this.blockHash; + } - // If the block store was initialized behind the coin view's tip, rewind it to on or before it's tip. - // The node will complete loading before connecting to peers so the chain will never know that a reorg happened. - coinViewTip = this.coindb.Rewind(new HashHeightPair(fork)); - }; + CatchUp(); } } - public void Initialize(IConsensusManager consensusManager) + private void CatchUp() { ChainedHeader chainTip = this.chainIndexer.Tip; - - this.coindb.Initialize(); - - Sync(); - HashHeightPair coinViewTip = this.coindb.GetTipHash(); // If the coin view is behind the block store then catch up from the block store. @@ -213,15 +213,20 @@ public void Initialize(IConsensusManager consensusManager) { try { - IConsensusRuleEngine consensusRuleEngine = consensusManager.ConsensusRules; + IConsensusRuleEngine consensusRuleEngine = this.consensusManager.ConsensusRules; var loadCoinViewRule = consensusRuleEngine.GetRule(); var saveCoinViewRule = consensusRuleEngine.GetRule(); var coinViewRule = consensusRuleEngine.GetRule(); var deploymentsRule = consensusRuleEngine.GetRule(); - foreach ((ChainedHeader chainedHeader, Block block) in this.blockStore.BatchBlocksFrom(this.chainIndexer[coinViewTip.Hash], this.chainIndexer, this.cancellationToken, batchSize: 1000)) + foreach (ChainedHeaderBlock chb in this.consensusManager.GetBlocksAfterBlock(this.chainIndexer[coinViewTip.Hash], 1000, this.cancellationToken)) { + ChainedHeader chainedHeader = chb?.ChainedHeader; + if (chainedHeader == null) + break; + + Block block = chb.Block; if (block == null) break; @@ -244,7 +249,7 @@ public void Initialize(IConsensusManager consensusManager) coinViewRule.RunAsync(utxoRuleContext).ConfigureAwait(false).GetAwaiter().GetResult(); // Saves the changes to the coinview. - saveCoinViewRule.RunAsync(utxoRuleContext).ConfigureAwait(false).GetAwaiter().GetResult(); + saveCoinViewRule.RunAsync(utxoRuleContext).ConfigureAwait(false).GetAwaiter().GetResult(); } } finally @@ -258,6 +263,15 @@ public void Initialize(IConsensusManager consensusManager) } } } + } + + public void Initialize(IConsensusManager consensusManager) + { + this.consensusManager = consensusManager; + + this.coindb.Initialize(); + + Sync(); this.logger.LogInformation("Coin view initialized at '{0}'.", this.coindb.GetTipHash()); } @@ -485,7 +499,7 @@ public void Flush(bool force = true) this.cachedRewindData.Clear(); this.rewindDataSizeBytes = 0; this.dirtyCacheCount = 0; - this.innerBlockHash = this.blockHash; + this.innerBlockHash = this.blockHash; this.lastCacheFlushTime = this.dateTimeProvider.GetUtcNow(); } } diff --git a/src/Stratis.Bitcoin.Features.Consensus/CoinViews/Coindb/Coindb.cs b/src/Stratis.Bitcoin.Features.Consensus/CoinViews/Coindb/Coindb.cs index 87fdf73e57..e04c729ee8 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/CoinViews/Coindb/Coindb.cs +++ b/src/Stratis.Bitcoin.Features.Consensus/CoinViews/Coindb/Coindb.cs @@ -50,17 +50,11 @@ namespace Stratis.Bitcoin.Features.Consensus.CoinViews public Coindb(Network network, DataFolder dataFolder, IDateTimeProvider dateTimeProvider, INodeStats nodeStats, DBreezeSerializer dBreezeSerializer) - : this(network, dataFolder.CoindbPath, dateTimeProvider, nodeStats, dBreezeSerializer) - { - } - - public Coindb(Network network, string dataFolder, IDateTimeProvider dateTimeProvider, - INodeStats nodeStats, DBreezeSerializer dBreezeSerializer) { Guard.NotNull(network, nameof(network)); - Guard.NotEmpty(dataFolder, nameof(dataFolder)); + Guard.NotNull(dataFolder, nameof(dataFolder)); - this.dataFolder = dataFolder; + this.dataFolder = dataFolder.CoindbPath; this.dBreezeSerializer = dBreezeSerializer; this.logger = LogManager.GetCurrentClassLogger(); this.network = network; @@ -70,14 +64,13 @@ public Coindb(Network network, string dataFolder, IDateTimeProvider dateTimeProv nodeStats.RegisterStats(this.AddBenchStats, StatsType.Benchmark, this.GetType().Name, 400); } + /// public void Initialize() { // Open a connection to a new DB and create if not found this.coinDb = new T(); this.coinDb.Open(this.dataFolder); - EndiannessFix(); - EnsureCoinDatabaseIntegrity(); Block genesis = this.network.GetGenesis(); @@ -156,6 +149,8 @@ private void EnsureCoinDatabaseIntegrity() { this.logger.LogInformation("Checking coin database integrity..."); + this.EndiannessFix(); + if (this.GetTipHash() == null) { this.logger.LogInformation($"Rebuilding coin database that has no tip information."); @@ -211,9 +206,12 @@ public FetchCoinsResponse FetchCoins(OutPoint[] utxos) public void SaveChanges(IList unspentOutputs, HashHeightPair oldBlockHash, HashHeightPair nextBlockHash, List rewindDataList = null) { + if (unspentOutputs.Count == 0 && rewindDataList.Count == 0) + return; + int insertedEntities = 0; - using (var batch = this.coinDb.GetWriteBatch(coinsTable, rewindTable, blockTable)) + using (var batch = this.coinDb.GetReadWriteBatch(coinsTable, rewindTable, blockTable)) { using (new StopwatchDisposable(o => this.performanceCounter.AddInsertTime(o))) { @@ -290,6 +288,20 @@ public int GetMinRewindHeight() } } + private bool TryGetCoins(ReadWriteBatch readWriteBatch, byte[] key, out Coins coins) + { + byte[] row2 = readWriteBatch.Get(coinsTable, key); + if (row2 == null) + { + coins = null; + return false; + } + + coins = this.dBreezeSerializer.Deserialize(row2); + + return true; + } + public HashHeightPair Rewind(HashHeightPair target) { HashHeightPair current = this.GetTipHash(); @@ -300,23 +312,32 @@ private HashHeightPair RewindInternal(int startHeight, HashHeightPair target) { HashHeightPair res = null; - using (var batch = this.coinDb.GetWriteBatch(coinsTable, rewindTable, blockTable)) + using (var batch = this.coinDb.GetReadWriteBatch(coinsTable, rewindTable, blockTable)) { for (int height = startHeight; height > (target?.Height ?? (startHeight - 1)) && height > (startHeight - MaxRewindBatchSize); height--) { - byte[] row = this.coinDb.Get(rewindTable, BitConverter.GetBytes(height).Reverse().ToArray()); + byte[] rowKey = BitConverter.GetBytes(height).Reverse().ToArray(); + byte[] row = this.coinDb.Get(rewindTable, rowKey); if (row == null) throw new InvalidOperationException($"No rewind data found for block at height {height}."); - batch.Delete(rewindTable, BitConverter.GetBytes(height)); + batch.Delete(rewindTable, rowKey); var rewindData = this.dBreezeSerializer.Deserialize(row); foreach (OutPoint outPoint in rewindData.OutputsToRemove) { - this.logger.LogDebug("Outputs of outpoint '{0}' will be removed.", outPoint); - batch.Delete(coinsTable, outPoint.ToBytes()); + byte[] key = outPoint.ToBytes(); + if (this.TryGetCoins(batch, key, out Coins coins)) + { + this.logger.LogDebug("Outputs of outpoint '{0}' will be removed.", outPoint); + batch.Delete(coinsTable, key); + } + else + { + throw new InvalidOperationException(string.Format("Outputs of outpoint '{0}' were not found when attempting removal.", outPoint)); + } } foreach (RewindDataOutput rewindDataOutput in rewindData.OutputsToRestore) diff --git a/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/LoadCoinviewRule.cs b/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/LoadCoinviewRule.cs index 9c70066530..3c42d49ee7 100644 --- a/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/LoadCoinviewRule.cs +++ b/src/Stratis.Bitcoin.Features.Consensus/Rules/CommonRules/LoadCoinviewRule.cs @@ -25,7 +25,6 @@ public override Task RunAsync(RuleContext context) // unless the coinview threshold is reached. this.Logger.LogDebug("Saving coinview changes."); var utxoRuleContext = context as UtxoRuleContext; - this.PowParent.UtxoSet.Sync(); this.PowParent.UtxoSet.SaveChanges(utxoRuleContext.UnspentOutputSet.GetCoins(), new HashHeightPair(oldBlock), new HashHeightPair(nextBlock)); return Task.CompletedTask; @@ -67,7 +66,7 @@ public override Task RunAsync(RuleContext context) { // Check that the current block has not been reorged. // Catching a reorg at this point will not require a rewind. - if (context.ValidationContext.BlockToValidate.Header.HashPrevBlock != this.PowParent.UtxoSet.GetTipHash().Hash) + if (context.ValidationContext.ChainedHeaderToValidate.Previous.HashBlock != this.PowParent.UtxoSet.GetTipHash().Hash) { this.Logger.LogDebug("Reorganization detected."); ConsensusErrors.InvalidPrevTip.Throw(); diff --git a/src/Stratis.Bitcoin.IntegrationTests/NodeContext.cs b/src/Stratis.Bitcoin.IntegrationTests/NodeContext.cs index b503f3ea3d..58f39ef1eb 100644 --- a/src/Stratis.Bitcoin.IntegrationTests/NodeContext.cs +++ b/src/Stratis.Bitcoin.IntegrationTests/NodeContext.cs @@ -28,7 +28,7 @@ public NodeContext(object caller, string name, Network network) this.FolderName = TestBase.CreateTestDir(caller, name); var dateTimeProvider = new DateTimeProvider(); var serializer = new DBreezeSerializer(this.Network.Consensus.ConsensusFactory); - this.Coindb = new Coindb(network, this.FolderName, dateTimeProvider, new NodeStats(dateTimeProvider, NodeSettings.Default(network), new Mock().Object), serializer); + this.Coindb = new Coindb(network, new DataFolder(this.FolderName), dateTimeProvider, new NodeStats(dateTimeProvider, NodeSettings.Default(network), new Mock().Object), serializer); this.Coindb.Initialize(); this.cleanList = new List { (IDisposable)this.Coindb }; } @@ -66,7 +66,7 @@ public void ReloadPersistentCoinView() this.cleanList.Remove((IDisposable)this.Coindb); var dateTimeProvider = new DateTimeProvider(); var serializer = new DBreezeSerializer(this.Network.Consensus.ConsensusFactory); - this.Coindb = new Coindb(this.Network, this.FolderName, dateTimeProvider, new NodeStats(dateTimeProvider, NodeSettings.Default(this.Network), new Mock().Object), serializer); + this.Coindb = new Coindb(this.Network, new DataFolder(this.FolderName), dateTimeProvider, new NodeStats(dateTimeProvider, NodeSettings.Default(this.Network), new Mock().Object), serializer); this.Coindb.Initialize(); this.cleanList.Add((IDisposable)this.Coindb); diff --git a/src/Stratis.Bitcoin/Consensus/IConsensusRuleEngine.cs b/src/Stratis.Bitcoin/Consensus/IConsensusRuleEngine.cs index a8bb3b5d78..2ff5f1167d 100644 --- a/src/Stratis.Bitcoin/Consensus/IConsensusRuleEngine.cs +++ b/src/Stratis.Bitcoin/Consensus/IConsensusRuleEngine.cs @@ -19,6 +19,7 @@ public interface IConsensusRuleEngine : IDisposable /// Initialize the rules engine. /// /// Last common header between chain repository and block store if it's available. + /// The consensus manager. void Initialize(ChainedHeader chainTip, IConsensusManager consensusManager); /// diff --git a/src/Stratis.Bitcoin/Database/ReadWriteBatch.cs b/src/Stratis.Bitcoin/Database/ReadWriteBatch.cs new file mode 100644 index 0000000000..6964019b48 --- /dev/null +++ b/src/Stratis.Bitcoin/Database/ReadWriteBatch.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Linq; +using NBitcoin; + +namespace Stratis.Bitcoin.Database +{ + /// + /// A batch that can be used to record changes that can be applied atomically. + /// + /// The supplied method will immediately reflect any changes that have + /// been made or retrieve the value from the underlying database. In contrast the database method + /// will only show the changes after the method is called. + public class ReadWriteBatch : IDbBatch + { + private readonly IDb db; + private readonly IDbBatch batch; + private Dictionary cache; + + public ReadWriteBatch(IDb db, params byte[] tables) + { + this.db = db; + this.batch = db.GetWriteBatch(tables); + this.cache = new Dictionary(new ByteArrayComparer()); + } + + /// + /// Records a value that will be written to the database when the method is invoked. + /// + /// The table key that identifies the value to be updated. + /// The value to be written to the table. + /// This class for fluent operations. + public IDbBatch Put(byte[] key, byte[] value) + { + this.cache[key] = value; + return this.batch.Put(key, value); + } + + /// + /// Records a value that will be written to the database when the method is invoked. + /// + /// The table that will be updated. + /// The table key that identifies the value to be updated. + /// The value to be written to the table. + /// This interface for fluent operations. + public IDbBatch Put(byte table, byte[] key, byte[] value) + { + this.cache[new byte[] { table }.Concat(key).ToArray()] = value; + return this.batch.Put(table, key, value); + } + + /// + /// Records a key that will be deleted from the database when the method is invoked. + /// + /// The table key that will be removed. + /// This interface for fluent operations. + public IDbBatch Delete(byte[] key) + { + this.cache[key] = null; + return this.batch.Delete(key); + } + + /// + /// Records a key that will be deleted from the database when the method is invoked. + /// + /// The table that will be updated. + /// The table key that will be removed. + /// This interface for fluent operations. + public IDbBatch Delete(byte table, byte[] key) + { + this.cache[new byte[] { table }.Concat(key).ToArray()] = null; + return this.batch.Delete(table, key); + } + + /// + /// Returns any changes that have been made to the batch or retrieves the value from the underlying database.. + /// + /// The table key of the value to retrieve. + /// This interface for fluent operations. + public byte[] Get(byte[] key) + { + if (this.cache.TryGetValue(key, out byte[] value)) + return value; + + return this.db.Get(key); + } + + /// + /// Returns any changes that have been made to the batch or retrieves the value from the underlying database.. + /// + /// The table of the value to be retrieved. + /// The table key of the value to retrieve. + /// This interface for fluent operations. + public byte[] Get(byte table, byte[] key) + { + if (this.cache.TryGetValue(new byte[] { table }.Concat(key).ToArray(), out byte[] value)) + return value; + + return this.db.Get(table, key); + } + + /// + /// Writes the recorded changes to the database. + /// + public void Write() + { + this.batch.Write(); + } + + public void Dispose() + { + this.batch.Dispose(); + } + } + + /// + /// Extension methods that build on the interface. + /// + public static class IDbExt + { + /// + /// Gets a . + /// + /// The database to get the batch for. + /// The . + public static ReadWriteBatch GetReadWriteBatch(this IDb db, params byte[] tables) + { + return new ReadWriteBatch(db, tables); + } + } +} \ No newline at end of file