你的浏览器不支持canvas

松子的博客

当你有了责任感,就值得被托付

如何构建自己的区块链 第三部分 - 编写挖矿和沟通的节点

Date: Author: Zarey C.Y. Chang

本文章采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。转载请注明来自Z2CARE的博客

独立翻译自:How to Build Your Own Blockchain Part 3 — Writing Nodes that Mine and Talk

大家好欢迎来到构建区块链第三部分,快速回顾第一部分, 我编写了代码,遍历了顶级的数学和单个节点挖掘自己的区块链的需求;我创建具有有效信息的新块,将它们保存到文件夹中,然后再开始挖掘新块。第二部分介绍了多个节点以及它们具有同步功能。如果节点1自己进行挖掘,而节点2希望获取节点1的区块链,那么现在可以这样做了。

在第3部分中,阅读下面的“长话短说”,看看我们将能得到什么。然后阅读文章的其余部分,以了解(希望如此)这一切是如何发生的。

本系列其他文章

长话短说

节点将竞争看谁因挖掘一个块而获得荣誉。这是一个比赛!为了做到这一点,我们调整mine.py来检查是否有有效的块,方法是只检查一部分nonce值,而不是在匹配之前检查所有的nonces值。然后APScheduler将处理使用不同nonce范围运行的挖掘作业。如果我们想要让node.py像个Flask的web服务一样挖矿,我们就将挖掘功能转移到后台。最后,我们有了不同的节点竞争第一次挖掘并广播他们挖掘到的块!

在我们开始之前, 要是你想检查所有的事就参考github上代码.这里有一些代码片段来说明我做了什么,但是如果你想看完整的代码,请到github上看。这段代码对我很有用,但我也在清理所有东西,并编写一个可用的README,以便人们可以自己克隆和运行它。如果像联系我,Twitter,和联系方式

用APScheduler挖掘和再次挖掘

这里的第一步是调整挖掘,以便在另一个节点找到了它正在处理的索引块时能够停止挖掘。 在第1部分中,挖掘是一个while循环,只有在找到有效的nonce时才会中断。当我们被告知另一个节点的成功,我们需要停止挖掘的能力。

我不会在这里忽悠人,我花了一段时间才找到最好的方法。阅读下面的列表,了解我的各种想法,以及它们为什么不奏效。

  1. 在APScheduler上运行,当我们的Flask应用程序遇到来自不同节点的块时,停止对该索引的挖掘作业,并开始对索引+ 1进行挖掘。
  2. 这似乎就是要做的事情,但我发现APScheduler的remove_job()函数在任务已经从队列中取出并运行时无法移除。所以我就无法停止挖掘。
  3. 用Celery? Celery肯定可以工作,我研究过它,但是坦率地说,从一个基本用例开始需要大量的代码和配置。参考介绍页面你们可以看到我在说什么。Celery肯定是一个选择,我不是说它不值得,但在这种情况下,它开销太大。
  4. Gevent?我以前用过,用它来一次抓取一堆页面,而不必等待请求返回。使收集页面更快。在这种情况下,它可以工作,但主要用于队列而不是单个作业。我不会一次挖掘一堆块,所以不需要创建多个Gevent会运行的块。我只需要有一个。
  5. rq?不行,在停止运行中的作业上有同样的问题。
  6. 在做了一些其他实现的调研后,比如在github上查看以太坊Python库,我发现它们挖掘时用到了术语roundsstarting_nonce.我们不会使用一个完整的while循环来遍历nonces直到找到正确的循环为止的方式,我们只会在[starting_nonce:starting_nonce+round]的值中检查nonces。如果我们成功了,我们就移动到下一个块。否则,我们就用不同的starting_nonce值再次尝试。
  7. 当调用挖掘作业时,它一次只检查特定数量的nonces。如果成功,它将返回有效的nonce和hash。如果没有,则返回None。把这个想法和APScheduler结合起来,我们似乎达实现了功能。
  8. 然后我意识到,既然我们有了这些功能,我就可以使用上面列出的任何库来进行挖掘。如果它允许你对作业进行排队,那么当您将挖掘分解为不同的部分时,它就好用。实际上,一个好的文章应该是使用所有工作的库来实现挖掘。如果你需要,请告诉我。 下面是mine.py重写的代码。重点如下。
  • 当你运行mine.py时,你只想要去挖掘,我们将要使用的APSchedule是‘BlockingScheduler’。稍后你讲看到我们使用BackgroundScheduler
  • 下面有多个函数用于具有不同初始级别的挖掘。你可以对链上的下一个块进行挖掘,对一个块后的块进行挖掘,或者尝试为您刚刚生成的块找到有效的nonce而挖掘。
  • 除了用于挖掘块的预定任务之外,我们还有一个侦听器用来检查返回值,查看是否继续使用上面第4(原文有误,应是6)节中描述的范围内的nonces来挖掘当前块,或者转到下一个块。
