X-Git-Url: https://git.novaco.in/?a=blobdiff_plain;f=Novacoin%2FCBlockStore.cs;h=62a021f970fa52b15b2cbef829372028de701e02;hb=6b0501a53995f78928332cfcd7ce1cae4b07486f;hp=279edc7245e07706ff6d94658031ba2ee69be05e;hpb=729e4ae7db34bd869fab8079f08968175fe3e5e8;p=NovacoinLibrary.git diff --git a/Novacoin/CBlockStore.cs b/Novacoin/CBlockStore.cs index 279edc7..62a021f 100644 --- a/Novacoin/CBlockStore.cs +++ b/Novacoin/CBlockStore.cs @@ -1,497 +1,1044 @@ -using System; +/** +* Novacoin classes library +* Copyright (C) 2015 Alex D. (balthazar.ad@gmail.com) + +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. + +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + + +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.Text; +using System.Diagnostics.Contracts; +using System.Linq; namespace Novacoin { - /// - /// Block headers table - /// - [Table("BlockStorage")] - class CBlockStoreItem + public class CBlockStore : IDisposable { - /// - /// Item ID in the database - /// - [PrimaryKey, AutoIncrement] - public int ItemID { get; set; } + public const uint nMagicNumber = 0xe5e9e8e4; + + private bool disposed = false; + private object LockObj = new object(); /// - /// PBKDF2+Salsa20 of block hash + /// SQLite connection object. /// - [Unique] - public byte[] Hash { get; set; } + private SQLiteConnection dbConn; /// - /// Version of block schema + /// Current SQLite platform /// - public uint nVersion { get; set; } + private ISQLitePlatform dbPlatform; /// - /// Previous block hash. + /// Block file. /// - public byte[] prevHash { get; set; } + private string strBlockFile; /// - /// Merkle root hash. + /// Index database file. /// - public byte[] merkleRoot { get; set; } + private string strDbFile; /// - /// Block timestamp. + /// Map of block tree nodes. + /// + /// blockHash => CBlockStoreItem /// - public uint nTime { get; set; } + private ConcurrentDictionary blockMap = new ConcurrentDictionary(); /// - /// Compressed difficulty representation. + /// Orphaned blocks map. /// - public uint nBits { get; set; } + private ConcurrentDictionary orphanMap = new ConcurrentDictionary(); + private ConcurrentDictionary orphanMapByPrev = new ConcurrentDictionary(); /// - /// Nonce counter. + /// Unconfirmed transactions. + /// + /// TxID => Transaction /// - public uint nNonce { get; set; } + private ConcurrentDictionary mapUnconfirmedTx = new ConcurrentDictionary(); /// - /// Block type flags + /// Map of the proof-of-stake hashes. This is necessary for stake duplication checks. /// - public BlockType BlockTypeFlag { get; set; } + private ConcurrentDictionary mapProofOfStake = new ConcurrentDictionary(); + + + private ConcurrentDictionary mapStakeSeen = new ConcurrentDictionary(); + private ConcurrentDictionary, uint256> mapStakeSeenOrphan = new ConcurrentDictionary, uint256>(); + /// - /// Stake modifier + /// Copy of chain state object. /// - public long nStakeModifier { get; set; } + private ChainState ChainParams; /// - /// Stake entropy bit + /// Cursor which is pointing us to the end of best chain. /// - public byte nEntropyBit { get; set; } + private CBlockStoreItem bestBlockCursor = null; /// - /// Block height + /// Cursor which is always pointing us to genesis block. /// - public uint nHeight { get; set; } + private CBlockStoreItem genesisBlockCursor = null; /// - /// Block position in file + /// Current and the only instance of block storage manager. Should be a property with private setter though it's enough for the beginning. /// - public long nBlockPos { get; set; } + public static CBlockStore Instance = null; /// - /// Block size in bytes + /// Block file stream with read/write access /// - public int nBlockSize { get; set; } + private Stream fStreamReadWrite; + private uint nTimeBestReceived; + private int nTransactionsUpdated; /// - /// Fill database item with data from given block header. + /// Init the block storage manager. /// - /// Block header - /// Header hash - public uint256 FillHeader(CBlockHeader header) + /// Path to index database + /// Path to block file + public CBlockStore(string IndexDB = "blockstore.dat", string BlockFile = "blk0001.dat") { - uint256 _hash; - Hash = _hash = header.Hash; + strDbFile = IndexDB; + strBlockFile = BlockFile; - nVersion = header.nVersion; - prevHash = header.prevHash; - merkleRoot = header.merkleRoot; - nTime = header.nTime; - nBits = header.nBits; - nNonce = header.nNonce; + bool firstInit = !File.Exists(strDbFile); + dbPlatform = new SQLitePlatformGeneric(); + dbConn = new SQLiteConnection(dbPlatform, strDbFile); - return _hash; - } + fStreamReadWrite = File.Open(strBlockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite); - /// - /// Reconstruct block header from item data. - /// - public CBlockHeader BlockHeader - { - get + Instance = this; + + if (firstInit) + { + lock (LockObj) + { + // Create tables + 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( + "01000000" + // nVersion=1 + "0000000000000000000000000000000000000000000000000000000000000000" + // prevhash is zero + "7b0502ad2f9f675528183f83d6385794fbcaa914e6d385c6cb1d866a3b3bb34c" + // merkle root + "398e1151" + // nTime=1360105017 + "ffff0f1e" + // nBits=0x1e0fffff + "d3091800" + // nNonce=1575379 + "01" + // nTxCount=1 + "01000000" + // nVersion=1 + "398e1151" + // nTime=1360105017 + "01" + // nInputs=1 + "0000000000000000000000000000000000000000000000000000000000000000" + // input txid is zero + "ffffffff" + // n=uint.maxValue + "4d" + // scriptSigLen=77 + "04ffff001d020f274468747470733a2f2f626974636f696e74616c6b2e6f72672f696e6465782e7068703f746f7069633d3133343137392e6d736731353032313936236d736731353032313936" + // scriptSig + "ffffffff" + // nSequence=uint.maxValue + "01" + // nOutputs=1 + "0000000000000000" + // nValue=0 + "00" + // scriptPubkeyLen=0 + "00000000" + // nLockTime=0 + "00" // sigLen=0 + )); + + // Write block to file. + var rootCursor = new CBlockStoreItem() + { + nHeight = 0 + }; + + rootCursor.FillHeader(genesisBlock.header); + + if (!AddItemToIndex(ref rootCursor, ref genesisBlock)) + { + throw new Exception("Unable to write genesis block"); + } + } + } + else { - CBlockHeader header = new CBlockHeader(); + var blockTreeItems = dbConn.Query("select * from [BlockStorage] order by [ItemId] asc"); - header.nVersion = nVersion; - header.prevHash = prevHash; - header.merkleRoot = merkleRoot; - header.nTime = nTime; - header.nBits = nBits; - header.nNonce = nNonce; + // Init list of block items + foreach (var item in blockTreeItems) + { + item.nStakeModifierChecksum = StakeModifier.GetModifierChecksum(item); + + blockMap.TryAdd(item.Hash, item); + + if (item.IsProofOfStake) + { + // build mapStakeSeen + mapStakeSeen.TryAdd(item.prevoutStake, item.nStakeTime); + } + } + + // Load data about the top node. + ChainParams = dbConn.Table().First(); - return header; + genesisBlockCursor = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])NetInfo.nHashGenesisBlock).First(); + bestBlockCursor = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", ChainParams.HashBestChain).First(); } } - /// - /// Read block from file. - /// - /// Stream with read access. - /// CBlock reference. - /// Result - public bool ReadFromFile(ref Stream reader, out CBlock block) + public bool GetTxOutCursor(COutPoint outpoint, out TxOutItem txOutCursor) { - var buffer = new byte[nBlockSize]; - block = null; + var queryResults = dbConn.Query("select o.* from [Outputs] o left join [MerkleNodes] m on (m.nMerkleNodeID = o.nMerkleNodeID) where m.[TransactionHash] = ?", (byte[])outpoint.hash); - try + if (queryResults.Count == 1) { - reader.Seek(nBlockPos, SeekOrigin.Begin); + txOutCursor = queryResults[0]; - if (nBlockSize != reader.Read(buffer, 0, nBlockSize)) - { - return false; - } + return true; + } + + // Tx not found + + txOutCursor = null; - block = new CBlock(buffer); + return false; + } + + public bool FetchInputs(ref CTransaction tx, ref Dictionary queued, ref Dictionary inputs, bool IsBlock, out bool Invalid) + { + Invalid = false; + if (tx.IsCoinBase) + { + // Coinbase transactions have no inputs to fetch. return true; } - catch (IOException) + + StringBuilder queryBuilder = new StringBuilder(); + + 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++) { - // I/O error - return false; + 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()); + + foreach (var item in queryResults) + { + 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()); } - catch (BlockException) + + if (queryResults.Count < tx.vin.Length) { - // Constructor exception - return false; + 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 + { + // 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; + } } + + return true; } - /// - /// 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) + private bool AddItemToIndex(ref CBlockStoreItem newCursor, ref CBlock block) { - try + uint256 blockHash = newCursor.Hash; + + if (blockMap.ContainsKey(blockHash)) { - byte[] blockBytes = block; + // Already have this block. + return false; + } - var magicBytes = BitConverter.GetBytes(CBlockStore.nMagicNumber); - var blkLenBytes = BitConverter.GetBytes(blockBytes.Length); + // Begin transaction + dbConn.BeginTransaction(); - // 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); + // Compute chain trust score + newCursor.nChainTrust = (newCursor.prev != null ? newCursor.prev.nChainTrust : 0) + newCursor.nBlockTrust; - // Save block size and current position in the block cursor fields. - nBlockPos = writer.Position; - nBlockSize = blockBytes.Length; + if (!newCursor.SetStakeEntropyBit(Entropy.GetStakeEntropyBit(newCursor.nHeight, blockHash))) + { + return false; // SetStakeEntropyBit() failed + } - // Write block and flush the stream. - writer.Write(blockBytes, 0, blockBytes.Length); - writer.Flush(); + // compute stake modifier + long nStakeModifier = 0; + bool fGeneratedStakeModifier = false; + if (!StakeModifier.ComputeNextStakeModifier(ref newCursor, ref nStakeModifier, ref fGeneratedStakeModifier)) + { + return false; // ComputeNextStakeModifier() failed + } - return true; + newCursor.SetStakeModifier(nStakeModifier, fGeneratedStakeModifier); + newCursor.nStakeModifierChecksum = StakeModifier.GetModifierChecksum(newCursor); + + if (!ModifierCheckpoints.Verify(newCursor.nHeight, newCursor.nStakeModifierChecksum)) + { + return false; // Stake modifier checkpoints mismatch } - catch (IOException) + + // Add to index + if (block.IsProofOfStake) { - // I/O error - return false; + newCursor.SetProofOfStake(); + + newCursor.prevoutStake = block.vtx[1].vin[0].prevout; + newCursor.nStakeTime = block.vtx[1].nTime; + + // Save proof-of-stake hash value + uint256 hashProofOfStake; + if (!GetProofOfStakeHash(ref blockHash, out hashProofOfStake)) + { + return false; // hashProofOfStake not found + } + newCursor.hashProofOfStake = hashProofOfStake; } - catch (Exception) + + if (!newCursor.WriteToFile(ref fStreamReadWrite, ref block)) { - // Some serialization error return false; } + + if (dbConn.Insert(newCursor) == 0) + { + return false; // Insert failed + } + + // Get last RowID. + newCursor.ItemID = dbPlatform.SQLiteApi.LastInsertRowid(dbConn.Handle); + + if (!blockMap.TryAdd(blockHash, newCursor)) + { + return false; // blockMap add failed + } + + if (newCursor.nChainTrust > ChainParams.nBestChainTrust) + { + // New best chain + + if (!SetBestChain(ref newCursor)) + { + return false; // SetBestChain failed. + } + } + + // Commit transaction + dbConn.Commit(); + + return true; } - } - /// - /// Block type. - /// - public enum BlockType - { - PROOF_OF_WORK, - PROOF_OF_WORK_MODIFIER, - PROOF_OF_STAKE, - PROOF_OF_STAKE_MODIFIER - }; - - /// - /// Transaction type. - /// - public enum TxType - { - TX_COINBASE, - TX_COINSTAKE, - TX_USER - } + private bool SetBestChain(ref CBlockStoreItem cursor) + { + uint256 hashBlock = cursor.Hash; - [Table("TransactionStorage")] - public class CTransactionStoreItem - { - /// - /// Transaction hash - /// - [PrimaryKey] - public byte[] TransactionHash { get; set; } + if (genesisBlockCursor == null && hashBlock == NetInfo.nHashGenesisBlock) + { + genesisBlockCursor = cursor; + } + else if (ChainParams.nHashBestChain == (uint256)cursor.prevHash) + { + if (!SetBestChainInner(cursor)) + { + return false; + } + } + else + { + // the first block in the new chain that will cause it to become the new best chain + var cursorIntermediate = cursor; - /// - /// Block hash - /// - [ForeignKey(typeof(CBlockStoreItem), Name = "Hash")] - public byte[] BlockHash { get; set; } + // list of blocks that need to be connected afterwards + var secondary = new List(); - /// - /// Transaction type flag - /// - public TxType txType { get; set; } + // 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 + while (cursorIntermediate.prev != null && cursorIntermediate.prev.nChainTrust > bestBlockCursor.nChainTrust) + { + secondary.Add(cursorIntermediate); + cursorIntermediate = cursorIntermediate.prev; + } - /// - /// Tx position in file - /// - public long nTxPos { get; set; } + // Switch to new best branch + if (!Reorganize(cursorIntermediate)) + { + InvalidChainFound(cursor); + return false; // reorganize failed + } - /// - /// Transaction size - /// - public int nTxSize { get; set; } + // Connect further blocks + foreach (var currentCursor in secondary) + { + CBlock block; + if (!currentCursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + // ReadFromDisk failed + break; + } - /// - /// Read transaction from file. - /// - /// Stream with read access. - /// CTransaction reference. - /// Result - public bool ReadFromFile(ref Stream reader, out CTransaction tx) + // 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++; + + if (!UpdateTopChain(cursor)) + { + return false; // unable to set top chain node. + } + + return true; + } + + private void InvalidChainFound(CBlockStoreItem cursor) { - var buffer = new byte[CTransaction.nMaxTxSize]; - tx = null; + throw new NotImplementedException(); + } - try + private bool Reorganize(CBlockStoreItem cursorIntermediate) + { + // Find the fork + var fork = bestBlockCursor; + var longer = cursorIntermediate; + + while (fork.ItemID != longer.ItemID) { - reader.Seek(nTxPos, SeekOrigin.Begin); // Seek to transaction offset + while (longer.nHeight > fork.nHeight) + { + if ((longer = longer.prev) == null) + { + return false; // longer.prev is null + } + } - if (nTxSize != reader.Read(buffer, 0, nTxSize)) + if (fork.ItemID == longer.ItemID) { - return false; + break; } - tx = new CTransaction(buffer); + if ((fork = fork.prev) == null) + { + return false; // fork.prev is null + } + } - return true; + // List of what to disconnect + var disconnect = new List(); + for (var cursor = bestBlockCursor; cursor.ItemID != fork.ItemID; cursor = cursor.prev) + { + disconnect.Add(cursor); } - catch (IOException) + + // List of what to connect + var connect = new List(); + for (var cursor = cursorIntermediate; cursor.ItemID != fork.ItemID; cursor = cursor.prev) { - // I/O error - return false; + connect.Add(cursor); } - catch (TransactionConstructorException) + connect.Reverse(); + + // Disconnect shorter branch + var txResurrect = new List(); + foreach (var blockCursor in disconnect) { - // Constructor error - return false; + 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); + } + } } - } - } - public class CBlockStore : IDisposable - { - public const uint nMagicNumber = 0xe5e9e8e4; + // 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 + } - private bool disposed = false; - private object LockObj = new object(); + if (!ConnectBlock(cursor, ref block)) + { + // Invalid block + return false; // ConnectBlock failed + } - /// - /// SQLite connection object. - /// - private SQLiteConnection dbConn; + // Queue memory transactions to delete + foreach (var tx in block.vtx) + { + txDelete.Add(tx); + } + } - /// - /// Block file. - /// - private string strBlockFile; + if (!UpdateTopChain(cursorIntermediate)) + { + return false; // UpdateTopChain failed + } - /// - /// Index database file. - /// - private string strDbFile; + // Resurrect memory transactions that were in the disconnected branch + foreach (var tx in txResurrect) + { + mapUnconfirmedTx.TryAdd(tx.Hash, tx); + } - /// - /// Map of block tree nodes. - /// - private ConcurrentDictionary blockMap = new ConcurrentDictionary(); + // Delete redundant memory transactions that are in the connected branch + foreach (var tx in txDelete) + { + CTransaction dummy; + mapUnconfirmedTx.TryRemove(tx.Hash, out dummy); + } - /// - /// Orphaned blocks map. - /// - private ConcurrentDictionary orphanMap = new ConcurrentDictionary(); - private ConcurrentDictionary orphanMapByPrev = new ConcurrentDictionary(); + return true; // Done + } - /// - /// Map of unspent items. - /// - private ConcurrentDictionary txMap = new ConcurrentDictionary(); + private bool DisconnectBlock(CBlockStoreItem blockCursor, ref CBlock block) + { + throw new NotImplementedException(); + } - public static CBlockStore Instance; + private bool SetBestChainInner(CBlockStoreItem cursor) + { + uint256 hash = cursor.Hash; + CBlock block; + if (!cursor.ReadFromFile(ref fStreamReadWrite, out block)) + { + return false; // Unable to read block from file. + } - /// - /// Block file stream with read access - /// - private Stream fStreamReadWrite; + // Adding to current best branch + if (!ConnectBlock(cursor, ref block) || !UpdateTopChain(cursor)) + { + InvalidChainFound(cursor); + return false; + } - /// - /// Init the block storage manager. - /// - /// Path to index database - /// Path to block file - public CBlockStore(string IndexDB = "blockstore.dat", string BlockFile = "blk0001.dat") + // Add to current best branch + var prevCursor = cursor.prev; + prevCursor.next = cursor; + + if (!UpdateDBCursor(ref prevCursor)) + { + return false; // unable to update + } + + // Delete redundant memory transactions + foreach (var tx in block.vtx) + { + CTransaction dummy; + mapUnconfirmedTx.TryRemove(tx.Hash, out dummy); + } + + return true; + } + + private bool ConnectBlock(CBlockStoreItem cursor, ref CBlock block, bool fJustCheck = false) { - strDbFile = IndexDB; - strBlockFile = BlockFile; + // 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. + } - bool firstInit = !File.Exists(strDbFile); - dbConn = new SQLiteConnection(new SQLitePlatformGeneric(), strDbFile); + bool fScriptChecks = cursor.nHeight >= HashCheckpoints.TotalBlocksEstimate; + var scriptFlags = scriptflag.SCRIPT_VERIFY_NOCACHE | scriptflag.SCRIPT_VERIFY_P2SH; - fStreamReadWrite = File.Open(strBlockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite); + long nFees = 0; + long nValueIn = 0; + long nValueOut = 0; + uint nSigOps = 0; - if (firstInit) + var queuedMerkleNodes = new Dictionary(); + var queuedOutputs = new Dictionary(); + + for (var nTx = 0; nTx < block.vtx.Length; nTx++) { - lock (LockObj) + var tx = block.vtx[nTx]; + var hashTx = tx.Hash; + + if (!queuedMerkleNodes.ContainsKey(hashTx)) { - // Create tables - dbConn.CreateTable(CreateFlags.AutoIncPK); - dbConn.CreateTable(CreateFlags.ImplicitPK); + var nTxPos = cursor.nBlockPos + block.GetTxOffset(nTx); + var mNode = new CMerkleNode(cursor.ItemID, nTxPos, tx); - var genesisBlock = new CBlock( - Interop.HexToArray( - "01000000" + // nVersion=1 - "0000000000000000000000000000000000000000000000000000000000000000" + // prevhash is zero - "7b0502ad2f9f675528183f83d6385794fbcaa914e6d385c6cb1d866a3b3bb34c" + // merkle root - "398e1151" + // nTime=1360105017 - "ffff0f1e" + // nBits=0x1e0fffff - "d3091800" + // nNonce=1575379 - "01" + // nTxCount=1 - "01000000" + // nVersion=1 - "398e1151" + // nTime=1360105017 - "01" + // nInputs=1 - "0000000000000000000000000000000000000000000000000000000000000000" + // input txid is zero - "ffffffff" + // n=uint.maxValue - "4d" + // scriptSigLen=77 - "04ffff001d020f274468747470733a2f2f626974636f696e74616c6b2e6f72672f696e6465782e7068703f746f7069633d3133343137392e6d736731353032313936236d736731353032313936" + // scriptSig - "ffffffff" + // nSequence=uint.maxValue - "01" + // nOutputs=1 - "0000000000000000" + // nValue=0 - "00" + // scriptPubkeyLen=0 - "00000000" + // nLockTime=0 - "00" // sigLen=0 - )); + queuedMerkleNodes.Add(hashTx, mNode); + } - // Write block to file. - var itemTemplate = new CBlockStoreItem() + 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(ref tx, ref queuedOutputs, ref inputs, true, out Invalid)) { - nHeight = 0 - }; + 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 + } + + long nTxValueIn = tx.GetValueIn(ref inputs); + long nTxValueOut = tx.nValueOut; - itemTemplate.FillHeader(genesisBlock.header); + nValueIn += nTxValueIn; + nValueOut += nTxValueOut; - if (!AddItemToIndex(ref itemTemplate, ref genesisBlock)) + if (!tx.IsCoinStake) { - throw new Exception("Unable to write genesis block"); + nFees += nTxValueIn - nTxValueOut; } + + if (!ConnectInputs(ref 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); } } - else + + if (!block.IsProofOfStake) { - var blockTreeItems = dbConn.Query("select * from [BlockStorage] order by [ItemId] asc"); + long nBlockReward = CBlock.GetProofOfWorkReward(cursor.nBits, nFees); - // Init list of block items - foreach (var item in blockTreeItems) + // Check coinbase reward + if (block.vtx[0].nValueOut > nBlockReward) { - blockMap.TryAdd(item.Hash, item); + return false; // coinbase reward exceeded } } - Instance = this; - } + cursor.nMint = nValueOut - nValueIn + nFees; + cursor.nMoneySupply = (cursor.prev != null ? cursor.prev.nMoneySupply : 0) + nValueOut - nValueIn; - public bool GetTransaction(uint256 TxID, ref CTransaction tx) - { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; - var QueryTx = dbConn.Query("select * from [TransactionStorage] where [TransactionHash] = ?", (byte[])TxID); + if (!UpdateDBCursor(ref cursor)) + { + return false; // Unable to commit changes + } - if (QueryTx.Count == 1) + if (fJustCheck) { - return QueryTx[0].ReadFromFile(ref reader, out tx); + return true; } - // Tx not found + // Flush merkle nodes. + var savedMerkleNodes = new Dictionary(); + foreach (var merklePair in queuedMerkleNodes) + { + var merkleNode = merklePair.Value; - return false; - } + if (!SaveMerkleNode(ref merkleNode)) + { + // Unable to save merkle tree cursor. + return false; + } - private bool AddItemToIndex(ref CBlockStoreItem itemTemplate, ref CBlock block) - { - var writer = new BinaryWriter(fStreamReadWrite).BaseStream; - uint256 blockHash = itemTemplate.Hash; + savedMerkleNodes.Add(merklePair.Key, merkleNode); + } - if (blockMap.ContainsKey(blockHash)) + // Write queued transaction changes + var newOutpointItems = new List(); + var updatedOutpointItems = new List(); + foreach (var outPair in queuedOutputs) { - // Already have this block. - return false; + 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); + } } - // TODO: compute chain trust, set stake entropy bit, record proof-of-stake hash value + if (updatedOutpointItems.Count != 0 && !UpdateOutpoints(ref updatedOutpointItems)) + { + return false; // Unable to update outpoints + } - // TODO: compute stake modifier + if (newOutpointItems.Count != 0 && !InsertOutpoints(ref newOutpointItems)) + { + return false; // Unable to insert outpoints + } - // Add to index - itemTemplate.BlockTypeFlag = block.IsProofOfStake ? BlockType.PROOF_OF_STAKE : BlockType.PROOF_OF_WORK; + return true; + } - if (!itemTemplate.WriteToFile(ref writer, ref block)) + /// + /// Insert set of new outpoints + /// + /// List of TxOutItem objects. + /// Result + private bool InsertOutpoints(ref List newOutpointItems) + { + return (dbConn.InsertAll(newOutpointItems, false) != 0); + } + + + /// + /// Update set of outpoints + /// + /// List of TxOutItem objects. + /// Result + private bool UpdateOutpoints(ref List updatedOutpointItems) + { + 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; } - dbConn.Insert(itemTemplate); + merkleNode.nMerkleNodeID = dbPlatform.SQLiteApi.LastInsertRowid(dbConn.Handle); - // We have no SetBestChain and ConnectBlock/Disconnect block yet, so adding these transactions manually. - for (int i = 0; i < block.vtx.Length; i++) - { - // Handle trasactions + return true; + } + + private bool ConnectInputs(ref 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 (!block.vtx[i].VerifyScripts()) + if (!tx.IsCoinBase) + { + long nValueIn = 0; + long nFees = 0; + for (uint i = 0; i < tx.vin.Length; i++) { - return false; - } + var prevout = tx.vin[i].prevout; + Contract.Assert(inputs.ContainsKey(prevout)); + var input = inputs[prevout]; - var nTxOffset = itemTemplate.nBlockPos + block.GetTxOffset(i); - TxType txnType = TxType.TX_USER; + 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 + } - if (block.vtx[i].IsCoinBase) - { - txnType = TxType.TX_COINBASE; } - else if (block.vtx[i].IsCoinStake) + + // 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++) { - txnType = TxType.TX_COINSTAKE; + 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; + } + } } - var NewTxItem = new CTransactionStoreItem() + if (tx.IsCoinStake) + { + if (HashCheckpoints.LastCheckpointTime < tx.nTime) + { + // Coin stake tx earns reward instead of paying fee + long nCoinAge; + if (!tx.GetCoinAge(ref inputs, out nCoinAge)) + { + return false; // unable to get coin age for coinstake + } + + long nReward = tx.nValueOut - nValueIn; + long 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 { - TransactionHash = block.vtx[i].Hash, - BlockHash = blockHash, - nTxPos = nTxOffset, - nTxSize = block.vtx[i].Size, - txType = txnType - }; + if (nValueIn < tx.nValueOut) + { + return false; // value in < value out + } + + // Tally transaction fees + long nTxFee = nValueIn - tx.nValueOut; + if (nTxFee < 0) + { + return false; // nTxFee < 0 + } - dbConn.Insert(NewTxItem); + nFees += nTxFee; + + if (!CTransaction.MoneyRange(nFees)) + { + return false; // nFees out of range + } + } + } - return blockMap.TryAdd(blockHash, itemTemplate); + 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; + } + + /// + /// Try to find proof-of-stake hash in the map. + /// + /// Block hash + /// Proof-of-stake hash + /// Proof-of-Stake hash value + private bool GetProofOfStakeHash(ref uint256 blockHash, out uint256 hashProofOfStake) + { + return mapProofOfStake.TryGetValue(blockHash, out hashProofOfStake); } public bool AcceptBlock(ref CBlock block) @@ -504,7 +1051,7 @@ namespace Novacoin return false; } - CBlockStoreItem prevBlockCursor = null; + CBlockStoreItem prevBlockCursor; if (!blockMap.TryGetValue(block.header.prevHash, out prevBlockCursor)) { // Unable to get the cursor. @@ -513,11 +1060,10 @@ namespace Novacoin var prevBlockHeader = prevBlockCursor.BlockHeader; - // TODO: proof-of-work/proof-of-stake verification 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; @@ -532,41 +1078,205 @@ namespace Novacoin } } - // TODO: Enforce rule that the coinbase starts with serialized block height + // Check that the block chain matches the known block chain up to a checkpoint + if (!HashCheckpoints.Verify(nHeight, nHash)) + { + return false; // rejected by checkpoint lock-in + } + + // Enforce rule that the coinbase starts with serialized block height + var expect = new CScript(); + expect.AddNumber((int)nHeight); + + byte[] expectBytes = expect; + byte[] scriptSig = block.vtx[0].vin[0].scriptSig; + + if (!expectBytes.SequenceEqual(scriptSig.Take(expectBytes.Length))) + { + return false; // coinbase doesn't start with serialized height. + } // Write block to file. - var itemTemplate = new CBlockStoreItem() + var newCursor = new CBlockStoreItem() { nHeight = nHeight, - nEntropyBit = Entropy.GetStakeEntropyBit(nHeight, nHash) }; - itemTemplate.FillHeader(block.header); + newCursor.FillHeader(block.header); - if (!AddItemToIndex(ref itemTemplate, ref block)) + if (!AddItemToIndex(ref newCursor, ref block)) { + dbConn.Rollback(); + return false; } return true; } - public bool GetBlock(uint256 blockHash, ref CBlock block) + /// + /// Get block by hash. + /// + /// Block hash + /// Block object reference + /// Block position reference + /// Result + public bool GetBlock(uint256 blockHash, ref CBlock block, ref long nBlockPos) + { + CBlockStoreItem cursor; + + if (!blockMap.TryGetValue(blockHash, out cursor)) + { + return false; // Unable to fetch block cursor + } + + nBlockPos = cursor.nBlockPos; + + return cursor.ReadFromFile(ref fStreamReadWrite, out block); + } + + /// + /// Get block and transaction by transaction hash. + /// + /// Transaction hash + /// Block reference + /// Block position reference + /// Result of operation + public bool GetBlockByTransactionID(uint256 TxID, out CBlock block, out long nBlockPos) { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; + block = null; + nBlockPos = -1; - var QueryBlock = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])blockHash); + var queryResult = dbConn.Query("select b.* from [BlockStorage] b left join [MerkleNodes] m on (b.[ItemID] = m.[nParentBlockID]) where m.[TransactionHash] = ?", (byte[])TxID); - if (QueryBlock.Count == 1) + if (queryResult.Count == 1) { - return QueryBlock[0].ReadFromFile(ref reader, out block); + CBlockStoreItem blockCursor = queryResult[0]; + + nBlockPos = blockCursor.nBlockPos; + + return blockCursor.ReadFromFile(ref fStreamReadWrite, out block); } - // Block not found + // Tx not found return false; } + 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) + { + var outpointKey = new COutPoint(transactionHash, o.nOut); + var outpointData = o; + + txouts.Add(outpointKey, outpointData); + } + + // There are some unspent inputs. + return true; + } + + // This transaction has been spent completely. + return false; + } + + /// + /// Get block cursor from map. + /// + /// block hash + /// Cursor or null + public CBlockStoreItem GetMapCursor(uint256 blockHash) + { + if (blockHash == 0) + { + // Genesis block has zero prevHash and no parent. + return null; + } + + 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) + { + 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) + { + return QueryBlockCursor[0]; + } + + // Nothing found. + return null; + } + + /// + /// Update cursor in memory and on disk. + /// + /// Block cursor + /// Result + public bool UpdateMapCursor(CBlockStoreItem cursor) + { + var original = blockMap[cursor.Hash]; + return blockMap.TryUpdate(cursor.Hash, cursor, original); + } + + /// + /// 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) { var blockHash = block.header.Hash; @@ -593,13 +1303,17 @@ namespace Novacoin if (block.IsProofOfStake) { - if (!block.SignatureOK || !block.vtx[1].VerifyScripts()) + uint256 hashProofOfStake = 0, targetProofOfStake = 0; + if (!StakeModifier.CheckProofOfStake(block.vtx[1], block.header.nBits, out hashProofOfStake, out targetProofOfStake)) { - // Proof-of-Stake signature validation failure. - return false; + return false; // do not error here as we expect this during initial block download + } + if (!mapProofOfStake.ContainsKey(blockHash)) + { + // add to mapProofOfStake + mapProofOfStake.TryAdd(blockHash, hashProofOfStake); } - // TODO: proof-of-stake validation } // TODO: difficulty verification @@ -609,12 +1323,22 @@ namespace Novacoin { if (block.IsProofOfStake) { - // TODO: limit duplicity on stake + var proof = block.ProofOfStake; + + // Limited duplicity on stake: prevents block flood attack + // Duplicate stake allowed only when there is orphan child block + if (mapStakeSeenOrphan.ContainsKey(proof) && !orphanMapByPrev.ContainsKey(blockHash)) + { + return false; // duplicate proof-of-stake + } + else + { + mapStakeSeenOrphan.TryAdd(proof, blockHash); + } } - var block2 = new CBlock(block); - orphanMap.TryAdd(blockHash, block2); - orphanMapByPrev.TryAdd(blockHash, block2); + orphanMap.TryAdd(blockHash, block); + orphanMapByPrev.TryAdd(blockHash, block); return true; } @@ -626,32 +1350,39 @@ namespace Novacoin return false; } - // Recursively process any orphan blocks that depended on this one - var orphansQueue = new List(); - orphansQueue.Add(blockHash); - - for (int i = 0; i < orphansQueue.Count; i++) + if (orphanMapByPrev.Count > 0) { - var hashPrev = orphansQueue[i]; + // Recursively process any orphan blocks that depended on this one + + var orphansQueue = new List(); + orphansQueue.Add(blockHash); - foreach (var pair in orphanMap) + for (int i = 0; i < orphansQueue.Count; i++) { - var orphanBlock = pair.Value; + var hashPrev = orphansQueue[i]; - if (orphanBlock.header.prevHash == blockHash) + foreach (var pair in orphanMapByPrev) { - if (AcceptBlock(ref orphanBlock)) + var orphanBlock = pair.Value; + + if (orphanBlock.header.prevHash == blockHash) { - orphansQueue.Add(pair.Key); - } + if (AcceptBlock(ref orphanBlock)) + { + orphansQueue.Add(pair.Key); + } + + CBlock dummy1; + orphanMap.TryRemove(pair.Key, out dummy1); - CBlock dummy1; - orphanMap.TryRemove(pair.Key, out dummy1); + uint256 dummyHash; + mapStakeSeenOrphan.TryRemove(orphanBlock.ProofOfStake, out dummyHash); + } } - } - CBlock dummy2; - orphanMap.TryRemove(hashPrev, out dummy2); + CBlock dummy2; + orphanMapByPrev.TryRemove(hashPrev, out dummy2); + } } return true; @@ -667,13 +1398,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 - - dbConn.BeginTransaction(); + fStream2.Seek(nOffset, SeekOrigin.Begin); // Seek to previous offset + previous block length - 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) @@ -681,7 +1409,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"); @@ -689,9 +1417,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) { @@ -712,18 +1440,9 @@ 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 - - if (nCount % 100 == 0 && nCount != 0) - { - Console.WriteLine("Commit..."); - dbConn.Commit(); - dbConn.BeginTransaction(); - } + Console.WriteLine("nCount={0}, Hash={1}, NumTx={2}, Time={3}", nCount, block.header.Hash, block.vtx.Length, DateTime.Now); } - dbConn.Commit(); - return true; }