< Summary

Information
Class: EF.Blockchain.Domain.Blockchain
Assembly: EF.Blockchain.Domain
File(s): C:\dev\@web3\web3-001-ef-blockchain\backend\EF.Blockchain\src\EF.Blockchain.Domain\Blockchain.cs
Line coverage
97%
Covered lines: 181
Uncovered lines: 5
Coverable lines: 186
Total lines: 327
Line coverage: 97.3%
Branch coverage
80%
Covered branches: 42
Total branches: 52
Branch coverage: 80.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Blocks()100%11100%
get_Mempool()100%11100%
get_NextIndex()100%11100%
.cctor()100%11100%
.ctor(...)100%11100%
CreateGenesis(...)100%11100%
GetLastBlock()100%11100%
GetDifficulty()100%11100%
AddTransaction(...)83.33%181892.85%
AddBlock(...)83.33%6696.15%
GetBlock(...)100%11100%
GetTransaction(...)100%66100%
IsValid()75%4493.75%
GetFeePerTx()100%11100%
GetNextBlock()75%44100%
GetTxInputs(...)100%22100%
GetTxOutputs(...)50%22100%
GetUtxo(...)75%8892.3%
GetBalance(...)50%22100%
GetRewardAmount(...)100%11100%

File(s)

C:\dev\@web3\web3-001-ef-blockchain\backend\EF.Blockchain\src\EF.Blockchain.Domain\Blockchain.cs