#mine.py

import apscheduler
from apscheduler.schedulers.blocking import BlockingScheduler

#if we're running mine.py, we don't want it in the background
#because the script would return after starting. So we want the
#BlockingScheduler to run the code.
sched = BlockingScheduler(standalone=True)

import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

STANDARD_ROUNDS = 100000

def mine_for_block(chain=None, rounds=STANDARD_ROUNDS, start_nonce=0):
  if not chain:
    chain = sync.sync_local() #gather last node

  prev_block = chain.most_recent_block()
  return mine_from_prev_block(prev_block, rounds=rounds, start_nonce=start_nonce)

def mine_from_prev_block(prev_block, rounds=STANDARD_ROUNDS, start_nonce=0):
  #create new block with correct
  new_block = utils.create_new_block_from_prev(prev_block=prev_block)
  return mine_block(new_block, rounds=rounds, start_nonce=start_nonce)

def mine_block(new_block, rounds=STANDARD_ROUNDS, start_nonce=0):
  #Attempting to find a valid nonce to match the required difficulty
  #of leading zeros. We're only going to try 1000
  nonce_range = [i+start_nonce for i in range(rounds)]
  for nonce in nonce_range:
    new_block.nonce = nonce
    new_block.update_self_hash()
    if str(new_block.hash[0:NUM_ZEROS]) == '0' * NUM_ZEROS:
      print "block %s mined. Nonce: %s" % (new_block.index, new_block.nonce)
      assert new_block.is_valid()
      return new_block, rounds, start_nonce

  #couldn't find a hash to work with, return rounds and start_nonce
  #as well so we can know what we tried
  return None, rounds, start_nonce

def mine_for_block_listener(event):
  new_block, rounds, start_nonce = event.retval
  #if didn't mine, new_block is None
  #we'd use rounds and start_nonce to know what the next
  #mining task should use
  if new_block:
    print "Mined a new block"
    new_block.self_save()
    sched.add_job(mine_from_prev_block, args=[new_block], kwargs={'rounds':STANDARD_ROUNDS, 'start_nonce':0}, id='mine_for_block') #add the block again
  else:
    print "No dice mining a new block. Restarting with different nonce range"
    sched.add_job(mine_for_block, kwargs={'rounds':rounds, 'start_nonce':start_nonce+rounds}, id='mine_for_block') #add the block again
sched.print_jobs()

if __name__ == '__main__':

  sched.add_job(mine_for_block, kwargs={'rounds':STANDARD_ROUNDS, 'start_nonce':0}, id='mine_for_block') #add the block again
  sched.add_listener(mine_for_block_listener, apscheduler.events.EVENT_JOB_EXECUTED)#, args=sched)
  sched.start()

当我们运行这个代码时,节点将会成功地挖掘,但是是在不同的作业中而不是只有一个作业。这是伟大的开端。

节点挖掘

我要添加的下一部分是node.py的功能,Flask节点也可以运行挖掘。就像我上面说的,运行mine.py将只进行挖掘,但我们需要在Flask节点下面的后台运行挖掘任务。为此,我们加载BackgroundScheduler,告诉导入的挖掘任务我们使用的是BackgroundScheduler调度器,而不是那个文件中的调度器,然后像以前一样添加作业和侦听器。

