From: CryptoManiac Date: Mon, 31 Aug 2015 03:49:55 +0000 (+0300) Subject: Block and transaction verifications X-Git-Url: https://git.novaco.in/?p=NovacoinLibrary.git;a=commitdiff_plain;h=6c6e3b0c68e764ea520dde48d5e44c757f1dabbb Block and transaction verifications There are still a lot of things labeled with TODO tag, though. --- diff --git a/Novacoin/CBlock.cs b/Novacoin/CBlock.cs index 6ff2da3..01bdb27 100644 --- a/Novacoin/CBlock.cs +++ b/Novacoin/CBlock.cs @@ -24,18 +24,18 @@ using System.Diagnostics.Contracts; namespace Novacoin { [Serializable] - public class BlockConstructorException : Exception + public class BlockException : Exception { - public BlockConstructorException() + public BlockException() { } - public BlockConstructorException(string message) + public BlockException(string message) : base(message) { } - public BlockConstructorException(string message, Exception inner) + public BlockException(string message, Exception inner) : base(message, inner) { } @@ -75,6 +75,7 @@ namespace Novacoin vtx[i] = new CTransaction(b.vtx[i]); } + signature = new byte[b.signature.Length]; b.signature.CopyTo(signature, 0); } @@ -99,7 +100,7 @@ namespace Novacoin } catch (Exception e) { - throw new BlockConstructorException("Deserialization failed", e); + throw new BlockException("Deserialization failed", e); } } @@ -111,6 +112,156 @@ namespace Novacoin vtx = new CTransaction[0]; } + public bool CheckBlock(bool fCheckPOW = true, bool fCheckMerkleRoot = true, bool fCheckSig = true) + { + var uniqueTX = new List(); // tx hashes + uint nSigOps = 0; // total sigops + + // Basic sanity checkings + if (vtx.Length == 0 || Size > 1000000) + { + return false; + } + + bool fProofOfStake = IsProofOfStake; + + // First transaction must be coinbase, the rest must not be + if (!vtx[0].IsCoinBase) + { + return false; + } + + if (!vtx[0].CheckTransaction()) + { + return false; + } + + uniqueTX.Add(vtx[0].Hash); + nSigOps += vtx[0].LegacySigOpCount; + + if (fProofOfStake) + { + // Proof-of-STake related checkings. Note that we know here that 1st transactions is coinstake. We don't need + // check the type of 1st transaction because it's performed earlier by IsProofOfStake() + + // nNonce must be zero for proof-of-stake blocks + if (header.nNonce != 0) + { + return false; + } + + // Coinbase output should be empty if proof-of-stake block + if (vtx[0].vout.Length != 1 || !vtx[0].vout[0].IsEmpty) + { + return false; + } + + // Check coinstake timestamp + if (header.nTime != vtx[1].nTime) + { + return false; + } + + // Check proof-of-stake block signature + if (fCheckSig && !SignatureOK) + { + return false; + } + + if (!vtx[1].CheckTransaction()) + { + return false; + } + + uniqueTX.Add(vtx[1].Hash); + nSigOps += vtx[1].LegacySigOpCount; + } + else + { + // Check proof of work matches claimed amount + if (fCheckPOW && !CheckProofOfWork(header.Hash, header.nBits)) + { + return false; + } + + // Check timestamp + if (header.nTime > NetUtils.FutureDrift(NetUtils.GetAdjustedTime())) + { + return false; + } + + // Check coinbase timestamp + if (header.nTime < NetUtils.PastDrift(vtx[0].nTime)) + { + return false; + } + } + + // Iterate all transactions starting from second for proof-of-stake block + // or first for proof-of-work block + for (int i = fProofOfStake ? 2 : 1; i < vtx.Length; i++) + { + var tx = vtx[i]; + + // Reject coinbase transactions at non-zero index + if (tx.IsCoinBase) + { + return false; + } + + // Reject coinstake transactions at index != 1 + if (tx.IsCoinStake) + { + return false; + } + + // Check transaction timestamp + if (header.nTime < tx.nTime) + { + return false; + } + + // Check transaction consistency + if (!tx.CheckTransaction()) + { + return false; + } + + // Add transaction hash into list of unique transaction IDs + uniqueTX.Add(tx.Hash); + + // Calculate sigops count + nSigOps += tx.LegacySigOpCount; + } + + // Check for duplicate txids. + if (uniqueTX.Count != vtx.Length) + { + return false; + } + + // Reject block if validation would consume too much resources. + if (nSigOps > 50000) + { + return false; + } + + // Check merkle root + if (fCheckMerkleRoot && hashMerkleRoot != header.merkleRoot) + { + return false; + } + + return true; + } + + private bool CheckProofOfWork(ScryptHash256 hash, uint nBits) + { + // TODO: stub! + + return true; + } + /// /// Is this a Proof-of-Stake block? /// diff --git a/Novacoin/CBlockHeader.cs b/Novacoin/CBlockHeader.cs index b9c9a4c..8e151e9 100644 --- a/Novacoin/CBlockHeader.cs +++ b/Novacoin/CBlockHeader.cs @@ -36,7 +36,7 @@ namespace Novacoin /// /// Previous block hash. /// - public Hash256 prevHash = new Hash256(); + public ScryptHash256 prevHash = new ScryptHash256(); /// /// Merkle root hash. @@ -68,7 +68,7 @@ namespace Novacoin public CBlockHeader(CBlockHeader h) { nVersion = h.nVersion; - prevHash = new Hash256(h.prevHash); + prevHash = new ScryptHash256(h.prevHash); merkleRoot = new Hash256(h.merkleRoot); nTime = h.nTime; nBits = h.nBits; @@ -80,7 +80,7 @@ namespace Novacoin Contract.Requires(bytes.Length == 80, "Any valid block header is exactly 80 bytes long."); nVersion = BitConverter.ToUInt32(bytes, 0); - prevHash = new Hash256(bytes, 4); + prevHash = new ScryptHash256(bytes, 4); merkleRoot = new Hash256(bytes, 36); nTime = BitConverter.ToUInt32(bytes, 68); nBits = BitConverter.ToUInt32(bytes, 72); diff --git a/Novacoin/CBlockStore.cs b/Novacoin/CBlockStore.cs index 540012b..28706a5 100644 --- a/Novacoin/CBlockStore.cs +++ b/Novacoin/CBlockStore.cs @@ -8,7 +8,7 @@ using SQLite.Net.Attributes; using SQLite.Net.Interop; using SQLite.Net.Platform.Generic; using SQLiteNetExtensions.Attributes; - +using System.Collections.Generic; namespace Novacoin { @@ -66,11 +66,26 @@ namespace Novacoin public BlockType BlockTypeFlag { get; set; } /// + /// Stake modifier + /// + public long nStakeModifier { get; set; } + + /// + /// Stake entropy bit + /// + public byte nEntropyBit { get; set; } + + /// /// Next block hash /// public byte[] NextHash { get; set; } /// + /// Block height + /// + public uint nHeight { get; set; } + + /// /// Block position in file /// public long nBlockPos { get; set; } @@ -110,7 +125,7 @@ namespace Novacoin CBlockHeader header = new CBlockHeader(); header.nVersion = nVersion; - header.prevHash = new Hash256(prevHash); + header.prevHash = new ScryptHash256(prevHash); header.merkleRoot = new Hash256(merkleRoot); header.nTime = nTime; header.nBits = nBits; @@ -149,12 +164,54 @@ namespace Novacoin // I/O error return false; } - catch (BlockConstructorException) + 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); + + // 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(); + + return true; + } + catch (IOException) + { + // I/O error + return false; + } + catch (Exception) + { + // Some serialization error + return false; + } + } } /// @@ -245,61 +302,96 @@ namespace Novacoin } } - /// - /// Block chain node - /// - public class CChainNode + public class CBlockStore : IDisposable { + public const uint nMagicNumber = 0xe5e9e8e4; + + private bool disposed = false; + private object LockObj = new object(); + /// - /// Block number + /// SQLite connection object. /// - public int nDepth; + private SQLiteConnection dbConn; /// - /// Block header + /// Block file. /// - public CBlockHeader blockHeader; + private string strBlockFile; /// - /// Block type flag + /// Index database file. /// - public BlockType blockType; + private string strDbFile; /// - /// Next block hash + /// Map of block tree nodes. /// - public ScryptHash256 hashNextBlock; - } + private ConcurrentDictionary blockMap = new ConcurrentDictionary(); - public class CBlockStore : IDisposable - { - private bool disposed = false; - private object LockObj = new object(); - private SQLiteConnection dbConn = null; - private string strBlockFile; + /// + /// Orphaned blocks map. + /// + private ConcurrentDictionary orphanMap = new ConcurrentDictionary(); + private ConcurrentDictionary orphanMapByPrev = new ConcurrentDictionary(); - private ConcurrentDictionary blockMap = new ConcurrentDictionary(); + /// + /// Map of unspent items. + /// private ConcurrentDictionary txMap = new ConcurrentDictionary(); - private CBlock genesisBlock = new CBlock(Interop.HexToArray("0100000000000000000000000000000000000000000000000000000000000000000000007b0502ad2f9f675528183f83d6385794fbcaa914e6d385c6cb1d866a3b3bb34c398e1151ffff0f1ed30918000101000000398e1151010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d020f274468747470733a2f2f626974636f696e74616c6b2e6f72672f696e6465782e7068703f746f7069633d3133343137392e6d736731353032313936236d736731353032313936ffffffff010000000000000000000000000000")); + + 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 + )); public static CBlockStore Instance; /// - /// Block file stream + /// Block file stream with read access /// private Stream reader; /// + /// Block file stream with write access + /// + private Stream writer; + + /// /// Init the block storage manager. /// /// Path to index database /// Path to block file - public CBlockStore(string IndexDB = "blockstore.dat", string BlockFile = "bootstrap.dat") + public CBlockStore(string IndexDB = "blockstore.dat", string BlockFile = "blk0001.dat") { + strDbFile = IndexDB; strBlockFile = BlockFile; - bool firstInit = !File.Exists(IndexDB); - dbConn = new SQLiteConnection(new SQLitePlatformGeneric(), IndexDB); + bool firstInit = !File.Exists(strDbFile); + dbConn = new SQLiteConnection(new SQLitePlatformGeneric(), strDbFile); + + var fStreamReadWrite = File.Open(strBlockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite); + writer = new BinaryWriter(fStreamReadWrite).BaseStream; + reader = new BinaryReader(fStreamReadWrite).BaseStream; if (firstInit) { @@ -309,50 +401,32 @@ namespace Novacoin dbConn.CreateTable(CreateFlags.AutoIncPK); dbConn.CreateTable(CreateFlags.ImplicitPK); - // Init store with genesis block - - var NewBlockItem = new CBlockStoreItem() + // Write block to file. + var itemTemplate = new CBlockStoreItem() { - BlockTypeFlag = genesisBlock.IsProofOfStake ? BlockType.PROOF_OF_STAKE : BlockType.PROOF_OF_WORK, - nBlockPos = 8, - nBlockSize = ((byte[])genesisBlock).Length + nHeight = 0 }; - var HeaderHash = NewBlockItem.FillHeader(genesisBlock.header); - var NewNode = new CChainNode() { blockHeader = genesisBlock.header, blockType = BlockType.PROOF_OF_WORK }; - - blockMap.TryAdd(HeaderHash, NewNode); - dbConn.Insert(NewBlockItem); + itemTemplate.FillHeader(genesisBlock.header); - var NewTxItem = new CTransactionStoreItem() + if (!AddItemToIndex(ref itemTemplate, ref genesisBlock)) { - TransactionHash = genesisBlock.vtx[0].Hash, - BlockHash = HeaderHash, - txType = TxType.TX_COINBASE, - nTxPos = 8 + genesisBlock.GetTxOffset(0), - nTxSize = genesisBlock.vtx[0].Size - }; - - dbConn.Insert(NewTxItem); + throw new Exception("Unable to write genesis block"); + } } } else { - var QueryGet = dbConn.Query("select * from [BlockStorage] order by [ItemId] asc"); + var blockTreeItems = dbConn.Query("select * from [BlockStorage] order by [ItemId] asc"); // Init list of block items - foreach (var storeItem in QueryGet) + foreach (var item in blockTreeItems) { - var currentNode = new CChainNode() { blockHeader = new CBlockHeader(storeItem.BlockHeader), blockType = storeItem.BlockTypeFlag }; - blockMap.TryAdd(new ScryptHash256(storeItem.Hash), currentNode); + blockMap.TryAdd(new ScryptHash256(item.Hash), item); } } - var fStream1 = File.OpenRead(strBlockFile); - reader = new BinaryReader(fStream1).BaseStream; - Instance = this; - } public bool GetTransaction(Hash256 TxID, ref CTransaction tx) @@ -369,6 +443,118 @@ namespace Novacoin return false; } + private bool AddItemToIndex(ref CBlockStoreItem itemTemplate, ref CBlock block) + { + var blockHash = new ScryptHash256(itemTemplate.Hash); + + if (blockMap.ContainsKey(blockHash)) + { + // Already have this block. + return false; + } + + // TODO: compute chain trust, set stake entropy bit, record proof-of-stake hash value + + // TODO: compute stake modifier + + // Add to index + itemTemplate.BlockTypeFlag = block.IsProofOfStake ? BlockType.PROOF_OF_STAKE : BlockType.PROOF_OF_WORK; + + if (!itemTemplate.WriteToFile(ref writer, ref block)) + { + return false; + } + + dbConn.Insert(itemTemplate); + + // 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 + + var nTxOffset = itemTemplate.nBlockPos + block.GetTxOffset(i); + TxType txnType = TxType.TX_USER; + + if (block.vtx[i].IsCoinBase) + { + txnType = TxType.TX_COINBASE; + } + else if (block.vtx[i].IsCoinStake) + { + txnType = TxType.TX_COINSTAKE; + } + + var NewTxItem = new CTransactionStoreItem() + { + TransactionHash = block.vtx[i].Hash, + BlockHash = blockHash, + nTxPos = nTxOffset, + nTxSize = block.vtx[i].Size, + txType = txnType + }; + + dbConn.Insert(NewTxItem); + } + + return blockMap.TryAdd(blockHash, itemTemplate); + } + + public bool AcceptBlock(ref CBlock block) + { + ScryptHash256 hash = block.header.Hash; + + if (blockMap.ContainsKey(hash)) + { + // Already have this block. + return false; + } + + CBlockStoreItem prevBlockCursor = null; + if (!blockMap.TryGetValue(block.header.prevHash, out prevBlockCursor)) + { + // Unable to get the cursor. + return false; + } + + 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) + { + // block's timestamp is too early + return false; + } + + // Check that all transactions are finalized + foreach (var tx in block.vtx) + { + if (!tx.IsFinal(nHeight, block.header.nTime)) + { + return false; + } + } + + // TODO: Enforce rule that the coinbase starts with serialized block height + + // Write block to file. + var itemTemplate = new CBlockStoreItem() + { + nHeight = nHeight + }; + + itemTemplate.FillHeader(block.header); + + if (!AddItemToIndex(ref itemTemplate, ref block)) + { + return false; + } + + return true; + } + public bool GetBlock(ScryptHash256 blockHash, ref CBlock block) { var QueryBlock = dbConn.Query("select * from [BlockStorage] where [Hash] = ?", (byte[])blockHash); @@ -383,26 +569,100 @@ namespace Novacoin return false; } - public bool ParseBlockFile(string BlockFile = "bootstrap.dat") + public bool ProcessBlock(ref CBlock block) { - strBlockFile = BlockFile; + ScryptHash256 blockHash = block.header.Hash; - // TODO: Rewrite completely. + if (blockMap.ContainsKey(blockHash)) + { + // We already have this block. + return false; + } - var QueryGet = dbConn.Query("select * from [BlockStorage] order by [ItemId] desc limit 1"); + if (orphanMap.ContainsKey(blockHash)) + { + // We already have block in the list of orphans. + return false; + } - var nOffset = 0L; + // TODO: Limited duplicity on stake and reserialization of block signature + + // Preliminary checks + if (!block.CheckBlock(true, true, true)) + { + return true; + } + + if (block.IsProofOfStake) + { + // TODO: proof-of-stake validation + } + + // TODO: difficulty verification - if (QueryGet.Count() == 1) + // If don't already have its previous block, shunt it off to holding area until we get it + if (!blockMap.ContainsKey(block.header.prevHash)) { - var res = QueryGet.First(); - nOffset = res.nBlockPos + res.nBlockSize; + if (block.IsProofOfStake) + { + // TODO: limit duplicity on stake + } + + var block2 = new CBlock(block); + orphanMap.TryAdd(blockHash, block2); + orphanMapByPrev.TryAdd(blockHash, block2); + + return true; } + // Store block to disk + if (!AcceptBlock(ref block)) + { + // Accept failed + 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++) + { + ScryptHash256 hashPrev = orphansQueue[i]; + + foreach (var pair in orphanMap) + { + var orphanBlock = pair.Value; + + if (orphanBlock.header.prevHash == blockHash) + { + if (AcceptBlock(ref orphanBlock)) + { + orphansQueue.Add(pair.Key); + } + + CBlock dummy1; + orphanMap.TryRemove(pair.Key, out dummy1); + } + } + + CBlock dummy2; + orphanMap.TryRemove(hashPrev, out dummy2); + } + + return true; + } + + public bool ParseBlockFile(string BlockFile = "bootstrap.dat") + { + // TODO: Rewrite completely. + + var nOffset = 0L; + var buffer = new byte[1000000]; // Max block size is 1Mb var intBuffer = new byte[4]; - var fStream2 = File.OpenRead(strBlockFile); + var fStream2 = File.OpenRead(BlockFile); var readerForBlocks = new BinaryReader(fStream2).BaseStream; readerForBlocks.Seek(nOffset, SeekOrigin.Begin); // Seek to previous offset + previous block length @@ -435,90 +695,27 @@ namespace Novacoin } var block = new CBlock(buffer); + var hash = block.header.Hash; - if (block.header.merkleRoot != block.hashMerkleRoot) + if (blockMap.ContainsKey(hash)) { - Console.WriteLine("MerkleRoot mismatch: {0} vs. {1} in block {2}.", block.header.merkleRoot, block.hashMerkleRoot, block.header.Hash); continue; } - if (block.IsProofOfStake && !block.SignatureOK) + if (!ProcessBlock(ref block)) { - Console.WriteLine("Proof-of-Stake signature is invalid for block {0}.", block.header.Hash); - continue; + throw new Exception("Invalid block: " + block.header.Hash); } - var NewStoreItem = new CBlockStoreItem() - { - BlockTypeFlag = block.IsProofOfStake ? BlockType.PROOF_OF_STAKE : BlockType.PROOF_OF_WORK, - nBlockPos = nOffset, - nBlockSize = nBlockSize - }; - - var NewChainNode = new CChainNode() - { - blockHeader = block.header, - blockType = NewStoreItem.BlockTypeFlag - }; - - var HeaderHash = NewStoreItem.FillHeader(block.header); - int nCount = blockMap.Count; - Console.WriteLine("nCount={0}, Hash={1}, Time={2}", nCount, HeaderHash, DateTime.Now); // Commit on each 100th block + Console.WriteLine("nCount={0}, Hash={1}, Time={2}", nCount, block.header.Hash, DateTime.Now); // Commit on each 100th block - if (nCount % 100 == 0) + if (nCount % 100 == 0 && nCount != 0) { Console.WriteLine("Commit..."); dbConn.Commit(); dbConn.BeginTransaction(); } - - if (!blockMap.TryAdd(HeaderHash, NewChainNode)) - { - Console.WriteLine("Duplicate block: {0}", HeaderHash); - continue; - } - - // Verify transactions - - foreach (var tx in block.vtx) - { - if (!tx.VerifyScripts()) - { - Console.WriteLine("Error checking tx {0}", tx.Hash); - continue; - } - } - - dbConn.Insert(NewStoreItem); - - for (int i = 0; i < block.vtx.Length; i++) - { - // Handle trasactions - - var nTxOffset = nOffset + block.GetTxOffset(i); - TxType txnType = TxType.TX_USER; - - if (block.vtx[i].IsCoinBase) - { - txnType = TxType.TX_COINBASE; - } - else if (block.vtx[i].IsCoinStake) - { - txnType = TxType.TX_COINSTAKE; - } - - var NewTxItem = new CTransactionStoreItem() - { - TransactionHash = block.vtx[i].Hash, - BlockHash = HeaderHash, - nTxPos = nTxOffset, - nTxSize = block.vtx[i].Size, - txType = txnType - }; - - dbConn.Insert(NewTxItem); - } } dbConn.Commit(); @@ -546,6 +743,7 @@ namespace Novacoin // Free other state (managed objects). reader.Dispose(); + writer.Dispose(); } if (dbConn != null) diff --git a/Novacoin/CKeyStore.cs b/Novacoin/CKeyStore.cs index ac7d811..8eb9e9d 100644 --- a/Novacoin/CKeyStore.cs +++ b/Novacoin/CKeyStore.cs @@ -69,7 +69,7 @@ namespace Novacoin /// Item creation time /// [Indexed] - public int nTime { get; set; } + public uint nTime { get; set; } } /// diff --git a/Novacoin/COutPoint.cs b/Novacoin/COutPoint.cs index 9d1dbb5..a9a347c 100644 --- a/Novacoin/COutPoint.cs +++ b/Novacoin/COutPoint.cs @@ -23,7 +23,7 @@ using System.Text; namespace Novacoin { - public class COutPoint + public class COutPoint : IComparable, IEquatable { /// /// Hash of parent transaction. @@ -88,7 +88,35 @@ namespace Novacoin return sb.ToString(); } - + /// + /// Compare this outpoint with some other. + /// + /// Other outpoint. + /// Result of comparison. + public int CompareTo(COutPoint o) + { + if (n > o.n) + { + return 1; + } + else if (n < o.n) + { + return -1; + } + + return 0; + + } + + /// + /// Equality comparer for outpoints. + /// + /// Other outpoint. + /// Result of comparison. + public bool Equals(COutPoint o) + { + return (o.n == n) && (o.hash == hash); + } } } diff --git a/Novacoin/CScript.cs b/Novacoin/CScript.cs index e2e17e0..8757bb7 100644 --- a/Novacoin/CScript.cs +++ b/Novacoin/CScript.cs @@ -352,14 +352,14 @@ namespace Novacoin /// /// Legacy mode flag /// Amount of sigops - public int GetSigOpCount(bool fAccurate) + public uint GetSigOpCount(bool fAccurate) { var wCodeBytes = new ByteQueue(codeBytes); instruction opcode; // Current instruction byte[] pushArgs; // OP_PUSHDATAn argument - int nCount = 0; + uint nCount = 0; var lastOpcode = instruction.OP_INVALIDOPCODE; // Scan instructions sequence @@ -373,7 +373,7 @@ namespace Novacoin { if (fAccurate && lastOpcode >= instruction.OP_1 && lastOpcode <= instruction.OP_16) { - nCount += ScriptCode.DecodeOP_N(lastOpcode); + nCount += (uint)ScriptCode.DecodeOP_N(lastOpcode); } else { @@ -391,7 +391,7 @@ namespace Novacoin /// /// pay-to-script-hash scriptPubKey /// SigOps count - public int GetSigOpCount(CScript scriptSig) + public uint GetSigOpCount(CScript scriptSig) { if (!IsPayToScriptHash) { @@ -402,12 +402,19 @@ namespace Novacoin // get the last item that the scriptSig // pushes onto the stack: ByteQueue wScriptSig = scriptSig.GetByteQueue(); + int nScriptSigSize = scriptSig.Size; instruction opcode; // Current instruction - byte[] pushArgs; // OP_PUSHDATAn argument + byte[] pushArgs = new byte[0]; // OP_PUSHDATAn argument + - while (ScriptCode.GetOp(ref wScriptSig, out opcode, out pushArgs)) + while (wScriptSig.Index < nScriptSigSize) { + if (!ScriptCode.GetOp(ref wScriptSig, out opcode, out pushArgs)) + { + return 0; + } + if (opcode > instruction.OP_16) { return 0; diff --git a/Novacoin/CTransaction.cs b/Novacoin/CTransaction.cs index ab1a5ef..d1de265 100644 --- a/Novacoin/CTransaction.cs +++ b/Novacoin/CTransaction.cs @@ -39,13 +39,22 @@ namespace Novacoin { } } - + /// /// Represents the transaction. Any transaction must provide one input and one output at least. /// public class CTransaction { /// + /// One coin = 1000000 satoshis. + /// + public const ulong nCoin = 1000000; + /// + /// Sanity checking threshold. + /// + public const ulong nMaxMoney = 2000000000 * nCoin; + + /// /// Version of transaction schema. /// public uint nVersion; @@ -111,6 +120,10 @@ namespace Novacoin nLockTime = tx.nLockTime; } + /// + /// Attempts to execute all transaction scripts and validate the results. + /// + /// Checking result. public bool VerifyScripts() { if (IsCoinBase) @@ -134,10 +147,124 @@ namespace Novacoin } /// + /// Calculate amount of signature operations without trying to properly evaluate P2SH scripts. + /// + public uint LegacySigOpCount + { + get + { + uint nSigOps = 0; + foreach (var txin in vin) + { + nSigOps += txin.scriptSig.GetSigOpCount(false); + } + foreach (var txout in vout) + { + nSigOps += txout.scriptPubKey.GetSigOpCount(false); + } + + return nSigOps; + } + } + + /// + /// Basic sanity checkings + /// + /// Checking result + public bool CheckTransaction() + { + if (Size > 250000 || vin.Length == 0 || vout.Length == 0) + { + return false; + } + + // Check for empty or overflow output values + ulong nValueOut = 0; + for (int i = 0; i < vout.Length; i++) + { + CTxOut txout = vout[i]; + if (txout.IsEmpty && !IsCoinBase && !IsCoinStake) + { + // Empty outputs aren't allowed for user transactions. + return false; + } + + nValueOut += txout.nValue; + if (!MoneyRange(nValueOut)) + { + return false; + } + } + + // Check for duplicate inputs + var InOutPoints = new List(); + foreach (var txin in vin) + { + if (InOutPoints.IndexOf(txin.prevout) != -1) + { + // Duplicate input. + return false; + } + InOutPoints.Add(txin.prevout); + } + + if (IsCoinBase) + { + if (vin[0].scriptSig.Size < 2 || vin[0].scriptSig.Size > 100) + { + // Script size is invalid + return false; + } + } + else + { + foreach (var txin in vin) + { + if (txin.prevout.IsNull) + { + // Null input in non-coinbase transaction. + return false; + } + } + } + + return true; + } + + public bool IsFinal(uint nBlockHeight = 0, uint nBlockTime = 0) + { + // Time based nLockTime + if (nLockTime == 0) + { + return true; + } + if (nBlockHeight == 0) + { + nBlockHeight = uint.MaxValue; // TODO: stupid stub here, should be best height instead. + } + if (nBlockTime == 0) + { + nBlockTime = NetUtils.GetAdjustedTime(); + } + if (nLockTime < (nLockTime < NetUtils.nLockTimeThreshold ? nBlockHeight : nBlockTime)) + { + return true; + } + foreach (var txin in vin) + { + if (!txin.IsFinal) + { + return false; + } + } + return true; + } + + /// /// Parse byte sequence and initialize new instance of CTransaction /// /// Byte sequence - public CTransaction(byte[] txBytes) + public CTransaction(byte[] txBytes) { try { @@ -317,5 +444,7 @@ namespace Novacoin return sb.ToString(); } - } + + public static bool MoneyRange(ulong nValue) { return (nValue <= nMaxMoney); } + } } diff --git a/Novacoin/CTxIn.cs b/Novacoin/CTxIn.cs index 44c21ad..af8539f 100644 --- a/Novacoin/CTxIn.cs +++ b/Novacoin/CTxIn.cs @@ -142,8 +142,7 @@ namespace Novacoin return inputBytes.ToArray(); } - - + public bool IsFinal { get { return (nSequence == uint.MaxValue); } diff --git a/Novacoin/Interop.cs b/Novacoin/Interop.cs index 5c6d317..f4c1e2e 100644 --- a/Novacoin/Interop.cs +++ b/Novacoin/Interop.cs @@ -66,9 +66,9 @@ namespace Novacoin return sb.ToString(); } - public static int GetTime() + public static uint GetTime() { - return (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; + return (uint)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; } } } diff --git a/Novacoin/Novacoin.csproj b/Novacoin/Novacoin.csproj index 825a890..7558f46 100644 --- a/Novacoin/Novacoin.csproj +++ b/Novacoin/Novacoin.csproj @@ -117,6 +117,7 @@ +