public async Task Cannot_add_unhashed_block() { await Assert.ThrowsAsync <SqliteException>(async() => { CurrencyBlock block = new CurrencyBlock(); await Fixture.Value.AddAsync(block); }); }
private void BeforeSave(CurrencyBlock block) { foreach (var transaction in block.Transactions) { transaction.Hash = HashProvider.ComputeHashBytes(transaction); } block.Hash = block.ToHashBytes(HashProvider); }
public async Task Cannot_add_non_unique_block() { await Assert.ThrowsAsync <SqliteException>(async() => { CurrencyBlock block = CoinSettings.GenesisBlock; BeforeSave(block); await Fixture.Value.AddAsync(block); }); }
public void Can_retrieve_genesis_block_by_hash() { CurrencyBlock block = CoinSettings.GenesisBlock; BeforeSave(block); var retrieved = Fixture.Value.GetByHashAsync(block.Hash); Assert.NotNull(retrieved); }
public async Task <CurrencyBlock> MineAsync(string address) { var lastBlock = await _blockchain.GetLastBlockAsync(); var transactions = _blockchain.StreamAllTransactions(); CurrencyBlock baseBlock = GenerateNextBlock(address, lastBlock, transactions); return((CurrencyBlock)_proofOfWork.ProveWorkFor(baseBlock, _blockchain.GetDifficulty(baseBlock.Index.GetValueOrDefault()))); }
public async Task <IActionResult> VerifyLastBlock([FromBody] CurrencyBlock block) { var result = await _node.CheckReceivedBlocksAsync(block); if (result == null) { return(Accepted(new { Message = "Requesting blockchain to check." })); } if (result.Value) { return(Ok(block)); } return(StatusCode((int)HttpStatusCode.Conflict, new { Message = "Blockchain is up to date." })); }
public async Task <CurrencyBlock> AddBlockAsync(CurrencyBlock block) { var previousBlock = await GetLastBlockAsync(); Contract.Assert(block != null); Contract.Assert(previousBlock != null); // It only adds the block if it's valid (we need to compare to the previous one) if (true || CheckBlock(block, previousBlock)) { await _blocks.AddAsync(block); // After adding the block it removes the transactions of this block from the list of pending transactions await _transactions.DeleteAsync(block.Transactions.Select(x => x.Id)); _logger?.LogInformation($"Block added: {block.Hash}"); _logger?.LogDebug($"Block added: {JsonConvert.SerializeObject(block)}"); return(block); } return(null); }
private static async Task TransformBlockAsync(CurrencyBlock block, IDbConnection db) { if (block == null) { return; } const string objectsSql = "SELECT bo.* " + "FROM 'Block' b " + "INNER JOIN 'BlockObject' bo ON bo.'BlockIndex' = b.'Index' " + "ORDER BY b.'Index', bo.'Index'"; block.Objects = (await db.QueryAsync <BlockObject>(objectsSql, new { block.Index })).AsList(); const string transactionsSql = "SELECT t.* " + "FROM 'BlockTransaction' t " + "WHERE t.'BlockIndex' = @Index"; block.Transactions = (await db.QueryAsync <Transaction>(transactionsSql, new { block.Index })).AsList(); foreach (var transaction in block.Transactions) { const string transactionItemSql = "SELECT i.* " + "FROM 'BlockTransactionItem' i " + "WHERE i.Type = @Type AND i.TransactionId = @Id"; transaction.Data = new TransactionData { Inputs = (await db.QueryAsync <TransactionItem>(transactionItemSql, new { Type = TransactionDataType.Input, transaction.Id })).AsList(), Outputs = (await db.QueryAsync <TransactionItem>(transactionItemSql, new { Type = TransactionDataType.Output, transaction.Id })).AsList() }; } }
public async Task AddAsync(CurrencyBlock block) { using (var db = new SqliteConnection($"Data Source={DataFile}")) { await db.OpenAsync(); using (var t = db.BeginTransaction()) { var index = await db.QuerySingleAsync <long>( "INSERT INTO 'Block' ('PreviousHash','Timestamp','Nonce','Hash') VALUES (@PreviousHash,@Timestamp,@Nonce,@Hash); " + "SELECT LAST_INSERT_ROWID();", block, t); foreach (var @object in block.Objects ?? Enumerable.Empty <BlockObject>()) { await db.ExecuteAsync("INSERT INTO 'BlockObject' ('BlockIndex','Id','Hash','Type') VALUES (@BlockIndex,@Id,@Hash,@Type);", new { BlockIndex = index, @object.Index, @object.Timestamp, @object.Hash }, t); } foreach (var transaction in block.Transactions ?? Enumerable.Empty <Transaction>()) { await db.ExecuteAsync("INSERT INTO 'BlockTransaction' ('BlockIndex','Id','Hash','Type') VALUES (@BlockIndex,@Id,@Hash,@Type);", new { BlockIndex = index, transaction.Id, transaction.Hash, transaction.Type }, t); foreach (var input in transaction.Data?.Inputs ?? Enumerable.Empty <TransactionItem>()) { await db.ExecuteAsync("INSERT INTO 'BlockTransactionItem' ('TransactionId','Type','Index','Address','Amount','Signature') VALUES (@TransactionId,@Type,@Index,@Address,@Amount,@Signature);", new { input.TransactionId, Type = TransactionDataType.Input, input.Index, input.Address, input.Amount, input.Signature }, t); } foreach (var output in transaction.Data?.Outputs ?? Enumerable.Empty <TransactionItem>()) { await db.ExecuteAsync("INSERT INTO 'BlockTransactionItem' ('TransactionId','Type','Index','Address','Amount','Signature') VALUES (@TransactionId,@Type,@Index,@Address,@Amount,@Signature);", new { output.TransactionId, Type = TransactionDataType.Output, output.Index, output.Address, output.Amount, output.Signature }, t); } } t.Commit(); block.Index = index; } } }
public bool CheckBlock(CurrencyBlock newBlock, CurrencyBlock previousBlock) { var blockHash = newBlock.ToHashBytes(_hashProvider); if (previousBlock.Index + 1 != newBlock.Index) { // Check if the block is the last one var message = $"Invalid index: expected '{previousBlock.Index + 1}' but got '{newBlock.Index}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } if (previousBlock.Hash != newBlock.PreviousHash) { // Check if the previous block is correct var message = $"Invalid previoushash: expected '{previousBlock.Hash}' got '{newBlock.PreviousHash}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } if (blockHash != newBlock.Hash) { // Check if the hash is correct var message = $"Invalid hash: expected '{blockHash}' got '{newBlock.Hash}'"; throw new BlockAssertionException(message); } if (newBlock.GetDifficulty() >= GetDifficulty(newBlock.Index ?? 0)) { // If the difficulty level of the proof-of-work challenge is correct var message = $"Invalid proof-of-work difficulty: expected '{newBlock.GetDifficulty()}' to be smaller than '{GetDifficulty(newBlock.Index ?? 0)}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } // For each transaction in this block, check if it is valid foreach (var transaction in newBlock.Transactions) { CheckTransaction(transaction); } // Check if the sum of output transactions are equal the sum of input transactions + the reward for the block miner { var sumOfInputsAmount = newBlock.Transactions.SelectMany(x => x.Data.Inputs).Select(x => x.Amount).Sum(); var sumOfOutputsAmount = newBlock.Transactions.SelectMany(x => x.Data.Outputs).Select(x => x.Amount).Sum(); if (sumOfInputsAmount < sumOfOutputsAmount) { var message = $"Invalid block balance: inputs sum '{sumOfInputsAmount}', outputs sum '{sumOfOutputsAmount}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } } // Check if there is only 1 fee transaction and 1 reward transaction { var feeTransactions = newBlock.Transactions.Count(x => x.Type == TransactionType.Fee); if (feeTransactions > 1) { var message = $"Invalid fee transaction count: expected '1' got '${feeTransactions}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } var rewardTransactions = newBlock.Transactions.Count(x => x.Type == TransactionType.Reward); if (rewardTransactions > 1) { var message = $"Invalid reward transaction count: expected '1' got '${rewardTransactions}'"; _logger?.LogError(message); throw new BlockAssertionException(message); } } return(true); }
public static string ToHash(this CurrencyBlock block, IHashProvider hashProvider) { return(hashProvider.ComputeHashString(block)); }
public static byte[] ToHashBytes(this CurrencyBlock block, IHashProvider hashProvider) { return(hashProvider.ComputeHashBytes(block)); }
private CurrencyBlock GenerateNextBlock(string address, Block previousBlock, IEnumerable <Transaction> pendingTransactions) { var index = previousBlock.Index + 1; var previousHash = previousBlock.Hash; var timestamp = DateTimeOffset.UtcNow.Ticks; _logger?.LogInformation($"Generating next block at index {index}, previousHash={previousHash}"); // Gets all available transactions without considering data size or quantity var transactions = pendingTransactions.ToList(); // Add fee transaction // INFO: usually it's a fee over transaction size (not amount) if (transactions.Count > 0) { var fee = _coinSettings.FeePerTransaction * transactions.Count; _logger?.LogInformation($"Found {transactions.Count} pending transactions, adding them to the new block with fee transaction of {fee}"); var id = CryptoUtil.RandomString(); var feeTransaction = new Transaction { Id = id, Hash = null, Type = TransactionType.Fee, Data = new TransactionData { Outputs = new List <TransactionItem> { new TransactionItem { Index = 1, TransactionId = id, Type = TransactionDataType.Output, Amount = fee, // satoshis format Address = address.FromHex() // INFO: Usually here is a locking script (to check who and when this transaction output can be used), in this case it's a simple destination address } } } }; transactions.Add(feeTransaction); } // Add reward transaction of 50 coins if (address != null) { var reward = _coinSettings.Mining.MiningReward; _logger?.LogInformation($"Adding reward transaction of {reward}"); var id = CryptoUtil.RandomString(); var rewardTransaction = new Transaction { Id = id, Hash = null, Type = TransactionType.Reward, Data = new TransactionData { Outputs = new List <TransactionItem> { new TransactionItem { Index = 1, TransactionId = id, Type = TransactionDataType.Output, Amount = reward, Address = address.FromHex() // INFO: Usually here is a locking script (to check who and when this transaction output can be used), in this case it's a simple destination address } } } }; transactions.Add(rewardTransaction); } var nextBlock = new CurrencyBlock { Index = index, Nonce = 0, PreviousHash = previousHash, Timestamp = timestamp, Transactions = transactions }; return(nextBlock); }