当我们运行这个代码时,我们会看到输出是一样的,我们有一个关于正在运行的作业的日志记录,并且能打开blockchain.json,刷新它,就能看到新的节点出来了。

#node.py
.....
import mine
.....
from apscheduler.schedulers.background import BackgroundScheduler
sched = BackgroundScheduler(standalone=True)
.....

if __name__ == '__main__':

.....

  mine.sched = sched #to override the BlockingScheduler in this case, sched is the BackgroundSchedule
  sched.add_job(mine.mine_for_block, kwargs={'rounds':STANDARD_ROUNDS, 'start_nonce':0}, id='mine_for_block') #add the block again
  sched.add_listener(mine.mine_for_block_listener, apscheduler.events.EVENT_JOB_EXECUTED)
  sched.start()

  node.run(host='127.0.0.1', port=port)

命令行解析

中场休息时间!以前,为了查看我们希望节点运行的端口,我只检查是否传递了一个参数,如果传递了,那就是端口。

if __name__ == '__main__':
  if len(sys.argv) >= 2:
    port = sys.argv[1]
  else:
    port = 5000

很简单,但太简单了。现在,由于我让节点也可以进行挖掘,所以我希望能够指定节点是否应该进行挖掘。

打开argparse。它非常好,只需要几行就可以定义我要寻找的参数。我们希望能够指出默认运行到5000的端口,并且也能够指明是否需要挖掘。

if __name__ == '__main__':
  #args!
  parser = argparse.ArgumentParser(description='JBC Node')
  parser.add_argument('--port', '-p', default='5000',
                 help='what port we will run the node on')
  parser.add_argument('--mine', '-m', dest='mine', action='store_true')
  args = parser.parse_args()

  #only mine if we want to
  if args.mine:
    mine.sched = sched #to override the BlockingScheduler in the
    #in this case, sched is the background sched
    sched.add_job(mine.mine_for_block, kwargs={'rounds':STANDARD_ROUNDS, 'start_nonce':0}, id='mining') #add the block again
    sched.add_listener(mine.mine_for_block_listener, apscheduler.events.EVENT_JOB_EXECUTED)#, args=sched)
  
  sched.start() #want this to start so we can validate on the schedule and not rely on Flask

  #now we know what port to use
  node.run(host='127.0.0.1', port=args.port)

例如,python node.py -m 将会在5000端口上运行节点和挖掘。python node.py -p 5001 将会再5001端口上运行节点和挖掘。python --port=5002 -m将会在5000端口上运行节点和挖掘。你懂了吧。

好的,回到挖掘。

监听节点

为了广播一个节点,我们需要一个Flask后端来接受block的dicts。我们不希望用节点运行验证工作——我们希望将此工作放入计划中。

首先,验证将检查它是否是一个有效的块,如果是,则保存它。

#node.py
@node.route('/mined', methods=['POST'])
def mined():
  possible_block_dict = request.get_json()
  sched.add_job(mine.validate_possible_block, args=[possible_block_dict], id='validate_possible_block') #add the block again
  return jsonify(received=True)

#mine.py

def validate_possible_block(possible_block_dict):
  possible_block = Block(possible_block_dict)
  if possible_block.is_valid():
    possible_block.self_save()
    return True
return False

为了进行测试,我们在一个端口上运行一个节点来进行挖掘,另一个节点在那里等待广播。我们观察非挖掘节点的chaindir,并将看到由其对等节点挖掘的节点出现。

一方面,与不同的区块链相比,我们使用Flask和http进行通信的方式非常简单。看看以太坊如何让节点互相通信。我想提一下,这样读者就不会认为所有区块链都会在http上来回传递简单的json信息。还有更好的方法。

竞争挖掘节点

到最后了!这篇文章的重点是拥有多个节点,它们都在挖掘,第一个节点得到一个有效的块广播给其他节点,它们都需要接收块,并开始挖掘下一个节点。

还记得上面我说过我们想要validate_possible_block作为一个作业吗?这是因为我们希望在没有挖掘作业运行时检查验证。如果块是有效的,我们希望删除调度队列中的挖掘作业。由于侦听器将nonce范围递增的下一个挖掘作业插入到队列中,因此我们删除该作业,然后在新的有效块之后插入一个挖掘区块作业,然后从那里开始。

