| | 1 | | using System.Security.Cryptography; |
| | 2 | | using System.Text; |
| | 3 | | using System.Text.Json.Serialization; |
| | 4 | |
|
| | 5 | | namespace EF.Blockchain.Domain; |
| | 6 | |
|
| | 7 | | /// <summary> |
| | 8 | | /// Represents a transaction on the blockchain, containing inputs, outputs, and metadata. |
| | 9 | | /// </summary> |
| | 10 | | public class Transaction |
| | 11 | | { |
| | 12 | | /// <summary> |
| | 13 | | /// Type of the transaction (e.g., REGULAR or FEE). |
| | 14 | | /// </summary> |
| 6272 | 15 | | public TransactionType Type { get; private set; } |
| | 16 | |
|
| | 17 | | /// <summary> |
| | 18 | | /// Timestamp in milliseconds when the transaction was created. |
| | 19 | | /// </summary> |
| 4216 | 20 | | public long Timestamp { get; private set; } |
| | 21 | |
|
| | 22 | | /// <summary> |
| | 23 | | /// Unique hash of the transaction. |
| | 24 | | /// </summary> |
| 157143 | 25 | | public string Hash { get; private set; } |
| | 26 | |
|
| | 27 | | /// <summary> |
| | 28 | | /// List of inputs that reference previous unspent outputs. |
| | 29 | | /// </summary> |
| 8608 | 30 | | public List<TransactionInput>? TxInputs { get; private set; } |
| | 31 | |
|
| | 32 | | /// <summary> |
| | 33 | | /// List of outputs generated by this transaction. |
| | 34 | | /// </summary> |
| 15024 | 35 | | public List<TransactionOutput> TxOutputs { get; private set; } |
| | 36 | |
|
| | 37 | | /// <summary> |
| | 38 | | /// Constructor used for JSON deserialization. |
| | 39 | | /// </summary> |
| | 40 | | [JsonConstructor] |
| 16 | 41 | | public Transaction( |
| 16 | 42 | | TransactionType type, |
| 16 | 43 | | long timestamp, |
| 16 | 44 | | string hash, |
| 16 | 45 | | List<TransactionInput>? txInputs = null, |
| 16 | 46 | | List<TransactionOutput>? txOutputs = null) |
| 16 | 47 | | { |
| 16 | 48 | | Type = type; |
| 16 | 49 | | Timestamp = timestamp; |
| 16 | 50 | | Hash = hash; |
| 16 | 51 | | TxInputs = txInputs; |
| 16 | 52 | | TxOutputs = txOutputs; |
| 16 | 53 | | } |
| | 54 | |
|
| | 55 | | /// <summary> |
| | 56 | | /// Creates a new transaction with optional parameters. Automatically computes the hash and sets output references. |
| | 57 | | /// </summary> |
| 1488 | 58 | | public Transaction( |
| 1488 | 59 | | TransactionType? type = null, |
| 1488 | 60 | | long? timestamp = null, |
| 1488 | 61 | | List<TransactionInput>? txInputs = null, |
| 1488 | 62 | | List<TransactionOutput>? txOutputs = null) |
| 1488 | 63 | | { |
| 1488 | 64 | | Type = type ?? TransactionType.REGULAR; |
| 1488 | 65 | | Timestamp = timestamp ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); |
| 1488 | 66 | | TxInputs = txInputs; |
| 1488 | 67 | | TxOutputs = txOutputs ?? new List<TransactionOutput>(); |
| | 68 | |
|
| 1488 | 69 | | Hash = GetHash(); |
| | 70 | |
|
| | 71 | | // Set tx reference hash for outputs |
| 6816 | 72 | | foreach (var txo in TxOutputs) |
| 1176 | 73 | | { |
| 1176 | 74 | | txo.SetTx(Hash); |
| 1176 | 75 | | } |
| 1488 | 76 | | } |
| | 77 | |
|
| | 78 | | /// <summary> |
| | 79 | | /// Calculates the SHA-256 hash of the transaction. |
| | 80 | | /// </summary> |
| | 81 | | /// <returns>The hex-encoded lowercase hash string.</returns> |
| | 82 | | public string GetHash() |
| 2608 | 83 | | { |
| 2608 | 84 | | var from = TxInputs != null && TxInputs.Any() |
| 1080 | 85 | | ? string.Join(",", TxInputs.Select(txi => txi.Signature)) |
| 2608 | 86 | | : ""; |
| | 87 | |
|
| 2608 | 88 | | var to = TxOutputs != null && TxOutputs.Any() |
| 2280 | 89 | | ? string.Join(",", TxOutputs.Select(txo => txo.GetHash())) |
| 2608 | 90 | | : ""; |
| | 91 | |
|
| 2608 | 92 | | var raw = $"{Type}{from}{to}{Timestamp}"; |
| 2608 | 93 | | using var sha256 = SHA256.Create(); |
| 2608 | 94 | | var bytes = Encoding.UTF8.GetBytes(raw); |
| 2608 | 95 | | var hashBytes = sha256.ComputeHash(bytes); |
| 2608 | 96 | | return Convert.ToHexString(hashBytes).ToLower(); |
| 2608 | 97 | | } |
| | 98 | |
|
| | 99 | | /// <summary> |
| | 100 | | /// Calculates the fee of the transaction (input sum - output sum). |
| | 101 | | /// </summary> |
| | 102 | | /// <returns>The fee amount.</returns> |
| | 103 | | public int GetFee() |
| 16 | 104 | | { |
| 16 | 105 | | if (TxInputs == null || !TxInputs.Any()) |
| 8 | 106 | | return 0; |
| | 107 | |
|
| 16 | 108 | | var inputSum = TxInputs.Sum(txi => txi.Amount); |
| 16 | 109 | | var outputSum = TxOutputs?.Sum(txo => txo.Amount) ?? 0; |
| | 110 | |
|
| 8 | 111 | | return inputSum - outputSum; |
| 16 | 112 | | } |
| | 113 | |
|
| | 114 | | /// <summary> |
| | 115 | | /// Validates the transaction, checking signatures, input/output consistency, hash integrity, and reward logic. |
| | 116 | | /// </summary> |
| | 117 | | /// <param name="difficulty">Blockchain difficulty at the time.</param> |
| | 118 | | /// <param name="totalFees">Total fees collected for this block.</param> |
| | 119 | | /// <returns>A <see cref="Validation"/> result indicating if the transaction is valid.</returns> |
| | 120 | | public Validation IsValid(int difficulty, int totalFees) |
| 824 | 121 | | { |
| 824 | 122 | | if (Hash != GetHash()) |
| 16 | 123 | | return new Validation(false, "Invalid hash"); |
| | 124 | |
|
| 1608 | 125 | | if (TxOutputs == null || !TxOutputs.Any() || TxOutputs.Any(txo => !txo.IsValid().Success)) |
| 8 | 126 | | return new Validation(false, "Invalid TXO"); |
| | 127 | |
|
| 800 | 128 | | if (TxInputs != null && TxInputs.Any()) |
| 416 | 129 | | { |
| 416 | 130 | | var inputValidation = TxInputs |
| 416 | 131 | | .Select(txi => txi.IsValid()) |
| 416 | 132 | | .Where(v => !v.Success) |
| 416 | 133 | | .ToList(); |
| | 134 | |
|
| 416 | 135 | | if (inputValidation.Any()) |
| 272 | 136 | | { |
| 544 | 137 | | var message = string.Join(" ", inputValidation.Select(v => v.Message)); |
| 272 | 138 | | return new Validation(false, $"Invalid tx: {message}"); |
| | 139 | | } |
| | 140 | |
|
| 288 | 141 | | var inputSum = TxInputs.Sum(txi => txi.Amount); |
| 288 | 142 | | var outputSum = TxOutputs.Sum(txo => txo.Amount); |
| 144 | 143 | | if (inputSum < outputSum) |
| 8 | 144 | | return new Validation(false, "Invalid tx: input amounts must be equals or greater than outputs amounts") |
| 136 | 145 | | } |
| | 146 | |
|
| 1040 | 147 | | if (TxOutputs.Any(txo => txo.Tx != Hash)) |
| 8 | 148 | | return new Validation(false, "Invalid TXO reference hash"); |
| | 149 | |
|
| 512 | 150 | | if (Type == TransactionType.FEE) |
| 392 | 151 | | { |
| 392 | 152 | | var txo = TxOutputs[0]; |
| 392 | 153 | | if (txo.Amount > Blockchain.GetRewardAmount(difficulty) + totalFees) |
| 8 | 154 | | return new Validation(false, "Invalid tx reward"); |
| 384 | 155 | | } |
| | 156 | |
|
| 504 | 157 | | return new Validation(); |
| 824 | 158 | | } |
| | 159 | |
|
| | 160 | | /// <summary> |
| | 161 | | /// Creates a special transaction that represents a mining reward (fee transaction). |
| | 162 | | /// </summary> |
| | 163 | | /// <param name="txo">The output that represents the reward to the miner.</param> |
| | 164 | | /// <returns>A fee-type transaction.</returns> |
| | 165 | | public static Transaction FromReward(TransactionOutput txo) |
| 288 | 166 | | { |
| 288 | 167 | | var tx = new Transaction( |
| 288 | 168 | | type: TransactionType.FEE, |
| 288 | 169 | | txOutputs: new List<TransactionOutput> { txo } |
| 288 | 170 | | ); |
| | 171 | |
|
| 288 | 172 | | tx.Hash = tx.GetHash(); |
| 288 | 173 | | tx.TxOutputs[0].SetTx(tx.Hash); |
| 288 | 174 | | return tx; |
| 288 | 175 | | } |
| | 176 | | } |