فهرست منبع

remove all `prev_block` parameters

they are no longer neccessary now that we build blockchains
incrementally
Malte Kraus 8 سال پیش
والد
کامیت
ee6ee093e1
7فایلهای تغییر یافته به همراه78 افزوده شده و 140 حذف شده
  1. 20 30
      src/block.py
  2. 20 66
      src/blockchain.py
  3. 9 2
      src/chainbuilder.py
  4. 2 4
      src/mining_strategy.py
  5. 20 30
      src/transaction.py
  6. 1 1
      tests/test_proto.py
  7. 6 7
      tests/test_verifications.py

+ 20 - 30
src/block.py

@@ -80,7 +80,7 @@ class Block:
         Create a new block for a certain blockchain, containing certain transactions.
         """
         tree = merkle_tree(transactions)
-        difficulty = blockchain.compute_difficulty()
+        difficulty = blockchain.compute_difficulty_next_block()
         if ts is None:
             ts = datetime.utcnow()
         if ts <= blockchain.head.time:
@@ -131,7 +131,7 @@ class Block:
         return self.finish_hash(hasher)
 
     def verify_difficulty(self):
-        """ Verify that the hash value is correct and fulfills its difficulty promise. """
+        """ Verifies that the hash value is correct and fulfills its difficulty promise. """
         # TODO: move this some better place
         if self.hash != self.get_hash():
             logging.warning("block has invalid hash value")
@@ -144,31 +144,22 @@ class Block:
         return True
 
     def verify_prev_block(self, chain: 'Blockchain'):
-        """ Verify the previous block pointer points to a valid block in the given block chain. """
-        if self.hash == GENESIS_BLOCK_HASH:
-            return True
-        prev = chain.get_block_by_hash(self.prev_block_hash)
-        if prev is None:
-            logging.warning("Previous block is missing in the block chain.")
+        """ Verifies that the previous block pointer points to the head of the given block chain and difficulty and height are correct. """
+        if chain.head.hash != self.prev_block_hash:
+            logging.warning("Previous block is not head of the block chain.")
             return False
-        if self.difficulty != chain.compute_difficulty(prev):
+        if self.difficulty != chain.compute_difficulty_next_block():
             logging.warning("Block has wrong difficulty.")
             return False
-        if prev.height + self.difficulty != self.height:
+        if chain.head.height + self.difficulty != self.height:
             logging.warning("Block has wrong height.")
             return False
         return True
 
     def verify_transactions(self, chain: 'Blockchain'):
-        """ Verify all transaction in this block are valid in the given block chain. """
-        if self.hash == GENESIS_BLOCK_HASH:
-            return True
-
+        """ Verifies that all transaction in this block are valid in the given block chain. """
         mining_reward = None
 
-        prev_block = chain.get_block_by_hash(self.prev_block_hash)
-        assert prev_block is not None
-
         trans_set = set(self.transactions)
         for t in self.transactions:
             if not t.inputs:
@@ -177,11 +168,11 @@ class Block:
                     return False
                 mining_reward = t
 
-            if not t.verify(chain, trans_set - {t}, prev_block):
+            if not t.verify(chain, trans_set - {t}):
                 return False
         if mining_reward is not None:
-            fees = sum(t.get_transaction_fee(chain) for t in self.transactions if t is not mining_reward)
-            reward = chain.compute_blockreward(chain.get_block_by_hash(self.prev_block_hash))
+            fees = sum(t.get_transaction_fee(chain) for t in self.transactions)
+            reward = chain.compute_blockreward_next_block()
             used = sum(t.amount for t in mining_reward.targets)
             if used > fees + reward:
                 logging.warning("mining reward is too large")
@@ -190,25 +181,24 @@ class Block:
 
     def verify_time(self, chain: 'Blockchain'):
         """
