def test_block_deserialization(self, b): from bigchaindb.common.crypto import hash_data from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction transaction = Transaction.create([b.me], [([b.me], 1)]) transaction.sign([b.me_private]) timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected = Block([transaction], b.me, timestamp, voters) block = { 'timestamp': timestamp, 'transactions': [transaction.to_dict()], 'node_pubkey': b.me, 'voters': voters, } block_body = { 'id': hash_data(serialize(block)), 'block': block, 'signature': None, } assert expected == Block.from_dict(block_body)
def test_get_votes_by_block_id(signed_create_tx, structurally_valid_vote): from bigchaindb.common.crypto import generate_key_pair from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create and insert a block block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) # create and insert some votes structurally_valid_vote['vote']['voting_for_block'] = block.id conn.db.votes.insert_one(structurally_valid_vote) # create a second vote under a different key _, pk = generate_key_pair() structurally_valid_vote['vote']['voting_for_block'] = block.id structurally_valid_vote['node_pubkey'] = pk structurally_valid_vote.pop('_id') conn.db.votes.insert_one(structurally_valid_vote) votes = list(query.get_votes_by_block_id(conn, block.id)) assert len(votes) == 2 assert votes[0]['vote']['voting_for_block'] == block.id assert votes[1]['vote']['voting_for_block'] == block.id
def test_sign_block(self, b, alice): from bigchaindb.common.crypto import PrivateKey, PublicKey from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction transactions = [ Transaction.create([alice.public_key], [([alice.public_key], 1)]) ] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { 'timestamp': timestamp, 'transactions': [tx.to_dict() for tx in transactions], 'node_pubkey': alice.public_key, 'voters': voters, } expected_block_serialized = serialize(expected_block).encode() expected = PrivateKey( alice.private_key).sign(expected_block_serialized) block = Block(transactions, alice.public_key, timestamp, voters) block = block.sign(alice.private_key) assert block.signature == expected.decode() public_key = PublicKey(alice.public_key) assert public_key.verify(expected_block_serialized, block.signature)
def test_from_db(self, b): from bigchaindb.models import Block, Transaction assets = [ {'msg': '1'}, {'msg': '2'}, {'msg': '3'}, ] txs = [] # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) txs.append(tx) # create the block block = Block(txs) # decouple assets assets_from_block, block_dict = block.decouple_assets() # write the assets and block separately b.write_assets(assets_from_block) b.write_block(block) # check the reconstructed block is the same as the original block block_from_db = Block.from_db(b, block_dict) assert block == block_from_db
def test_get_last_voted_block_id(genesis_block, signed_create_tx, b): from bigchaindb.backend import connect, query from bigchaindb.models import Block from bigchaindb.common.exceptions import CyclicBlockchainError conn = connect() # check that the last voted block is the genesis block assert query.get_last_voted_block_id(conn, b.me) == genesis_block.id # create and insert a new vote and block block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) vote = b.vote(block.id, genesis_block.id, True) conn.db.votes.insert_one(vote) assert query.get_last_voted_block_id(conn, b.me) == block.id # force a bad chain vote.pop('_id') vote['vote']['voting_for_block'] = genesis_block.id vote['vote']['previous_block'] = block.id conn.db.votes.insert_one(vote) with pytest.raises(CyclicBlockchainError): query.get_last_voted_block_id(conn, b.me)
def test_filter_unspent_outputs(b, user_pk, user_sk): out = [([user_pk], 1)] tx1 = Transaction.create([user_pk], out * 3) tx1.sign([user_sk]) # There are 3 inputs inputs = tx1.to_inputs() # Each spent individually tx2 = Transaction.transfer([inputs[0]], out, tx1.id) tx2.sign([user_sk]) tx3 = Transaction.transfer([inputs[1]], out, tx1.id) tx3.sign([user_sk]) tx4 = Transaction.transfer([inputs[2]], out, tx1.id) tx4.sign([user_sk]) # The CREATE and first TRANSFER are valid. tx2 produces a new unspent. for tx in [tx1, tx2]: block = Block([tx]) b.write_block(block) b.write_vote(b.vote(block.id, '', True)) # The second TRANSFER is invalid. inputs[1] remains unspent. block = Block([tx3]) b.write_block(block) b.write_vote(b.vote(block.id, '', False)) # The third TRANSFER is undecided. It procuces a new unspent. block = Block([tx4]) b.write_block(block) outputs = b.fastquery.get_outputs_by_public_key(user_pk) spents = b.fastquery.filter_unspent_outputs(outputs) assert set(spents) == {inputs[0].fulfills, inputs[2].fulfills}
def get_block(self, block_id, include_status=False): """Get the block with the specified `block_id` (and optionally its status) Returns the block corresponding to `block_id` or None if no match is found. Args: block_id (str): transaction id of the transaction to get include_status (bool): also return the status of the block the return value is then a tuple: (block, status) """ # get block from database block_dict = backend.query.get_block(self.connection, block_id) # get the asset ids from the block if block_dict: asset_ids = Block.get_asset_ids(block_dict) txn_ids = Block.get_txn_ids(block_dict) # get the assets from the database assets = self.get_assets(asset_ids) # get the metadata from the database metadata = self.get_metadata(txn_ids) # add the assets to the block transactions block_dict = Block.couple_assets(block_dict, assets) # add the metadata to the block transactions block_dict = Block.couple_metadata(block_dict, metadata) status = None if include_status: if block_dict: status = self.block_election_status(block_dict) return block_dict, status else: return block_dict
def test_get_asset_ids(self, b): from bigchaindb.models import Block, Transaction assets = [ {'msg': '1'}, {'msg': '2'}, {'msg': '3'}, ] txs = [] # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) txs.append(tx) # create the block block = Block(txs) # decouple assets assets_from_block, block_dict = block.decouple_assets() # get the asset_ids and check that they are the same as the `CREATE` # transactions asset_ids = Block.get_asset_ids(block_dict) assert asset_ids == [tx.id for tx in txs[:-1]]
def get_last_voted_block(self): """Returns the last block that this node voted on.""" try: # get the latest value for the vote timestamp (over all votes) max_timestamp = self.connection.run( r.table('votes', read_mode=self.read_mode).filter( r.row['node_pubkey'] == self.me).max( r.row['vote']['timestamp']))['vote']['timestamp'] last_voted = list( self.connection.run( r.table('votes', read_mode=self.read_mode).filter( r.row['vote']['timestamp'] == max_timestamp).filter( r.row['node_pubkey'] == self.me))) except r.ReqlNonExistenceError: # return last vote if last vote exists else return Genesis block res = self.connection.run( r.table('bigchain', read_mode=self.read_mode).filter( util.is_genesis_block)) block = list(res)[0] return Block.from_dict(block) # Now the fun starts. Since the resolution of timestamp is a second, # we might have more than one vote per timestamp. If this is the case # then we need to rebuild the chain for the blocks that have been retrieved # to get the last one. # Given a block_id, mapping returns the id of the block pointing at it. mapping = { v['vote']['previous_block']: v['vote']['voting_for_block'] for v in last_voted } # Since we follow the chain backwards, we can start from a random # point of the chain and "move up" from it. last_block_id = list(mapping.values())[0] # We must be sure to break the infinite loop. This happens when: # - the block we are currenty iterating is the one we are looking for. # This will trigger a KeyError, breaking the loop # - we are visiting again a node we already explored, hence there is # a loop. This might happen if a vote points both `previous_block` # and `voting_for_block` to the same `block_id` explored = set() while True: try: if last_block_id in explored: raise exceptions.CyclicBlockchainError() explored.add(last_block_id) last_block_id = mapping[last_block_id] except KeyError: break res = self.connection.run( r.table('bigchain', read_mode=self.read_mode).get(last_block_id)) return Block.from_dict(res)
def test_compare_blocks(self, b): from bigchaindb.models import Block, Transaction transactions = [Transaction.create([b.me], [([b.me], 1)])] assert Block() != 'invalid comparison' assert Block(transactions) == Block(transactions)
def test_decouple_assets(self, b): from bigchaindb.models import Block, Transaction assets = [ {'msg': '1'}, {'msg': '2'}, {'msg': '3'}, ] txs = [] # create 3 assets for asset in assets: tx = Transaction.create([b.me], [([b.me], 1)], asset=asset) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([b.me], 1)], asset_id=txs[0].id) txs.append(tx) # create the block block = Block(txs) # decouple assets assets_from_block, block_dict = block.decouple_assets() assert len(assets_from_block) == 3 for i in range(3): assert assets_from_block[i]['data'] == assets[i] assert assets_from_block[i]['id'] == txs[i].id # check the `TRANSFER` transaction was not changed assert block.transactions[3].to_dict() == \ block_dict['block']['transactions'][3]
def test_block_invalid_signature(self, b, alice): from bigchaindb.common.crypto import hash_data from bigchaindb.common.exceptions import InvalidSignature from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction transaction = Transaction.create([alice.public_key], [([alice.public_key], 1)]) transaction.sign([alice.private_key]) timestamp = gen_timestamp() block = { 'timestamp': timestamp, 'transactions': [transaction.to_dict()], 'node_pubkey': alice.public_key, } block_body = { 'id': hash_data(serialize(block)), 'block': block, 'signature': 'an invalid signature', } with raises(InvalidSignature): Block.from_dict(block_body).validate(b)
def get_block(self, block_id, include_status=False): """Get the block with the specified `block_id` (and optionally its status) Returns the block corresponding to `block_id` or None if no match is found. Args: block_id (str): transaction id of the transaction to get include_status (bool): also return the status of the block the return value is then a tuple: (block, status) """ # get block from database block_dict = backend.query.get_block(self.connection, block_id) # get the asset ids from the block if block_dict: asset_ids = Block.get_asset_ids(block_dict) # get the assets from the database assets = self.get_assets(asset_ids) # add the assets to the block transactions block_dict = Block.couple_assets(block_dict, assets) status = None if include_status: if block_dict: status = self.block_election_status(block_dict) return block_dict, status else: return block_dict
def test_block_serialization(self, b, alice): from bigchaindb.common.crypto import hash_data from bigchaindb.common.utils import gen_timestamp, serialize from bigchaindb.models import Block, Transaction transactions = [ Transaction.create([alice.public_key], [([alice.public_key], 1)]) ] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { 'timestamp': timestamp, 'transactions': [tx.to_dict() for tx in transactions], 'node_pubkey': alice.public_key, 'voters': voters, } expected = { 'id': hash_data(serialize(expected_block)), 'block': expected_block, 'signature': None, } block = Block(transactions, alice.public_key, timestamp, voters) assert block.to_dict() == expected
def test_get_spent_for_tx_with_multiple_inputs(carol): from bigchaindb.backend import connect, query from bigchaindb.models import Block, Transaction conn = connect() tx_0 = Transaction.create( [carol.public_key], [([carol.public_key], 1), ([carol.public_key], 1), ([carol.public_key], 2)], ).sign([carol.private_key]) block = Block(transactions=[tx_0]) conn.db.bigchain.insert_one(block.to_dict()) spents = list(query.get_spent(conn, tx_0.id, 0)) assert not spents tx_1 = Transaction.transfer( tx_0.to_inputs()[2:3], [([carol.public_key], 1), ([carol.public_key], 1)], asset_id=tx_0.id, ).sign([carol.private_key]) block = Block(transactions=[tx_1]) conn.db.bigchain.insert_one(block.to_dict()) spents = list(query.get_spent(conn, tx_0.id, 0)) assert not spents tx_2 = Transaction.transfer( tx_0.to_inputs()[0:1] + tx_1.to_inputs()[1:2], [([carol.public_key], 2)], asset_id=tx_0.id, ).sign([carol.private_key]) block = Block(transactions=[tx_2]) conn.db.bigchain.insert_one(block.to_dict()) spents = list(query.get_spent(conn, tx_0.id, 1)) assert not spents
def test_count_blocks(signed_create_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create and insert some blocks block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) conn.db.bigchain.insert_one(block.to_dict()) assert query.count_blocks(conn) == 2
def test_write_block(signed_create_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create and write block block = Block(transactions=[signed_create_tx]) query.write_block(conn, block.to_dict()) block_db = conn.db.bigchain.find_one({'id': block.id}, {'_id': False}) assert block_db == block.to_dict()
def blockdata(b, user_pk, user2_pk): txs = [Transaction.create([user_pk], [([user2_pk], 1)]), Transaction.create([user2_pk], [([user_pk], 1)]), Transaction.create([user_pk], [([user_pk], 1), ([user2_pk], 1)])] blocks = [] for i in range(3): block = Block([txs[i]]) b.write_block(block) blocks.append(block.to_dict()) b.write_vote(b.vote(blocks[1]['id'], '', True)) b.write_vote(b.vote(blocks[2]['id'], '', False)) return blocks, [b['id'] for b in blocks]
def test_get_block(signed_create_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create and insert block block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) block_db = query.get_block(conn, block.id) assert block_db == block.to_dict()
def test_block_initialization(self, monkeypatch): from bigchaindb.models import Block monkeypatch.setattr('time.time', lambda: 1) block = Block() assert block.transactions == [] assert block.timestamp == '1' assert block.node_pubkey is None assert block.signature is None with raises(TypeError): Block('not a list or None')
def test_block_invalid_id_deserialization(self, b, alice): from bigchaindb.common.exceptions import InvalidHash from bigchaindb.models import Block block = { 'id': 'an invalid id', 'block': { 'node_pubkey': alice.public_key, } } with raises(InvalidHash): Block.from_dict(block)
def test_get_owned_ids(signed_create_tx, user_pk): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create and insert a block block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) [(block_id, tx)] = list(query.get_owned_ids(conn, user_pk)) assert block_id == block.id assert tx == signed_create_tx.to_dict()
def test_get_asset_by_id(create_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create asset and block create_tx.asset = {'msg': 'aaa'} block = Block(transactions=[create_tx]) conn.db.bigchain.insert_one(block.to_dict()) asset = list(query.get_asset_by_id(conn, create_tx.id)) assert len(asset) == 1 assert asset[0]['asset'] == create_tx.asset
def check_for_quorum(self, next_vote): """ Checks if block has enough invalid votes to make a decision Args: next_vote: The next vote. """ try: block_id = next_vote['vote']['voting_for_block'] node = next_vote['node_pubkey'] except KeyError: return next_block = self.bigchain.get_block(block_id) result = self.bigchain.block_election(next_block) self.handle_block_events(result, block_id) if result['status'] == self.bigchain.BLOCK_INVALID: return Block.from_dict(next_block) # Log the result if result['status'] != self.bigchain.BLOCK_UNDECIDED: msg = 'node:%s block:%s status:%s' % \ (node, block_id, result['status']) # Extra data can be accessed via the log formatter. # See logging.dictConfig. logger_results.debug(msg, extra={ 'current_vote': next_vote, 'election_result': result, })
def test_get_block_status_from_transaction(create_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block conn = connect() # create a block block = Block(transactions=[create_tx], voters=['aaa', 'bbb', 'ccc']) # insert block conn.db.bigchain.insert_one(block.to_dict()) block_db = list( query.get_blocks_status_from_transaction(conn, create_tx.id)) assert len(block_db) == 1 block_db = block_db.pop() assert block_db['id'] == block.id assert block_db['block']['voters'] == block.voters
def test_get_transaction_from_block(user_pk): from bigchaindb.backend import connect, query from bigchaindb.models import Transaction, Block conn = connect() # create a block with 2 transactions txs = [ Transaction.create([user_pk], [([user_pk], 1)]), Transaction.create([user_pk], [([user_pk], 1)]), ] block = Block(transactions=txs) conn.db.bigchain.insert_one(block.to_dict()) tx_db = query.get_transaction_from_block(conn, txs[0].id, block.id) assert tx_db == txs[0].to_dict() assert query.get_transaction_from_block(conn, txs[0].id, 'aaa') is None assert query.get_transaction_from_block(conn, 'aaa', block.id) is None
def test_couple_assets(self, b, alice): from bigchaindb.models import Block, Transaction assets = [ { 'msg': '1' }, { 'msg': '2' }, { 'msg': '3' }, ] txs = [] # create 3 assets for asset in assets: tx = Transaction.create([alice.public_key], [([alice.public_key], 1)], asset=asset) tx.sign([alice.private_key]) txs.append(tx) # create a `TRANSFER` transaction. # the asset in `TRANSFER` transactions is not extracted tx = Transaction.transfer(txs[0].to_inputs(), [([alice.public_key], 1)], asset_id=txs[0].id) tx.sign([alice.private_key]) txs.append(tx) # create the block block = Block(txs) # decouple assets assets_from_block, block_dict = block.decouple_assets() # reconstruct the block block_dict_reconstructed = Block.couple_assets(block_dict, assets_from_block) # check that the reconstructed block is the same as the original block assert block == Block.from_dict(block_dict_reconstructed)
def test_get_last_voted_block_returns_genesis_if_no_votes_has_been_casted( self, b): from bigchaindb.models import Block from bigchaindb.backend import query genesis = query.get_genesis_block(b.connection) genesis = Block.from_db(b, genesis) gb = b.get_last_voted_block() assert gb == genesis assert b.validate_block(gb) == gb
def test_get_spending_transactions(user_pk): from bigchaindb.backend import connect, query from bigchaindb.models import Block, Transaction conn = connect() out = [([user_pk], 1)] tx1 = Transaction.create([user_pk], out * 3) inputs = tx1.to_inputs() tx2 = Transaction.transfer([inputs[0]], out, tx1.id) tx3 = Transaction.transfer([inputs[1]], out, tx1.id) tx4 = Transaction.transfer([inputs[2]], out, tx1.id) block = Block([tx1, tx2, tx3, tx4]) conn.db.bigchain.insert_one(block.to_dict()) links = [inputs[0].fulfills.to_dict(), inputs[2].fulfills.to_dict()] res = list(query.get_spending_transactions(conn, links)) # tx3 not a member because input 1 not asked for assert res == [(block.id, tx2.to_dict()), (block.id, tx4.to_dict())]
def test_block_dupe_tx(self, b, alice): from bigchaindb.models import Block, Transaction from bigchaindb.common.exceptions import DuplicateTransaction tx = Transaction.create([alice.public_key], [([alice.public_key], 1)]) block = Block([tx, tx], alice.public_key) block.sign(alice.private_key) b.store_block(block.to_dict()) with raises(DuplicateTransaction): block.validate(b)
def test_get_txids_filtered(signed_create_tx, signed_transfer_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Block, Transaction conn = connect() # create and insert two blocks, one for the create and one for the # transfer transaction block = Block(transactions=[signed_create_tx]) conn.db.bigchain.insert_one(block.to_dict()) block = Block(transactions=[signed_transfer_tx]) conn.db.bigchain.insert_one(block.to_dict()) asset_id = Transaction.get_asset_id([signed_create_tx, signed_transfer_tx]) # Test get by just asset id txids = set(query.get_txids_filtered(conn, asset_id)) assert txids == {signed_create_tx.id, signed_transfer_tx.id} # Test get by asset and CREATE txids = set(query.get_txids_filtered(conn, asset_id, Transaction.CREATE)) assert txids == {signed_create_tx.id} # Test get by asset and TRANSFER txids = set(query.get_txids_filtered(conn, asset_id, Transaction.TRANSFER)) assert txids == {signed_transfer_tx.id}
def create_block(self, validated_transactions): """Creates a block given a list of `validated_transactions`. Note that this method does not validate the transactions. Transactions should be validated before calling create_block. Args: validated_transactions (list(Transaction)): list of validated transactions. Returns: Block: created block. """ # Prevent the creation of empty blocks if len(validated_transactions) == 0: raise exceptions.OperationError('Empty block creation is not ' 'allowed') voters = self.nodes_except_me + [self.me] block = Block(validated_transactions, self.me, gen_timestamp(), voters) block = block.sign(self.me_private) return block
def check_for_quorum(self, next_vote): """ Checks if block has enough invalid votes to make a decision Args: next_vote: The next vote. """ next_block = self.bigchain.get_block( next_vote['vote']['voting_for_block']) block_status = self.bigchain.block_election_status(next_block['id'], next_block['block']['voters']) if block_status == self.bigchain.BLOCK_INVALID: return Block.from_dict(next_block)
def validate_block(self, block): if not self.bigchain.has_previous_vote(block['id']): try: block = Block.from_dict(block) except (exceptions.InvalidHash): # XXX: if a block is invalid we should skip the `validate_tx` # step, but since we are in a pipeline we cannot just jump to # another function. Hackish solution: generate an invalid # transaction and propagate it to the next steps of the # pipeline. return block['id'], [self.invalid_dummy_tx] try: block._validate_block(self.bigchain) except exceptions.ValidationError: # XXX: if a block is invalid we should skip the `validate_tx` # step, but since we are in a pipeline we cannot just jump to # another function. Hackish solution: generate an invalid # transaction and propagate it to the next steps of the # pipeline. return block.id, [self.invalid_dummy_tx] return block.id, block.transactions
def get_last_voted_block(self): """Returns the last block that this node voted on.""" return Block.from_dict(backend.query.get_last_voted_block(self.connection, self.me))