def test_block_basic(): """Test the definition of a basic Block""" block = Block(0, 'block data') assert block.index == 0 assert block.previous_hash is None assert block.difficulty == 0 assert block.data == 'block data' assert block.proof == 0 block = Block(1, 'block data', timestamp=0) assert block.index == 1 assert block.previous_hash is None assert block.difficulty == 0 assert block.data == 'block data' assert block.proof == 0 assert block.timestamp == 0 assert block.hash == 'bc9ef25d3e98e4fd29add80d59b5ffc315176b8de85167569be118c9f3f42c2b' # Next block evaluation block = Block(block, 'Second block', timestamp=1) assert block.index == 2 assert block.previous_hash == 'bc9ef25d3e98e4fd29add80d59b5ffc315176b8de85167569be118c9f3f42c2b' assert block.difficulty == 0 assert block.data == 'Second block' assert block.proof == 0 assert block.timestamp == 1 assert block.hash == '5641942dc909b7a89f79a0d8bc022aca797cc66001194d82711cc508e90e997b' # Validate hash calculation assert Block.calculate_hash(block) == block.hash
def test_block_valid(): """Test validation of a block""" block = Block(0, 'Test Data', timestamp=0) assert block.hash_satisfies_difficulty assert block.is_valid block.difficulty = 4 assert block.hash_satisfies_difficulty is False assert block.is_valid is False block.proof = 49 assert block.proof == 49 assert block.hash_satisfies_difficulty assert block.is_valid # Introduce a fake hash block.hash = '0f59bbd5a22a6484cc206b7ee9d4bc1744bed9c5649ff332f88a0eec461f58c8' assert block.hash == '0f59bbd5a22a6484cc206b7ee9d4bc1744bed9c5649ff332f88a0eec461f58c8' assert block.hash_satisfies_difficulty assert block.is_valid is False
def test_block_comparisons(): """Test the comparison of different blocks""" block_0 = Block(0, 'block data', timestamp=0) block_1 = Block(0, 'block data', timestamp=0) block_2 = Block(1, 'block data', timestamp=0) assert block_0 == block_0 assert block_0 == block_1 assert block_0 != block_2
def __init__(self, block=None): """BlockChain object""" # Difficulty update parameters # 59 Seconds - the minimum interval between blocks is a minute self.minimum_interval = 59 # 10 minutes - the objective interval between blocks self.block_interval = 600 # 144 block - the time for evaluate intervals (one per day 6 * 24) self.difficulty_interval = 144 # Currency values self.__unspent = None self.amount_mining = 0 # Validate the inputs if block is None: self.__candidate = Block.genesis_block() elif isinstance(block, Block): self.__candidate = block else: raise Exception("The input parameter must be a Block object") if self.__candidate.is_valid: self.chain = [self.__candidate] self.__candidate = None else: self.chain = []
def test_update_difficulty(): """Test increase and decrease difficulty values""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1, 0, 0, 0), difficulty=4, mining=True) blockchain = BlockChain(block) # Change the interval for tests blockchain.difficulty_interval = 2 # Blocks generates in a minute: the difficulty must increase for minute in range(1, 7): blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 0, minute, 0)) blockchain.mining_candidate() assert blockchain.chain[3].difficulty == 4 assert blockchain.chain[4].difficulty == 5 assert blockchain.chain[5].difficulty == 5 assert blockchain.chain[6].difficulty == 6 # Blocks generates in any eleven minutes: the difficulty must decrease for minute in range(17, 60, 11): blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 0, minute, 0)) blockchain.mining_candidate() blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 1, 1, 0)) blockchain.mining_candidate() assert blockchain.chain[9].difficulty == 7 assert blockchain.chain[10].difficulty == 6
def test_replace_chain(): """Test replace a chain""" # Generate the genesis block block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) # Generate a old chain old_chain = BlockChain(block) # Generate longer chain blockchain = BlockChain(block) # Cannot be replace the chain assert old_chain.replace_chain(blockchain) is False # Add new block assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 2)) assert blockchain.candidate_proof(4) # Replace the chain assert old_chain.replace_chain(blockchain) # Check the chain values assert old_chain.num_blocks == 2 assert old_chain.candidate_block is None
def new_cryptocurrency(address, amount, timestamp=None, proof=0, difficulty=0, mining=False): """Create a new cryptocurrency Args: address (string): the address of the first user amount (Double): the amount for the first user timestamp (String): the genesis block time proof (Integer): the proof difficulty (Integer): the number of zeros in the hash to validate the block mining (Boolean): logical value indicating if the block must be mined Return: (BlockChain): A new blockchain """ output = OutputTransaction(address, amount) transaction = Transaction(timestamp, output) block = Block.genesis_block([transaction], timestamp=timestamp, proof=proof, difficulty=difficulty, mining=mining) blokchain = BlockChain(block) blokchain.amount_mining = amount return blokchain
def test_add_candidates(): """Validate to include a new candidate""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) assert blockchain.last_block == block assert blockchain.candidate_block is None assert blockchain.num_blocks == 1 assert blockchain.is_valid # Add a new candidate assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 2)) assert blockchain.last_block == block assert blockchain.candidate_block is not None assert blockchain.num_blocks == 1 assert blockchain.is_valid # Fail in the mining process assert blockchain.mining_candidate(maximum_iter=0) is False # Mining the candidate block assert blockchain.mining_candidate() # The blockchain is now valid assert blockchain.last_block != block assert blockchain.candidate_block is None assert blockchain.num_blocks == 2 assert blockchain.is_valid
def test_basic_blockchain(): """Test the basic blockchain""" # BlockChain without values blockchain = BlockChain() assert blockchain.last_block is not None assert blockchain.candidate_block is None assert blockchain.num_blocks == 1 assert blockchain.is_valid # Manual definition of the genesis block block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4) blockchain = BlockChain(block) assert blockchain.last_block is None assert blockchain.candidate_block == block assert blockchain.num_blocks == 0 assert blockchain.is_valid is False # it is not possible to add a new candidate assert blockchain.add_candidate(None) is False # Mining the candidate block assert blockchain.mining_candidate() # The blockchain is now valid assert blockchain.last_block == block assert blockchain.candidate_block is None assert blockchain.num_blocks == 1 assert blockchain.is_valid
def test_validate_blockchain(): """Test BlockChain validation""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) # Add a new candidate assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 2)) assert blockchain.candidate_proof(4) assert blockchain.is_valid # Alter the index blockchain.chain[1].index = 2 assert blockchain.chain[1].is_valid is False assert blockchain.is_valid is False # Mining new id blockchain.chain[1].mining() assert blockchain.chain[1].is_valid assert blockchain.is_valid is False # Change the block blockchain.chain[1] = block assert blockchain.is_valid is False
def test_block_difficulty(): """Test proof-of-work validation""" block = Block(0, 'Test Data', timestamp=0) assert block.hash_satisfies_difficulty block.difficulty = 4 assert block.hash_satisfies_difficulty is False block.proof = 49 assert block.proof == 49 assert block.hash_satisfies_difficulty block.proof = 5 assert block.proof == 5 assert block.hash_satisfies_difficulty is False
def add_candidate(self, data, timestamp=None, proof=0): """Insert a new candidate in the chain Return: (logical): true where the candidate can be assigned """ if self.__candidate is not None: return False else: # Get the timestamp value where it is None if timestamp is None: timestamp = datetime.now() # Validate minimum timestamp period interval = timestamp - self.last_block.timestamp if interval.total_seconds() < self.minimum_interval: return False # Evaluate the difficulty if self.num_blocks > self.difficulty_interval and self.num_blocks % self.difficulty_interval == 0: interval = self.chain[-1].timestamp - self.chain[ -self.difficulty_interval - 1].timestamp interval = interval.total_seconds() / self.difficulty_interval if interval > self.block_interval: difficulty = self.last_block.difficulty - 1 elif interval < self.block_interval: difficulty = self.last_block.difficulty + 1 else: difficulty = self.last_block.difficulty self.__candidate = Block(self.last_block.index + 1, data, previous_hash=self.last_block.hash, timestamp=timestamp, proof=proof, difficulty=difficulty) return True
def test_minimum_interval(): """Test minimum interval period""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1, 0, 0, 0), difficulty=4, mining=True) blockchain = BlockChain(block) # A block in a minute assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 0, 1, 0)) assert blockchain.mining_candidate() # A block in 30 seconds is not valid assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 0, 1, 30)) is False
def test_mining(): """Test the mining process in the Block""" block = Block(0, 'Test data', timestamp=0) block.difficulty = 8 assert block.hash_satisfies_difficulty is False assert block.is_valid is False # Calculate a valid proof assert block.mining() assert block.hash_satisfies_difficulty assert block.is_valid # Set a value which cannot get a valid result assert block.mining(init=0, maximum_iter=100) is False assert block.hash_satisfies_difficulty is False assert block.is_valid is False
def test_genesis_block(): """Validate the genesis block test""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=6, mining=True) assert block.index == 0 assert block.previous_hash == '5e9fe54187feed1f12324ffa7bd9dc3d662706e1fd66a97eafbbefa912262aa2' assert block.difficulty == 6 assert block.hash == '01392e12860c25f46adad36d001d9c9aeafc280cf8be290021e61c2ec2cb54de' assert block.timestamp == datetime(2000, 1, 1) assert block.data is None assert block.proof == 46
def test_mining_candidate(): """Test mining candidate """ block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) # Add a new candidate assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 2)) assert blockchain.mining_candidate() # Add an empty candidate assert blockchain.add_candidate(None) assert blockchain.mining_candidate() # Add an invalid candidate (same date) assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1)) is False assert blockchain.mining_candidate() is False
def test_change_block_contains(): """Test different attacks to the blockchain""" data = ["Avocado", "Apple", "Cherry", "Orange", "Strawberry"] block = Block.genesis_block(data[0], timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) for minute in range(1, len(data)): assert blockchain.add_candidate(data[minute], timestamp=datetime(2000, 1, 1, 0, minute, 0)) assert blockchain.mining_candidate() assert blockchain.is_valid # Alter the blockchain data blockchain.chain[2].data = 'Khaki' assert blockchain.is_valid is False
def test_block_repr(): """Test the report function""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) assert repr(blockchain) == \ "Block: 0 (2000-01-01 00:00:00) - Hash: 015921ac58652b4e01d109cbb3ad10b55836e700a93e761036343ce8a82023ae\n" for minute in range(1, 6): blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1, 0, minute, 0)) blockchain.mining_candidate() assert repr(blockchain) == \ "Block: 5 (2000-01-01 00:05:00) - Hash: 0332726221951b73e5fb945914341aa1e92014151680375cdf8c632256dc9a54\n" + \ "Block: 4 (2000-01-01 00:04:00) - Hash: 0bf9e27631d08c326b15f7b459b47f91d9202c07ede76ce8503d636e09070402\n" + \ "Block: 3 (2000-01-01 00:03:00) - Hash: 07ded3c477dc38fe3d0bc434b2924430f81231a0d7878f906e2a655c1f17d444\n" + \ "Block: 2 (2000-01-01 00:02:00) - Hash: 0f8ed5b1cc4dca95c0c04b5b64a1ac409d636dbfd294282092f579d3872b1c15\n" + \ "Block: 1 (2000-01-01 00:01:00) - Hash: 037eeba6b1779e5edd02e4a2b4909260193adaab7ef8abaf04ddc8ab2fc39c95\n" + \ "\nand 1 block hidden"
def test_add_candidates_prof(): """Test to update the proof to a candidate""" block = Block.genesis_block(timestamp=datetime(2000, 1, 1), difficulty=4, mining=True) blockchain = BlockChain(block) # Add a new candidate assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 2)) # Add proof to candidate assert blockchain.candidate_proof(3) is False assert blockchain.candidate_proof(4) # Add an empty candidate assert blockchain.add_candidate(None) # Add an invalid candidate (same date) assert blockchain.add_candidate(None, timestamp=datetime(2000, 1, 1)) is False assert blockchain.candidate_proof(3) is False assert blockchain.candidate_proof(4) is False
def test_creation_block(): """Test errors during the construcion of an object""" with pytest.raises(Exception): Block()
class BlockChain: """BlockChain object""" def __init__(self, block=None): """BlockChain object""" # Difficulty update parameters # 59 Seconds - the minimum interval between blocks is a minute self.minimum_interval = 59 # 10 minutes - the objective interval between blocks self.block_interval = 600 # 144 block - the time for evaluate intervals (one per day 6 * 24) self.difficulty_interval = 144 # Currency values self.__unspent = None self.amount_mining = 0 # Validate the inputs if block is None: self.__candidate = Block.genesis_block() elif isinstance(block, Block): self.__candidate = block else: raise Exception("The input parameter must be a Block object") if self.__candidate.is_valid: self.chain = [self.__candidate] self.__candidate = None else: self.chain = [] def __repr__(self): """ Return repr(self). """ last_block = max(0, self.num_blocks - 5) result = "" for step in reversed(range(last_block, self.num_blocks)): result = "%sBlock: %d (%s) - Hash: %s\n" % \ (result, self.chain[step].index, self.chain[step].timestamp, self.chain[step].hash) if last_block > 0: result = '%s\nand %d block hidden' % (result, last_block) return result @property def is_valid(self): """Indicate if the blockchain is valid The blockchain is valid when 1) The blocks are valid 2) All blocks has previous block hash and a valid id 3) All blocks are valid and Return: (Logical): True if BlockChain is valid """ if self.chain: for step in range(self.num_blocks - 1, 0, -1): if not self.chain[step].is_valid: return False elif self.chain[step].previous_hash != self.chain[step - 1].hash: return False elif self.chain[step].index != self.chain[step - 1].index + 1: return False return self.chain[0].is_valid return False @property def candidate_block(self): """Return the candidate for block for the chain""" return self.__candidate @property def last_block(self): """Return the last block in the BlockChain""" if self.chain: return self.chain[-1] return None @property def num_blocks(self): """Return the number of blocks in the chain""" return len(self.chain) def add_candidate(self, data, timestamp=None, proof=0): """Insert a new candidate in the chain Return: (logical): true where the candidate can be assigned """ if self.__candidate is not None: return False else: # Get the timestamp value where it is None if timestamp is None: timestamp = datetime.now() # Validate minimum timestamp period interval = timestamp - self.last_block.timestamp if interval.total_seconds() < self.minimum_interval: return False # Evaluate the difficulty if self.num_blocks > self.difficulty_interval and self.num_blocks % self.difficulty_interval == 0: interval = self.chain[-1].timestamp - self.chain[ -self.difficulty_interval - 1].timestamp interval = interval.total_seconds() / self.difficulty_interval if interval > self.block_interval: difficulty = self.last_block.difficulty - 1 elif interval < self.block_interval: difficulty = self.last_block.difficulty + 1 else: difficulty = self.last_block.difficulty self.__candidate = Block(self.last_block.index + 1, data, previous_hash=self.last_block.hash, timestamp=timestamp, proof=proof, difficulty=difficulty) return True def add_transaction(self, key, address, amount): """Add a transaction in the blockchain Add a transaction to the candidate list from the signer's account to the destination account for the indicated amount. Args: key (String): the private key of the user address (String): the destination address amount (Double): the amount of the Return: (Boolean): True if the transaction can be done """ wallet = self.get_wallet(key) transaction = wallet.generate_transaction_to(address, amount) if transaction is None: return False unspent = self.get_unspent_list() return unspent.append_unconfirmed(transaction) def candidate_proof(self, proof): """Set the proof for a candidate""" self.__candidate.proof = proof if self.__candidate.is_valid: self.chain.append(self.__candidate) self.__candidate = None self.__unspent = None return True return False def generate_candidate(self, address, timestamp=None, proof=0): """Generate a new candidate block with a reward to the miner Args: address (String): the address fo the miner timestamp (String): the genesis block time proof (Integer): the proof Return: (logical): true where the candidate can be assigned """ output = OutputTransaction(address, self.amount_mining) transaction = Transaction(timestamp, output) self.__unspent.append_unconfirmed(transaction) data = self.__unspent.unconfirmed return self.add_candidate(data, timestamp=timestamp, proof=proof) def get_unspent_list(self): """Get the list of unspent transaction Return: (UnspentList): the list unspent transactions """ if self.__unspent is None: self.__unspent = UnspentList() for block in self.chain: for transaction in block.data: assert self.__unspent.append_unconfirmed(transaction) if block.is_valid: self.__unspent.confirm_unconfirmed() return self.__unspent def get_wallet(self, key=None): """Get the wallet of a user Args: key (String): the private key of the user Return: (Wallet): the wallet of the user """ return Wallet(key, self) def mining_candidate(self, init=None, maximum_iter=1000): """Mining the Candidate Block Implements the search of an integer for the proof which satisficed the required difficult. The initial value can be configured using the `init` property. The maximum number of values to tried can be also configured using the `init` property. Args: self (Block): A Block object init (Integer): The values to use in the first proof maximum_iter (Integer): The maximum number of iterations in the mining process Return: (Logical): True if a valid proof has been found """ if self.__candidate is None: return False if self.__candidate.mining(init, maximum_iter): self.chain.append(self.__candidate) self.__candidate = None self.__unspent = None return True return False @staticmethod def new_cryptocurrency(address, amount, timestamp=None, proof=0, difficulty=0, mining=False): """Create a new cryptocurrency Args: address (string): the address of the first user amount (Double): the amount for the first user timestamp (String): the genesis block time proof (Integer): the proof difficulty (Integer): the number of zeros in the hash to validate the block mining (Boolean): logical value indicating if the block must be mined Return: (BlockChain): A new blockchain """ output = OutputTransaction(address, amount) transaction = Transaction(timestamp, output) block = Block.genesis_block([transaction], timestamp=timestamp, proof=proof, difficulty=difficulty, mining=mining) blokchain = BlockChain(block) blokchain.amount_mining = amount return blokchain def replace_chain(self, new_chain): """Replace this chair for a longest one Return: (logical): True is the replacement is valid """ if isinstance(new_chain, BlockChain): if new_chain.num_blocks > self.num_blocks: if new_chain.chain[0] == self.chain[0]: if new_chain.is_valid: self.chain = new_chain.chain self.__candidate = new_chain.candidate_block return True return False
def test_block_repr(): """Test the report function""" assert repr(Block(0, ' data', timestamp=0)) == \ 'Block: 0 (0)\n Hash: d8345cd456a98ac9533f1c9f662685318413d76333741100ecdf34b112488e5e\n Previous: None'