def _rebuild_tx_pool_unlocked(r, tx_pool: Mapping[bytes, Transaction], b: Block) -> Dict[bytes, Transaction]: """Rebuild the tx_pool (ie keep only the valid transactions) using utxo-block of b to check for transaction validity. Return the updated transaction pool. Should be called with the utxo-block and tx_pool locks held.""" utxo_block = {TransactionInput.loadb(i): TransactionOutput.loadb(o) for i, o \ in r.hgetall("blockchain:utxo-block:".encode() + b.current_hash).items()} def is_unspent(txin: TransactionInput) -> bool: if txin in utxo_block: return True prev_tx = tx_pool.get(txin.transaction_id) if prev_tx is None: return False return all(is_unspent(i) for i in prev_tx.inputs) tx_to_remove: Set[Transaction] = set() for t in tx_pool.values(): # A transaction is valid only if all its inputs are either in UTXO-block # or are outputs of other valid transactions in the pool if not all(is_unspent(i) for i in t.inputs): tx_to_remove.add(t) tx_pool = {tid: t for tid, t in tx_pool.items() if t not in tx_to_remove} if tx_to_remove: r.hdel("blockchain:tx_pool", *(t.id for t in tx_to_remove)) return tx_pool
def _check_for_new_block() -> None: """Check if there are at least CAPACITY transactions that can go in a new block (ie transactions in the pool with all their inputs in UTXO-block[last_block]). If so and if a miner is not already running, start mining for a new block. Should be called with NO locks held.""" logging.debug("Checking for new block") CAPACITY = block.get_capacity() r = util.get_db() with r.lock("blockchain:last_block:lock"), \ r.lock("blockchain:miner_pid:lock"), \ r.lock("blockchain:tx_pool:lock"), \ r.lock("blockchain:utxo-block:lock"): # NOTE: If a miner is running, we expect it to add a new block, so we # abort. If mining succeeds, this function will be called again by # new_recv_block(). If it fails (another valid block is received) this # will again be called by new_recv_block() miner_pidb = r.get("blockchain:miner_pid") if miner_pidb is not None: logging.debug("Miner already running with PID %d", util.btoui(miner_pidb)) return tx_pool = {Transaction.loadb(tb) for tb in r.hvals("blockchain:tx_pool")} if len(tx_pool) < CAPACITY: logging.debug("Cannot create new block yet (not enough transactions)") return last_block = get_block() utxo_block = {TransactionInput.loadb(i): TransactionOutput.loadb(o) for i, o \ in r.hgetall("blockchain:utxo-block:".encode() + last_block.current_hash).items()} new_block_tx: List[Transaction] = [] # NOTE: Since there are >= CAPACITY transactions in the pool, and we # don't mind transaction inter-dependence in the same block, a new # block can be created, so this loop will terminate while True: for t in tx_pool: # Search for t.inputs in UTXO-block[last_block] as well as in new_block_tx if all(i in utxo_block or \ any(nt.id == i.transaction_id for nt in new_block_tx) for i in t.inputs): new_block_tx.append(t) if len(new_block_tx) == CAPACITY: new_block = Block(index=last_block.index + 1, previous_hash=last_block.current_hash, transactions=new_block_tx) # NOTE: We don't delete the new block_tx from the pool, because # mining might fail. They will be deleted eventually when they # enter the main branch. miner_pid = new_block.finalize() r.set("blockchain:miner_pid", util.uitob(miner_pid)) logging.debug("Miner started with PID %d", miner_pid) return tx_pool.difference_update(new_block_tx)
def _rebuild_utxo_tx_unlocked(r, b: Block, tx_pool: Mapping[bytes, Transaction]) -> None: """Reinitialize UTXO-tx as a copy of UTXO-block[b] and simulate adding all transactions in tx_pool. Should be called with the utxo-block and the utxo-tx locks held.""" r.delete("blockchain:utxo-tx") utxo_tx = {TransactionInput.loadb(i): TransactionOutput.loadb(o) for i, o \ in r.hgetall("blockchain:utxo-block:".encode() + b.current_hash).items()} while tx_pool: tx_to_remove: Set[Transaction] = set() for t in tx_pool.values(): if all(i in utxo_tx for i in t.inputs): for i in t.inputs: del utxo_tx[i] for o in t.outputs: utxo_tx[TransactionInput(t.id, o.index)] = o tx_to_remove.add(t) tx_pool = {tid: t for tid, t in tx_pool.items() if t not in tx_to_remove} # NOTE: utxo_tx is not empty because UTXO-block[recv_block] is not empty r.hmset("blockchain:utxo-tx", {i.dumpb(): o.dumpb() for i, o in utxo_tx.items()})
def generate_transaction(recipient_id: int, amount: float, mute: bool = False) -> bool: """If possible (there are enough UTXOs) generate a new transaction giving amount NBC to recipient and the change back to us. If mute is True don't broadcast it.""" logging.debug("Transaction requested: %f NBC to node %d", amount, recipient_id) sender = wallet.get_public_key().dumpb() recipient = wallet.get_public_key(recipient_id).dumpb() r = util.get_db() inputs: List[TransactionInput] = [] input_amount = 0.0 with r.lock("blockchain:tx_pool:lock"), \ r.lock("blockchain:utxo-tx:lock"): for ib, ob in r.hgetall("blockchain:utxo-tx").items(): o = TransactionOutput.loadb(ob) if o.recipient == sender: inputs.append(TransactionInput.loadb(ib)) input_amount += o.amount if input_amount >= amount: t = Transaction(recipient=recipient, amount=amount, inputs=inputs, input_amount=input_amount) # Add to transaction pool r.hset("blockchain:tx_pool", t.id, t.dumpb()) # "Add to wallet if mine" r.hdel("blockchain:utxo-tx", *(i.dumpb() for i in t.inputs)) r.hmset("blockchain:utxo-tx", {TransactionInput(t.id, o.index).dumpb(): \ o.dumpb() for o in t.outputs}) break else: # Not enough UTXOs logging.error("Cannot send %f NBC to node %d (not enough coins)", amount, recipient_id) return False logging.debug("Generated transaction %s", util.bintos(t.id)) _check_for_new_block() if not mute: logging.debug("Broadcasting transaction %s", util.bintos(t.id)) chatter.broadcast_transaction(t, util.get_peer_ids()) return True