def validate_possible_block(possible_block_dict):
  possible_block = Block(possible_block_dict)
  if possible_block.is_valid():
    #this means someone else won
    possible_block.self_save()
    #we want to kill and restart the mining block so it knows it lost
    try:
      sched.remove_job('mining') 
      print "removed running mine job in validating possible block"
    except apscheduler.jobstores.base.JobLookupError:
      print "mining job didn't exist when validating possible block"
    print "readding mine for block validating_possible_block"
    sched.add_job(mine_for_block, kwargs={'rounds':STANDARD_ROUNDS, 'start_nonce':0}, id='mining') #add the block again

    return True
  return False

当运行代码并查看节点时,要查看哪个节点获得了该块的挖掘并不简单。本文不讨论块中的数据,但是我们需要一个简单的方法来告诉世界哪个节点赢了。

node.py中,我们要写一个data.txt文件,其中包含由该端口上的节点挖掘的块的数据。然后在utils.py中,我们创建一个未挖掘的、无效的块,我们读取数据文件并将其输入到头文件中。

#node.py

if __name__ == '__main__':
  .....
  filename = '%sdata.txt' % (CHAINDATA_DIR)
  with open(filename, 'w') as data_file:
    data_file.write("Mined by node on port %s" % args.port)
  .....

#utils.py

def create_new_block_from_prev(prev_block=None):
  if not prev_block:
  #index zero and arbitrary previous hash
    index = 0
    prev_hash = ''
  else:
    index = int(prev_block.index) + 1
    prev_hash = prev_block.hash

  filename = '%sdata.txt' % (CHAINDATA_DIR)
  with open(filename, 'r') as data_file:
    data = data_file.read()
  nonce = 0
  timestamp = datetime.datetime.utcnow().strftime('%Y%m%d%H%M%S%f')
  block_info_dict = dict_from_block_attributes(index=index, timestamp=timestamp, data=data, prev_hash=prev_hash, nonce=nonce)
  print block_info_dict
  new_block = block.Block(block_info_dict)
  return new_block

另外,当我们试图在区块链上保存实际数据时,拥有一个包含数据信息的文件似乎是一件好事。嗯…。

当你启动这两个节点时,$python node.py -m以及在5001硬链接目录中执行$python node.py -p 5001 -m。我们可以观察所有的日志记录,等待节点被写到每个chaindata目录,然后看到新的节点同时出现,以及不同的节点获胜。

用四个节点竞争的例子运行这个程序,坐在那里观看chaindir并查看哪个节点挖掘了出现的下一个块是非常棒的事。观看你编写的代码运行起来是编程中最好的感觉之一。记住这一点。

下面是我在端口5000和5001上使用节点运行挖掘时的几个屏幕截图。5000端口赢得了9个块,5001端口赢得了7个。

image

5000端口

image

5001端口

结论

看看这个,我们有能力让一组分布式节点相互竞争,尝试挖掘块,为挖掘获得酬劳,并能够在其他块想要运行和挖掘区块链时与它们同步。这条线很长。但还有更多的工作要做。

每个块中唯一的数据是哪个节点赢得了挖掘。区块链的概念是分布式数据存储(如果您听说过术语“账本”)。我们需要更多的数据。区块链只会继续使用广播的第一个节点。如果两个节点同时找到一个有效的节点会怎样?我们如何确定要使用哪个块?沿着这些思路,我们如何确保链的起始块永远不变?

我认为本系列的下一个主要部分是处理block header。现在它把字符串连在一起然后计算哈希值。如果你看看比特币和Ethereum区块链,它们有更复杂的头信息,这样矿工们就能准确地知道哪些区块是有效的,无论它们在哪里计算。另一个问题是存储散列所需的难度。现在它在配置中。

这将是下一个。

如果你还在阅读,请随时与我联系,向我提问或问好。我喜欢和人们交流。 Twitter, contact, Github. 下次再见。


对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。