Tinychain源码阅读笔记5-挖矿
main
中除load_from_disk
的余下部分
workers = []
server = ThreadedTCPServer(('0.0.0.0', PORT), TCPHandler)
def start_worker(fnc):
workers.append(threading.Thread(target=fnc, daemon=True))
workers[-1].start()
logger.info(f'[p2p] listening on {PORT}')
start_worker(server.serve_forever)
if peer_hostnames:
logger.info(
f'start initial block download from {len(peer_hostnames)} peers')
send_to_peer(GetBlocksMsg(active_chain[-1].id))
ibd_done.wait(60.) # Wait a maximum of 60 seconds for IBD to complete.
start_worker(mine_forever)
[w.join() for w in workers]
首先开启一个监听PORT
的永久服务线程,并且将最后一个区块信息发送给其他节点,线程会阻塞60s等待回应。然后开启了mine_forever
这个线程
def mine_forever():
while True:
my_address = init_wallet()[2]
block = assemble_and_solve_block(my_address)
if block:
connect_block(block)
save_to_disk()
先取挖矿地址,再挖掘区块
def assemble_and_solve_block(pay_coinbase_to_addr, txns=None):
"""
Construct a Block by pulling transactions from the mempool, then mine it.
"""
with chain_lock:
prev_block_hash = active_chain[-1].id if active_chain else None
block = Block(
version=0,
prev_block_hash=prev_block_hash,
merkle_hash='',
timestamp=int(time.time()),
bits=get_next_work_required(prev_block_hash),
nonce=0,
txns=txns or [],
)
if not block.txns:
block = select_from_mempool(block)
fees = calculate_fees(block)
my_address = init_wallet()[2]
coinbase_txn = Transaction.create_coinbase(
my_address, (get_block_subsidy() + fees), len(active_chain))
block = block._replace(txns=[coinbase_txn, *block.txns])
block = block._replace(merkle_hash=get_merkle_root_of_txns(block.txns).val)
if len(serialize(block)) > Params.MAX_BLOCK_SERIALIZED_SIZE:
raise ValueError('txns specified create a block too large')
return mine(block)
初始化了一个Block
,然后从mempool
内存池中添加交易到Block
。
def select_from_mempool(block: Block) -> Block:
"""Fill a Block with transactions from the mempool."""
added_to_block = set()
def check_block_size(b) -> bool:
return len(serialize(block)) < Params.MAX_BLOCK_SERIALIZED_SIZE
def try_add_to_block(block, txid) -> Block:
if txid in added_to_block:
return block
tx = mempool[txid]
# For any txin that can't be found in the main chain, find its
# transaction in the mempool (if it exists) and add it to the block.
for txin in tx.txins:
if txin.to_spend in utxo_set:
continue
in_mempool = find_utxo_in_mempool(txin)
if not in_mempool:
logger.debug(f"Couldn't find UTXO for {txin}")
return None
block = try_add_to_block(block, in_mempool.txid)
if not block:
logger.debug(f"Couldn't add parent")
return None
newblock = block._replace(txns=[*block.txns, tx])
if check_block_size(newblock):
logger.debug(f'added tx {tx.id} to block')
added_to_block.add(txid)
return newblock
else:
return block
for txid in mempool:
newblock = try_add_to_block(block, txid)
if check_block_size(newblock):
block = newblock
else:
break
return block
首先来看mempool
结构,txid
为key,Transaction
为value的字典结构。
# Set of yet-unmined transactions.
mempool: Dict[str, Transaction] = {}
首先遍历mempool
中的交易,逐个添加到 block
中,重点分析try_add_to_block
。
added_to_block
是个存txid
的set
集合,表示已添加到block
中的交易。
然后我们要检查txid
对应的 tx
的交易输入,因为其交易输入所引用的utxo
有可能在utxo_set
中,也有可能在mempool
中交易的交易输出中。如果在utxo_set
中,这个txin
就没问题,否则,调用find_utxo_in_mempool
函数找到mempool
中对应的utxo
。
def find_utxo_in_mempool(txin) -> UnspentTxOut:
txid, idx = txin.to_spend
try:
txout = mempool[txid].txouts[idx]
except Exception:
logger.debug("Couldn't find utxo in mempool for %s", txin)
return None
return UnspentTxOut(
*txout, txid=txid, is_coinbase=False, height=-1, txout_idx=idx)
如果在mempool
中,还要先把所引用的utxo
所在的交易先添加进block
,这笔交易也是当前交易的父交易。添加完交易后,做区块大小检查。
fees = calculate_fees(block)
my_address = init_wallet()[2]
coinbase_txn = Transaction.create_coinbase(
my_address, (get_block_subsidy() + fees), len(active_chain))
block = block._replace(txns=[coinbase_txn, *block.txns])
block = block._replace(merkle_hash=get_merkle_root_of_txns(block.txns).val)
if len(serialize(block)) > Params.MAX_BLOCK_SERIALIZED_SIZE:
raise ValueError('txns specified create a block too large')
return mine(block)
计算交易费、创建coinbase交易、获取merkel_hash值,并把这些内容添加到区块中,并再次检测区块大小。
def calculate_fees(block) -> int:
"""
Given the txns in a Block, subtract the amount of coin output from the
inputs. This is kept as a reward by the miner.
"""
fee = 0
def utxo_from_block(txin):
tx = [t.txouts for t in block.txns if t.id == txin.to_spend.txid]
return tx[0][txin.to_spend.txout_idx] if tx else None
def find_utxo(txin):
return utxo_set.get(txin.to_spend) or utxo_from_block(txin)
for txn in block.txns:
spent = sum(find_utxo(i).value for i in txn.txins)
sent = sum(o.value for o in txn.txouts)
fee += (spent - sent)
return fee
计算交易费代码很简单,即交易输入与交易输出金额的差值。
@classmethod
def create_coinbase(cls, pay_to_addr, value, height):
return cls(
txins=[TxIn(
to_spend=None,
# Push current block height into unlock_sig so that this
# transaction's ID is unique relative to other coinbase txns.
unlock_sig=str(height).encode(),
unlock_pk=None,
sequence=0)],
txouts=[TxOut(
value=value,
to_address=pay_to_addr)],
)
创建coinbase交易函数是Transaction类的classmethod方法,coinbase交易只有交易输出而没有交易输入。
最后,函数return mine(block)
def mine(block):
start = time.time()
nonce = 0
target = (1 << (256 - block.bits))
mine_interrupt.clear()
while int(sha256d(block.header(nonce)), 16) >= target:
nonce += 1
if nonce % 10000 == 0 and mine_interrupt.is_set():
logger.info('[mining] interrupted')
mine_interrupt.clear()
return None
block = block._replace(nonce=nonce)
duration = int(time.time() - start) or 0.001
khs = (block.nonce // duration) // 1000
logger.info(
f'[mining] block found! {duration} s - {khs} KH/s - {block.id}')
return block
mine函数即计算区块的工作量证明nonce,将nonce不断的增加直至双哈希nonce值小于target,这里暂时不清楚为什么nonce是10000倍数并且线程阻塞信号被设置时,挖矿会停止。得到nonce后,区块数据填充完毕,一个区块就这样被矿工挖掘出来了。