X-Git-Url: https://git.novaco.in/?a=blobdiff_plain;f=Novacoin%2FCBlockStore.cs;h=022459b0590a50dbce914abb2c91f483a3c165b3;hb=624ac1021490395614a0cbee619c79860c22061a;hp=2c43d6563c6d2376c3571f4e7261459584cae6c9;hpb=be9d844557911f95165d2c9875c4f5b2822cfc92;p=NovacoinLibrary.git diff --git a/Novacoin/CBlockStore.cs b/Novacoin/CBlockStore.cs index 2c43d65..022459b 100644 --- a/Novacoin/CBlockStore.cs +++ b/Novacoin/CBlockStore.cs @@ -1,4 +1,23 @@ -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; @@ -9,20 +28,20 @@ using SQLite.Net.Interop; using SQLite.Net.Platform.Generic; using SQLiteNetExtensions.Attributes; using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Text; namespace Novacoin { - /// - /// Block headers table - /// [Table("BlockStorage")] - class CBlockStoreItem + public class CBlockStoreItem : IBlockStorageItem { + #region IBlockStorageItem /// /// Item ID in the database /// [PrimaryKey, AutoIncrement] - public int ItemID { get; set; } + public long ItemID { get; set; } /// /// PBKDF2+Salsa20 of block hash @@ -33,77 +52,130 @@ namespace Novacoin /// /// 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; } /// - /// Stake entropy bit + /// Proof-of-Stake hash /// - public byte nEntropyBit { get; set; } + [Column("hashProofOfStake")] + public byte[] hashProofOfStake { get; set; } /// - /// Next block hash + /// Stake generation outpoint. /// - public byte[] NextHash { get; set; } + [Column("prevoutStake")] + public byte[] prevoutStake { get; set; } /// - /// Block height + /// Stake generation time. /// + [Column("nStakeTime")] + public uint nStakeTime { get; set; } + + /// + /// Block height, encoded in VarInt format + /// + [Column("nHeight")] public uint nHeight { get; set; } /// - /// Block position in file + /// Chain trust score, serialized and trimmed uint256 representation. + /// + [Column("ChainTrust")] + public byte[] ChainTrust { 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 /// - public long nBlockPos { get; set; } + [Column("BlockSize")] + public byte[] BlockSize { get; set; } + #endregion /// - /// Block size in bytes + /// Accessor and mutator for BlockPos value. /// - public int nBlockSize { get; set; } + [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); } + } /// /// Fill database item with data from given block header. /// /// Block header /// Header hash - public ScryptHash256 FillHeader(CBlockHeader header) + public uint256 FillHeader(CBlockHeader header) { - ScryptHash256 _hash; - Hash = _hash = header.Hash; + uint256 _hash = header.Hash; + + Hash = _hash; nVersion = header.nVersion; prevHash = header.prevHash; @@ -125,8 +197,8 @@ namespace Novacoin CBlockHeader header = new CBlockHeader(); header.nVersion = nVersion; - header.prevHash = new ScryptHash256(prevHash); - header.merkleRoot = new Hash256(merkleRoot); + header.prevHash = prevHash; + header.merkleRoot = merkleRoot; header.nTime = nTime; header.nBits = nBits; header.nNonce = nNonce; @@ -189,13 +261,13 @@ namespace Novacoin // 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(blkLenBytes, 0, blkLenBytes.Length); writer.Write(blockBytes, 0, blockBytes.Length); writer.Flush(); @@ -212,6 +284,234 @@ namespace Novacoin return false; } } + + /// + /// Previous block cursor + /// + [Ignore] + public CBlockStoreItem prev + { + get { return CBlockStore.Instance.GetMapCursor(prevHash); } + } + + /// + /// Next block cursor + /// + [Ignore] + public CBlockStoreItem next + { + get + { + if (nextHash == null) + { + return null; + } + + return CBlockStore.Instance.GetMapCursor(nextHash); + } + set + { + nextHash = value.Hash; + + CBlockStore.Instance.UpdateMapCursor(this); + } + } + + [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 < NetInfo.nChainChecksSwitchTime) + { + return IsProofOfStake ? (new uint256(1) << 256) / (nTarget + 1) : 1; + } + + /* New protocol */ + + // Calculate work amount for block + var nPoWTrust = NetInfo.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 + /// + [Ignore] + public uint256 nChainTrust { + get { return Interop.AppendWithZeros(ChainTrust); } + set { ChainTrust = Interop.TrimArray(value); } + } + + public long nMint { get; internal set; } + public long nMoneySupply { get; internal set; } } /// @@ -219,51 +519,70 @@ namespace Novacoin /// public enum BlockType { - PROOF_OF_WORK, - PROOF_OF_WORK_MODIFIER, - PROOF_OF_STAKE, - PROOF_OF_STAKE_MODIFIER + 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 TxType + public enum TxFlags : byte { TX_COINBASE, TX_COINSTAKE, TX_USER } - [Table("TransactionStorage")] - public class CTransactionStoreItem + /// + /// Output flags. + /// + public enum OutputFlags : byte + { + AVAILABLE, // Unspent output + SPENT // Spent output + } + + [Table("MerkleNodes")] + public class CMerkleNode : IMerkleNode { + #region IMerkleNode /// - /// Transaction hash + /// Node identifier /// - [PrimaryKey] - public byte[] TransactionHash { get; set; } + [PrimaryKey, AutoIncrement] + public long nMerkleNodeID { get; set; } /// - /// Block hash + /// Reference to parent block database item. /// - [ForeignKey(typeof(CBlockStoreItem), Name = "Hash")] - public byte[] BlockHash { get; set; } + [ForeignKey(typeof(CBlockStoreItem), Name = "ItemId")] + public long nParentBlockID { get; set; } /// /// Transaction type flag /// - public TxType txType { get; set; } + [Column("TransactionFlags")] + public TxFlags TransactionFlags { get; set; } /// - /// Tx position in file + /// Transaction hash /// - public long nTxPos { get; set; } + [Column("TransactionHash")] + public byte[] TransactionHash { get; set; } /// - /// Transaction size + /// Transaction offset from the beginning of block header, encoded in VarInt format. /// - public int nTxSize { get; set; } + [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. @@ -271,14 +590,15 @@ namespace Novacoin /// Stream with read access. /// CTransaction reference. /// Result - public bool ReadFromFile(ref Stream reader, out CTransaction tx) + public bool ReadFromFile(ref Stream reader, long nBlockPos, out CTransaction tx) { - var buffer = new byte[250000]; // Max transaction size is 250kB + var buffer = new byte[CTransaction.nMaxTxSize]; + tx = null; try { - reader.Seek(nTxPos, SeekOrigin.Begin); // Seek to transaction offset + reader.Seek(nBlockPos + nTxOffset, SeekOrigin.Begin); // Seek to transaction offset if (nTxSize != reader.Read(buffer, 0, nTxSize)) { @@ -300,6 +620,138 @@ namespace Novacoin return false; } } + + /// + /// Transaction offset accessor + /// + [Ignore] + public long nTxOffset + { + get { return (long) VarInt.DecodeVarInt(TxOffset); } + private set { TxOffset = VarInt.EncodeVarInt(value); } + } + + /// + /// Transaction size accessor + /// + [Ignore] + public int nTxSize + { + get { return (int)VarInt.DecodeVarInt(TxSize); } + private set { TxSize = VarInt.EncodeVarInt(value); } + } + + public CMerkleNode(CTransaction tx) + { + nTxOffset = -1; + nParentBlockID = -1; + + nTxSize = tx.Size; + TransactionHash = tx.Hash; + + if (tx.IsCoinBase) + { + TransactionFlags |= TxFlags.TX_COINBASE; + } + else if (tx.IsCoinStake) + { + TransactionFlags |= TxFlags.TX_COINSTAKE; + } + else + { + TransactionFlags |= TxFlags.TX_USER; + } + } + + public CMerkleNode(long nBlockId, long nOffset, CTransaction tx) + { + nParentBlockID = nBlockId; + + nTxOffset = nOffset; + nTxSize = tx.Size; + TransactionHash = tx.Hash; + + if (tx.IsCoinBase) + { + TransactionFlags |= TxFlags.TX_COINBASE; + } + else if (tx.IsCoinStake) + { + TransactionFlags |= TxFlags.TX_COINSTAKE; + } + else + { + TransactionFlags |= TxFlags.TX_USER; + } + } + + } + + [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. + /// + [Ignore] + public uint nOut + { + get { return (uint)VarInt.DecodeVarInt(OutputNumber); } + private set { OutputNumber = VarInt.EncodeVarInt(value); } + } + + /// + /// Getter for output value. + /// + [Ignore] + public ulong nValue + { + get { return VarInt.DecodeVarInt(OutputValue); } + private set { OutputValue = VarInt.EncodeVarInt(value); } + } + + /// + /// Getter ans setter for IsSpent flag. + /// + [Ignore] + public bool IsSpent + { + get { return (outputFlags & OutputFlags.SPENT) != 0; } + set { outputFlags |= value ? OutputFlags.SPENT : OutputFlags.AVAILABLE; } + } + + public TxOutItem(CTxOut o, uint nOut) + { + nValue = o.nValue; + scriptPubKey = o.scriptPubKey; + this.nOut = nOut; + } } public class CBlockStore : IDisposable @@ -315,6 +767,11 @@ namespace Novacoin private SQLiteConnection dbConn; /// + /// Current SQLite platform + /// + private ISQLitePlatform dbPlatform; + + /// /// Block file. /// private string strBlockFile; @@ -326,50 +783,65 @@ namespace Novacoin /// /// Map of block tree nodes. + /// + /// blockHash => CBlockStoreItem /// - private ConcurrentDictionary blockMap = new ConcurrentDictionary(); + private ConcurrentDictionary blockMap = new ConcurrentDictionary(); /// /// Orphaned blocks map. /// - private ConcurrentDictionary orphanMap = new ConcurrentDictionary(); - private ConcurrentDictionary orphanMapByPrev = new ConcurrentDictionary(); + private ConcurrentDictionary orphanMap = new ConcurrentDictionary(); + private ConcurrentDictionary orphanMapByPrev = new ConcurrentDictionary(); + + /// + /// Unconfirmed transactions. + /// + /// TxID => Transaction + /// + private ConcurrentDictionary mapUnconfirmedTx = new ConcurrentDictionary(); + + /// + /// Map of the proof-of-stake hashes. This is necessary for stake duplication checks. + /// + private ConcurrentDictionary mapProofOfStake = new ConcurrentDictionary(); + + + private ConcurrentDictionary mapStakeSeen = new ConcurrentDictionary(); + private ConcurrentDictionary mapStakeSeenOrphan = new ConcurrentDictionary(); + + /// + /// Trust score for the longest chain. + /// + private uint256 nBestChainTrust = 0; /// - /// Map of unspent items. + /// Top block of the best chain. /// - private ConcurrentDictionary txMap = new ConcurrentDictionary(); + private uint256 nHashBestChain = 0; - private CBlock 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 - )); + /// + /// Cursor which is pointing us to the end of best chain. + /// + private CBlockStoreItem bestBlockCursor = null; + + /// + /// Cursor which is always pointing us to genesis block. + /// + private CBlockStoreItem genesisBlockCursor = null; - public static CBlockStore Instance; + /// + /// Current and the only instance of block storage manager. Should be a property with private setter though it's enough for the beginning. + /// + public static CBlockStore Instance = null; /// - /// Block file stream with read access + /// Block file stream with read/write access /// private Stream fStreamReadWrite; + private uint nBestHeight; + private uint nTimeBestReceived; + private int nTransactionsUpdated; /// /// Init the block storage manager. @@ -382,17 +854,45 @@ 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); + Instance = this; + if (firstInit) { lock (LockObj) { // Create tables dbConn.CreateTable(CreateFlags.AutoIncPK); - dbConn.CreateTable(CreateFlags.ImplicitPK); + dbConn.CreateTable(CreateFlags.AutoIncPK); + dbConn.CreateTable(CreateFlags.ImplicitPK); + + 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 itemTemplate = new CBlockStoreItem() @@ -415,21 +915,26 @@ namespace Novacoin // Init list of block items foreach (var item in blockTreeItems) { - blockMap.TryAdd(new ScryptHash256(item.Hash), item); + blockMap.TryAdd(item.Hash, item); + + if (item.IsProofOfStake) + { + // build mapStakeSeen + mapStakeSeen.TryAdd(item.prevoutStake, item.nStakeTime); + } } } - - Instance = this; } - public bool GetTransaction(Hash256 TxID, ref CTransaction tx) + public bool GetTxOutCursor(COutPoint outpoint, ref TxOutItem txOutCursor) { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; - var QueryTx = dbConn.Query("select * from [TransactionStorage] where [TransactionHash] = ?", (byte[])TxID); + var queryResults = dbConn.Query("select o.* from [Outputs] o left join [MerkleNodes] m on (m.nMerkleNodeID = o.nMerkleNodeID) where m.[TransactionHash] = ?", (byte[])outpoint.hash); - if (QueryTx.Count == 1) + if (queryResults.Count == 1) { - return QueryTx[0].ReadFromFile(ref reader, out tx); + txOutCursor = queryResults[0]; + + return true; } // Tx not found @@ -437,10 +942,109 @@ namespace Novacoin return false; } + interface InputsJoin : ITxOutItem + { + byte[] TransactionHash { get; set; } + } + + public bool FetchInputs(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; + } + + 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++) + { + 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); + + item.IsSpent = true; + + // Add output data to dictionary + inputs.Add(inputsKey, (TxOutItem) item); + } + + 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 (!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; + } + private bool AddItemToIndex(ref CBlockStoreItem itemTemplate, ref CBlock block) { var writer = new BinaryWriter(fStreamReadWrite).BaseStream; - var blockHash = new ScryptHash256(itemTemplate.Hash); + uint256 blockHash = itemTemplate.Hash; if (blockMap.ContainsKey(blockHash)) { @@ -448,62 +1052,508 @@ namespace Novacoin return false; } - // TODO: compute chain trust, set stake entropy bit, record proof-of-stake hash value + // Compute chain trust score + itemTemplate.nChainTrust = (itemTemplate.prev != null ? itemTemplate.prev.nChainTrust : 0) + itemTemplate.nBlockTrust; - // TODO: compute stake modifier + if (!itemTemplate.SetStakeEntropyBit(Entropy.GetStakeEntropyBit(itemTemplate.nHeight, blockHash))) + { + return false; // SetStakeEntropyBit() failed + } + + // Save proof-of-stake hash value + if (itemTemplate.IsProofOfStake) + { + uint256 hashProofOfStake; + if (!GetProofOfStakeHash(blockHash, out hashProofOfStake)) + { + return false; // hashProofOfStake not found + } + itemTemplate.hashProofOfStake = hashProofOfStake; + } + + // compute stake modifier + long nStakeModifier = 0; + bool fGeneratedStakeModifier = false; + if (!StakeModifier.ComputeNextStakeModifier(itemTemplate, ref nStakeModifier, ref fGeneratedStakeModifier)) + { + return false; // ComputeNextStakeModifier() failed + } + + itemTemplate.SetStakeModifier(nStakeModifier, fGeneratedStakeModifier); + itemTemplate.nStakeModifierChecksum = StakeModifier.GetStakeModifierChecksum(itemTemplate); + + // TODO: verify stake modifier checkpoints // Add to index - itemTemplate.BlockTypeFlag = block.IsProofOfStake ? BlockType.PROOF_OF_STAKE : BlockType.PROOF_OF_WORK; + if (block.IsProofOfStake) + { + itemTemplate.SetProofOfStake(); + + itemTemplate.prevoutStake = block.vtx[1].vin[0].prevout; + itemTemplate.nStakeTime = block.vtx[1].nTime; + } if (!itemTemplate.WriteToFile(ref writer, ref block)) { return false; } - dbConn.Insert(itemTemplate); + if (dbConn.Insert(itemTemplate) == 0) + { + return false; // Insert failed + } + + // Get last RowID. + itemTemplate.ItemID = dbPlatform.SQLiteApi.LastInsertRowid(dbConn.Handle); + + if (!blockMap.TryAdd(blockHash, itemTemplate)) + { + return false; // blockMap add failed + } + + if (itemTemplate.nChainTrust > nBestChainTrust) + { + // New best chain + + if (!SetBestChain(ref itemTemplate)) + { + return false; // SetBestChain failed. + } + } + + return true; + } + + private bool SetBestChain(ref CBlockStoreItem cursor) + { + uint256 hashBlock = cursor.Hash; + + if (genesisBlockCursor == null && hashBlock == NetInfo.nHashGenesisBlock) + { + genesisBlockCursor = cursor; + } + else if (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; + + // list of blocks that need to be connected afterwards + 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 + while (cursorIntermediate.prev != null && cursorIntermediate.prev.nChainTrust > bestBlockCursor.nChainTrust) + { + secondary.Add(cursorIntermediate); + cursorIntermediate = cursorIntermediate.prev; + } + + // Switch to new best branch + if (!Reorganize(cursorIntermediate)) + { + 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; + } + } + } + + nHashBestChain = cursor.Hash; + bestBlockCursor = cursor; + nBestHeight = cursor.nHeight; + nBestChainTrust = cursor.nChainTrust; + nTimeBestReceived = Interop.GetTime(); + nTransactionsUpdated++; + + return true; + } + + private void InvalidChainFound(CBlockStoreItem cursor) + { + throw new NotImplementedException(); + } + + 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 (!WriteHashBestChain(cursorIntermediate.Hash)) + { + return false; // WriteHashBestChain failed + } + + // Make sure it's successfully written to disk + dbConn.Commit(); + + // 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(); + } + + 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. + } + + // Adding to current best branch + if (!ConnectBlock(cursor, ref block) || !WriteHashBestChain(hash)) + { + InvalidChainFound(cursor); + return false; + } + + // Add to current best branch + cursor.prev.next = cursor; - // We have no SetBestChain and ConnectBlock/Disconnect block yet, so adding these transactions manually. - for (int i = 0; i < block.vtx.Length; i++) + dbConn.Commit(); + + // Delete redundant memory transactions + foreach (var tx in block.vtx) { - // Handle trasactions + CTransaction dummy; + mapUnconfirmedTx.TryRemove(tx.Hash, out dummy); + } - if (!block.vtx[i].VerifyScripts()) + return true; + } + + private bool ConnectBlock(CBlockStoreItem cursor, ref CBlock block, bool fJustCheck=false) + { + // 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 fScriptChecks = cursor.nHeight >= Checkpoints.TotalBlocksEstimate; + var scriptFlags = scriptflag.SCRIPT_VERIFY_NOCACHE | scriptflag.SCRIPT_VERIFY_P2SH; + + ulong nFees = 0; + ulong nValueIn = 0; + ulong nValueOut = 0; + uint nSigOps = 0; + + var queuedMerkleNodes = new Dictionary(); + var queued = new Dictionary(); + + for (var nTx = 0; nTx < block.vtx.Length; nTx++) + { + var tx = block.vtx[nTx]; + var hashTx = tx.Hash; + var nTxPos = cursor.nBlockPos + block.GetTxOffset(nTx); + + 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; } - var nTxOffset = itemTemplate.nBlockPos + block.GetTxOffset(i); - TxType txnType = TxType.TX_USER; + 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 queued, 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(inputs); + if (nSigOps > CBlock.nMaxSigOps) + { + return false; // too many sigops + } + + ulong nTxValueIn = tx.GetValueIn(inputs); + ulong nTxValueOut = tx.nValueOut; + + nValueIn += nTxValueIn; + nValueOut += nTxValueOut; + + if (!tx.IsCoinStake) + { + nFees += nTxValueIn - nTxValueOut; + } + + if (!ConnectInputs(tx, inputs, queued, cursor, fScriptChecks, scriptFlags)) + { + return false; + } + } - if (block.vtx[i].IsCoinBase) + for (var i = 0u; i < tx.vout.Length; i++) { - txnType = TxType.TX_COINBASE; + var mNode = new CMerkleNode(cursor.ItemID, nTxPos, tx); + queuedMerkleNodes.Add(hashTx, mNode); + + var outKey = new COutPoint(hashTx, i); + var outData = new TxOutItem(tx.vout[i], i); + + outData.IsSpent = false; + + queued.Add(outKey, outData); } - else if (block.vtx[i].IsCoinStake) + } + + if (!block.IsProofOfStake) + { + ulong nBlockReward = CBlock.GetProofOfWorkReward(cursor.nBits, nFees); + + // Check coinbase reward + if (block.vtx[0].nValueOut > nBlockReward) { - txnType = TxType.TX_COINSTAKE; + 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; + } + + // Write queued transaction changes + var actualMerkleNodes = new Dictionary(); + var queuedOutpointItems = new List(); + foreach(KeyValuePair outPair in queued) + { + uint256 txID = outPair.Key.hash; + CMerkleNode merkleNode; - var NewTxItem = new CTransactionStoreItem() + if (actualMerkleNodes.ContainsKey(txID)) + { + merkleNode = actualMerkleNodes[txID]; + } + else { - TransactionHash = block.vtx[i].Hash, - BlockHash = blockHash, - nTxPos = nTxOffset, - nTxSize = block.vtx[i].Size, - txType = txnType - }; + merkleNode = queuedMerkleNodes[txID]; + if (!SaveMerkleNode(ref merkleNode)) + { + // Unable to save merkle tree cursor. + return false; + } + actualMerkleNodes.Add(txID, merkleNode); + } + + var outItem = outPair.Value; + outItem.nMerkleNodeID = merkleNode.nMerkleNodeID; - dbConn.Insert(NewTxItem); + queuedOutpointItems.Add(outItem); + } + + if (!SaveOutpoints(ref queuedOutpointItems)) + { + return false; // Unable to save outpoints } - return blockMap.TryAdd(blockHash, itemTemplate); + // TODO: the remaining stuff lol :D + + throw new NotImplementedException(); + } + + /// + /// Insert set of outpoints + /// + /// List of TxOutItem objects. + /// Result + private bool SaveOutpoints(ref List queuedOutpointItems) + { + return dbConn.InsertAll(queuedOutpointItems, 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, Dictionary inputs, Dictionary queued, CBlockStoreItem cursor, bool fScriptChecks, scriptflag scriptFlags) + { + throw new NotImplementedException(); + } + + private bool WriteHashBestChain(uint256 hash) + { + throw new NotImplementedException(); + } + + /// + /// Try to find proof-of-stake hash in the map. + /// + /// Block hash + /// Proof-of-stake hash + /// Proof-of-Stake hash value + private bool GetProofOfStakeHash(uint256 blockHash, out uint256 hashProofOfStake) + { + return mapProofOfStake.TryGetValue(blockHash, out hashProofOfStake); } public bool AcceptBlock(ref CBlock block) { - ScryptHash256 hash = block.header.Hash; + uint256 nHash = block.header.Hash; - if (blockMap.ContainsKey(hash)) + if (blockMap.ContainsKey(nHash)) { // Already have this block. return false; @@ -522,7 +1572,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; @@ -542,7 +1592,7 @@ namespace Novacoin // Write block to file. var itemTemplate = new CBlockStoreItem() { - nHeight = nHeight + nHeight = nHeight, }; itemTemplate.FillHeader(block.header); @@ -555,25 +1605,164 @@ namespace Novacoin return true; } - public bool GetBlock(ScryptHash256 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); + } + + + /// + /// Interface for join + /// + interface IBlockJoinMerkle : IBlockStorageItem, IMerkleNode { - var reader = new BinaryReader(fStreamReadWrite).BaseStream; + } - var QueryBlock = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])blockHash); + /// + /// Get block and transaction by transaction hash. + /// + /// 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) + { + var queryResult = dbConn.Query("select * 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 = (CBlockStoreItem) queryResult[0]; + CMerkleNode txCursor = (CMerkleNode)queryResult[0]; + + var reader = new BinaryReader(fStreamReadWrite).BaseStream; + + if (!txCursor.ReadFromFile(ref reader, blockCursor.nBlockPos, out tx)) + { + return false; // Unable to read transaction + } + + return blockCursor.ReadFromFile(ref reader, 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; } + public bool WriteNodes(ref CMerkleNode[] merkleNodes) + { + + + return true; + } + + /// + /// 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; + } + + /// + /// 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) { - ScryptHash256 blockHash = block.header.Hash; + var blockHash = block.header.Hash; if (blockMap.ContainsKey(blockHash)) { @@ -589,15 +1778,33 @@ namespace Novacoin // TODO: Limited duplicity on stake and reserialization of block signature - // Preliminary checks if (!block.CheckBlock(true, true, true)) { - return true; + // Preliminary checks failure. + return false; } if (block.IsProofOfStake) { + if (!block.SignatureOK) + { + // Proof-of-Stake signature validation failure. + return false; + } + // TODO: proof-of-stake validation + + uint256 hashProofOfStake = 0, targetProofOfStake = 0; + if (!StakeModifier.CheckProofOfStake(block.vtx[1], block.header.nBits, ref hashProofOfStake, ref targetProofOfStake)) + { + 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: difficulty verification @@ -625,12 +1832,12 @@ namespace Novacoin } // Recursively process any orphan blocks that depended on this one - var orphansQueue = new List(); + var orphansQueue = new List(); orphansQueue.Add(blockHash); for (int i = 0; i < orphansQueue.Count; i++) { - ScryptHash256 hashPrev = orphansQueue[i]; + var hashPrev = orphansQueue[i]; foreach (var pair in orphanMap) { @@ -661,7 +1868,7 @@ namespace Novacoin var nOffset = 0L; - var buffer = new byte[1000000]; // Max block size is 1Mb + var buffer = new byte[CBlock.nMaxBlockSize]; // Max block size is 1Mb var intBuffer = new byte[4]; var fStream2 = File.OpenRead(BlockFile); @@ -669,8 +1876,6 @@ namespace Novacoin readerForBlocks.Seek(nOffset, SeekOrigin.Begin); // Seek to previous offset + previous block length - dbConn.BeginTransaction(); - while (readerForBlocks.Read(buffer, 0, 4) == 4) // Read magic number { var nMagic = BitConverter.ToUInt32(buffer, 0); @@ -712,12 +1917,13 @@ 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(); - } + }*/ } dbConn.Commit();