X-Git-Url: https://git.novaco.in/?a=blobdiff_plain;f=Novacoin%2FCBlockStore.cs;h=f0c830f66d32c86d3b3b3df8161ba504d17a3818;hb=2dd4edf513fe24ff6a6c2ff260598c2f72a5756f;hp=c6361218e6bb0ff4b2809d92c5ecd7aef440d685;hpb=514b68fc3103fca11d6d06ddb65ed8d2a14507e1;p=NovacoinLibrary.git diff --git a/Novacoin/CBlockStore.cs b/Novacoin/CBlockStore.cs index c636121..f0c830f 100644 --- a/Novacoin/CBlockStore.cs +++ b/Novacoin/CBlockStore.cs @@ -19,681 +19,18 @@ using System; using System.IO; -using System.Linq; using System.Collections.Concurrent; using SQLite.Net; -using SQLite.Net.Attributes; using SQLite.Net.Interop; using SQLite.Net.Platform.Generic; -using SQLiteNetExtensions.Attributes; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Text; +using System.Diagnostics.Contracts; +using System.Linq; namespace Novacoin { - [Table("BlockStorage")] - public class CBlockStoreItem : IBlockStorageItem - { - #region IBlockStorageItem - /// - /// Item ID in the database - /// - [PrimaryKey, AutoIncrement] - public long ItemID { get; set; } - - /// - /// PBKDF2+Salsa20 of block hash - /// - [Unique] - public byte[] Hash { get; set; } - - /// - /// Version of block schema - /// - [Column("nVersion")] - public uint nVersion { get; set; } - - /// - /// Previous block hash. - /// - [Column("prevHash")] - public byte[] prevHash { get; set; } - - /// - /// Merkle root hash. - /// - [Column("merkleRoot")] - public byte[] merkleRoot { get; set; } - - /// - /// Block timestamp. - /// - [Column("nTime")] - public uint nTime { get; set; } - - /// - /// Compressed difficulty representation. - /// - [Column("nBits")] - public uint nBits { get; set; } - - /// - /// Nonce counter. - /// - [Column("nNonce")] - public uint nNonce { get; set; } - - /// - /// Next block hash. - /// - [Column("nextHash")] - public byte[] nextHash { get; set; } - - /// - /// Block type flags - /// - [Column("BlockTypeFlag")] - public BlockType BlockTypeFlag { get; set; } - - /// - /// Stake modifier - /// - [Column("nStakeModifier")] - public long nStakeModifier { get; set; } - - /// - /// Proof-of-Stake hash - /// - [Column("hashProofOfStake")] - public byte[] hashProofOfStake { get; set; } - - /// - /// Stake generation outpoint. - /// - [Column("prevoutStake")] - public byte[] prevoutStake { get; set; } - - /// - /// Stake generation time. - /// - [Column("nStakeTime")] - public uint nStakeTime { get; set; } - - /// - /// Block height, encoded in VarInt format - /// - [Column("Height")] - public byte[] Height { get; set; } - - /// - /// Block position in file, encoded in VarInt format - /// - [Column("BlockPos")] - public byte[] BlockPos { get; set; } - - /// - /// Block size in bytes, encoded in VarInt format - /// - [Column("BlockSize")] - public byte[] BlockSize { get; set; } - #endregion - - /// - /// Accessor and mutator for BlockPos value. - /// - [Ignore] - public long nBlockPos - { - get { return (long)VarInt.DecodeVarInt(BlockPos); } - set { BlockPos = VarInt.EncodeVarInt(value); } - } - - /// - /// Accessor and mutator for BlockSize value. - /// - [Ignore] - public int nBlockSize - { - get { return (int)VarInt.DecodeVarInt(BlockSize); } - set { BlockSize = VarInt.EncodeVarInt(value); } - } - - /// - /// Accessor and mutator for Height value. - /// - [Ignore] - public uint nHeight - { - get { return (uint)VarInt.DecodeVarInt(Height); } - set { Height = VarInt.EncodeVarInt(value); } - } - - - /// - /// Fill database item with data from given block header. - /// - /// Block header - /// Header hash - public uint256 FillHeader(CBlockHeader header) - { - uint256 _hash = header.Hash; - - Hash = _hash; - - nVersion = header.nVersion; - prevHash = header.prevHash; - merkleRoot = header.merkleRoot; - nTime = header.nTime; - nBits = header.nBits; - nNonce = header.nNonce; - - return _hash; - } - - /// - /// Reconstruct block header from item data. - /// - public CBlockHeader BlockHeader - { - get - { - CBlockHeader header = new CBlockHeader(); - - header.nVersion = nVersion; - header.prevHash = prevHash; - header.merkleRoot = merkleRoot; - header.nTime = nTime; - header.nBits = nBits; - header.nNonce = nNonce; - - return header; - } - } - - /// - /// Read block from file. - /// - /// Stream with read access. - /// CBlock reference. - /// Result - public bool ReadFromFile(ref Stream reader, out CBlock block) - { - var buffer = new byte[nBlockSize]; - block = null; - - try - { - reader.Seek(nBlockPos, SeekOrigin.Begin); - - if (nBlockSize != reader.Read(buffer, 0, nBlockSize)) - { - return false; - } - - block = new CBlock(buffer); - - return true; - } - catch (IOException) - { - // I/O error - return false; - } - catch (BlockException) - { - // Constructor exception - return false; - } - } - - /// - /// Writes given block to file and prepares cursor object for insertion into the database. - /// - /// Stream with write access. - /// CBlock reference. - /// Result - public bool WriteToFile(ref Stream writer, ref CBlock block) - { - try - { - byte[] blockBytes = block; - - var magicBytes = BitConverter.GetBytes(CBlockStore.nMagicNumber); - var blkLenBytes = BitConverter.GetBytes(blockBytes.Length); - - // Seek to the end and then append magic bytes there. - writer.Seek(0, SeekOrigin.End); - writer.Write(magicBytes, 0, magicBytes.Length); - writer.Write(blkLenBytes, 0, blkLenBytes.Length); - - // Save block size and current position in the block cursor fields. - nBlockPos = writer.Position; - nBlockSize = blockBytes.Length; - - // Write block and flush the stream. - writer.Write(blockBytes, 0, blockBytes.Length); - writer.Flush(); - - return true; - } - catch (IOException) - { - // I/O error - return false; - } - catch (Exception) - { - // Some serialization error - return false; - } - } - - /// - /// Previous block cursor - /// - [Ignore] - public CBlockStoreItem prev { - get { return CBlockStore.Instance.GetCursor(prevHash); } - } - - /// - /// Next block cursor - /// - [Ignore] - public CBlockStoreItem next - { - get - { - if (nextHash == null) - { - return null; - } - - return CBlockStore.Instance.GetCursor(nextHash); - } - set - { - CBlockStoreItem newCursor = this; - newCursor.nextHash = value.Hash; - - CBlockStore.Instance.UpdateCursor(this, ref newCursor); - } - } - - [Ignore] - bool IsInMainChain - { - get { return (next != null); } - } - - /// - /// STake modifier generation flag - /// - [Ignore] - public bool GeneratedStakeModifier - { - get { return (BlockTypeFlag & BlockType.BLOCK_STAKE_MODIFIER) != 0; } - } - - /// - /// Stake entropy bit - /// - [Ignore] - public uint StakeEntropyBit - { - get { return ((uint)(BlockTypeFlag & BlockType.BLOCK_STAKE_ENTROPY) >> 1); } - } - - /// - /// Sets stake modifier and flag. - /// - /// New stake modifier. - /// Set generation flag? - public void SetStakeModifier(long nModifier, bool fGeneratedStakeModifier) - { - nStakeModifier = nModifier; - if (fGeneratedStakeModifier) - BlockTypeFlag |= BlockType.BLOCK_STAKE_MODIFIER; - } - - /// - /// Set entropy bit. - /// - /// Entropy bit value (0 or 1). - /// False if value is our of range. - public bool SetStakeEntropyBit(byte nEntropyBit) - { - if (nEntropyBit > 1) - return false; - BlockTypeFlag |= (nEntropyBit != 0 ? BlockType.BLOCK_STAKE_ENTROPY : 0); - return true; - } - - /// - /// Set proof-of-stake flag. - /// - public void SetProofOfStake() - { - BlockTypeFlag |= BlockType.BLOCK_PROOF_OF_STAKE; - } - - /// - /// Block has no proof-of-stake flag. - /// - [Ignore] - public bool IsProofOfWork - { - get { return (BlockTypeFlag & BlockType.BLOCK_PROOF_OF_STAKE) == 0; } - } - - /// - /// Block has proof-of-stake flag set. - /// - [Ignore] - public bool IsProofOfStake - { - get { return (BlockTypeFlag & BlockType.BLOCK_PROOF_OF_STAKE) != 0; } - } - - /// - /// Block trust score. - /// - [Ignore] - public uint256 nBlockTrust - { - get - { - uint256 nTarget = 0; - nTarget.Compact = nBits; - - /* Old protocol */ - if (nTime < NetUtils.nChainChecksSwitchTime) - { - return IsProofOfStake ? (new uint256(1) << 256) / (nTarget + 1) : 1; - } - - /* New protocol */ - - // Calculate work amount for block - var nPoWTrust = NetUtils.nPoWBase / (nTarget + 1); - - // Set nPowTrust to 1 if we are checking PoS block or PoW difficulty is too low - nPoWTrust = (IsProofOfStake || !nPoWTrust) ? 1 : nPoWTrust; - - // Return nPoWTrust for the first 12 blocks - if (prev == null || prev.nHeight < 12) - return nPoWTrust; - - CBlockStoreItem currentIndex = prev; - - if (IsProofOfStake) - { - var nNewTrust = (new uint256(1) << 256) / (nTarget + 1); - - // Return 1/3 of score if parent block is not the PoW block - if (!prev.IsProofOfWork) - { - return nNewTrust / 3; - } - - int nPoWCount = 0; - - // Check last 12 blocks type - while (prev.nHeight - currentIndex.nHeight < 12) - { - if (currentIndex.IsProofOfWork) - { - nPoWCount++; - } - currentIndex = currentIndex.prev; - } - - // Return 1/3 of score if less than 3 PoW blocks found - if (nPoWCount < 3) - { - return nNewTrust / 3; - } - - return nNewTrust; - } - else - { - var nLastBlockTrust = prev.nChainTrust - prev.prev.nChainTrust; - - // Return nPoWTrust + 2/3 of previous block score if two parent blocks are not PoS blocks - if (!prev.IsProofOfStake || !prev.prev.IsProofOfStake) - { - return nPoWTrust + (2 * nLastBlockTrust / 3); - } - - int nPoSCount = 0; - - // Check last 12 blocks type - while (prev.nHeight - currentIndex.nHeight < 12) - { - if (currentIndex.IsProofOfStake) - { - nPoSCount++; - } - currentIndex = currentIndex.prev; - } - - // Return nPoWTrust + 2/3 of previous block score if less than 7 PoS blocks found - if (nPoSCount < 7) - { - return nPoWTrust + (2 * nLastBlockTrust / 3); - } - - nTarget.Compact = prev.nBits; - - if (!nTarget) - { - return 0; - } - - var nNewTrust = (new uint256(1) << 256) / (nTarget + 1); - - // Return nPoWTrust + full trust score for previous block nBits - return nPoWTrust + nNewTrust; - } - } - } - - /// - /// Stake modifier checksum. - /// - public uint nStakeModifierChecksum; - - /// - /// Chain trust score - /// - public uint256 nChainTrust; - } - - /// - /// Block type. - /// - public enum BlockType - { - BLOCK_PROOF_OF_STAKE = (1 << 0), // is proof-of-stake block - BLOCK_STAKE_ENTROPY = (1 << 1), // entropy bit for stake modifier - BLOCK_STAKE_MODIFIER = (1 << 2), // regenerated stake modifier - }; - - /// - /// Transaction type. - /// - public enum TxFlags : byte - { - TX_COINBASE, - TX_COINSTAKE, - TX_USER - } - - /// - /// Output flags. - /// - public enum OutputFlags : byte - { - AVAILABLE, // Unspent output - SPENT // Spent output - } - - [Table("MerkleNodes")] - public class CMerkleNode : IMerkleNode - { - #region IMerkleNode - /// - /// Node identifier - /// - [PrimaryKey, AutoIncrement] - public long nMerkleNodeID { get; set; } - - /// - /// Reference to parent block database item. - /// - [ForeignKey(typeof(CBlockStoreItem), Name = "ItemId")] - public long nParentBlockID { get; set; } - - /// - /// Transaction type flag - /// - [Column("TransactionFlags")] - public TxFlags TransactionFlags { get; set; } - - /// - /// Transaction hash - /// - [Column("TransactionHash")] - public byte[] TransactionHash { get; set; } - - /// - /// Transaction offset from the beginning of block header, encoded in VarInt format. - /// - [Column("TxOffset")] - public byte[] TxOffset { get; set; } - - /// - /// Transaction size, encoded in VarInt format. - /// - [Column("TxSize")] - public byte[] TxSize { get; set; } - #endregion - - /// - /// Read transaction from file. - /// - /// Stream with read access. - /// CTransaction reference. - /// Result - public bool ReadFromFile(ref Stream reader, long nBlockPos, out CTransaction tx) - { - var buffer = new byte[CTransaction.nMaxTxSize]; - - tx = null; - - try - { - reader.Seek(nBlockPos + nTxOffset, SeekOrigin.Begin); // Seek to transaction offset - - if (nTxSize != reader.Read(buffer, 0, nTxSize)) - { - return false; - } - - tx = new CTransaction(buffer); - - return true; - } - catch (IOException) - { - // I/O error - return false; - } - catch (TransactionConstructorException) - { - // Constructor error - return false; - } - } - - /// - /// Transaction offset accessor - /// - [Ignore] - public long nTxOffset - { - get { return (long) VarInt.DecodeVarInt(TxOffset); } - } - - /// - /// Transaction size accessor - /// - [Ignore] - public int nTxSize - { - get { return (int)VarInt.DecodeVarInt(TxSize); } - } - - } - - [Table("Outputs")] - public class TxOutItem : ITxOutItem - { - /// - /// Reference to transaction item. - /// - [ForeignKey(typeof(CMerkleNode), Name = "nMerkleNodeID")] - public long nMerkleNodeID { get; set; } - - /// - /// Output flags - /// - public OutputFlags outputFlags { get; set; } - - /// - /// Output number in VarInt format. - /// - public byte[] OutputNumber { get; set; } - - /// - /// Output value in VarInt format. - /// - public byte[] OutputValue { get; set; } - - /// - /// Second half of script which contains spending instructions. - /// - public byte[] scriptPubKey { get; set; } - - /// - /// Getter for output number. - /// - public uint nOut - { - get { return (uint)VarInt.DecodeVarInt(OutputNumber); } - } - - /// - /// Getter for output value. - /// - public ulong nValue - { - get { return VarInt.DecodeVarInt(OutputValue); } - } - - /// - /// Getter ans setter for IsSpent flag. - /// - public bool IsSpent - { - get { return (outputFlags & OutputFlags.SPENT) != 0; } - set { outputFlags |= value ? OutputFlags.SPENT : OutputFlags.AVAILABLE; } - } - } - public class CBlockStore : IDisposable { public const uint nMagicNumber = 0xe5e9e8e4; @@ -707,6 +44,11 @@ namespace Novacoin private SQLiteConnection dbConn; /// + /// Current SQLite platform + /// + private ISQLitePlatform dbPlatform; + + /// /// Block file. /// private string strBlockFile; @@ -745,15 +87,11 @@ namespace Novacoin private ConcurrentDictionary mapStakeSeen = new ConcurrentDictionary(); private ConcurrentDictionary mapStakeSeenOrphan = new ConcurrentDictionary(); - /// - /// Trust score for the longest chain. - /// - private uint256 nBestChainTrust = 0; /// - /// Top block of the best chain. + /// Copy of chain state object. /// - private uint256 nHashBestChain = 0; + private ChainState ChainParams; /// /// Cursor which is pointing us to the end of best chain. @@ -774,6 +112,8 @@ namespace Novacoin /// Block file stream with read/write access /// private Stream fStreamReadWrite; + private uint nTimeBestReceived; + private int nTransactionsUpdated; /// /// Init the block storage manager. @@ -786,7 +126,8 @@ namespace Novacoin strBlockFile = BlockFile; bool firstInit = !File.Exists(strDbFile); - dbConn = new SQLiteConnection(new SQLitePlatformGeneric(), strDbFile); + dbPlatform = new SQLitePlatformGeneric(); + dbConn = new SQLiteConnection(dbPlatform, strDbFile); fStreamReadWrite = File.Open(strBlockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite); @@ -800,6 +141,16 @@ namespace Novacoin dbConn.CreateTable(CreateFlags.AutoIncPK); dbConn.CreateTable(CreateFlags.AutoIncPK); dbConn.CreateTable(CreateFlags.ImplicitPK); + dbConn.CreateTable(CreateFlags.AutoIncPK); + + ChainParams = new ChainState() + { + nBestChainTrust = 0, + nBestHeight = 0, + nHashBestChain = 0 + }; + + dbConn.Insert(ChainParams); var genesisBlock = new CBlock( Interop.HexToArray( @@ -854,6 +205,12 @@ namespace Novacoin mapStakeSeen.TryAdd(item.prevoutStake, item.nStakeTime); } } + + // Load data about the top node. + ChainParams = dbConn.Table().First(); + + genesisBlockCursor = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])NetInfo.nHashGenesisBlock).First(); + bestBlockCursor = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", ChainParams.HashBestChain).First(); } } @@ -872,51 +229,106 @@ namespace Novacoin return false; } - - public bool FetchInputs(ref CTransaction tx, ref Dictionary queued, out TxOutItem[] inputs, bool IsBlock, out bool Invalid) + + public bool FetchInputs(CTransaction tx, ref Dictionary queued, ref Dictionary inputs, bool IsBlock, out bool Invalid) { - Invalid = true; - inputs = null; + Invalid = false; + + if (tx.IsCoinBase) + { + // Coinbase transactions have no inputs to fetch. + return true; + } StringBuilder queryBuilder = new StringBuilder(); - - queryBuilder.Append("select o.* from [Outputs] o left join [MerkleNodes] m on (m.[nMerkleNodeID] = o.[nMerkleNodeID]) where "); + + queryBuilder.Append("select o.*, m.[TransactionHash] from [Outputs] o left join [MerkleNodes] m on (m.[nMerkleNodeID] = o.[nMerkleNodeID]) where "); for (var i = 0; i < tx.vin.Length; i++) { - queryBuilder.AppendFormat(" {0} (m.[TransactionHash] = x'{1}' and o.[OutputNumber] = x'{2}')", - (i > 0 ? "or" : string.Empty), Interop.ToHex(tx.vin[i].prevout.hash), + queryBuilder.AppendFormat(" {0} (m.[TransactionHash] = x'{1}' and o.[OutputNumber] = x'{2}')", + (i > 0 ? "or" : string.Empty), Interop.ToHex(tx.vin[i].prevout.hash), Interop.ToHex(VarInt.EncodeVarInt(tx.vin[i].prevout.n) )); } - var queryResults = dbConn.Query(queryBuilder.ToString()); + var queryResults = dbConn.Query(queryBuilder.ToString()); - if (queryResults.Count < tx.vin.Length) + foreach (var item in queryResults) { - // It seems than some transactions are being spent in the same block. + if (item.IsSpent) + { + return false; // Already spent + } + + var inputsKey = new COutPoint(item.TransactionHash, item.nOut); + + // Add output data to dictionary + inputs.Add(inputsKey, item.getTxOutItem()); + } + if (queryResults.Count < tx.vin.Length) + { if (IsBlock) { - + // It seems that some transactions are being spent in the same block. + + foreach (var txin in tx.vin) + { + var outPoint = txin.prevout; + + if (inputs.ContainsKey(outPoint)) + { + continue; // We have already seen this input. + } + + if (!queued.ContainsKey(outPoint)) + { + return false; // No such transaction + } + + // Add output data to dictionary + inputs.Add(outPoint, queued[outPoint]); + + // Mark output as spent + // queued[outPoint].IsSpent = true; + } } else { - // TODO: use mapUnconfirmed + // Unconfirmed transaction + + foreach (var txin in tx.vin) + { + var outPoint = txin.prevout; + CTransaction txPrev; + + if (!mapUnconfirmedTx.TryGetValue(outPoint.hash, out txPrev)) + { + return false; // No such transaction + } + + if (outPoint.n > txPrev.vout.Length) + { + Invalid = true; + + return false; // nOut is out of range + } + + // TODO: return inputs from map + throw new NotImplementedException(); + + } return false; } } - inputs = queryResults.ToArray(); - Invalid = false; - return true; } private bool AddItemToIndex(ref CBlockStoreItem itemTemplate, ref CBlock block) { - var writer = new BinaryWriter(fStreamReadWrite).BaseStream; uint256 blockHash = itemTemplate.Hash; if (blockMap.ContainsKey(blockHash)) @@ -925,6 +337,9 @@ namespace Novacoin return false; } + // Begin transaction + dbConn.BeginTransaction(); + // Compute chain trust score itemTemplate.nChainTrust = (itemTemplate.prev != null ? itemTemplate.prev.nChainTrust : 0) + itemTemplate.nBlockTrust; @@ -964,46 +379,52 @@ namespace Novacoin itemTemplate.prevoutStake = block.vtx[1].vin[0].prevout; itemTemplate.nStakeTime = block.vtx[1].nTime; + itemTemplate.hashProofOfStake = mapProofOfStake[blockHash]; } - if (!itemTemplate.WriteToFile(ref writer, ref block)) + if (!itemTemplate.WriteToFile(ref fStreamReadWrite, ref block)) { return false; } - if (dbConn.Insert(itemTemplate) == 0 || !blockMap.TryAdd(blockHash, itemTemplate)) + if (dbConn.Insert(itemTemplate) == 0) { - return false; + return false; // Insert failed } - if (itemTemplate.nChainTrust > nBestChainTrust) + // Get last RowID. + itemTemplate.ItemID = dbPlatform.SQLiteApi.LastInsertRowid(dbConn.Handle); + + if (!blockMap.TryAdd(blockHash, itemTemplate)) { - // New best chain + return false; // blockMap add failed + } - // TODO: SetBestChain implementation + if (itemTemplate.nChainTrust > ChainParams.nBestChainTrust) + { + // New best chain - /* if (!SetBestChain(ref itemTemplate)) { return false; // SetBestChain failed. } - */ } + // Commit transaction + dbConn.Commit(); + return true; } private bool SetBestChain(ref CBlockStoreItem cursor) { - dbConn.BeginTransaction(); - uint256 hashBlock = cursor.Hash; - if (genesisBlockCursor == null && hashBlock == NetUtils.nHashGenesisBlock) + if (genesisBlockCursor == null && hashBlock == NetInfo.nHashGenesisBlock) { genesisBlockCursor = cursor; } - else if (nHashBestChain == (uint256)cursor.prevHash) + else if (ChainParams.nHashBestChain == (uint256)cursor.prevHash) { if (!SetBestChainInner(cursor)) { @@ -1013,10 +434,10 @@ namespace Novacoin else { // the first block in the new chain that will cause it to become the new best chain - CBlockStoreItem cursorIntermediate = cursor; + var cursorIntermediate = cursor; // list of blocks that need to be connected afterwards - List secondary = new List(); + var secondary = new List(); // Reorganize is costly in terms of db load, as it works in a single db transaction. // Try to limit how much needs to be done inside @@ -1029,16 +450,38 @@ namespace Novacoin // Switch to new best branch if (!Reorganize(cursorIntermediate)) { - dbConn.Rollback(); InvalidChainFound(cursor); return false; // reorganize failed } + // Connect further blocks + foreach (var currentCursor in secondary) + { + CBlock block; + if (!currentCursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + // ReadFromDisk failed + break; + } + // errors now are not fatal, we still did a reorganisation to a new chain in a valid way + if (!SetBestChainInner(currentCursor)) + { + break; + } + } } + bestBlockCursor = cursor; + nTimeBestReceived = Interop.GetTime(); + nTransactionsUpdated++; - throw new NotImplementedException(); + if (!UpdateTopChain(cursor)) + { + return false; // unable to set top chain node. + } + + return true; } private void InvalidChainFound(CBlockStoreItem cursor) @@ -1048,6 +491,117 @@ namespace Novacoin private bool Reorganize(CBlockStoreItem cursorIntermediate) { + // Find the fork + var fork = bestBlockCursor; + var longer = cursorIntermediate; + + while (fork.ItemID != longer.ItemID) + { + while (longer.nHeight > fork.nHeight) + { + if ((longer = longer.prev) == null) + { + return false; // longer.prev is null + } + } + + if (fork.ItemID == longer.ItemID) + { + break; + } + + if ((fork = fork.prev) == null) + { + return false; // fork.prev is null + } + } + + // List of what to disconnect + var disconnect = new List(); + for (var cursor = bestBlockCursor; cursor.ItemID != fork.ItemID; cursor = cursor.prev) + { + disconnect.Add(cursor); + } + + // List of what to connect + var connect = new List(); + for (var cursor = cursorIntermediate; cursor.ItemID != fork.ItemID; cursor = cursor.prev) + { + connect.Add(cursor); + } + connect.Reverse(); + + // Disconnect shorter branch + var txResurrect = new List(); + foreach (var blockCursor in disconnect) + { + CBlock block; + if (!blockCursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + return false; // ReadFromFile for disconnect failed. + } + if (!DisconnectBlock(blockCursor, ref block)) + { + return false; // DisconnectBlock failed. + } + + // Queue memory transactions to resurrect + foreach (var tx in block.vtx) + { + if (!tx.IsCoinBase && !tx.IsCoinStake) + { + txResurrect.Add(tx); + } + } + } + + + // Connect longer branch + var txDelete = new List(); + foreach (var cursor in connect) + { + CBlock block; + if (!cursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + return false; // ReadFromDisk for connect failed + } + + if (!ConnectBlock(cursor, ref block)) + { + // Invalid block + return false; // ConnectBlock failed + } + + // Queue memory transactions to delete + foreach (var tx in block.vtx) + { + txDelete.Add(tx); + } + } + + if (!UpdateTopChain(cursorIntermediate)) + { + return false; // UpdateTopChain failed + } + + // Resurrect memory transactions that were in the disconnected branch + foreach (var tx in txResurrect) + { + mapUnconfirmedTx.TryAdd(tx.Hash, tx); + } + + // Delete redundant memory transactions that are in the connected branch + foreach (var tx in txDelete) + { + CTransaction dummy; + mapUnconfirmedTx.TryRemove(tx.Hash, out dummy); + } + + return true; // Done + } + + private bool DisconnectBlock(CBlockStoreItem blockCursor, ref CBlock block) + { throw new NotImplementedException(); } @@ -1055,19 +609,26 @@ namespace Novacoin { uint256 hash = cursor.Hash; CBlock block; + if (!cursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + return false; // Unable to read block from file. + } // Adding to current best branch - if (!ConnectBlock(cursor, false, out block) || !WriteHashBestChain(hash)) + if (!ConnectBlock(cursor, ref block) || !UpdateTopChain(cursor)) { - dbConn.Rollback(); InvalidChainFound(cursor); return false; } // Add to current best branch - cursor.prev.next = cursor; + var prevCursor = cursor.prev; + prevCursor.next = cursor; - dbConn.Commit(); + if (!UpdateDBCursor(ref prevCursor)) + { + return false; // unable to update + } // Delete redundant memory transactions foreach (var tx in block.vtx) @@ -1079,28 +640,390 @@ namespace Novacoin return true; } - private bool ConnectBlock(CBlockStoreItem cursor, bool fJustCheck, out CBlock block) + private bool ConnectBlock(CBlockStoreItem cursor, ref CBlock block, bool fJustCheck = false) { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; - if (cursor.ReadFromFile(ref reader, out block)) - { - return false; // Unable to read block from file. - } - // Check it again in case a previous version let a bad block in, but skip BlockSig checking if (!block.CheckBlock(!fJustCheck, !fJustCheck, false)) { return false; // Invalid block found. } - // TODO: the remaining stuff lol :D + bool fScriptChecks = cursor.nHeight >= Checkpoints.TotalBlocksEstimate; + var scriptFlags = scriptflag.SCRIPT_VERIFY_NOCACHE | scriptflag.SCRIPT_VERIFY_P2SH; - throw new NotImplementedException(); + ulong nFees = 0; + ulong nValueIn = 0; + ulong nValueOut = 0; + uint nSigOps = 0; + + var queuedMerkleNodes = new Dictionary(); + var queuedOutputs = new Dictionary(); + + for (var nTx = 0; nTx < block.vtx.Length; nTx++) + { + var tx = block.vtx[nTx]; + var hashTx = tx.Hash; + + if (!queuedMerkleNodes.ContainsKey(hashTx)) + { + var nTxPos = cursor.nBlockPos + block.GetTxOffset(nTx); + var mNode = new CMerkleNode(cursor.ItemID, nTxPos, tx); + + queuedMerkleNodes.Add(hashTx, mNode); + } + + Dictionary txouts; + if (GetOutputs(hashTx, out txouts)) + { + // Do not allow blocks that contain transactions which 'overwrite' older transactions, + // unless those are already completely spent. + return false; + } + + nSigOps += tx.LegacySigOpCount; + if (nSigOps > CBlock.nMaxSigOps) + { + return false; // too many sigops + } + + var inputs = new Dictionary(); + + if (tx.IsCoinBase) + { + nValueOut += tx.nValueOut; + } + else + { + bool Invalid; + if (!FetchInputs(tx, ref queuedOutputs, ref inputs, true, out Invalid)) + { + return false; // Unable to fetch some inputs. + } + + // Add in sigops done by pay-to-script-hash inputs; + // this is to prevent a "rogue miner" from creating + // an incredibly-expensive-to-validate block. + nSigOps += tx.GetP2SHSigOpCount(ref inputs); + if (nSigOps > CBlock.nMaxSigOps) + { + return false; // too many sigops + } + + ulong nTxValueIn = tx.GetValueIn(ref inputs); + ulong nTxValueOut = tx.nValueOut; + + nValueIn += nTxValueIn; + nValueOut += nTxValueOut; + + if (!tx.IsCoinStake) + { + nFees += nTxValueIn - nTxValueOut; + } + + if (!ConnectInputs(tx, ref inputs, ref queuedOutputs, ref cursor, true, fScriptChecks, scriptFlags)) + { + return false; + } + } + + for (var i = 0u; i < tx.vout.Length; i++) + { + var outKey = new COutPoint(hashTx, i); + var outData = new TxOutItem() + { + nMerkleNodeID = -1, + nValue = tx.vout[i].nValue, + scriptPubKey = tx.vout[i].scriptPubKey, + IsSpent = false, + nOut = i + }; + + queuedOutputs.Add(outKey, outData); + } + } + + if (!block.IsProofOfStake) + { + ulong nBlockReward = CBlock.GetProofOfWorkReward(cursor.nBits, nFees); + + // Check coinbase reward + if (block.vtx[0].nValueOut > nBlockReward) + { + return false; // coinbase reward exceeded + } + } + + cursor.nMint = (long)(nValueOut - nValueIn + nFees); + cursor.nMoneySupply = (cursor.prev != null ? cursor.prev.nMoneySupply : 0) + (long)nValueOut - (long)nValueIn; + + if (!UpdateDBCursor(ref cursor)) + { + return false; // Unable to commit changes + } + + if (fJustCheck) + { + return true; + } + + // Flush merkle nodes. + var savedMerkleNodes = new Dictionary(); + foreach (var merklePair in queuedMerkleNodes) + { + var merkleNode = merklePair.Value; + + if (!SaveMerkleNode(ref merkleNode)) + { + // Unable to save merkle tree cursor. + return false; + } + + savedMerkleNodes.Add(merklePair.Key, merkleNode); + } + + // Write queued transaction changes + var newOutpointItems = new List(); + var updatedOutpointItems = new List(); + foreach (var outPair in queuedOutputs) + { + var outItem = outPair.Value; + + if (outItem.nMerkleNodeID == -1) + { + // This outpoint doesn't exist yet, adding to insert list. + + outItem.nMerkleNodeID = savedMerkleNodes[outPair.Key.hash].nMerkleNodeID; + newOutpointItems.Add(outItem); + } + else + { + // This outpount already exists, adding to update list. + + updatedOutpointItems.Add(outItem); + } + } + + if (updatedOutpointItems.Count != 0 && !UpdateOutpoints(ref updatedOutpointItems)) + { + return false; // Unable to update outpoints + } + + if (newOutpointItems.Count != 0 && !InsertOutpoints(ref newOutpointItems)) + { + return false; // Unable to insert outpoints + } + + return true; + } + + /// + /// Insert set of new outpoints + /// + /// List of TxOutItem objects. + /// Result + private bool InsertOutpoints(ref List newOutpointItems) + { + return (dbConn.InsertAll(newOutpointItems, false) != 0); } - private bool WriteHashBestChain(uint256 hash) + + /// + /// Update set of outpoints + /// + /// List of TxOutItem objects. + /// Result + private bool UpdateOutpoints(ref List updatedOutpointItems) { - throw new NotImplementedException(); + return (dbConn.UpdateAll(updatedOutpointItems, false) != 0); + } + + /// + /// Insert merkle node into db and set actual record id value. + /// + /// Merkle node object reference. + /// Result + private bool SaveMerkleNode(ref CMerkleNode merkleNode) + { + if (dbConn.Insert(merkleNode) == 0) + { + return false; + } + + merkleNode.nMerkleNodeID = dbPlatform.SQLiteApi.LastInsertRowid(dbConn.Handle); + + return true; + } + + private bool ConnectInputs(CTransaction tx, ref Dictionary inputs, ref Dictionary queued, ref CBlockStoreItem cursorBlock, bool fBlock, bool fScriptChecks, scriptflag scriptFlags) + { + // Take over previous transactions' spent items + // fBlock is true when this is called from AcceptBlock when a new best-block is added to the blockchain + + if (!tx.IsCoinBase) + { + ulong nValueIn = 0; + ulong nFees = 0; + for (uint i = 0; i < tx.vin.Length; i++) + { + var prevout = tx.vin[i].prevout; + Contract.Assert(inputs.ContainsKey(prevout)); + var input = inputs[prevout]; + + CBlockStoreItem parentBlockCursor; + + if (input.nMerkleNodeID == -1) + { + // This input seems as is confirmed by the same block. + + if (!queued.ContainsKey(prevout)) + { + return false; // No such output has been queued by this block. + } + + // TODO: Ensure that neither coinbase nor coinstake outputs are + // available for spending in the generation block. + } + else + { + // This input has been confirmed by one of the earlier accepted blocks. + + var merkleItem = GetMerkleCursor(input, out parentBlockCursor); + + if (merkleItem == null) + { + return false; // Unable to find merkle node + } + + // If prev is coinbase or coinstake, check that it's matured + if (merkleItem.IsCoinBase || merkleItem.IsCoinStake) + { + if (cursorBlock.nHeight - parentBlockCursor.nHeight < NetInfo.nGeneratedMaturity) + { + return false; // tried to spend non-matured generation input. + } + } + + // check transaction timestamp + if (merkleItem.nTime > tx.nTime) + { + return false; // transaction timestamp earlier than input transaction + } + } + + // Check for negative or overflow input values + nValueIn += input.nValue; + if (!CTransaction.MoneyRange(input.nValue) || !CTransaction.MoneyRange(nValueIn)) + { + return false; // txin values out of range + } + + } + + // The first loop above does all the inexpensive checks. + // Only if ALL inputs pass do we perform expensive ECDSA signature checks. + // Helps prevent CPU exhaustion attacks. + for (int i = 0; i < tx.vin.Length; i++) + { + var prevout = tx.vin[i].prevout; + Contract.Assert(inputs.ContainsKey(prevout)); + var input = inputs[prevout]; + + // Check for conflicts (double-spend) + if (input.IsSpent) + { + return false; + } + + // Skip ECDSA signature verification when connecting blocks (fBlock=true) + // before the last blockchain checkpoint. This is safe because block merkle hashes are + // still computed and checked, and any change will be caught at the next checkpoint. + if (fScriptChecks) + { + // Verify signature + if (!ScriptCode.VerifyScript(tx.vin[i].scriptSig, input.scriptPubKey, tx, i, (int)scriptflag.SCRIPT_VERIFY_P2SH, 0)) + { + return false; // VerifyScript failed. + } + } + + // Mark outpoint as spent + input.IsSpent = true; + inputs[prevout] = input; + + // Write back + if (fBlock) + { + if (input.nMerkleNodeID != -1) + { + // Input has been confirmed earlier. + queued.Add(prevout, input); + } + else + { + // Input has been confirmed by current block. + queued[prevout] = input; + } + } + } + + if (tx.IsCoinStake) + { + // ppcoin: coin stake tx earns reward instead of paying fee + ulong nCoinAge; + if (!tx.GetCoinAge(ref inputs, out nCoinAge)) + { + return false; // unable to get coin age for coinstake + } + + ulong nReward = tx.nValueOut - nValueIn; + + ulong nCalculatedReward = CBlock.GetProofOfStakeReward(nCoinAge, cursorBlock.nBits, tx.nTime) - tx.GetMinFee(1, false, CTransaction.MinFeeMode.GMF_BLOCK) + CTransaction.nCent; + + if (nReward > nCalculatedReward) + { + return false; // coinstake pays too much + } + } + else + { + if (nValueIn < tx.nValueOut) + { + return false; // value in < value out + } + + // Tally transaction fees + ulong nTxFee = nValueIn - tx.nValueOut; + if (nTxFee < 0) + { + return false; // nTxFee < 0 + } + + nFees += nTxFee; + + if (!CTransaction.MoneyRange(nFees)) + { + return false; // nFees out of range + } + } + + } + + return true; + } + + + /// + /// Set new top node or current best chain. + /// + /// + /// + private bool UpdateTopChain(CBlockStoreItem cursor) + { + ChainParams.HashBestChain = cursor.Hash; + ChainParams.nBestChainTrust = cursor.nChainTrust; + ChainParams.nBestHeight = cursor.nHeight; + + return dbConn.Update(ChainParams) != 0; } /// @@ -1137,7 +1060,7 @@ namespace Novacoin uint nHeight = prevBlockCursor.nHeight + 1; // Check timestamp against prev - if (NetUtils.FutureDrift(block.header.nTime) < prevBlockHeader.nTime) + if (NetInfo.FutureDrift(block.header.nTime) < prevBlockHeader.nTime) { // block's timestamp is too early return false; @@ -1164,35 +1087,33 @@ namespace Novacoin if (!AddItemToIndex(ref itemTemplate, ref block)) { + dbConn.Rollback(); + return false; } return true; } + /// + /// GEt block by hash. + /// + /// Block hash + /// Block object reference + /// Block position reference + /// Result public bool GetBlock(uint256 blockHash, ref CBlock block, ref long nBlockPos) { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; + CBlockStoreItem cursor; - var QueryBlock = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])blockHash); - - if (QueryBlock.Count == 1) + if (!blockMap.TryGetValue(blockHash, out cursor)) { - nBlockPos = QueryBlock[0].nBlockPos; - return QueryBlock[0].ReadFromFile(ref reader, out block); + return false; // Unable to fetch block cursor } - // Block not found + nBlockPos = cursor.nBlockPos; - return false; - } - - - /// - /// Interface for join - /// - interface IBlockJoinMerkle : IBlockStorageItem, IMerkleNode - { + return cursor.ReadFromFile(ref fStreamReadWrite, out block); } /// @@ -1200,31 +1121,53 @@ namespace Novacoin /// /// Transaction hash /// Block reference - /// Transaction reference /// Block position reference - /// Transaction position reference /// Result of operation - public bool GetBlockByTransactionID(uint256 TxID, ref CBlock block, ref CTransaction tx, ref long nBlockPos, ref long nTxPos) + public bool GetBlockByTransactionID(uint256 TxID, out CBlock block, out long nBlockPos) { - var queryResult = dbConn.Query("select *, from [BlockStorage] b left join [MerkleNodes] m on (b.[ItemID] = m.[nParentBlockID]) where m.[TransactionHash] = ?", (byte[])TxID); + block = null; + nBlockPos = -1; + + var queryResult = dbConn.Query("select b.* from [BlockStorage] b left join [MerkleNodes] m on (b.[ItemID] = m.[nParentBlockID]) where m.[TransactionHash] = ?", (byte[])TxID); if (queryResult.Count == 1) { - CBlockStoreItem blockCursor = (CBlockStoreItem) queryResult[0]; - CMerkleNode txCursor = (CMerkleNode)queryResult[0]; + CBlockStoreItem blockCursor = queryResult[0]; + + nBlockPos = blockCursor.nBlockPos; - var reader = new BinaryReader(fStreamReadWrite).BaseStream; + return blockCursor.ReadFromFile(ref fStreamReadWrite, out block); + } + + // Tx not found + + return false; + } - if (!txCursor.ReadFromFile(ref reader, blockCursor.nBlockPos, out tx)) + public bool GetOutputs(uint256 transactionHash, out Dictionary txouts, bool fUnspentOnly=true) + { + txouts = null; + + var queryParams = new object[] { (byte[])transactionHash, fUnspentOnly ? OutputFlags.AVAILABLE : (OutputFlags.AVAILABLE | OutputFlags.SPENT) }; + var queryResult = dbConn.Query("select o.* from [Outputs] o left join [MerkleNodes] m on m.[nMerkleNodeID] = o.[nMerkleNodeID] where m.[TransactionHash] = ? and outputFlags = ?", queryParams); + + if (queryResult.Count != 0) + { + txouts = new Dictionary(); + + foreach (var o in queryResult) { - return false; // Unable to read transaction + var outpointKey = new COutPoint(transactionHash, o.nOut); + var outpointData = o; + + txouts.Add(outpointKey, outpointData); } - return blockCursor.ReadFromFile(ref reader, out block); + // There are some unspent inputs. + return true; } - // Tx not found - + // This transaction has been spent completely. return false; } @@ -1233,7 +1176,7 @@ namespace Novacoin /// /// block hash /// Cursor or null - public CBlockStoreItem GetCursor(uint256 blockHash) + public CBlockStoreItem GetMapCursor(uint256 blockHash) { if (blockHash == 0) { @@ -1241,20 +1184,52 @@ namespace Novacoin return null; } - // First, check our block map. - CBlockStoreItem item = null; - if (blockMap.TryGetValue(blockHash, out item)) + CBlockStoreItem cursor = null; + blockMap.TryGetValue(blockHash, out cursor); + + return cursor; + } + + /// + /// Get merkle node cursor by output metadata. + /// + /// Output metadata object + /// Merkle node cursor or null + public CMerkleNode GetMerkleCursor(TxOutItem item, out CBlockStoreItem blockCursor) + { + blockCursor = null; + + // Trying to get cursor from the database. + var QueryMerkleCursor = dbConn.Query("select * from [MerkleNodes] where [nMerkleNodeID] = ?", item.nMerkleNodeID); + + if (QueryMerkleCursor.Count == 1) { - return item; + var merkleNode = QueryMerkleCursor[0]; + + // Search for block + var results = blockMap.Where(x => x.Value.ItemID == merkleNode.nParentBlockID).Select(x => x.Value).ToArray(); + + blockCursor = results[0]; + + return merkleNode; } + // Nothing found. + return null; + } + + /// + /// Load cursor from database. + /// + /// Block hash + /// Block cursor object + public CBlockStoreItem GetDBCursor(uint256 blockHash) + { // Trying to get cursor from the database. var QueryBlockCursor = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])blockHash); if (QueryBlockCursor.Count == 1) { - blockMap.TryAdd(blockHash, QueryBlockCursor[0]); - return QueryBlockCursor[0]; } @@ -1265,17 +1240,22 @@ namespace Novacoin /// /// Update cursor in memory and on disk. /// - /// Original cursor - /// New cursor - /// - public bool UpdateCursor(CBlockStoreItem originalItem, ref CBlockStoreItem newItem) + /// Block cursor + /// Result + public bool UpdateMapCursor(CBlockStoreItem cursor) { - if (blockMap.TryUpdate(originalItem.Hash, newItem, originalItem)) - { - return dbConn.Update(newItem) != 0; - } + var original = blockMap[cursor.Hash]; + return blockMap.TryUpdate(cursor.Hash, cursor, original); + } - return false; + /// + /// Update cursor record in database. + /// + /// Block cursor object + /// Result + public bool UpdateDBCursor(ref CBlockStoreItem cursor) + { + return dbConn.Update(cursor) != 0; } public bool ProcessBlock(ref CBlock block) @@ -1313,7 +1293,7 @@ namespace Novacoin // TODO: proof-of-stake validation uint256 hashProofOfStake = 0, targetProofOfStake = 0; - if (!StakeModifier.CheckProofOfStake(block.vtx[1], block.header.nBits, ref hashProofOfStake, ref targetProofOfStake)) + if (!StakeModifier.CheckProofOfStake(block.vtx[1], block.header.nBits, out hashProofOfStake, out targetProofOfStake)) { return false; // do not error here as we expect this during initial block download } @@ -1390,13 +1370,10 @@ namespace Novacoin var intBuffer = new byte[4]; var fStream2 = File.OpenRead(BlockFile); - var readerForBlocks = new BinaryReader(fStream2).BaseStream; - readerForBlocks.Seek(nOffset, SeekOrigin.Begin); // Seek to previous offset + previous block length + fStream2.Seek(nOffset, SeekOrigin.Begin); // Seek to previous offset + previous block length - dbConn.BeginTransaction(); - - while (readerForBlocks.Read(buffer, 0, 4) == 4) // Read magic number + while (fStream2.Read(buffer, 0, 4) == 4) // Read magic number { var nMagic = BitConverter.ToUInt32(buffer, 0); if (nMagic != 0xe5e9e8e4) @@ -1404,7 +1381,7 @@ namespace Novacoin throw new Exception("Incorrect magic number."); } - var nBytesRead = readerForBlocks.Read(buffer, 0, 4); + var nBytesRead = fStream2.Read(buffer, 0, 4); if (nBytesRead != 4) { throw new Exception("BLKSZ EOF"); @@ -1412,9 +1389,9 @@ namespace Novacoin var nBlockSize = BitConverter.ToInt32(buffer, 0); - nOffset = readerForBlocks.Position; + nOffset = fStream2.Position; - nBytesRead = readerForBlocks.Read(buffer, 0, nBlockSize); + nBytesRead = fStream2.Read(buffer, 0, nBlockSize); if (nBytesRead == 0 || nBytesRead != nBlockSize) { @@ -1435,18 +1412,17 @@ namespace Novacoin } int nCount = blockMap.Count; - Console.WriteLine("nCount={0}, Hash={1}, Time={2}", nCount, block.header.Hash, DateTime.Now); // Commit on each 100th block + Console.WriteLine("nCount={0}, Hash={1}, NumTx={2}, Time={3}", nCount, block.header.Hash, block.vtx.Length, DateTime.Now); // Commit on each 100th block + /* if (nCount % 100 == 0 && nCount != 0) { Console.WriteLine("Commit..."); dbConn.Commit(); dbConn.BeginTransaction(); - } + }*/ } - dbConn.Commit(); - return true; }