| | 1 | | using System.Security.Cryptography; |
| | 2 | | using System.Text; |
| | 3 | |
|
| | 4 | | namespace EF.Blockchain.Domain; |
| | 5 | |
|
| | 6 | | /// <summary> |
| | 7 | | /// Represents a block in the blockchain. Each block contains a list of transactions and metadata like index, timestamp, |
| | 8 | | /// </summary> |
| | 9 | | public class Block |
| | 10 | | { |
| | 11 | | /// <summary> |
| | 12 | | /// The index of the block in the blockchain. |
| | 13 | | /// </summary> |
| 79856 | 14 | | public int Index { get; private set; } |
| | 15 | |
|
| | 16 | | /// <summary> |
| | 17 | | /// The Unix timestamp when the block was created. |
| | 18 | | /// </summary> |
| 79680 | 19 | | public long Timestamp { get; private set; } |
| | 20 | |
|
| | 21 | | /// <summary> |
| | 22 | | /// The SHA-256 hash of the block. |
| | 23 | | /// </summary> |
| 157520 | 24 | | public string Hash { get; private set; } |
| | 25 | |
|
| | 26 | | /// <summary> |
| | 27 | | /// The hash of the previous block in the chain. |
| | 28 | | /// </summary> |
| 79680 | 29 | | public string PreviousHash { get; private set; } |
| | 30 | |
|
| | 31 | | /// <summary> |
| | 32 | | /// List of transactions included in this block. |
| | 33 | | /// </summary> |
| 240168 | 34 | | public List<Transaction> Transactions { get; private set; } = new(); |
| | 35 | |
|
| | 36 | | /// <summary> |
| | 37 | | /// The nonce value used for proof-of-work (mining). |
| | 38 | | /// </summary> |
| 235248 | 39 | | public int Nonce { get; private set; } |
| | 40 | |
|
| | 41 | | /// <summary> |
| | 42 | | /// The wallet address of the miner who mined this block. |
| | 43 | | /// </summary> |
| 80688 | 44 | | public string Miner { get; private set; } |
| | 45 | |
|
| | 46 | | /// <summary> |
| | 47 | | /// Creates a new instance of a block. |
| | 48 | | /// </summary> |
| | 49 | | /// <param name="index">The index of the block in the chain.</param> |
| | 50 | | /// <param name="previousHash">Hash of the previous block.</param> |
| | 51 | | /// <param name="transactions">List of transactions included.</param> |
| | 52 | | /// <param name="timestamp">Timestamp of block creation.</param> |
| | 53 | | /// <param name="hash">Optional predefined hash.</param> |
| | 54 | | /// <param name="nonce">Nonce value used for mining.</param> |
| | 55 | | /// <param name="miner">Miner's wallet address.</param> |
| 832 | 56 | | public Block(int? index = null, |
| 832 | 57 | | string? previousHash = null, |
| 832 | 58 | | List<Transaction>? transactions = null, |
| 832 | 59 | | long? timestamp = null, |
| 832 | 60 | | string? hash = null, |
| 832 | 61 | | int? nonce = null, |
| 832 | 62 | | string? miner = null) |
| 832 | 63 | | { |
| 832 | 64 | | Index = index ?? 0; |
| 832 | 65 | | Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds(); |
| 832 | 66 | | PreviousHash = previousHash ?? string.Empty; |
| 832 | 67 | | Transactions = transactions ?? new(); |
| 832 | 68 | | Nonce = nonce ?? 0; |
| 832 | 69 | | Miner = miner ?? string.Empty; |
| 832 | 70 | | Hash = string.IsNullOrEmpty(hash) ? GetHash() : hash; |
| 832 | 71 | | } |
| | 72 | |
|
| | 73 | | /// <summary> |
| | 74 | | /// Generates the SHA-256 hash for the block based on its current data. |
| | 75 | | /// </summary> |
| | 76 | | /// <returns>The generated hash string.</returns> |
| | 77 | | public string GetHash() |
| 78712 | 78 | | { |
| 78712 | 79 | | var txs = Transactions != null && Transactions.Any() |
| 225558 | 80 | | ? Transactions.Select(tx => tx.Hash).Aggregate((a, b) => a + b) |
| 78712 | 81 | | : ""; |
| | 82 | |
|
| 78712 | 83 | | return ComputeHash(Index, Timestamp, txs, PreviousHash, Nonce, Miner); |
| 78712 | 84 | | } |
| | 85 | |
|
| | 86 | | /// <summary> |
| | 87 | | /// Computes a SHA-256 hash based on provided parameters. |
| | 88 | | /// </summary> |
| | 89 | | /// <param name="index">Block index.</param> |
| | 90 | | /// <param name="timestamp">Block timestamp.</param> |
| | 91 | | /// <param name="txs">Concatenated transaction hashes.</param> |
| | 92 | | /// <param name="previousHash">Hash of the previous block.</param> |
| | 93 | | /// <param name="nonce">Nonce used in mining.</param> |
| | 94 | | /// <param name="miner">Miner's wallet address.</param> |
| | 95 | | /// <returns>Computed SHA-256 hash.</returns> |
| | 96 | | public static string ComputeHash(int index, |
| | 97 | | long timestamp, |
| | 98 | | string txs, |
| | 99 | | string previousHash, |
| | 100 | | int nonce = 0, |
| | 101 | | string? miner = null) |
| 78712 | 102 | | { |
| 78712 | 103 | | var rawData = $"{index}{txs}{timestamp}{previousHash}{nonce}{miner}"; |
| 78712 | 104 | | using var sha256 = SHA256.Create(); |
| 78712 | 105 | | var bytes = Encoding.UTF8.GetBytes(rawData); |
| 78712 | 106 | | return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLower(); |
| 78712 | 107 | | } |
| | 108 | |
|
| | 109 | | /// <summary> |
| | 110 | | /// Mines the block by incrementing the nonce until the hash starts with the required number of zeros. |
| | 111 | | /// </summary> |
| | 112 | | /// <param name="difficulty">Number of leading zeros required in the hash.</param> |
| | 113 | | /// <param name="miner">Miner's wallet address.</param> |
| | 114 | | public void Mine(int difficulty, string miner) |
| 664 | 115 | | { |
| 664 | 116 | | Miner = miner; |
| 664 | 117 | | var prefix = new string('0', difficulty); |
| | 118 | |
|
| | 119 | | do |
| 77792 | 120 | | { |
| 77792 | 121 | | Nonce++; |
| 77792 | 122 | | Hash = GetHash(); |
| 77792 | 123 | | } |
| 77792 | 124 | | while (!Hash.StartsWith(prefix)); |
| 664 | 125 | | } |
| | 126 | |
|
| | 127 | | /// <summary> |
| | 128 | | /// Validates this block using previous block data and difficulty. |
| | 129 | | /// </summary> |
| | 130 | | /// <param name="previousHash">Hash of the previous block.</param> |
| | 131 | | /// <param name="previousIndex">Index of the previous block.</param> |
| | 132 | | /// <param name="difficulty">Current difficulty level.</param> |
| | 133 | | /// <param name="feePerTx">Expected fee per transaction.</param> |
| | 134 | | /// <returns>A <see cref="Validation"/> result indicating if the block is valid.</returns> |
| | 135 | | public Validation IsValid(string previousHash, int previousIndex, int difficulty, int feePerTx) |
| 416 | 136 | | { |
| 416 | 137 | | if (Transactions != null && Transactions.Any()) |
| 400 | 138 | | { |
| 400 | 139 | | var feeTxs = Transactions |
| 776 | 140 | | .Where(tx => tx.Type == TransactionType.FEE) |
| 400 | 141 | | .ToList(); |
| | 142 | |
|
| 400 | 143 | | if (!feeTxs.Any()) |
| 16 | 144 | | return new Validation(false, "No fee tx"); |
| | 145 | |
|
| 384 | 146 | | if (feeTxs.Count > 1) |
| 8 | 147 | | return new Validation(false, "Too many fees"); |
| | 148 | |
|
| 752 | 149 | | if (!feeTxs[0].TxOutputs.Any(txo => txo.ToAddress == Miner)) |
| 8 | 150 | | return new Validation(false, "Invalid fee tx: different from miner"); |
| | 151 | |
|
| 1096 | 152 | | var totalFees = feePerTx * Transactions.Count(tx => tx.Type != TransactionType.FEE); |
| | 153 | |
|
| 368 | 154 | | var validations = Transactions |
| 728 | 155 | | .Select(tx => tx.IsValid(difficulty, totalFees)) |
| 728 | 156 | | .Where(v => !v.Success) |
| 272 | 157 | | .Select(v => v.Message) |
| 368 | 158 | | .ToList(); |
| | 159 | |
|
| 368 | 160 | | if (validations.Any()) |
| 272 | 161 | | { |
| 272 | 162 | | var errorMsg = string.Join(" ", validations); |
| 272 | 163 | | return new Validation(false, "Invalid block due to invalid tx: " + errorMsg); |
| | 164 | | } |
| 96 | 165 | | } |
| | 166 | |
|
| 112 | 167 | | if (previousIndex != Index - 1) |
| 32 | 168 | | return new Validation(false, "Invalid index"); |
| | 169 | |
|
| 80 | 170 | | if (Timestamp < 1) |
| 8 | 171 | | return new Validation(false, "Invalid timestamp"); |
| | 172 | |
|
| 72 | 173 | | if (PreviousHash != previousHash) |
| 8 | 174 | | return new Validation(false, "Invalid previous hash"); |
| | 175 | |
|
| 64 | 176 | | if (Nonce < 1 || string.IsNullOrEmpty(Miner)) |
| 8 | 177 | | return new Validation(false, "No mined"); |
| | 178 | |
|
| 56 | 179 | | var prefix = new string('0', difficulty); |
| 56 | 180 | | if (Hash != GetHash() || !Hash.StartsWith(prefix)) |
| 16 | 181 | | return new Validation(false, "Invalid hash"); |
| | 182 | |
|
| 40 | 183 | | return new Validation(); |
| 416 | 184 | | } |
| | 185 | |
|
| | 186 | | /// <summary> |
| | 187 | | /// Creates a new block from a given <see cref="BlockInfo"/> object. |
| | 188 | | /// </summary> |
| | 189 | | /// <param name="blockInfo">The block info used to create a block.</param> |
| | 190 | | /// <returns>A new <see cref="Block"/> instance.</returns> |
| | 191 | | public static Block FromBlockInfo(BlockInfo blockInfo) |
| 8 | 192 | | { |
| 8 | 193 | | return new Block |
| 8 | 194 | | { |
| 8 | 195 | | Index = blockInfo.Index, |
| 8 | 196 | | PreviousHash = blockInfo.PreviousHash, |
| 8 | 197 | | Transactions = blockInfo.Transactions |
| 8 | 198 | | }; |
| 8 | 199 | | } |
| | 200 | | } |