Skip to content

Commit

Permalink
Ensure coin view database integrity (#719)
Browse files Browse the repository at this point in the history
  • Loading branch information
fassadlr authored Sep 27, 2021
1 parent 117a2fa commit 1ba5981
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 81 deletions.
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();
this.coindb.Initialize(new ChainedHeader(this.network.GetGenesis().Header, this.network.GenesisHash, 0));

this.chainIndexer = new ChainIndexer(this.network);
this.stakeChainStore = new StakeChainStore(this.network, this.chainIndexer, (IStakedb)this.coindb, this.loggerFactory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public DBreezeCoindb(Network network, string folder, IDateTimeProvider dateTimeP
nodeStats.RegisterStats(this.AddBenchStats, StatsType.Benchmark, this.GetType().Name, 300);
}

public void Initialize()
public void Initialize(ChainedHeader chainTip)
{
Block genesis = this.network.GetGenesis();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ namespace Stratis.Bitcoin.Features.Consensus.CoinViews
/// </summary>
public interface ICoindb
{
/// <summary>
/// Initialize the coindb.
/// </summary>
void Initialize();
/// <summary> Initialize the coin database.</summary>
/// <param name="chainTip">The current chain's tip.</param>
void Initialize(ChainedHeader chainTip);

/// <summary>
/// Retrieves the block hash of the current tip of the coinview.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using System.Linq;
using System.Text;
using LevelDB;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NLog;
using Stratis.Bitcoin.Configuration;
using Stratis.Bitcoin.Utilities;

Expand Down Expand Up @@ -45,35 +45,36 @@ public class LevelDbCoindb : ICoindb, IStakedb, IDisposable
private readonly DBreezeSerializer dBreezeSerializer;

public LevelDbCoindb(Network network, DataFolder dataFolder, IDateTimeProvider dateTimeProvider,
ILoggerFactory loggerFactory, INodeStats nodeStats, DBreezeSerializer dBreezeSerializer)
: this(network, dataFolder.CoindbPath, dateTimeProvider, loggerFactory, nodeStats, dBreezeSerializer)
INodeStats nodeStats, DBreezeSerializer dBreezeSerializer)
: this(network, dataFolder.CoindbPath, dateTimeProvider, nodeStats, dBreezeSerializer)
{
}

public LevelDbCoindb(Network network, string dataFolder, IDateTimeProvider dateTimeProvider,
ILoggerFactory loggerFactory, INodeStats nodeStats, DBreezeSerializer dBreezeSerializer)
INodeStats nodeStats, DBreezeSerializer dBreezeSerializer)
{
Guard.NotNull(network, nameof(network));
Guard.NotEmpty(dataFolder, nameof(dataFolder));

this.dataFolder = dataFolder;
this.dBreezeSerializer = dBreezeSerializer;
this.logger = loggerFactory.CreateLogger(this.GetType().FullName);
this.logger = LogManager.GetCurrentClassLogger();
this.network = network;
this.performanceCounter = new BackendPerformanceCounter(dateTimeProvider);

if (nodeStats.DisplayBenchStats)
nodeStats.RegisterStats(this.AddBenchStats, StatsType.Benchmark, this.GetType().Name, 400);
}

public void Initialize()
public void Initialize(ChainedHeader chainTip)
{
// Open a connection to a new DB and create if not found
var options = new Options { CreateIfMissing = true };
this.leveldb = new DB(options, this.dataFolder);

// Check if key bytes are in the wrong endian order.
HashHeightPair current = this.GetTipHash();

if (current != null)
{
byte[] row = this.leveldb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(current.Height)).ToArray());
Expand All @@ -84,7 +85,7 @@ public void Initialize()
byte[] row2 = (current.Height > 1) ? this.leveldb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(current.Height - 1)).ToArray()) : new byte[] { };
if (row2 != null)
{
this.logger.LogInformation("Fixing the coin db.");
this.logger.Info("Fixing the coin db.");

var rows = new Dictionary<int, byte[]>();

Expand Down Expand Up @@ -125,12 +126,44 @@ public void Initialize()
}
}

EnsureCoinDatabaseIntegrity(chainTip);

