def serialize(self, include_signature=True): milestone = config.get_milestone(self.height - 1) if milestone["block"]["idFullSha256"]: if len(self.previous_block) != 64: raise Exception( "Previous block shoud be SHA256, but found a non SHA256 block id" ) self.previous_block_hex = self.previous_block.encode("utf-8") else: self.previous_block_hex = Block.to_bytes_hex(self.previous_block) bytes_data = bytes() bytes_data += write_bit32(self.version) bytes_data += write_bit32(self.timestamp) bytes_data += write_bit32(self.height) bytes_data += unhexlify(self.previous_block_hex) bytes_data += write_bit32(self.number_of_transactions) bytes_data += write_bit64(int(self.total_amount)) bytes_data += write_bit64(int(self.total_fee)) bytes_data += write_bit64(int(self.reward)) bytes_data += write_bit32(self.payload_length) bytes_data += unhexlify(self.payload_hash.encode("utf-8")) bytes_data += unhexlify(self.generator_public_key) if include_signature and self.block_signature: bytes_data += unhexlify(self.block_signature.encode("utf-8")) return hexlify(bytes_data)
def consume_queue(self): while True: serialized_block = self.process_queue.pop_block() if serialized_block: last_block = self.database.get_last_block() block = Block.from_serialized(serialized_block) status = self.process_block(block, last_block) logger.info(status) if status in [BLOCK_ACCEPTED, BLOCK_DISCARDED_BUT_CAN_BE_BROADCASTED]: # TODO: Broadcast only current block milestone = config.get_milestone(block.height) current_slot = slots.get_slot_number(block.height, time.get_time()) if current_slot * milestone["blocktime"] <= block.timestamp: # TODO: THIS IS MISSING logger.error("MISSING: IMPLEMENT BROADCASTING") else: # TODO: change this logger.info("Nothing to process. Sleeping for 1 sec") sleep(1) # Our chain can get out of sync when it doesn't receive all the blocks # to the p2p endpoint, so if we're not in sync, force sync to the last # block last_block = self.database.get_last_block() if not self.is_synced(last_block): logger.info("Force syncing with the network as we got out of sync") self.sync_chain() logger.info("Done force syncing")
def can_be_applied_to_wallet(self, wallet, wallet_manager, block_height): """Checks if transaction can be applied to the wallet :param (Wallet) wallet: wallet you want to apply the transaction to :param (WalletManager) wallet_manager: wallet manager :param (int) block_height: current block height :returns (bool): True if can be applied, False otherwise """ if wallet.multisignature: logger.error("Multi signatures are currently not supported") return False if self.sender_public_key != wallet.public_key: logger.error("Sender public key does not match the wallet") return False balance = wallet.balance - self.amount - self.fee if balance < 0: logger.error("Insufficient balance in the wallet") return False if wallet.second_public_key: if not self.verify_second_signature(wallet.second_public_key): logger.error("Failed to verify second-signature") return False elif self.second_signature or self.sign_signature: milestone = config.get_milestone(block_height) # Accept invalid second signature fields prior the applied patch if not milestone["ignoreInvalidSecondSignatureField"]: logger.error("Wallet does not allow second signatures") return False return True
def load_active_delegate_wallets(self, height): max_delegates = config.get_milestone(height)["activeDelegates"] if height > 1 and height % max_delegates != 1: # TODO: exception raise Exception("Trying to build delegates outside of round change") delegate_wallets = [] keys = self.redis.keys(self.key_for_username("*")) addresses = self.redis.mget(keys) for address in addresses: wallet = self.find_by_address(address.decode()) delegate_wallets.append(wallet) if len(delegate_wallets) < max_delegates: raise Exception( "Expected to find {} delegates but only found {}.".format( max_delegates, len(delegate_wallets) ) ) # Sort delegate wallets by balance and use public key as a tiebreaker # Sort wallets by balance descending and by public key ascending. Because # vote_balance is a number, use a negative number to sort it descending # and public_key to sort it ascending. delegate_wallets.sort(key=lambda x: (-x.vote_balance, x.public_key)) # for wallet in delegate_wallets[:60]: # print(wallet.username, wallet.public_key, wallet.balance) delegate_wallets = delegate_wallets[:max_delegates] logger.info("Loaded %s active delegates", len(delegate_wallets)) return delegate_wallets
def is_synced(self, last_block): """Checks if blockchain is synced to at least second to last height :param Block last_block: last block that is in the database """ current_time = time.get_time() blocktime = config.get_milestone(last_block.height)["blocktime"] return (current_time - last_block.timestamp) < (3 * blocktime)
def get_id_hex(self): payload_hash = unhexlify(self.serialize()) full_hash = sha256(payload_hash).digest() milestone = config.get_milestone(self.height) if milestone["block"]["idFullSha256"]: return hexlify(full_hash) small_hash = full_hash[:8][::-1] return hexlify(small_hash)
def block_exists(self, block): key = "process_queue:block:{}:{}".format(block.height, block.id) if self.db.exists(key): self.db.incr(key) return True else: blocktime = config.get_milestone(block.height)["blocktime"] # Expire the key after `blocktime` seconds self.db.set(key, 0, ex=blocktime) return False
def _deserialize_previous_block(self, buff): """ For deserializing previous block id, we need to check the milestone for previous block """ milestone = config.get_milestone(self.height - 1) if milestone["block"]["idFullSha256"]: self.previous_block_hex = hexlify(buff.pop_bytes(32)) self.previous_block = self.previous_block_hex.decode("utf-8") else: self.previous_block_hex = hexlify(buff.pop_bytes(8)) self.previous_block = str(int(self.previous_block_hex, 16))
def get_slot_number(height, epoch_time): # TODO: find a better way to get milestone data milestone = config.get_milestone(height) return math.floor(epoch_time / milestone["blocktime"])
def is_forging_allowed(height, epoch_time): milestone = config.get_milestone(height) return epoch_time % milestone["blocktime"] < milestone["blocktime"] / 2
def verify(self): errors = [] # TODO: find a better way to get milestone data milestone = config.get_milestone(self.height) # Check that the previous block is set if it's not a genesis block if self.height > 1 and not self.previous_block: errors.append("Invalid previous block") # Chech that the block reward matches with the one specified in config if self.reward != milestone["reward"]: errors.append("Invalid block reward: {} expected: {}".format( self.reward, milestone["reward"])) # Verify block signature is_valid_signature = self.verify_signature() if not is_valid_signature: errors.append("Failed to verify block signature") # Check if version is correct on the block if self.version != milestone["block"]["version"]: errors.append("Invalid block version") # Check that the block timestamp is not in the future is_invalid_timestamp = slots.get_slot_number( self.height, self.timestamp) > slots.get_slot_number( self.height, time.get_time()) if is_invalid_timestamp: errors.append("Invalid block timestamp") # Check if all transactions are valid invalid_transactions = [ trans for trans in self.transactions if not trans.verify() ] if len(invalid_transactions) > 0: errors.append("One or more transactions are not verified") # Check that number of transactions and block.number_of_transactions match if len(self.transactions) != self.number_of_transactions: errors.append("Invalid number of transactions") # Check that number of transactions is not too high (except for genesis block) if (self.height > 1 and len(self.transactions) > milestone["block"]["maxTransactions"]): errors.append("Too many transactions") # Check if transactions add up to the block values applied_transactions = [] total_amount = 0 total_fee = 0 bytes_data = bytes() for transaction in self.transactions: if transaction.id in applied_transactions: errors.append("Encountered duplicate transaction: {}".format( transaction.id)) applied_transactions.append(transaction.id) total_amount += transaction.amount total_fee += transaction.fee bytes_data += unhexlify(transaction.id) if total_amount != self.total_amount: errors.append("Invalid total amount") if total_fee != self.total_fee: errors.append("Invalid total fee") if len(bytes_data) > milestone["block"]["maxPayload"]: errors.append("Payload is too large") if sha256(bytes_data).hexdigest() != self.payload_hash: errors.append("Invalid payload hash") return len(errors) == 0, errors
def get_id(self): id_hex = self.get_id_hex() milestone = config.get_milestone(self.height) if milestone["block"]["idFullSha256"]: return id_hex.decode("utf-8") return str(int(id_hex, 16))
def start(self): """Starts the blockchain. Depending of the state of the blockchain it will decide what needs to be done in order to correctly start syncing. """ logger.info("Starting the blockchain") apply_genesis_round = False try: block = self.database.get_last_block() # If block is not found in the db, insert a genesis block if not block: logger.info("No block found in the database") block = Block.from_dict(config.genesis_block) if block.payload_hash != config.network["nethash"]: logger.error( "FATAL: The genesis block payload hash is different from " "the configured nethash" ) self.stop() return else: self.database.save_block(block) apply_genesis_round = True logger.info("Verifying database integrity") is_valid = False errors = None for _ in range(5): is_valid, errors = self.database.verify_blockchain() if is_valid: break else: logger.error("Database is corrupted: {}".format(errors)) milestone = config.get_milestone(block.height) previous_round = math.floor( (block.height - 1) / milestone["activeDelegates"] ) if previous_round <= 1: raise Exception( "FATAL: Database is corrupted: {}".format(errors) ) logger.info("Rolling back to round {}".format(previous_round)) self.database.rollback_to_round(previous_round) logger.info("Rolled back to round {}".format(previous_round)) else: raise Exception( "FATAL: After rolling back for 5 rounds, database is still " "corrupted: {}".format(errors) ) logger.info("Verified database integrity") # if (stateStorage.networkStart) { # await blockchain.database.buildWallets(block.data.height); # await blockchain.database.saveWallets(true); # await blockchain.database.applyRound(block.data.height); # await blockchain.transactionPool.buildWallets(); # return blockchain.dispatch("STARTED"); # } logger.info("Last block in database: %s", block.height) # if the node is shutdown between round, the round has already been applied # so we delete it to start a new, fresh round if is_new_round(block.height + 1): current_round, _, _ = calculate_round(block.height + 1) logger.info( "Start of new round detected %s. Removing it in order to correctly " "start the chain with new round.", current_round, ) self.database.delete_round(current_round) # Rebuild wallets self.database.wallets.build() self.transaction_pool.build_wallets() if apply_genesis_round: self.database.apply_round(block.height) self.sync_chain() logger.info("Blockhain is syced!") # Blockchain was just synced, so remove all blocks from process queue # as it was just synced. We clear it only on the start of the chain, to # awoid processing old blocks. If we ever run sync while it's already # runing, we don't want to run clear after sync as that might leave us # with missing blocks which will cause the blockchain to always sync back # rather than sync by accepting block from peers. self.process_queue.clear() self.consume_queue() except Exception as e: self.stop() # TODO: log exception raise e # TODO:
def _calculate_static_fee(transaction, block_height): fees = config.get_milestone(block_height)["fees"]["staticFees"] static_fee = fees[str(transaction.type)] if transaction.type == TRANSACTION_TYPE_MULTI_SIGNATURE: return static_fee * (len(transaction.asset["multisignature"]["keysgroup"]) + 1) return static_fee
def is_new_round(height): """Checks if height is at the start of new round """ max_delegates = config.get_milestone(height)["activeDelegates"] return height % max_delegates == 1
def calculate_round(height): max_delegates = config.get_milestone(height)["activeDelegates"] current_round = math.floor((height - 1) / max_delegates) + 1 next_round = math.floor(height / max_delegates) + 1 return current_round, next_round, max_delegates