def create(dev_config: DevConfig, block_number: int, prev_headerhash: bytes, prev_timestamp: int, transactions: list, miner_address: bytes, seed_height: Optional[int], seed_hash: Optional[bytes]): block = Block() # Process transactions hashedtransactions = [] fee_reward = 0 for tx in transactions: fee_reward += tx.fee # Prepare coinbase tx total_reward_amount = BlockHeader.block_reward_calc( block_number, dev_config) + fee_reward coinbase_tx = CoinBase.create(dev_config, total_reward_amount, miner_address, block_number) hashedtransactions.append(coinbase_tx.txhash) Block._copy_tx_pbdata_into_block( block, coinbase_tx) # copy memory rather than sym link for tx in transactions: hashedtransactions.append(tx.txhash) Block._copy_tx_pbdata_into_block( block, tx) # copy memory rather than sym link txs_hash = merkle_tx_hash( hashedtransactions) # FIXME: Find a better name, type changes tmp_blockheader = BlockHeader.create(dev_config=dev_config, blocknumber=block_number, prev_headerhash=prev_headerhash, prev_timestamp=prev_timestamp, hashedtransactions=txs_hash, fee_reward=fee_reward, seed_height=seed_height, seed_hash=seed_hash) block.blockheader = tmp_blockheader block._data.header.MergeFrom(tmp_blockheader.pbdata) block.set_nonces(dev_config, 0, 0) return block
def test_create(self, time_mock): b = BlockHeader.create(dev_config=config.dev, blocknumber=1, prev_headerhash=b'headerhash', prev_timestamp=10, hashedtransactions=b'some_data', fee_reward=1, seed_height=0, seed_hash=None) self.assertIsNotNone(b) b = BlockHeader.create(dev_config=config.dev, blocknumber=1, prev_headerhash=b'headerhash', prev_timestamp=10, hashedtransactions=b'some_data', fee_reward=1, seed_height=0, seed_hash=None)
def test_create_fails_when_prev_timestamp_is_negative(self, time_mock): # The only way to get it to fail in this mode is to pass a negative timestamp. Which should never happen IRL. time_mock.return_value = 0 b = BlockHeader.create(config.dev, 1, sha256(b'prev'), -10, sha256(b'txs'), 10, seed_height=0, seed_hash=None) self.assertIsNone(b)
def test_GetLastBlockHeader(self): self.block_header_params["blocknumber"] = 20 block_header = BlockHeader.create(**self.block_header_params) self.chain_manager.height = 200 self.xrdnode.get_blockheader_and_metadata = MagicMock( return_value=[block_header, BlockMetadata()]) req = xrdmining_pb2.GetLastBlockHeaderReq(height=20) answer = self.service.GetLastBlockHeader(request=req, context=None) self.assertEqual(180, answer.depth) self.assertEqual(20, answer.height)
def setUp(self): with mock.patch('xrd.core.misc.ntp.getTime', return_value=1615270948) as time_mock: self.block_header = BlockHeader.create(config.dev, 1, sha256(b'prev'), time_mock.return_value, sha256(b'txs'), 10, 0, None) self.fee_reward = 10 self.coinbase_amount = self.block_header.block_reward + self.fee_reward # this variable is for validate_parent_child_relation() self.m_parent_block = Mock( name='mock Parent Block', block_number=self.block_header.block_number - 1, headerhash=self.block_header.prev_headerhash, timestamp=self.block_header.timestamp - 1)
def test_GetBlockMiningCompatible(self): block_header = BlockHeader.create(**self.block_header_params) self.xrdnode.get_blockheader_and_metadata = MagicMock( return_value=[block_header, BlockMetadata()]) req = xrdmining_pb2.GetBlockMiningCompatibleReq(height=10) answer = self.service.GetBlockMiningCompatible(request=req, context=None) self.assertEqual(10, answer.blockheader.block_number) self.assertEqual(1, answer.blockheader.reward_fee) # if xrdNode responds with None, None, the GRPC response should be blank too self.xrdnode.get_blockheader_and_metadata = MagicMock( return_value=[None, None]) answer = self.service.GetBlockMiningCompatible(request=req, context=None) self.assertEqual(0, answer.blockheader.block_number) self.assertEqual(0, answer.blockheader.reward_fee)
def __init__(self, protobuf_block=None): self._data = protobuf_block if protobuf_block is None: self._data = xrd_pb2.Block() self.blockheader = BlockHeader(self._data.header)
class Block(object): def __init__(self, protobuf_block=None): self._data = protobuf_block if protobuf_block is None: self._data = xrd_pb2.Block() self.blockheader = BlockHeader(self._data.header) def __eq__(self, other): equality = (self.block_number == other.block_number) and ( self.headerhash == other.headerhash ) and (self.prev_headerhash == other.prev_headerhash) and ( self.timestamp == other.timestamp) and (self.mining_nonce == other.mining_nonce) return equality @property def size(self): return self._data.ByteSize() @property def pbdata(self): """ Returns a protobuf object that contains persistable data representing this object :return: A protobuf Block object :rtype: xrd_pb2.Block """ return self._data @property def block_number(self): return self.blockheader.block_number @property def headerhash(self): return self.blockheader.headerhash @property def prev_headerhash(self): return self.blockheader.prev_headerhash @property def transactions(self): return self._data.transactions @property def mining_nonce(self): return self.blockheader.mining_nonce @property def block_reward(self): return self.blockheader.block_reward @property def fee_reward(self): return self.blockheader.fee_reward @property def timestamp(self): return self.blockheader.timestamp def mining_blob(self, dev_config: DevConfig) -> bytes: return self.blockheader.mining_blob(dev_config) def mining_nonce_offset(self, dev_config: DevConfig) -> bytes: return self.blockheader.nonce_offset(dev_config) @staticmethod def from_json(json_data): pbdata = xrd_pb2.Block() Parse(json_data, pbdata) return Block(pbdata) def verify_blob(self, blob: bytes, dev_config: DevConfig) -> bool: return self.blockheader.verify_blob(blob, dev_config) def set_nonces(self, dev_config: DevConfig, mining_nonce: int, extra_nonce: int = 0): self.blockheader.set_nonces(dev_config, mining_nonce, extra_nonce) self._data.header.MergeFrom(self.blockheader.pbdata) def to_json(self) -> str: # FIXME: Remove once we move completely to protobuf return MessageToJson(self._data, sort_keys=True) def serialize(self) -> str: return self._data.SerializeToString() @staticmethod def deserialize(data): pbdata = xrd_pb2.Block() pbdata.ParseFromString(bytes(data)) block = Block(pbdata) return block @staticmethod def _copy_tx_pbdata_into_block(block, tx): block._data.transactions.extend([tx.pbdata]) @staticmethod def create(dev_config: DevConfig, block_number: int, prev_headerhash: bytes, prev_timestamp: int, transactions: list, miner_address: bytes, seed_height: Optional[int], seed_hash: Optional[bytes]): block = Block() # Process transactions hashedtransactions = [] fee_reward = 0 for tx in transactions: fee_reward += tx.fee # Prepare coinbase tx total_reward_amount = BlockHeader.block_reward_calc( block_number, dev_config) + fee_reward coinbase_tx = CoinBase.create(dev_config, total_reward_amount, miner_address, block_number) hashedtransactions.append(coinbase_tx.txhash) Block._copy_tx_pbdata_into_block( block, coinbase_tx) # copy memory rather than sym link for tx in transactions: hashedtransactions.append(tx.txhash) Block._copy_tx_pbdata_into_block( block, tx) # copy memory rather than sym link txs_hash = merkle_tx_hash( hashedtransactions) # FIXME: Find a better name, type changes tmp_blockheader = BlockHeader.create(dev_config=dev_config, blocknumber=block_number, prev_headerhash=prev_headerhash, prev_timestamp=prev_timestamp, hashedtransactions=txs_hash, fee_reward=fee_reward, seed_height=seed_height, seed_hash=seed_hash) block.blockheader = tmp_blockheader block._data.header.MergeFrom(tmp_blockheader.pbdata) block.set_nonces(dev_config, 0, 0) return block def update_mining_address(self, dev_config: DevConfig, mining_address: bytes): coinbase_tx = Transaction.from_pbdata(self.transactions[0]) coinbase_tx.update_mining_address(mining_address) hashedtransactions = [] for tx in self.transactions: hashedtransactions.append(tx.transaction_hash) self.blockheader.update_merkle_root(dev_config, merkle_tx_hash(hashedtransactions)) self._data.header.MergeFrom(self.blockheader.pbdata) def validate(self, chain_manager, future_blocks: OrderedDict) -> bool: if chain_manager.get_block_is_duplicate(self): logger.warning('Duplicate Block #%s %s', self.block_number, bin2hstr(self.headerhash)) return False parent_block = chain_manager.get_block(self.prev_headerhash) dev_config = chain_manager.get_config_by_block_number( self.block_number) # If parent block not found in state, then check if its in the future block list if not parent_block: try: parent_block = future_blocks[self.prev_headerhash] except KeyError: logger.warning('Parent block not found') logger.warning('Parent block headerhash %s', bin2hstr(self.prev_headerhash)) return False if not self._validate_parent_child_relation(parent_block): logger.warning('Failed to validate blocks parent child relation') return False if not chain_manager.validate_mining_nonce(self.blockheader, dev_config): logger.warning('Failed PoW Validation') return False if len(self.transactions) == 0: return False try: state_container = chain_manager.new_state_container( set(), self.block_number, False, None) coinbase_txn = Transaction.from_pbdata(self.transactions[0]) coinbase_amount = coinbase_txn.amount if not coinbase_txn.validate_all(state_container): return False except Exception as e: logger.warning('Exception %s', e) return False # Build transaction merkle tree, calculate fee reward, and then see if BlockHeader also agrees. hashedtransactions = [] for tx in self.transactions: tx = Transaction.from_pbdata(tx) hashedtransactions.append(tx.txhash) fee_reward = 0 for index in range(1, len(self.transactions)): fee_reward += self.transactions[index].fee qn = Qryptonight() seed_block = chain_manager.get_block_by_number( qn.get_seed_height(self.block_number)) self.blockheader._seed_height = seed_block.block_number self.blockheader._seed_hash = seed_block.headerhash if not self.blockheader.validate(fee_reward, coinbase_amount, merkle_tx_hash(hashedtransactions), dev_config): return False return True def is_future_block(self, dev_config: DevConfig) -> bool: if self.timestamp > ntp.getTime() + dev_config.block_max_drift: return True return False def _validate_parent_child_relation(self, parent_block) -> bool: return self.blockheader.validate_parent_child_relation(parent_block) @staticmethod def put_block(state: State, block, batch): state._db.put_raw(block.headerhash, block.serialize(), batch) @staticmethod def get_block(state: State, header_hash: bytes): try: data = state._db.get_raw(header_hash) return Block.deserialize(data) except KeyError: logger.debug('[get_block] Block header_hash %s not found', bin2hstr(header_hash).encode()) except Exception as e: logger.error('[get_block] %s', e) return None @staticmethod def get_block_size_limit(state: State, block, dev_config: DevConfig): # NOTE: Miner block_size_list = [] for _ in range(0, 10): block = Block.get_block(state, block.prev_headerhash) if not block: return None block_size_list.append(block.size) if block.block_number == 0: break return max(dev_config.block_min_size_limit_in_bytes, dev_config.size_multiplier * median(block_size_list)) @staticmethod def remove_blocknumber_mapping(state: State, block_number, batch): state._db.delete(str(block_number).encode(), batch) @staticmethod def put_block_number_mapping(state: State, block_number: int, block_number_mapping, batch): state._db.put_raw( str(block_number).encode(), MessageToJson(block_number_mapping, sort_keys=True).encode(), batch) @staticmethod def get_block_number_mapping(state: State, block_number: int): try: data = state._db.get_raw(str(block_number).encode()) block_number_mapping = xrd_pb2.BlockNumberMapping() return Parse(data, block_number_mapping) except KeyError: logger.debug('[get_block_number_mapping] Block #%s not found', block_number) except Exception as e: logger.error('[get_block_number_mapping] %s', e) return None @staticmethod def get_block_by_number(state: State, block_number: int): block_number_mapping = Block.get_block_number_mapping( state, block_number) if not block_number_mapping: return None return Block.get_block(state, block_number_mapping.headerhash) @staticmethod def get_block_header_hash_by_number(state: State, block_number: int): block_number_mapping = Block.get_block_number_mapping( state, block_number) if not block_number_mapping: return None return block_number_mapping.headerhash @staticmethod def last_block(state: State): block_number = state.get_mainchain_height() return Block.get_block_by_number(state, block_number)
def test_init(self, time_mock): block_header = BlockHeader() self.assertIsNotNone(block_header) # just to avoid warnings
def test_block_reward_calc_genesis_is_total_coin_supply(self, time_mock): genesis = BlockHeader.create(config.dev, 0, sha256(b'Random Scifi Book Title'), time_mock.return_value, sha256(b'txs'), 10, 0, None) self.assertEqual(config.dev.supplied_coins, genesis.block_reward)
def test_create_uses_prev_timestamp_when_genesis_block(self, time_mock): genesis = BlockHeader.create(config.dev, 0, sha256(b'Random Scifi Book Title'), time_mock.return_value, sha256(b'txs'), 10, 0, None) self.assertEqual(genesis.timestamp, time_mock.return_value)