Block genesis = this.network.GetGenesis();

if (this.GetTipHash() == null)
this.SetBlockHash(new HashHeightPair(genesis.GetHash(), 0));

this.logger.LogInformation("Coinview initialized with tip '{0}'.", this.persistedCoinviewTip);
this.logger.Info("Coinview initialized with tip '{0}'.", this.persistedCoinviewTip);
}

private void EnsureCoinDatabaseIntegrity(ChainedHeader chainTip)
{
this.logger.Info("Checking coin database integrity...");

var heightToCheck = chainTip.Height;

// Find the height up to where rewind data is stored above chain tip.
do
{
heightToCheck += 1;

byte[] row = this.leveldb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(heightToCheck).Reverse()).ToArray());
if (row == null)
break;

} while (true);

using (var batch = new WriteBatch())
{
for (int height = heightToCheck - 1; height > chainTip.Height; height--)
{
this.logger.Info($"Fixing coin database, deleting rewind data at height {height} above tip '{chainTip}'.");

RewindInternal(batch, height);
}
}

this.logger.Info("Coin database integrity good.");
}

private void SetBlockHash(HashHeightPair nextBlockHash)
Expand Down Expand Up @@ -167,7 +200,7 @@ public FetchCoinsResponse FetchCoins(OutPoint[] utxos)
byte[] row = this.leveldb.Get(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray());
Coins outputs = row != null ? this.dBreezeSerializer.Deserialize<Coins>(row) : null;

this.logger.LogDebug("Outputs for '{0}' were {1}.", outPoint, outputs == null ? "NOT loaded" : "loaded");
this.logger.Debug("Outputs for '{0}' were {1}.", outPoint, outputs == null ? "NOT loaded" : "loaded");