-        Verifies that blocks are not from far in the future, but always a bit younger
-        than the previous block.
+        Verifies that blocks are not from far in the future, but a bit younger
+        than the head of `chain`.
         """
-        if self.hash == GENESIS_BLOCK_HASH:
-            return True
-
         if self.time - timedelta(hours=2) > datetime.utcnow():
             logging.warning("discarding block because it is from the far future")
             return False
-        prev_block = chain.get_block_by_hash(self.prev_block_hash)
-        assert prev_block is not None
-        if self.time <= prev_block.time:
+        if self.time <= chain.head.time:
             logging.warning("discarding block because it is younger than its predecessor")
             return False
         return True
 
     def verify(self, chain: 'Blockchain'):
-        """ Verifies this block contains only valid data consistent with the given block chain. """
-        if self.height == 0 and self.hash != GENESIS_BLOCK_HASH:
+        """
+        Verifies that this block contains only valid data and can be applied on top of the block
+        chain `chain`.
+        """
+        assert self.hash not in chain.block_indices
+        if self.height == 0:
             logging.warning("only the genesis block may have height=0")
             return False
         return self.verify_difficulty() and self.verify_merkle() and self.verify_prev_block(chain) \

+ 20 - 66
src/blockchain.py

@@ -9,7 +9,7 @@ from .proof_of_work import DIFFICULTY_BLOCK_INTERVAL, DIFFICULTY_TARGET_TIMEDELT
 
 class Blockchain:
     """
-    A block chain: a ordrered list of blocks.
+    A block chain: a ordered list of valid blocks.
 
     :ivar blocks: The blocks in this chain, oldest first.
     :vartype blocks: List[Block]
@@ -29,13 +29,19 @@ class Blockchain:
         self.unspent_coins = {}
 
     def try_append(self, block: 'Block') -> 'Optional[Blockchain]':
+        """
+        If `block` is valid on top of this chain, returns a new block chain including that block.
+        Otherwise, it returns `None`.
+        """
+
+        if not block.verify(self):
+            return None
+
         unspent_coins = self.unspent_coins.copy()
 
         for t in block.transactions:
             for inp in t.inputs:
-                if inp not in unspent_coins:
-                    logging.warning("Aborting computation of unspent transactions because a transaction spent an unavailable coin.")
-                    return None
+                assert inp in unspent_coins, "Aborting computation of unspent transactions because a transaction spent an unavailable coin."
                 del unspent_coins[inp]
             for i, target in enumerate(t.targets):
                 unspent_coins[TransactionInput(t.get_hash(), i)] = target
@@ -46,42 +52,8 @@ class Blockchain:
         chain.block_indices = self.block_indices.copy()
         chain.block_indices[block.hash] = len(self.blocks)
 
-        if not block.verify(chain):
-            return None
-
         return chain
 
-    def get_transaction_by_hash(self, hash_val: bytes) -> 'Optional[Transaction]':
-        """ Returns a transaction from its hash, or None. """
-        # TODO: build a hash table with this info
-        for block in self.blocks[::-1]:
-            for trans in block.transactions:
-                if trans.get_hash() == hash_val:
-                    return trans
-        return None
-
-    def is_coin_still_valid(self, transaction_input: 'TransactionInput',
-                            prev_block: 'Block'=None) -> bool:
-        """
-        Validates that the coins that were sent in the transaction identified
-        by `transaction_hash_val` to the nth receiver (n=output_idx) have not been
-        spent before the given block.
-
-        :param transaction_input: The coin to check.
-        :param prev_block: The youngest block in this block chain that should be considered for
-                           the validation.
-        """
-        if prev_block is None or prev_block is self.head:
-            return transaction_input in self.unspent_coins
-
-        idx = self.block_indices[prev_block.hash]
-        assert self.blocks[idx] is prev_block
-        for block in self.blocks[idx::-1]:
-            for trans in block.transactions:
-                if transaction_input in trans.inputs:
-                    return False
-        return True
-
     def get_block_by_hash(self, hash_val: bytes) -> 'Optional[Block]':
         """ Returns a block by its hash value, or None if it cannot be found. """
         idx = self.block_indices.get(hash_val)
@@ -89,17 +61,6 @@ class Blockchain:
             return None
         return self.blocks[idx]
 
