Skip to content

Commit

Permalink
Merge ea3c34e into 2fbb314
Browse files Browse the repository at this point in the history
  • Loading branch information
mrnaveira committed Jun 26, 2022
2 parents 2fbb314 + ea3c34e commit 1e209a6
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 30 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ MAX_NONCE = 1000000
DIFFICULTY = 10

# Amount of milliseconds the miner wil wait before checking new transactions
TRANSACTION_WAITING_MS = 10000
TRANSACTION_WAITING_MS = 10000

# Recipient address of the miner, to receive block mining rewards
MINER_ADDRESS = 00000000000000000000000000000000000000000000000000000000000000
38 changes: 29 additions & 9 deletions src/miner.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::{
model::{Block, BlockHash, Blockchain, TransactionPool, TransactionVec},
model::{
Address, Block, BlockHash, Blockchain, Transaction, TransactionPool, TransactionVec,
BLOCK_SUBSIDY,
},
util::{
execution::{sleep_millis, Runnable},
Context,
Expand All @@ -15,6 +18,7 @@ pub enum MinerError {
}

pub struct Miner {
miner_address: Address,
max_blocks: u64,
max_nonce: u64,
tx_waiting_ms: u64,
Expand All @@ -31,9 +35,10 @@ impl Runnable for Miner {

impl Miner {
pub fn new(context: &Context) -> Miner {
let target = Miner::create_target(context.config.difficulty);
let target = Self::create_target(context.config.difficulty);

Miner {
miner_address: context.config.miner_address.clone(),
max_blocks: context.config.max_blocks,
max_nonce: context.config.max_nonce,
tx_waiting_ms: context.config.tx_waiting_ms,
Expand Down Expand Up @@ -70,7 +75,7 @@ impl Miner {

// try to find a valid next block of the blockchain
let last_block = self.blockchain.get_last_block();
let mining_result = self.mine_block(&last_block, transactions.clone());
let mining_result = self.mine_block(&last_block, &transactions.clone());
match mining_result {
Some(block) => {
info!("valid block found for index {}", block.index);
Expand Down Expand Up @@ -100,9 +105,14 @@ impl Miner {
// Tries to find the next valid block of the blockchain
// It will create blocks with different "nonce" values until one has a hash that matches the difficulty
// Returns either a valid block (that satisfies the difficulty) or "None" if no block was found
fn mine_block(&self, last_block: &Block, transactions: TransactionVec) -> Option<Block> {
fn mine_block(&self, last_block: &Block, transactions: &TransactionVec) -> Option<Block> {
// Add the coinbase transaction as the first transaction in the block
let coinbase = self.create_coinbase_transaction();
let mut block_transactions = transactions.clone();
block_transactions.insert(0, coinbase);

for nonce in 0..self.max_nonce {
let next_block = self.create_next_block(last_block, transactions.clone(), nonce);
let next_block = self.create_next_block(last_block, block_transactions.clone(), nonce);

// A valid block must have a hash with enough starting zeroes
// To check that, we simply compare against a binary data mask
Expand All @@ -128,6 +138,14 @@ impl Miner {
// hash of the new block is automatically calculated on creation
Block::new(index, nonce, previous_hash, transactions)
}

fn create_coinbase_transaction(&self) -> Transaction {
Transaction {
sender: Address::default(),
recipient: self.miner_address.clone(),
amount: BLOCK_SUBSIDY,
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -182,7 +200,7 @@ mod tests {
// check that the block is mined
let miner = create_miner(difficulty, max_nonce);
let last_block = create_empty_block();
let result = miner.mine_block(&last_block, Vec::new());
let result = miner.mine_block(&last_block, &Vec::new());
assert!(result.is_some());

// check that the block is valid
Expand All @@ -202,7 +220,7 @@ mod tests {
// check that the block is not mined
let miner = create_miner(difficulty, max_nonce);
let last_block = create_empty_block();
let result = miner.mine_block(&last_block, Vec::new());
let result = miner.mine_block(&last_block, &Vec::new());
assert!(result.is_none());
}

Expand Down Expand Up @@ -232,9 +250,9 @@ mod tests {
// the mined block must be valid
assert_mined_block_is_valid(mined_block, genesis_block, blockchain.difficulty);

// the mined block must include the transaction added previously
// the mined block must include the transaction added previously plus the coinbase
let mined_transactions = &mined_block.transactions;
assert_eq!(mined_transactions.len(), 1);
assert_eq!(mined_transactions.len(), 2);

// the transaction pool must be empty
// because the transaction was added to the block when mining
Expand Down Expand Up @@ -265,6 +283,7 @@ mod tests {
}

fn create_miner(difficulty: u32, max_nonce: u64) -> Miner {
let miner_address = Address::default();
let max_blocks = 1;
let tx_waiting_ms = 1;
let target = Miner::create_target(difficulty);
Expand All @@ -273,6 +292,7 @@ mod tests {
let pool = TransactionPool::new();

Miner {
miner_address,
max_blocks,
max_nonce,
tx_waiting_ms,
Expand Down
2 changes: 1 addition & 1 deletion src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod transaction_pool;
// It also avoids verbose module imports from other files
pub use address::Address;
pub use block::{Block, BlockHash};
pub use blockchain::{Blockchain, BlockchainError};
pub use blockchain::{Blockchain, BlockchainError, BLOCK_SUBSIDY};
pub use transaction::Transaction;
pub use transaction_pool::{TransactionPool, TransactionVec};

Expand Down
11 changes: 10 additions & 1 deletion src/model/address.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
convert::{TryFrom, TryInto},
fmt,
str::FromStr,
};

use serde::{Deserialize, Serialize};
Expand All @@ -20,7 +21,7 @@ pub enum AddressError {
InvalidLength,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
#[serde(try_from = "String", into = "String")]
pub struct Address([Byte; LEN]);

Expand All @@ -47,6 +48,14 @@ impl TryFrom<String> for Address {
}
}

impl FromStr for Address {
type Err = AddressError;

fn from_str(s: &str) -> Result<Self, AddressError> {
Address::try_from(s.to_string())
}
}

impl From<Address> for String {
fn from(account: Address) -> Self {
account.to_string()
Expand Down
85 changes: 73 additions & 12 deletions src/model/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub type BlockVec = Vec<Block>;
// We don't need to export this because concurrency is encapsulated in this file
type SyncedBlockVec = Arc<Mutex<BlockVec>>;

pub const BLOCK_SUBSIDY: u64 = 100;

// Error types to return when trying to add blocks with invalid fields
#[derive(Error, PartialEq, Debug)]
#[allow(clippy::enum_variant_names)]
Expand All @@ -24,6 +26,9 @@ pub enum BlockchainError {

#[error("Invalid difficulty")]
InvalidDifficulty,

#[error("Invalid coinbase transaction")]
InvalidCoinbaseTransaction,
}

// Struct that holds all the blocks in the blockhain
Expand Down Expand Up @@ -51,6 +56,22 @@ impl Blockchain {
}
}

fn create_genesis_block() -> Block {
let index = 0;
let nonce = 0;
let previous_hash = BlockHash::default();
let transactions = Vec::new();

let mut block = Block::new(index, nonce, previous_hash, transactions);

// to easily sync multiple nodes in a network, the genesis blocks must match
// so we clear the timestamp so the hash of the genesis block is predictable
block.timestamp = 0;
block.hash = block.calculate_hash();

block
}

// Returns a copy of the most recent block in the blockchain
pub fn get_last_block(&self) -> Block {
let blocks = self.blocks.lock().unwrap();
Expand Down Expand Up @@ -96,31 +117,35 @@ impl Blockchain {
return Err(BlockchainError::InvalidDifficulty.into());
}

self.validate_coinbase(&block)?;

// append the block to the end
blocks.push(block);

Ok(())
}

fn create_genesis_block() -> Block {
let index = 0;
let nonce = 0;
let previous_hash = BlockHash::default();
let transactions = Vec::new();
fn validate_coinbase(&self, block: &Block) -> Result<()> {
let coinbase_err = Err(BlockchainError::InvalidCoinbaseTransaction.into());

let mut block = Block::new(index, nonce, previous_hash, transactions);
let coinbase = match block.transactions.first() {
Some(transaction) => transaction,
None => return coinbase_err,
};

// to easily sync multiple nodes in a network, the genesis blocks must match
// so we clear the timestamp so the hash of the genesis block is predictable
block.timestamp = 0;
block.hash = block.calculate_hash();
// In coinbase transactions, we only care about the amount
if coinbase.amount != BLOCK_SUBSIDY {
return coinbase_err;
}

block
Ok(())
}
}

#[cfg(test)]
mod tests {
use crate::model::{Address, Transaction};

use super::*;

const NO_DIFFICULTY: u32 = 0;
Expand Down Expand Up @@ -150,7 +175,12 @@ mod tests {

// create a valid block
let previous_hash = blockchain.get_last_block().hash;
let block = Block::new(1, 0, previous_hash, Vec::new());
let coinbase = Transaction {
sender: Address::default(),
recipient: Address::default(),
amount: BLOCK_SUBSIDY,
};
let block = Block::new(1, 0, previous_hash, vec![coinbase]);

// add it to the blockchain and check it was really added
let result = blockchain.add_block(block.clone());
Expand Down Expand Up @@ -222,6 +252,37 @@ mod tests {
assert_err(result, BlockchainError::InvalidDifficulty);
}

#[test]
fn should_not_let_adding_block_with_no_coinbase() {
let blockchain = Blockchain::new(NO_DIFFICULTY);

// create a block without a coinbase
let previous_hash = blockchain.get_last_block().hash;
let block = Block::new(1, 0, previous_hash, vec![]);

// try adding the invalid block, it should return an error
let result = blockchain.add_block(block.clone());
assert_err(result, BlockchainError::InvalidCoinbaseTransaction);
}

#[test]
fn should_not_let_adding_block_with_invalid_coinbase() {
let blockchain = Blockchain::new(NO_DIFFICULTY);

// create a block with an invalid coinbase amount
let previous_hash = blockchain.get_last_block().hash;
let coinbase = Transaction {
sender: Address::default(),
recipient: Address::default(),
amount: BLOCK_SUBSIDY + 1,
};
let block = Block::new(1, 0, previous_hash, vec![coinbase]);

// try adding the invalid block, it should return an error
let result = blockchain.add_block(block.clone());
assert_err(result, BlockchainError::InvalidCoinbaseTransaction);
}

fn assert_err(result: Result<(), anyhow::Error>, error_type: BlockchainError) {
let err = result.unwrap_err().downcast::<BlockchainError>().unwrap();
assert_eq!(err, error_type);
Expand Down
4 changes: 4 additions & 0 deletions src/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use dotenv::dotenv;
use std::env;
use std::str::FromStr;

use crate::model::Address;

type StringVec = Vec<String>;

// Encapsulates configuration values to be used across the application
Expand All @@ -21,6 +23,7 @@ pub struct Config {
pub max_nonce: u64,
pub difficulty: u32,
pub tx_waiting_ms: u64,
pub miner_address: Address,
}

// The implementation reads the values from environment variables
Expand All @@ -43,6 +46,7 @@ impl Config {
max_nonce: Config::read_envvar::<u64>("MAX_NONCE", 1_000_000),
difficulty: Config::read_envvar::<u32>("DIFFICULTY", 10),
tx_waiting_ms: Config::read_envvar::<u64>("TRANSACTION_WAITING_MS", 10000),
miner_address: Config::read_envvar::<Address>("MINER_ADDRESS", Address::default()),
}
}

Expand Down
16 changes: 11 additions & 5 deletions tests/api_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod common;

use serial_test::serial;

use crate::common::{Api, Block, BlockHash, ServerBuilder, Transaction, ALICE, BOB};
use crate::common::{Api, Block, BlockHash, ServerBuilder, Transaction, ALICE, BLOCK_SUBSIDY, BOB};

#[test]
#[serial]
Expand Down Expand Up @@ -52,9 +52,9 @@ fn test_should_let_add_transactions() {
assert_eq!(mined_block.index, 1);
assert_eq!(mined_block.previous_hash, genesis_block.hash);

// ...and contains the transaction that we added
assert_eq!(mined_block.transactions.len(), 1);
let mined_transaction = mined_block.transactions.first().unwrap();
// ...and contains the transaction that we added (plus the coinbase)
assert_eq!(mined_block.transactions.len(), 2);
let mined_transaction = mined_block.transactions.last().unwrap();
assert_eq!(*mined_transaction, transaction);
}

Expand All @@ -64,6 +64,11 @@ fn test_should_let_add_transactions() {
fn test_should_let_add_valid_block() {
let node = ServerBuilder::new().start();
let genesis_block = node.get_last_block();
let coinbase = Transaction {
sender: ALICE.to_string(),
recipient: ALICE.to_string(),
amount: BLOCK_SUBSIDY,
};

let valid_block = Block {
// there is the genesis block already, so the next index is 1
Expand All @@ -75,7 +80,8 @@ fn test_should_let_add_valid_block() {
// the api automatically recalculates the hash...
// ...so no need to add a valid one here
hash: BlockHash::default(),
transactions: [].to_vec(),
// must include the coinbase transaction
transactions: vec![coinbase],
};
let res = node.add_block(&valid_block);
assert_eq!(res.status().as_u16(), 200);
Expand Down
Loading

0 comments on commit 1e209a6

Please sign in to comment.