res.UnspentOutputs.Add(outPoint, new UnspentOutput(outPoint, outputs));
}
Expand All @@ -187,7 +220,7 @@ public void SaveChanges(IList<UnspentOutput> unspentOutputs, HashHeightPair oldB
HashHeightPair current = this.GetTipHash();
if (current != oldBlockHash)
{
this.logger.LogTrace("(-)[BLOCKHASH_MISMATCH]");
this.logger.Trace("(-)[BLOCKHASH_MISMATCH]");
throw new InvalidOperationException("Invalid oldBlockHash");
}

Expand All @@ -198,7 +231,7 @@ public void SaveChanges(IList<UnspentOutput> unspentOutputs, HashHeightPair oldB
{
if (coin.Coins == null)
{
this.logger.LogDebug("Outputs of transaction ID '{0}' are prunable and will be removed from the database.", coin.OutPoint);
this.logger.Debug("Outputs of transaction ID '{0}' are prunable and will be removed from the database.", coin.OutPoint);
batch.Delete(new byte[] { coinsTable }.Concat(coin.OutPoint.ToBytes()).ToArray());
}
else
Expand All @@ -212,7 +245,7 @@ public void SaveChanges(IList<UnspentOutput> unspentOutputs, HashHeightPair oldB
for (int i = 0; i < toInsert.Count; i++)
{
var coin = toInsert[i];
this.logger.LogDebug("Outputs of transaction ID '{0}' are NOT PRUNABLE and will be inserted into the database. {1}/{2}.", coin.OutPoint, i, toInsert.Count);
this.logger.Debug("Outputs of transaction ID '{0}' are NOT PRUNABLE and will be inserted into the database. {1}/{2}.", coin.OutPoint, i, toInsert.Count);

batch.Put(new byte[] { coinsTable }.Concat(coin.OutPoint.ToBytes()).ToArray(), this.dBreezeSerializer.Serialize(coin.Coins));
}
Expand All @@ -223,7 +256,7 @@ public void SaveChanges(IList<UnspentOutput> unspentOutputs, HashHeightPair oldB
{
var nextRewindIndex = rewindData.PreviousBlockHash.Height + 1;

this.logger.LogDebug("Rewind state #{0} created.", nextRewindIndex);
this.logger.Debug("Rewind state #{0} created.", nextRewindIndex);

batch.Put(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(nextRewindIndex).Reverse()).ToArray(), this.dBreezeSerializer.Serialize(rewindData));
}
Expand Down Expand Up @@ -261,40 +294,41 @@ public int GetMinRewindHeight()
/// <inheritdoc />
public HashHeightPair Rewind()
{
HashHeightPair res = null;
using (var batch = new WriteBatch())
{
HashHeightPair current = this.GetTipHash();
return RewindInternal(batch, current.Height);
}
}

byte[] row = this.leveldb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(current.Height).Reverse()).ToArray());

if (row == null)
throw new InvalidOperationException($"No rewind data found for block `{current}`");
private HashHeightPair RewindInternal(WriteBatch batch, int height)
{
byte[] row = this.leveldb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(height).Reverse()).ToArray());

batch.Delete(BitConverter.GetBytes(current.Height));
if (row == null)
throw new InvalidOperationException($"No rewind data found for block at height {height}.");

var rewindData = this.dBreezeSerializer.Deserialize<RewindData>(row);
batch.Delete(BitConverter.GetBytes(height));

foreach (OutPoint outPoint in rewindData.OutputsToRemove)
{
this.logger.LogDebug("Outputs of outpoint '{0}' will be removed.", outPoint);
batch.Delete(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray());
}
var rewindData = this.dBreezeSerializer.Deserialize<RewindData>(row);

foreach (RewindDataOutput rewindDataOutput in rewindData.OutputsToRestore)
{
this.logger.LogDebug("Outputs of outpoint '{0}' will be restored.", rewindDataOutput.OutPoint);
batch.Put(new byte[] { coinsTable }.Concat(rewindDataOutput.OutPoint.ToBytes()).ToArray(), this.dBreezeSerializer.Serialize(rewindDataOutput.Coins));
}
foreach (OutPoint outPoint in rewindData.OutputsToRemove)
{
this.logger.Debug("Outputs of outpoint '{0}' will be removed.", outPoint);
batch.Delete(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray());
}

res = rewindData.PreviousBlockHash;
foreach (RewindDataOutput rewindDataOutput in rewindData.OutputsToRestore)
{
this.logger.Debug("Outputs of outpoint '{0}' will be restored.", rewindDataOutput.OutPoint);
batch.Put(new byte[] { coinsTable }.Concat(rewindDataOutput.OutPoint.ToBytes()).ToArray(), this.dBreezeSerializer.Serialize(rewindDataOutput.Coins));
}

this.leveldb.Write(batch, new WriteOptions() { Sync = true });
this.leveldb.Write(batch, new WriteOptions() { Sync = true });

this.SetBlockHash(rewindData.PreviousBlockHash);
}
this.SetBlockHash(rewindData.PreviousBlockHash);

return res;
return rewindData.PreviousBlockHash;
}

public RewindData GetRewindData(int height)
Expand Down Expand Up @@ -332,7 +366,7 @@ public void GetStake(IEnumerable<StakeItem> blocklist)
{
foreach (StakeItem blockStake in blocklist)
{
this.logger.LogTrace("Loading POS block hash '{0}' from the database.", blockStake.BlockId);
this.logger.Trace("Loading POS block hash '{0}' from the database.", blockStake.BlockId);
byte[] stakeRow = this.leveldb.Get(new byte[] { stakeTable }.Concat(blockStake.BlockId.ToBytes(false)).ToArray());

if (stakeRow != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public RocksDbCoindb(
nodeStats.RegisterStats(this.AddBenchStats, StatsType.Benchmark, this.GetType().Name, 400);
}

public void Initialize()
public void Initialize(ChainedHeader chainTip)
{
this.dbOptions = new DbOptions().SetCreateIfMissing(true);
this.rocksDb = RocksDb.Open(this.dbOptions, this.dataFolder);
Expand All @@ -62,6 +62,7 @@ public void Initialize()
if (current != null)
{
byte[] row = this.rocksDb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(current.Height)).ToArray());

// Fix the table if required.
if (row != null)
{
Expand Down Expand Up @@ -110,6 +111,8 @@ public void Initialize()
}
}

EnsureCoinDatabaseIntegrity(chainTip);

Block genesis = this.network.GetGenesis();

if (this.GetTipHash() == null)
Expand All @@ -118,6 +121,35 @@ public void Initialize()
this.logger.Info("Coinview initialized with tip '{0}'.", this.persistedCoinviewTip);
}

private void EnsureCoinDatabaseIntegrity(ChainedHeader chainTip)
{
this.logger.Info("Checking coin database integrity...");

var heightToCheck = chainTip.Height;

// Find the height up to where rewind data is stored above chain tip.
do
{
heightToCheck += 1;

byte[] row = this.rocksDb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(heightToCheck).Reverse()).ToArray());
if (row == null)
break;

} while (true);