-    def verify_all_transactions(self) -> bool:
-        """ Verify the transactions in all blocks in this chain. """
-        for block in self.blocks:
-            if not block.verify_transactions(self):
-                return False
-        return True
-
-    def verify_all(self) -> bool:
-        """ Verify all blocks in this block chain. """
-        return all(block.verify(self) for block in self.blocks)
-
     @property
     def head(self):
         """
@@ -109,21 +70,18 @@ class Blockchain:
         """
         return self.blocks[-1]
 
-    def compute_difficulty(self, prev_block: 'Block'=None) -> int:
-        """ Compute the desired difficulty for the block after `prev_block` (defaults to `head`). """
+    def compute_difficulty_next_block(self) -> int:
+        """ Compute the desired difficulty for the block following this chain's `head`. """
         target_timedelta = Fraction(int(DIFFICULTY_TARGET_TIMEDELTA.total_seconds() * 1000 * 1000))
 
-        if prev_block is None:
-            prev_block = self.head
-
-        block_idx = self.block_indices[prev_block.hash] + 1
+        block_idx = len(self.blocks)
         if block_idx % DIFFICULTY_BLOCK_INTERVAL != 0:
-            return prev_block.difficulty
+            return self.head.difficulty
 
-        duration = prev_block.time - self.blocks[block_idx - DIFFICULTY_BLOCK_INTERVAL].time
+        duration = self.head.time - self.blocks[block_idx - DIFFICULTY_BLOCK_INTERVAL].time
         duration = Fraction(int(duration.total_seconds() * 1000 * 1000))
 
-        prev_difficulty = Fraction(prev_block.difficulty)
+        prev_difficulty = Fraction(self.head.difficulty)
         hash_rate = prev_difficulty * DIFFICULTY_BLOCK_INTERVAL / duration
 
         new_difficulty = hash_rate * target_timedelta / DIFFICULTY_BLOCK_INTERVAL
@@ -135,15 +93,11 @@ class Blockchain:
 
         return int(new_difficulty)
 
-    def compute_blockreward(self, prev_block: 'Block'=None) -> int:
-        """ Compute the block reward that is expected for the block following `prev_block`. """
-        # TODO: remove prev_block param
-        if prev_block is None:
-            prev_block = self.head
-        assert prev_block is not None
+    def compute_blockreward_next_block(self) -> int:
+        """ Compute the block reward that is expected for the block following this chain's `head`. """
         reward = 1000
-        l = self.block_indices[prev_block.hash]
-        while l > 0:
+        l = len(self.blocks) - 1
+        while l > 0 and reward > 0:
             l = l - 10000
             reward = reward // 2
         return reward

+ 9 - 2
src/chainbuilder.py

@@ -118,8 +118,13 @@ class ChainBuilder:
         """ Event handler that is called by the network layer when a transaction is received. """
         self._assert_thread_safety()
         hash_val = transaction.get_hash()
-        if self.primary_block_chain.get_transaction_by_hash(hash_val) is None and \
-                hash_val not in self.unconfirmed_transactions:
+
+        def input_ok(inp):
+            return inp in self.primary_block_chain.unspent_coins or \
+                    inp.transaction_hash in self.unconfirmed_transactions
+
+        if hash_val not in self.unconfirmed_transactions and \
+                all(input_ok(inp) for inp in transaction.inputs):
             self.unconfirmed_transactions[hash_val] = transaction
             self.protocol.broadcast_transaction(transaction)
 
@@ -141,6 +146,8 @@ class ChainBuilder:
         self._retry_expired_requests()
         self._clean_block_requests()
 
+        # TODO: restore valid transactions from the old primary block chain
+
         self.protocol.broadcast_primary_block(chain.head)
 
     def _build_blockchain(self, checkpoint: 'Blockchain', blocks: 'List[Block]'):

+ 2 - 4
src/mining_strategy.py