#LineLine coverage
 1namespace EF.Blockchain.Domain;
 2
 3/// <summary>
 4/// Represents the core blockchain logic, managing blocks, transactions, mining, and validation.
 5/// </summary>
 6public class Blockchain
 7{
 8    /// <summary>
 9    /// All confirmed blocks in the blockchain.
 10    /// </summary>
 315111    public List<Block> Blocks { get; private set; }
 12
 13    /// <summary>
 14    /// Pool of pending transactions waiting to be mined.
 15    /// </summary>
 192016    public List<Transaction> Mempool { get; private set; }
 17
 18    /// <summary>
 19    /// Index to be used for the next block.
 20    /// </summary>
 142421    public int NextIndex { get; private set; } = 0;
 22
 23    /// <summary>
 24    /// Number of blocks required to increase mining difficulty.
 25    /// </summary>
 826    public static readonly int DIFFICULTY_FACTOR = 5;
 27
 28    /// <summary>
 29    /// Maximum number of transactions per block.
 30    /// </summary>
 831    public static readonly int TX_PER_BLOCK = 2;
 32
 33    /// <summary>
 34    /// Maximum mining difficulty.
 35    /// </summary>
 836    public static readonly int MAX_DIFFICULTY = 62;
 37
 38    /// <summary>
 39    /// Initializes a new blockchain with a genesis block mined by the given miner.
 40    /// </summary>
 41    /// <param name="miner">The public key (wallet address) of the genesis miner.</param>
 28042    public Blockchain(string miner)
 28043    {
 28044        Blocks = new List<Block>();
 28045        Mempool = new List<Transaction>();
 46
 28047        var genesisBlock = CreateGenesis(miner);
 28048        Blocks.Add(genesisBlock);
 28049        NextIndex++;
 28050    }
 51
 52    private Block CreateGenesis(string miner)
 28053    {
 28054        var amount = GetRewardAmount(GetDifficulty());
 55
 28056        var txOutput = new TransactionOutput(toAddress: miner, amount: amount);
 57
 28058        var feeTx = Transaction.FromReward(txOutput);
 59
 28060        var blockGenesis = new Block(
 28061            index: NextIndex,
 28062            previousHash: "",
 28063            transactions: new List<Transaction> { feeTx }
 28064        );
 65
 28066        blockGenesis.Mine(GetDifficulty(), miner);
 67
 28068        return blockGenesis;
 28069    }
 70
 71    /// <summary>
 72    /// Returns the last confirmed block.
 73    /// </summary>
 74    public Block GetLastBlock()
 64875    {
 64876        return Blocks.Last();
 64877    }
 78
 79    /// <summary>
 80    /// Calculates the current mining difficulty based on the number of blocks.
 81    /// </summary>
 82    public int GetDifficulty()
 117583    {
 117584        return (int)Math.Ceiling((double)Blocks.Count / DIFFICULTY_FACTOR) + 1;
 117585    }
 86
 87    /// <summary>
 88    /// Attempts to add a transaction to the mempool after validating it.
 89    /// </summary>
 90    public Validation AddTransaction(Transaction transaction)
 5691    {
 5692        if (transaction.TxInputs != null && transaction.TxInputs.Any())
 5693        {
 5694            var from = transaction.TxInputs[0].FromAddress;
 95
 5696            var pendingTx = Mempool
 897                .Where(tx => tx.TxInputs != null)
 898                .SelectMany(tx => tx.TxInputs!)
 899                .Where(txi => txi.FromAddress == from)
 56100                .ToList();
 101
 56102            if (pendingTx.Any())
 8103                return new Validation(false, "This wallet has a pending transaction");
 104
 48105            var utxo = GetUtxo(from);
 216106            foreach (var txi in transaction.TxInputs)
 48107            {
 48108                var match = utxo.FirstOrDefault(txo =>
 80109                    txo.Tx == txi.PreviousTx && txo.Amount >= txi.Amount);
 110
 48111                if (match == null)
 24112                    return new Validation(false, "Invalid tx: the TXO is already spent or nonexistent");
 24113            }
 24114        }
 115
 24116        var validation = transaction.IsValid(GetDifficulty(), GetFeePerTx());
 24117        if (!validation.Success)
 0118            return new Validation(false, "Invalid tx: " + validation.Message);
 119
 72120        if (Blocks.Any(b => b.Transactions.Any(tx => tx.Hash == transaction.Hash)))
 0121            return new Validation(false, "Duplicated tx in blockchain");
 122
 24123        Mempool.Add(transaction);
 124
 24125        return new Validation(true, transaction.Hash);
 56126    }
 127
 128    /// <summary>
 129    /// Attempts to add a new mined block to the blockchain after validation.
 130    /// </summary>
 131    public Validation AddBlock(Block block)
 312132    {
 312133        var nextBlockInfo = GetNextBlock();
 312134        if (nextBlockInfo == null)
 8135            return new Validation(false, "There is no next block info");
 136
 304137        var validation = block.IsValid(
 304138            nextBlockInfo.PreviousHash,
 304139            nextBlockInfo.Index - 1,
 304140            nextBlockInfo.Difficulty,
 304141            nextBlockInfo.FeePerTx
 304142        );
 143
 304144        if (!validation.Success)
 280145            return new Validation(false, $"Invalid block: {validation.Message}");
 146
 24147        var txs = block.Transactions
 48148            .Where(tx => tx.Type != TransactionType.FEE)
 24149            .Select(tx => tx.Hash)
 24150            .ToList();
 151
 24152        var newMempool = Mempool
 64153            .Where(tx => !txs.Contains(tx.Hash))
 24154            .ToList();
 155
 24156        if (newMempool.Count + txs.Count != Mempool.Count)
 0157            return new Validation(false, "Invalid tx in block: mempool");
 158
 24159        Mempool = newMempool;
 24160        Blocks.Add(block);
 24161        NextIndex++;
 162
 24163        return new Validation(true, block.Hash);
 312164    }
 165
 166    /// <summary>
 167    /// Finds a block by its hash.
 168    /// </summary>
 169    public Block? GetBlock(string hash)
 8170    {
 16171        return Blocks.FirstOrDefault(b => b.Hash == hash);
 8172    }
 173
 174    /// <summary>
 175    /// Finds a transaction by hash in the mempool or blockchain.
 176    /// </summary>
 177    public TransactionSearch GetTransaction(string hash)
 32178    {
 64179        var mempoolIndex = Mempool.FindIndex(tx => tx.Hash == hash);
 32180        if (mempoolIndex != -1)
 16181        {
 16182            return new TransactionSearch(
 16183                transaction: Mempool[mempoolIndex],
 16184                mempoolIndex: mempoolIndex,
 16185                blockIndex: -1
 16186            );
 187        }
 188
 64189        var blockIndex = Blocks.FindIndex(b => b.Transactions.Any(tx => tx.Hash == hash));
 16190        if (blockIndex != -1)
 8191        {
 16192            var transaction = Blocks[blockIndex].Transactions.First(tx => tx.Hash == hash);
 8193            return new TransactionSearch(
 8194                transaction: transaction,
 8195                mempoolIndex: -1,
 8196                blockIndex: blockIndex
 8197            );
 198        }
 199
 8200        return new TransactionSearch(
 8201            blockIndex: -1,
 8202            mempoolIndex: -1,
 8203            transaction: null!
 8204        );
 32205    }
 206
 207    /// <summary>
 208    /// Validates the integrity of the entire blockchain from the latest block to the genesis.
 209    /// </summary>
 210    public Validation IsValid()
 32211    {
 64212        for (int i = Blocks.Count - 1; i > 0; i--)
 8213        {
 8214            var currentBlock = Blocks[i];
 8215            var previousBlock = Blocks[i - 1];
 8216            var validation = currentBlock.IsValid(
 8217                previousBlock.Hash,
 8218                previousBlock.Index,
 8219                GetDifficulty(),
 8220                GetFeePerTx()
 8221            );
 222
 8223            if (!validation.Success)
 8224                return new Validation(false, $"Invalid block #{currentBlock.Index}: {validation.Message}");
 0225        }
 24226        return new Validation();
 32227    }
 228
 229    /// <summary>
 230    /// Returns the current fixed fee per transaction.
 231    /// </summary>
 232    public int GetFeePerTx()
 360233    {
 360234        return 1;
 360235    }
 236
 237    /// <summary>
 238    /// Builds a <see cref="BlockInfo"/> object with data needed to mine the next block.
 239    /// </summary>
 240    public BlockInfo? GetNextBlock()
 336241    {
 336242        if (Mempool == null || Mempool.Count == 0)
 16243            return null;
 244
 320245        var transactions = Mempool.Take(Blockchain.TX_PER_BLOCK).ToList();
 246
 320247        var difficulty = GetDifficulty();
 320248        var previousHash = GetLastBlock().Hash;
 320249        var index = Blocks.Count;
 320250        var feePerTx = GetFeePerTx();
 320251        var maxDifficulty = MAX_DIFFICULTY;
 252
 320253        return new BlockInfo(
 320254            transactions: transactions,
 320255            difficulty: difficulty,
 320256            previousHash: previousHash,
 320257            index: index,
 320258            feePerTx: feePerTx,
 320259            maxDifficulty: maxDifficulty
 320260        );
 336261    }
 262
 263    /// <summary>
 264    /// Returns all transaction inputs for a specific wallet.
 265    /// </summary>
 266    public List<TransactionInput> GetTxInputs(string wallet)
 88267    {
 88268        return Blocks
 104269            .SelectMany(b => b.Transactions)
 104270            .Where(tx => tx.TxInputs != null && tx.TxInputs.Any())
 16271            .SelectMany(tx => tx.TxInputs!)
 16272            .Where(txi => txi.FromAddress == wallet)
 88273            .ToList();
 88274    }
 275
 276    /// <summary>
 277    /// Returns all transaction outputs for a specific wallet.
 278    /// </summary>
 279    public List<TransactionOutput> GetTxOutputs(string wallet)
 88280    {
 88281        return Blocks
 104282            .SelectMany(b => b.Transactions)
 104283            .Where(tx => tx.TxOutputs != null && tx.TxOutputs.Any())
 96284            .SelectMany(tx => tx.TxOutputs!)
 104285            .Where(txo => txo.ToAddress == wallet)
 88286            .ToList();
 88287    }
 288
 289    /// <summary>
 290    /// Returns the list of unspent transaction outputs (UTXOs) for a wallet.
 291    /// </summary>
 292    public List<TransactionOutput> GetUtxo(string wallet)
 88293    {
 88294        var txIns = GetTxInputs(wallet);
 88295        var txOuts = GetTxOutputs(wallet);
 296
 88297        if (txIns == null || txIns.Count == 0)
 72298            return txOuts;
 299
 80300        foreach (var txi in txIns)
 16301        {
 32302            var index = txOuts.FindIndex(txo => txo.Amount == txi.Amount);
 303
 16304            if (index != -1)
 0305                txOuts.RemoveAt(index);
 16306        }
 307
 16308        return txOuts;
 88309    }
 310
 311    /// <summary>
 312    /// Returns the total balance of a wallet based on UTXOs.
 313    /// </summary>
 314    public int GetBalance(string wallet)
 24315    {
 24316        var utxo = GetUtxo(wallet);
 40317        return utxo?.Sum(txo => txo.Amount) ?? 0;
 24318    }
 319
 320    /// <summary>
 321    /// Calculates the mining reward based on the current difficulty.
 322    /// </summary>
 323    public static int GetRewardAmount(int difficulty)
 672324    {
 672325        return (64 - difficulty) * 10;
 672326    }
 327}