using (var batch = new WriteBatch())
{
for (int height = heightToCheck - 1; height > chainTip.Height; height--)
{
this.logger.Info($"Fixing coin database, deleting rewind data at height {height} above tip '{chainTip}'.");
RewindInternal(batch, height);
}
}

this.logger.Info("Coin database integrity good.");
}

private void SetBlockHash(HashHeightPair nextBlockHash)
{
this.persistedCoinviewTip = nextBlockHash;
Expand Down Expand Up @@ -247,40 +279,41 @@ public int GetMinRewindHeight()
/// <inheritdoc />
public HashHeightPair Rewind()
{
HashHeightPair previousBlockHash = null;
using (var batch = new WriteBatch())
{
HashHeightPair current = this.GetTipHash();
return RewindInternal(batch, current.Height);
}
}

byte[] row = this.rocksDb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(current.Height).Reverse()).ToArray());

if (row == null)
throw new InvalidOperationException($"No rewind data found for block `{current}`");
private HashHeightPair RewindInternal(WriteBatch batch, int height)
{
byte[] row = this.rocksDb.Get(new byte[] { rewindTable }.Concat(BitConverter.GetBytes(height).Reverse()).ToArray());

batch.Delete(BitConverter.GetBytes(current.Height));
if (row == null)
throw new InvalidOperationException($"No rewind data found for block at height {height}.");

var rewindData = this.dBreezeSerializer.Deserialize<RewindData>(row);
batch.Delete(BitConverter.GetBytes(height));

foreach (OutPoint outPoint in rewindData.OutputsToRemove)
{
this.logger.Debug("Outputs of outpoint '{0}' will be removed.", outPoint);
batch.Delete(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray());
}
var rewindData = this.dBreezeSerializer.Deserialize<RewindData>(row);

foreach (RewindDataOutput rewindDataOutput in rewindData.OutputsToRestore)
{
this.logger.Debug("Outputs of outpoint '{0}' will be restored.", rewindDataOutput.OutPoint);
batch.Put(new byte[] { coinsTable }.Concat(rewindDataOutput.OutPoint.ToBytes()).ToArray(), this.dBreezeSerializer.Serialize(rewindDataOutput.Coins));
}
foreach (OutPoint outPoint in rewindData.OutputsToRemove)
{
this.logger.Debug("Outputs of outpoint '{0}' will be removed.", outPoint);
batch.Delete(new byte[] { coinsTable }.Concat(outPoint.ToBytes()).ToArray());
}

previousBlockHash = rewindData.PreviousBlockHash;
foreach (RewindDataOutput rewindDataOutput in rewindData.OutputsToRestore)
{
this.logger.Debug("Outputs of outpoint '{0}' will be restored.", rewindDataOutput.OutPoint);
batch.Put(new byte[] { coinsTable }.Concat(rewindDataOutput.OutPoint.ToBytes()).ToArray(), this.dBreezeSerializer.Serialize(rewindDataOutput.Coins));
}

this.rocksDb.Write(batch);
this.rocksDb.Write(batch);

this.SetBlockHash(rewindData.PreviousBlockHash);
}
this.SetBlockHash(rewindData.PreviousBlockHash);

return previousBlockHash;
return rewindData.PreviousBlockHash;
}

public RewindData GetRewindData(int height)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public RewindData GetRewindData(int height)
throw new NotImplementedException();
}

public void Initialize()
public void Initialize(ChainedHeader chainTip = null)
{
}
}
Expand Down
Loading

0 comments on commit 1ba5981

Please sign in to comment.