@@ -17,8 +17,6 @@ def create_block(blockchain: 'Blockchain', unconfirmed_transactions: 'List[Trans
                                      this block.
     :param reward_pubkey: The key that should receive block rewards.
     """
-    head = blockchain.head
-
     transactions = set()
     for t in unconfirmed_transactions:
         if t.verify(blockchain, transactions):
@@ -26,9 +24,9 @@ def create_block(blockchain: 'Blockchain', unconfirmed_transactions: 'List[Trans
             transactions.add(t)
 
 
-    reward = blockchain.compute_blockreward(head)
+    reward = blockchain.compute_blockreward_next_block()
     fees = sum(t.get_transaction_fee(blockchain) for t in transactions)
-    trans = Transaction([], [TransactionTarget(reward_pubkey, reward + fees)], [], iv=head.hash)
+    trans = Transaction([], [TransactionTarget(reward_pubkey, reward + fees)], [], iv=blockchain.head.hash)
     transactions.add(trans)
 
     return Block.create(blockchain, list(transactions))

+ 20 - 30
src/transaction.py

@@ -136,7 +136,7 @@ class Transaction:
             self.signatures.append(private_key.sign(self.get_hash()))
 
     def _verify_signatures(self, chain: 'Blockchain'):
-        """ Verify that all inputs are signed and the signatures are valid. """
+        """ Verifies that all inputs are signed and the signatures are valid. """
         if len(self.signatures) != len(self.inputs):
             logging.warning("wrong number of signatures")
             return False
@@ -148,19 +148,16 @@ class Transaction:
 
     def _verify_single_sig(self, sig: bytes, inp: TransactionInput, chain: 'Blockchain') -> bool:
         """ Verifies the signature on a single input. """
-        trans = chain.get_transaction_by_hash(inp.transaction_hash)
-        if trans is None:
+        outp = chain.unspent_coins.get(inp)
+        if outp is None:
             logging.warning("Referenced transaction input could not be found.")
             return False
-        sender_pk = trans.targets[inp.output_idx].recipient_pk
-        if not sender_pk.verify_sign(self.get_hash(), sig):
+        if not outp.recipient_pk.verify_sign(self.get_hash(), sig):
             logging.warning("Transaction signature does not verify.")
             return False
         return True
 
-
-    def _verify_single_spend(self, chain: 'Blockchain', other_trans: set,
-                             prev_block: 'Block') -> bool:
+    def _verify_single_spend(self, chain: 'Blockchain', other_trans: set) -> bool:
         """ Verifies that all inputs have not been spent yet. """
         inp_set = set(self.inputs)
         if len(self.inputs) != len(inp_set):
@@ -172,42 +169,35 @@ class Transaction:
                             " same block.")
             return False
 
-        for i in self.inputs:
-            if not chain.is_coin_still_valid(i, prev_block):
-                logging.debug("Transaction refers to a coin that was already spent.")
-                return False
+        if any(i not in chain.unspent_coins for i in self.inputs):
+            logging.debug("Transaction refers to a coin that was already spent.")
+            return False
         return True
 
     def get_transaction_fee(self, chain: 'Blockchain'):
         """ Computes the transaction fees this transaction provides. """
-        input_amount = 0
-        for inp in self.inputs:
-            trans = chain.get_transaction_by_hash(inp.transaction_hash)
-            if trans is None:
-                raise ValueError("Referenced transaction input could not be found.")
-            input_amount += trans.targets[inp.output_idx].amount
-        output_amount = 0
-        for outp in self.targets:
-            output_amount += outp.amount
+        if not self.inputs:
+            return 0 # block reward transaction pays no fees
+
+        input_amount = sum(chain.unspent_coins[inp].amount for inp in self.inputs)
+        output_amount = sum(outp.amount for outp in self.targets)
         return input_amount - output_amount
 
     def _verify_amounts(self, chain: 'Blockchain') -> bool:
         """
-        Verifies that the receivers get less or equal money than the senders.
+        Verifies that transaction fees are non-negative and output amounts are positive.
         """
-        if self.inputs and self.get_transaction_fee(chain) < 0:
+        if self.get_transaction_fee(chain) < 0:
             logging.warning("Transferred amounts are larger than the inputs.")
             return False
-        for outp in self.targets:
-            if outp.amount <= 0:
-                logging.warning("Transferred amounts must be positive.")
-                return False
+        if any(outp.amount <= 0 for outp in self.targets):
+            logging.warning("Transferred amounts must be positive.")
+            return False
         return True
 
-    def verify(self, chain: 'Blockchain', other_trans: 'Set[Transaction]',
-               prev_block: 'Block'=None) -> bool:
+    def verify(self, chain: 'Blockchain', other_trans: 'Set[Transaction]') -> bool:
         """ Verifies that this transaction is completely valid. """
-        return self._verify_single_spend(chain, other_trans, prev_block) and \
+        return self._verify_single_spend(chain, other_trans) and \
                self._verify_signatures(chain) and self._verify_amounts(chain)
 
 from .blockchain import Blockchain

+ 1 - 1
tests/test_proto.py

@@ -50,4 +50,4 @@ def test_proto():
 
     assert not trans.verify(miner1.chainbuilder.primary_block_chain, set()), "inserted transaction should be spent and therefore invalid"
 
-    assert chain1.is_coin_still_valid(TransactionInput(trans.get_hash(), 0)), "someone spent our coins?"
+    assert TransactionInput(trans.get_hash(), 0) in chain1.unspent_coins, "someone spent our coins?"

+ 6 - 7
tests/test_verifications.py

@@ -10,7 +10,6 @@ def block_test(proof_of_work_res=True):
             src.block.verify_proof_of_work = src.proof_of_work.verify_proof_of_work
 
             chain = Blockchain()
-            assert chain.verify_all()
 
             try:
                 fn(chain)
@@ -26,7 +25,7 @@ def trans_test(fn):
     @block_test()
     def wrapper(gen_chain):
         key = Signing.generate_private_key()
-        reward_trans = Transaction([], [TransactionTarget(key, gen_chain.compute_blockreward(gen_chain.head))])
+        reward_trans = Transaction([], [TransactionTarget(key, gen_chain.compute_blockreward_next_block())])
         chain = extend_blockchain(gen_chain, [reward_trans])
 
         fn(chain, reward_trans)
@@ -41,7 +40,7 @@ def test_double_spend1(chain, reward_trans):
     extend_blockchain(chain, [trans1], verify_res=False)
 
     # spending the output of trans1 must work:
-    assert chain.is_coin_still_valid(trans_as_input(trans1))
+    assert trans_as_input(trans1) in chain.unspent_coins
 
 @trans_test
 def test_double_spend2(chain, reward_trans):
@@ -98,24 +97,24 @@ def test_zero_block_reward(chain, reward_trans):
 @trans_test
 def test_too_large_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
-    target1 = TransactionTarget(key, chain.compute_blockreward() + 1)
+    target1 = TransactionTarget(key, chain.compute_blockreward_next_block() + 1)
     trans1 = Transaction([], [target1], iv=b"1")
     extend_blockchain(chain, [trans1], verify_res=False)
 
     trans2 = new_trans(reward_trans, fee=1)
-    target2 = TransactionTarget(key, chain.compute_blockreward() + 2)
+    target2 = TransactionTarget(key, chain.compute_blockreward_next_block() + 2)
     trans3 = Transaction([], [target2], iv=b"2")
     extend_blockchain(chain, [trans2, trans3], verify_res=False)
 
 @trans_test
 def test_max_block_reward(chain, reward_trans):
     key = reward_trans.targets[0].recipient_pk
-    target1 = TransactionTarget(key, chain.compute_blockreward())
+    target1 = TransactionTarget(key, chain.compute_blockreward_next_block())
     trans1 = Transaction([], [target1], iv=b"1")
     extend_blockchain(chain, [trans1], verify_res=True)
 
     trans2 = new_trans(reward_trans, fee=1)
-    target2 = TransactionTarget(key, chain.compute_blockreward() + 1)
+    target2 = TransactionTarget(key, chain.compute_blockreward_next_block() + 1)
     trans3 = Transaction([], [target2], iv=b"2")
     extend_blockchain(chain, [trans2, trans3], verify_res=True)