【主要内容】
今天继续分析从github上获取的开源代码怎么实现简单区块链的入门知识,共用时间29分钟。
(此外整理作笔记花费了约60分钟)
详细学习过程见文末学习过程屏幕录像。
今天主要继续分析上前天断点调试后得到的变量中间值,进而反推执行过程,以注释之前还不理解 的代码部分,今天算是从表层理解了私钥签名之后,然后使用公钥进行验证签名的代码实现过程。
最后约八分钟开始搜索准备接下来的eth智能合约的开发学习的资料。
【学习笔记】
一、尝试理解在矿工所用的前端页面进行发送者的私钥签名的验签算法部分:
源代码如下:
·```
def verify_transaction_signature(self, sender_address, signature, transaction):
"""
矿工检查发起一次交易广播的发送者提供的私钥签名是否与它自己的公钥(sender_address)签名的交易相对应。
"""
ls=binascii.unhexlify(sender_address)
‘‘‘
sender_address在处理之前的值是:
30819f300d06092a864886f70d010101050003818d0030818902818100ba63d702fc7e02c385734cb3846e5d8d7319ab3937e3b5610d24d093b0d174e4e5de21fe9a15fb6167772004751b13755ec66cf484ad07cb12f96f0ceaa762ba43fb054886add87e8ac6e6111733b3a1ae59914357ae4bf0ef819d30924f01d554c1700f653ea9511207b030e44f4d684b59fe5ce444788451ac4b48d41dddf30203010001
看得出来这是一个ASI编码或UNID编码的字符串。
通过binascii.unhexlify()方法转换为十六进制
结果为:
b‘0\x81\x9f0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x81\x8d\x000\x81\x89\x02\x81\x81\x00\xbac\xd7\x02\xfc~\x02\xc3\x85sL\xb3\x84n]\x8ds\x19\xab97\xe3\xb5a\r$\xd0\x93\xb0\xd1t\xe4\xe5\xde!\xfe\x9a\x15\xfbagw \x04u\x1b\x13u^\xc6l\xf4\x84\xad\x07\xcb\x12\xf9o\x0c\xea\xa7b\xbaC\xfb\x05H\x86\xad\xd8~\x8a\xc6\xe6\x11\x173\xb3\xa1\xaeY\x91CW\xaeK\xf0\xef\x81\x9d0\x92O\x01\xd5T\xc1p\x0fe>\xa9Q\x12\x07\xb00\xe4OMhKY\xfe\\\xe4Dx\x84Q\xacKH\xd4\x1d\xdd\xf3\x02\x03\x01\x00\x01‘
‘‘‘
public_key = RSA.importKey(ls) #获取发送方的公钥
‘‘‘
现在的Public_key的值是:
RsaKey(n=130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171, e=65537)
_e:Integer(65537)
_n:Integer(130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171)
e:65537
n:1308875088397447753638284892924929424444
‘‘‘
#binascii模块用于二进制与ASCII编码的相互转换,unhexlify方法的作用是:(https://www.cnblogs.com/lyhabc/p/7995254.html)
#unhexlify方法是将用其它编码表示的字符串,转换为用十六进制编码表示的byte字符串,这儿其实是发送方的公钥的十六进制byte字节。
#RSA.importKey方法导入标准格式编码的RSA密钥(公共或私有半密钥【什么叫私有半密钥,这是机器 翻译的。】)。
#---这篇博文似乎作了详尽说明:https://www.jianshu.com/p/6a39610122fa
#---------------------------------------------------------------
#使用发送方的公钥来验证发送方的签名的开始
verifier = PKCS1_v1_5.new(public_key) #--这儿使用了Crypto.Signature子库中的一种填充方法:PKCS1_v1_5(另有一种填充方法:PKCS1_OAEP)
#通过PKCS1_v1_5.new方法将发送者的公钥填充为(资料显示为签名或验签,此处是签名还是验签呢?)
‘‘‘
现在verifier对象的值的描述如下:
<Crypto.Signature.pkcs1_15.PKCS115_SigScheme object at 0x00000184BB443E48>
_key:RsaKey(n=130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171, e=65537)
_verify:<bound method PKCS115_SigScheme.verify of <Crypto.Signature.pkcs1_15.PKCS115_SigScheme object at 0x00000184BB443E48>>
verify:<bound method _pycrypto_verify of <
‘‘‘
h = SHA.new(str(transaction).encode(‘utf8‘)) #此处得到的h对象与客户端页添加签名信息的signature对象时处理的结果应当是一样的。
‘‘‘
#h = SHA.new(str(transaction).encode(‘utf8‘))的理解:
transaction是当前矿工要验证的一次交易的交易信息本身(是一个字典)
encode(‘utf8‘)表示将这个交易信息中的所有字符都转换为utf-8编码
SHA.new方法使用这个信息(是不是已经被转换成了一个符串?)来算出一个新的HASH值。
‘‘‘
ls2=binascii.unhexlify(signature)
#上一行将签名信息变成十六进制的byte字符串
‘‘‘
signature,处理前的值是:(这和客户端进行签名后得到的值一模一样。)
‘1d2358b76ba1eeeef12efefee609626e985880b76be7044ca0dc8ad5563408d330ddbb37d6d864184657f61bd8b336ff6ac87c1b602907c624fcebc8f2837cdf15c723d7dbbb102c5bbe61c822b9f2d8ec622743d6bac46d623ad8aa7f78cd55d22127312b4d758656ee86506e0fcdf13b4c2c7dda34e954efc18bd84f95ee06‘
处理后:
b‘\x1d#X\xb7k\xa1\xee\xee\xf1.\xfe\xfe\xe6\tbn\x98X\x80\xb7k\xe7\x04L\xa0\xdc\x8a\xd5V4\x08\xd30\xdd\xbb7\xd6\xd8d\x18FW\xf6\x1b\xd8\xb36\xffj\xc8|\x1b`)\x07\xc6$\xfc\xeb\xc8\xf2\x83|\xdf\x15\xc7#\xd7\xdb\xbb\x10,[\xbea\xc8"\xb9\xf2\xd8\xecb\‘C\xd6\xba\xc4mb:\xd8\xaa\x7fx\xcdU\xd2!\‘1+Mu\x86V\xee\x86Pn\x0f\xcd\xf1;L,}\xda4\xe9T\xef\xc1\x8b\xd8O\x95\xee\x06‘
‘‘‘
return verifier.verify(h,ls2 ) #验证发送方的签名 #误报错误,可以直接运行
‘‘‘
上一行,使用发送方的公钥处理后的对象verifier(对公钥作了什么处理,我还没有理解)
通过verify方法对信息内容得到的新hash值 h 与发送者的 私钥 签名 字节串 signature
进行 签名合法性验证,以验证交易信息transaction的发送方的确是公钥sender_address或public_key拥有者
#-----------------------------
对比客户端的签名过程与此处的检查签名合法性的过程,基本操作是完全一样的流程,但玄妙之处在于,这儿是通过publickey(公钥)来得到verifier对象对signature对象(它是通过私钥来得到的)进行合法性验证。
这就非对称式加密的核心所在。
‘‘‘
```
当然由于我缺少基本的密码学知识,所以这部分的深层次原理还没有完全理解。恳请高手指导。
先来复习一下昨天我所了解的发送者私钥签名的流程:
第一步:对发送者的【私钥】进行处理,这使用了RSA.importKey()方法,但我不知道具体做了什么。
第二步:通过发送者【私钥】得到一个signer对象(这个对象我也不甚了了),使用了
PKCS1_v1_5.new(private_key)方法进行填充。
第三步:把完整的交易信息先转换成字符串,然后使用SHA.new()方法生成一个新的hash字符串(我不确定是不是字符串,研究不够透彻)。
第四步:使用前面的signer对象的sign()方法来将新的hash字符串混合(这就是签名的过程?)
第五步:转换编码后,得到签名信息。
然后今天我发现,在前端矿工进行验签时,使用的发送方的公钥来进行验签,但流程居然十分相似——
第一步:对发送者的【公钥】进行处理,这使用了RSA.importKey()方法,但我不知道具体做了什么。
第二步:通过发送者【公钥】得到一个verifier对象(这个对象我也不甚了了),使用了
PKCS1_v1_5.new(public_key)方法进行填充。
第三步:把完整的交易信息先转换成字符串,然后使用SHA.new()方法生成一个新的hash字符串(我不确定是不是字符串,研究不够透彻)。这一步与签名算法部分完全一样!!
第四步:对私钥签名后的签名信息对象signature进行编码处理。
第五步:使用前面的verifier对象的verify ()方法来将新的hash字符串(第三步得到的)与signatrue对象进行对比检查(具体过程与算法实现看不懂!)来确定签名信息是否与发送者的【公钥】是对应的。
对于密码学算法的这一点,我是非常的惊奇的,这就是非对称加密的精华所在了吧!
二、对【blockchain.py】页面的补充注释:
【blockchain.py】客户端页面
```
‘‘‘
title : blockchain.py
description : A blockchain implemenation
author : Adil Moujahid
date_created : 20180212
date_modified : 20180309
version : 0.5
usage : python blockchain.py
python blockchain.py -p 5000
python blockchain.py --port 5000
python_version : 3.6.1
Comments : The blockchain implementation is mostly based on [1].
I made a few modifications to the original code in order to add RSA encryption to the transactions
based on [2], changed the proof of work algorithm, and added some Flask routes to interact with the
blockchain from the dashboards
References : [1] https://github.com/dvf/blockchain/blob/master/blockchain.py
[2] https://github.com/julienr/ipynb_playground/blob/master/bitcoin/dumbcoin/dumbcoin.ipynb
‘‘‘
from collections import OrderedDict
import binascii
import Crypto
import Crypto.Random
from Crypto.Hash import SHA
from Crypto.PublicKey import RSA
‘‘‘
#关于RSA模块的详细说明博文:https://www.jianshu.com/p/7d79562717b3
(下面是这篇博文的开头部分:)
Crypto 算法库在 python 中最初叫 pycrypto,这个作者有点懒,好几年没有更新,后来就有大佬写了个替代库 pycryptodome。这个库目前只支持 python3,安装也很简单pip install就行了!
详细的用法可以看看 官方文档(https://www.pycryptodome.org/en/latest/src/api.html)
常见对称密码在 Crypto.Cipher 库下,主要有:DES 3DES AES RC4 Salsa20
非对称密码在 Crypto.PublicKey 库下,主要有:RSA ECC DSA
哈希密码在 Crypto.Hash 库下,常用的有:MD5 SHA-1 SHA-128 SHA-256
随机数在 Crypto.Random 库下
实用小工具在 Crypto.Util 库下
数字签名在 Crypto.Signature 库下
#----------------------
这篇博文对加密-解密,签名-验签的过程讲得很详细
https://www.cnblogs.com/pcheng/p/9629621.html
#注意点:
RSA加密对明文的长度有所限制,规定需加密的明文最大长度=密钥长度-11(单位是字节,即byte),
所以在加密和解密的过程中需要分块进行。而密钥默认是1024位,即1024位/8位-11=128-11=117字节。
所以默认加密前的明文最大长度117字节,解密密文最大长度为128字。那么为啥两者相差11字节呢?
是因为RSA加密使用到了填充模式(padding),即内容不足117字节时会自动填满,
用到填充模式自然会占用一定的字节,而且这部分字节也是参与加密的。
‘‘‘
from Crypto.Signature import PKCS1_v1_5
import hashlib
import json
from time import time
from urllib.parse import urlparse
from uuid import uuid4
import requests
from flask import Flask, jsonify, request, render_template
from flask_cors import CORS
MINING_SENDER = "THE BLOCKCHAIN" #矿工的挖矿(即成功创建一个区块)奖励的交易发出方
MINING_REWARD = 1
MINING_DIFFICULTY = 2 #表示工作量证明算力要求中的——得到的这个十六进制表示的字符串的前MINING_DIFFICULTY位([0:MINING_DIFFICULTY]切片)应当是全是‘0‘字符这个条件要求。
class Blockchain:
def __init__(self):
self.transactions = [] #此列表用于记录目前在区块链网络中已经经矿工确认合法的交易信息,等待写入新区块中的交易信息。
self.chain = [] #此列表表示区块链对象本身。
self.nodes = set() #建立一个无序元素集合。此集合用于存储区块链网络中已发现的所有节点信息
#Generate random number to be used as node_id
self.node_id = str(uuid4()).replace(‘-‘, ‘‘) #此测试区块链网络中,此节点的Id标识 。
#在这个类初始化的方法中,就创建了【创世区块】,且成为本区块链网络的第一个加入的区块(下一行代码完成)
self.create_block(0, ‘00‘) #创世区块诞生
#--添加一个区块链网络中新发现的一个节点到已知节点集合中。(添加的节点的信息是节点的url信息中的核心部分,即纯域名部分或相对路径)
def register_node(self, node_url):
"""
添加一个区块链网络中新发现的一个节点到已知节点集合中。
"""
#Checking node_url has valid format
#检查节点的格式,通过urlparse方法将这个节点的url分割成六个部分
#--下面是对urlparse的研究结论:
‘‘‘
#来自模块:from urllib.parse import urlparse
urlparse方法的效果
将给定的url分解为以下五部分:
[0]:‘scheme‘
[1]:‘netloc‘
[2]:‘path‘
[3]:‘query‘
[4]:‘fragment‘
一、
http://www.baidu.com/m/
分解信息与返回的五个部分:
fragment:‘‘
hostname:‘www.baidu.com‘
netloc:‘www.baidu.com‘
password:None
path:‘/m/‘
port:None
query:‘a=21&b=23‘
scheme:‘http‘
username:None
[0]:‘http‘
[1]:‘www.baidu.com‘
[2]:‘/m/‘
[3]:‘a=21&b=23‘
[4]:‘‘
二、
http:8080//www.baidu.com/m/
分解信息与返回的五个部分:
fragment:‘‘
hostname:None
netloc:‘‘
password:None
path:‘8080//www.baidu.com/m/‘
port:None
query:‘a=21&b=23‘
scheme:‘http‘
username:None
[0]:‘http‘
[1]:‘‘
[2]:‘8080//www.baidu.com/m/‘
[3]:‘a=21&b=23‘
[4]:‘‘
三、
ftp://username:pass@www.baidu.com/m/
分解信息与返回的五个部分:
fragment:‘‘
hostname:‘www.baidu.com‘
netloc:‘username:pass@www.baidu.com‘
password:‘pass‘
path:‘/m/‘
port:None
query:‘a=21&b=23‘
scheme:‘ftp‘
username:‘username‘
[0]:‘ftp‘
[1]:‘username:pass@www.baidu.com‘
[2]:‘/m/‘
[3]:‘a=21&b=23‘
[4]:‘‘
‘‘‘
parsed_url = urlparse(node_url)
if parsed_url.netloc: #如果网络地址不为空,那么就添加没有http://之类修饰的纯的地址,如:www.baidu.com
self.nodes.add(parsed_url.netloc)
elif parsed_url.path: #如果网络地址为空,那么就添加相对Url的路径
# Accepts an URL without scheme like ‘192.168.0.5:5000‘.
self.nodes.add(parsed_url.path)
else:
raise ValueError(‘Invalid URL‘) #说明这是一个非标准的Url
#--矿工检查发起一次交易广播的发送者提供的私钥签名是否与它自己的公钥(sender_address)签名的交易相对应。
def verify_transaction_signature(self, sender_address, signature, transaction):
"""
矿工检查发起一次交易广播的发送者提供的私钥签名是否与它自己的公钥(sender_address)签名的交易相对应。
"""
ls=binascii.unhexlify(sender_address)
‘‘‘
sender_address在处理之前的值是:
30819f300d06092a864886f70d010101050003818d0030818902818100ba63d702fc7e02c385734cb3846e5d8d7319ab3937e3b5610d24d093b0d174e4e5de21fe9a15fb6167772004751b13755ec66cf484ad07cb12f96f0ceaa762ba43fb054886add87e8ac6e6111733b3a1ae59914357ae4bf0ef819d30924f01d554c1700f653ea9511207b030e44f4d684b59fe5ce444788451ac4b48d41dddf30203010001
看得出来这是一个ASI编码或UNID编码的字符串。
通过binascii.unhexlify()方法转换为十六进制
结果为:
b‘0\x81\x9f0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x81\x8d\x000\x81\x89\x02\x81\x81\x00\xbac\xd7\x02\xfc~\x02\xc3\x85sL\xb3\x84n]\x8ds\x19\xab97\xe3\xb5a\r$\xd0\x93\xb0\xd1t\xe4\xe5\xde!\xfe\x9a\x15\xfbagw \x04u\x1b\x13u^\xc6l\xf4\x84\xad\x07\xcb\x12\xf9o\x0c\xea\xa7b\xbaC\xfb\x05H\x86\xad\xd8~\x8a\xc6\xe6\x11\x173\xb3\xa1\xaeY\x91CW\xaeK\xf0\xef\x81\x9d0\x92O\x01\xd5T\xc1p\x0fe>\xa9Q\x12\x07\xb00\xe4OMhKY\xfe\\\xe4Dx\x84Q\xacKH\xd4\x1d\xdd\xf3\x02\x03\x01\x00\x01‘
‘‘‘
public_key = RSA.importKey(ls) #获取发送方的公钥
‘‘‘
现在的Public_key的值是:
RsaKey(n=130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171, e=65537)
_e:Integer(65537)
_n:Integer(130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171)
e:65537
n:1308875088397447753638284892924929424444
‘‘‘
#binascii模块用于二进制与ASCII编码的相互转换,unhexlify方法的作用是:(https://www.cnblogs.com/lyhabc/p/7995254.html)
#unhexlify方法是将用其它编码表示的字符串,转换为用十六进制编码表示的byte字符串,这儿其实是发送方的公钥的十六进制byte字节。
#RSA.importKey方法导入标准格式编码的RSA密钥(公共或私有半密钥【什么叫私有半密钥,这是机器 翻译的。】)。
#---这篇博文似乎作了详尽说明:https://www.jianshu.com/p/6a39610122fa
#---------------------------------------------------------------
#使用发送方的公钥来验证发送方的签名的开始
verifier = PKCS1_v1_5.new(public_key) #--这儿使用了Crypto.Signature子库中的一种填充方法:PKCS1_v1_5(另有一种填充方法:PKCS1_OAEP)
#通过PKCS1_v1_5.new方法将发送者的公钥填充为(资料显示为签名或验签,此处是签名还是验签呢?)
‘‘‘
现在verifier对象的值的描述如下:
<Crypto.Signature.pkcs1_15.PKCS115_SigScheme object at 0x00000184BB443E48>
_key:RsaKey(n=130887508839744775363828489292492942444463434346971086658287181395248393849272716452901010865933107018951332471959936992676702744651630598299834435409087847044817115913438678900174924682299501670221020517043795850861688069799360443152949010476579188812260442594535889700621204848910086446699487189153307549171, e=65537)
_verify:<bound method PKCS115_SigScheme.verify of <Crypto.Signature.pkcs1_15.PKCS115_SigScheme object at 0x00000184BB443E48>>
verify:<bound method _pycrypto_verify of <
‘‘‘
h = SHA.new(str(transaction).encode(‘utf8‘)) #此处得到的h对象与客户端页添加签名信息的signature对象时处理的结果应当是一样的。
‘‘‘
#h = SHA.new(str(transaction).encode(‘utf8‘))的理解:
transaction是当前矿工要验证的一次交易的交易信息本身(是一个字典)
encode(‘utf8‘)表示将这个交易信息中的所有字符都转换为utf-8编码
SHA.new方法使用这个信息(是不是已经被转换成了一个符串?)来算出一个新的HASH值。
‘‘‘
ls2=binascii.unhexlify(signature)
#上一行将签名信息变成十六进制的byte字符串
‘‘‘
signature,处理前的值是:(这和客户端进行签名后得到的值一模一样。)
‘1d2358b76ba1eeeef12efefee609626e985880b76be7044ca0dc8ad5563408d330ddbb37d6d864184657f61bd8b336ff6ac87c1b602907c624fcebc8f2837cdf15c723d7dbbb102c5bbe61c822b9f2d8ec622743d6bac46d623ad8aa7f78cd55d22127312b4d758656ee86506e0fcdf13b4c2c7dda34e954efc18bd84f95ee06‘
处理后:
b‘\x1d#X\xb7k\xa1\xee\xee\xf1.\xfe\xfe\xe6\tbn\x98X\x80\xb7k\xe7\x04L\xa0\xdc\x8a\xd5V4\x08\xd30\xdd\xbb7\xd6\xd8d\x18FW\xf6\x1b\xd8\xb36\xffj\xc8|\x1b`)\x07\xc6$\xfc\xeb\xc8\xf2\x83|\xdf\x15\xc7#\xd7\xdb\xbb\x10,[\xbea\xc8"\xb9\xf2\xd8\xecb\‘C\xd6\xba\xc4mb:\xd8\xaa\x7fx\xcdU\xd2!\‘1+Mu\x86V\xee\x86Pn\x0f\xcd\xf1;L,}\xda4\xe9T\xef\xc1\x8b\xd8O\x95\xee\x06‘
‘‘‘
return verifier.verify(h,ls2 ) #验证发送方的签名 #误报错误,可以直接运行
‘‘‘
上一行,使用发送方的公钥处理后的对象verifier(对公钥作了什么处理,我还没有理解)
通过verify方法对信息内容得到的新hash值 h 与发送者的 私钥 签名 字节串 signature
进行 签名合法性验证,以验证交易信息transaction的发送方的确是公钥sender_address或public_key拥有者
#-----------------------------
对比客户端的签名过程与此处的检查签名合法性的过程,基本操作是完全一样的流程,但玄妙之处在于,这儿是通过publickey(公钥)来得到verifier对象对signature对象(它是通过私钥来得到的)进行合法性验证。
这就非对称式加密的核心所在。
‘‘‘
#----如果verify_transaction_signature方法已验证发起一次交易广播的发送者提供的私钥签名合法,则将此次交易添加到待完成交易列表中。(此交易将等待写入下一次新产生的一个区块中)
def submit_transaction(self, sender_address, recipient_address, value, signature):
"""
如果verify_transaction_signature方法已验证发起一次交易广播的发送者提供的私钥签名合法,则将此次交易添加到待完成交易列表中。(此交易将等待写入下一次新产生的一个区块中)
"""
#OrderedDict类来自于模块:collections
#OrderedDict用于对字典对象中的元素进行排序,OrderedDict会根据放入元素的先后顺序进行排序,也可以通过sort进行指定元素信息的排序
transaction = OrderedDict({‘sender_address‘: sender_address,
‘recipient_address‘: recipient_address,
‘value‘: value})
if sender_address == MINING_SENDER:
#如果当前交易内容是对矿工的挖矿奖励,那么——
#--将此交易信息添加到待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)中-----
self.transactions.append(transaction)
return len(self.chain) + 1
else:
#如果当前交易是节点到节点之间的转账交易 ,那么——
#--下一句代码验证交易的合法性,即通过交易发送方的交易发送方地址(这儿就是公钥)和发送方的私钥签名来验证。
transaction_verification = self.verify_transaction_signature(sender_address, signature, transaction)
#--将此交易信息添加到待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)中-----
if transaction_verification:
#---如果验证发送方发起的交易合法,那么将交易信息添加到交易 信息列表中——
self.transactions.append(transaction)
return len(self.chain) + 1
else:
return False
#--将已经写入交易信息的一个新区块添加到区块链的末尾,其中previous_hash指定了此区块之前的一个区块,因此就链接在其之后。
def create_block(self, nonce, previous_hash):
"""
将已经写入交易信息的一个新区块添加到区块链的末尾,其中previous_hash指定了此区块之前的一个区块,因此就链接在其之后。
"""
#在这个新的区块中,包含了以下信息:
#‘block_number‘:当前区块编号,这儿就是区块链的顺序号,即链上的第几块区块
#‘timestamp‘:生成此块(应当是将交易信息写入此块)的时间戳
#‘transactions‘:所有写入到当前区块的交易信息
#‘nonce‘:矿工通过算力证明(工作量证明)成功得到的Number Once值,证明其合法创建了一个区块(当前区块)
#‘previous_hash‘:在当前区块添加到区块链之前,区块链原来的最后一个区块的哈希值。(此值表明了当前区块的上一区块的位置,直到定位连接的作用)
block = {‘block_number‘: len(self.chain) + 1,
‘timestamp‘: time(),
‘transactions‘: self.transactions,
‘nonce‘: nonce,
‘previous_hash‘: previous_hash}
# Reset the current list of transactions
#因为已经将待处理(等待写入下一下新创建的区块中)交易信息列表(变量是:transactions)中的所有交易信息写入了区块并添加到区块链末尾,则此处清除此列表中的内容
self.transactions = []
#将当前区块添加到区块链末端
self.chain.append(block)
return block #返回此区块(此时此区块已添加在区块链中了)
def hash(self, block):
"""
Create a SHA-256 hash of a block
根据一个区块 来生成这个区块的哈希值(散列值)
"""
# We must make sure that the Dictionary is Ordered, or we‘ll have inconsistent hashes
#我们必须确保字典是有序的,否则我们会有不一致的哈希值,下一行代码中,sort_keys=True指明了要进行排序 。
block_string = json.dumps(block, sort_keys=True).encode()
‘‘‘
解析:json.dumps(block, sort_keys=True).encode()
首先通过json.dumps方法将一个区块打散,并进行排序(保证每一次对于同一个区块都是同样的排序)
这个时候区块被转换成了一个json字符串(不知道怎么描述)
然后,通过json字符串的encode()方法进行编码处理。
其中encode方法有两个可选形参,第一个是编码描述字符串,另一个是预定义错误信息
默认情况下,编码描述字符串参数就是:默认编码为 ‘utf-8‘。此处就是默认编码为‘utf-8‘
字符串编码常用类型有:utf-8,gb2312,cp936,gbk等。
‘‘‘
ls=hashlib.sha256(block_string)
ls2=ls.hexdigest()
return ls2
‘‘‘
解析hashlib.sha256(block_string).hexdigest()
hashlib.sha256(block_string) #来自模块:hashlib (用于加密相关的操作,代替了md5模块和sha模块)(参见此博文:https://www.cnblogs.com/wang-yc/p/5616663.html)
用sha256加密方法对block_string进行加密(其它加密算法还有:SHA1,SHA224,SHA256,SHA384,SHA512,MD5)
.hexdigest 哈希字符串的 【摘要算法】(因为哈希值的计算过程就是摘要计算过程)
(具体说明参见博文:https://www.cnblogs.com/yrxns/p/7727471.html)
摘要的返回结果有以下两种:
1.hash.digest()
返回摘要,作为二进制数据字符串值,二进制结果如:b单引号斜杠x0c斜杠xc1u斜杠xb9斜杠xc0斜杠xf1斜杠xb6斜杠xa81斜杠xc3斜杠x99斜杠xe2iw&a单引号
2.hash.hexdigest()
返回摘要,作为十六进制数据字符串值,十六进制结果如:0cc175b9c0f1b6a831c399e269772661
这里就把加密后的hash值作为十六进制字符串返回。
据我了解,多数区块链项目都使用的是十六进制。
‘‘‘
#此方法通过算法获取一个Number Once值,以通过工作量证明得到生成一个新区块的权限,返回这个Number Once值
def proof_of_work(self):
"""
Proof of work algorithm
此方法通过算法获取一个Number Once值,以通过工作量证明得到生成一个新区块的权限,返回这个Number Once值
"""
last_block = self.chain[-1] #取出区块链现在的最后一个区块
last_hash = self.hash(last_block) #取出这最后 一个区块的哈希值(散列值)
#下面通过循环来使Number Once的值从0开始每次增加1来进行尝试,直到得到一个符合算法要求 的Number Once值为止
nonce = 0
while self.valid_proof(self.transactions, last_hash, nonce) is False:
#如果得到的Number Once值不符合要求,那么就继续寻找。
nonce += 1
return nonce #返回这个符合算法要求的Number Once值。
#此函数是上一个方法函数的附属部分,用于检查哈希值是否满足挖掘条件。此函数用于工作函数的证明中。
def valid_proof(self, transactions, last_hash, nonce, difficulty=MINING_DIFFICULTY):
"""
检查哈希值是否满足挖掘条件。此函数用于工作函数的证明中。
"""
guess = (str(transactions)+str(last_hash)+str(nonce)).encode()
‘‘‘
上一行代码解析:
根据传入的参数nonce(就是要找的那个Number Once值)来进行尝试运算,得到一个转码为utf-8格式的字符串
‘‘‘
guess_hash = hashlib.sha256(guess).hexdigest()
‘‘‘
将此字符串(guess)进行sha256方式加密,并转换为十六进制的字符串
‘‘‘
return guess_hash[:difficulty] == ‘0‘*difficulty
‘‘‘
变量difficulty表示工作量证明算力要求中的——得到的这个十六进制表示的字符串的前difficulty位([0:difficulty]切片)应当是0这个条件要求。
‘0‘*difficulty 就是 difficulty个‘0‘组成的字符串。
guess_hash[:difficulty]就是取出 guess_hash 字符串中的前difficulty个字符,以检查这些字符是否都是‘0‘
如果符合要求,就返回True,否则 就返回False
‘‘‘
def valid_chain(self, chain):
"""
check if a bockchain is valid
检查bockchain是否有效,即检查是否每个区块都合法
"""
last_block = chain[0]
#上一行这里取得的是创世区块,意味着必须从头检查整个区块链上从创世区块到链上最后一个区块为止的所有区块的链接关系
#下面的while循环就是为了检查链上每一个区块与其连接的前一个区块是否合法相关,通过 检查 previous_hash 来判断
current_index = 1
while current_index < len(chain):
block = chain[current_index]
#print(last_block)
#print(block)
#print("\n-----------\n")
# Check that the hash of the block is correct
#检查块的哈希是否正确
if block[‘previous_hash‘] != self.hash(last_block):
#如果发现当前在检查的区块的previous_hash值与它实际连接的前一区块的hash值不同,则证明此链条有问题,终止检查
return False
# Check that the Proof of Work is correct
#检查工作证明是否正确
#Delete the reward transaction
#删除奖励交易,下一行代码中,切片时的[:-1]没有包含原列表中的最后一条交易信息,即矿工奖励 的交易信息。
transactions = block[‘transactions‘][:-1] #硬复制出当前区块中存储的除最后一条交易信息之外的其它全部交易信息列表,[:]才表示全部复制
# Need to make sure that the dictionary is ordered. Otherwise we‘ll get a different hash
#需要确保字典是有序的。否则我们会得到一个不同的哈希
transaction_elements = [‘sender_address‘, ‘recipient_address‘, ‘value‘]
transactions = [OrderedDict((k, transaction[k]) for k in transaction_elements) for transaction in transactions]
‘‘‘
(k, transaction[k]) for k in transaction_elements
我的理解是,就是按照 [‘sender_address‘, ‘recipient_address‘, ‘value‘]这个列表中指明 的元素顺序将所有交易信息列表强制排序。
‘‘‘
#重新检查当前块的工作量证明得到 的Number Once值的合法性
if not self.valid_proof(transactions, block[‘previous_hash‘], block[‘nonce‘], MINING_DIFFICULTY):
#如果重新检查发现这个Number Once值不合法,则证明这个链条是不正确的。
return False
last_block = block #让当前区块变成前一个区块,以迭代到一下次循环
current_index += 1 #让下一个区块变成当前区块的index计数
return True
def resolve_conflicts(self):
"""
Resolve conflicts between blockchain‘s nodes
by replacing our chain with the longest one in the network.
解决区块链节点之间的冲突
用网络中最长的链替换我们的链。
"""
neighbours = self.nodes
new_chain = None
# We‘re only looking for chains longer than ours
#将本节点所存储的区块链信息与其它节点存储的区块链信息进行对比,以找出 比本节点 链条还要 长的链条 ,是否 存在 。
max_length = len(self.chain) #本节点的存储的区块链条的长度(即有多少 个区块)
# Grab and verify the chains from all the nodes in our network
#获取所有已知区块链网络中的节点中存储的区块链条,并分析其是否比本节点的链条长度要长
for node in neighbours:
print(‘http://‘ + node + ‘/chain‘)
#到每个节点 的chain页面去获取此节点的区块链条信息,返回结果包含了一个chain对象本身 和 它的长度 信息,详细见本文件的建构chain页面的函数
response = requests.get(‘http://‘ + node + ‘/chain‘)
if response.status_code == 200:
length = response.json()[‘length‘] #通过json类把返回的对象取出来
chain = response.json()[‘chain‘]
# Check if the length is longer and the chain is valid
#下一行既检查了区块链的长度,也检查了区块链是否合法
if length > max_length and self.valid_chain(chain):
#如果此节点的区块链长度比本节点区块链长度长,且链条合法,则证明是值得覆盖本节点链条的合法链条
max_length = length
new_chain = chain
# Replace our chain if we discovered a new, valid chain longer than ours
if new_chain:
self.chain = new_chain #用找到的比本节点区块链链条长的链条覆盖本节点的旧链条,意为更新
return True
return False #如果没有发现别的节点上的链条比本节点的链条更长,那么 就返回 FALSE
# Instantiate the Node
# 通过flask模块的初始化方法,建立本节点的网络服务
app = Flask(__name__)
CORS(app)
# Instantiate the Blockchain
# 实例化前面建构的类,得到一个矿工使用的区块链节点实例对象
blockchain = Blockchain()
#通过flask模块来建构起本节点网络服务所需要的网页
@app.route(‘/‘)
def index():
# 首页,显示本节点 经过验证准备写入下一个新挖区块中的全部交易信息列表
# 也显示已经写入区块链中的交易信息列表(是否只显示本节点的已写入链条中的交易信息列表?)
return render_template(‘./index.html‘)
@app.route(‘/configure‘)
def configure():
#配置页面,用于设置添加新的此区块链网络中的其它节点
return render_template(‘./configure.html‘)
@app.route(‘/transactions/new‘, methods=[‘POST‘])
def new_transaction():
values = request.form
# Check that the required fields are in the POST‘ed data
#--------检查客户端传来的参数中是否包含了所有的字段---------
required = [‘sender_address‘, ‘recipient_address‘, ‘amount‘, ‘signature‘]
if not all(k in values for k in required):
#上一句在检查客户端post过来的参数是否包含了必要的内容。(即required变量中指定的那些内容一定要有)
return ‘Missing values‘, 400
# Create a new Transaction
#---------下一行代码检查 当前 要处理的 一次交易 信息的合法性,如果合法,就添加到待写入 交易信息的列表中---------------
transaction_result = blockchain.submit_transaction(values[‘sender_address‘], values[‘recipient_address‘], values[‘amount‘], values[‘signature‘])
#---------下一行代码根据检查 这次交易信息的结果 来作对应处理,如果交易 信息审核通过且已添加到了交易 信息列表中,那么就返回成功信息,如果没有审核通过,那么说明信息有问题,返回提示
if transaction_result == False:
response = {‘message‘: ‘Invalid Transaction!‘}
return jsonify(response), 406
else:
response = {‘message‘: ‘Transaction will be added to Block ‘+ str(transaction_result)}
return jsonify(response), 201
@app.route(‘/transactions/get‘, methods=[‘GET‘])
def get_transactions():
#Get transactions from transactions pool
#将所有准备写入新区块的交易信息列表的内容取出并返回
transactions = blockchain.transactions
response = {‘transactions‘: transactions}
return jsonify(response), 200
@app.route(‘/chain‘, methods=[‘GET‘])
def full_chain():
‘‘‘
chain页面将返回本节点存储的区块链条的完整信息和长度信息。
‘‘‘
response = {
‘chain‘: blockchain.chain,
‘length‘: len(blockchain.chain),
}
return jsonify(response), 200
@app.route(‘/mine‘, methods=[‘GET‘])
def mine():
# We run the proof of work algorithm to get the next proof...
#-----此页面通过本节点的工作量证明算法得到Number Once值,然后获得挖取一个区块的权限-------
last_block = blockchain.chain[-1] #---当前区块链中最长链的最后一个区块,blockchain指当前测试的区块链网络本身,是由类blockchain实例化而得到的对象。
nonce = blockchain.proof_of_work() #---取得了一个可以实现优先创建(挖出)下一个区块的工作量证明的 Number Once值。
# 由于当前去检查发现的发布广播的交易发起者的一次交易完成,且成功通过工作量算法证明,成功创建(挖出)了一个新区块
# 则此矿工将获得奖励,下面确认的等待写入新区块的交易信息,就是这个奖励交易(就是直接给此矿工一笔数字代币)的信息,详见下面方法的定义位置。
blockchain.submit_transaction(sender_address=MINING_SENDER, recipient_address=blockchain.node_id, value=MINING_REWARD, signature="")
#我理解为是区块链本身作为发送方, #此交易接收方正是当前节点本身就使用node_id,#此参数是奖励的数字代币金额,#最后一个参数是发起交易方的私钥签名,由于发起交易方是区块链本身,因此签名为空(个人理解 )
# Forge the new Block by adding it to the chain
previous_hash = blockchain.hash(last_block) #取出当前区块链中最长链的最后一个区块的Hash值,用作要新加入区块的前导HASH(用于连接)
block = blockchain.create_block(nonce, previous_hash) #将新区块(此区块包含了两条交易信息:一条是之前由交易发起者广播的交易 ,另一条是矿工的奖励交易)添加到区块链的最后。
response = {
‘message‘: "New Block Forged",
‘block_number‘: block[‘block_number‘],
‘transactions‘: block[‘transactions‘],
‘nonce‘: block[‘nonce‘],
‘previous_hash‘: block[‘previous_hash‘],
}
return jsonify(response), 200
@app.route(‘/nodes/register‘, methods=[‘POST‘])
def register_nodes():
#从浏览器客户端获取通过post传递过来的节点信息
#----此处是从/configure页面客户端来获取的-----------------
values = request.form
#----下一行代码的意思 是,因为/configure页面的客户界面中,要求多个节点使用,号来分割-----------
nodes = values.get(‘nodes‘).replace(" ", "").split(‘,‘)
#---------------------------------------------------------------------------------------
if nodes is None:
return "Error: Please supply a valid list of nodes", 400
#---------------------------------------------------------------------------------------
for node in nodes:
#通过blockchain实例的注册方法将节点添加到节点集合中去
blockchain.register_node(node)
response = {
‘message‘: ‘New nodes have been added‘,
‘total_nodes‘: [node for node in blockchain.nodes],
}
return jsonify(response), 201
@app.route(‘/nodes/resolve‘, methods=[‘GET‘])
def consensus():
#-------返回当前使用的链条的状态,即是本节点的链条 ,还是别的节点的链条 ,但返回的链条 仍然是整个网络中最长的合法的链条
#-------解决多个区块链网络节点间的节点冲突,更新为区块链网络中最长的那条链条-------------------
replaced = blockchain.resolve_conflicts()
#-------下面区分是使用的本节点的链条,还是已经从别的节点更新了新的链条--------
if replaced:
#----如果使用的本节点的链条,那么返回如下:
response = {
‘message‘: ‘Our chain was replaced‘,
‘new_chain‘: blockchain.chain
}
else:
#----如果更新自别的节点的链条 ,那么返回如下:
response = {
‘message‘: ‘Our chain is authoritative‘,
‘chain‘: blockchain.chain
}
return jsonify(response), 200
@app.route(‘/nodes/get‘, methods=[‘GET‘])
def get_nodes():
#------返回本节点已存储的节点列表
nodes = list(blockchain.nodes)
response = {‘nodes‘: nodes}
return jsonify(response), 200
if __name__ == ‘__main__‘:
from argparse import ArgumentParser
parser = ArgumentParser()#创建一个 参数 接收的解释器,由此对象(这里是:parser)来负责解释参数信息
#ArgumentParser()方法已默认添加有-h,--help参数的解释器。如果在调用当前Py文件时,出现了parser对象还无法解释的参数关键字,则也会调用-h参数来进行解释(这时一般会报错)
‘‘‘
对ArgumentParser()方法的详细说明:
ArgumentParser(prog=None, usage=None,description=None, epilog=None, parents=[],formatter_class=argparse.HelpFormatter, prefix_chars=‘-‘,fromfile_prefix_chars=None, argument_default=None,conflict_handler=‘error‘, add_help=True)
这些参数都有默认值,当调用parser.print_help()或者运行程序时由于参数不正确(此时python解释器其实也是调用了pring_help()方法)时,会打印这些描述信息,一般只需要传递description参数
‘‘‘
parser.add_argument(‘-p‘, ‘--port‘, default=5000, type=int, help=‘指定此web服务器供客户端访问要使用的商品号‘)
#如果add_argument()方法没有指定任何实参,则部分博文中没有这一句还是没有任何区别
‘‘‘
add_argument()方法详细说明:
add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest])
其中:
name or flags:命令行参数名或者选项(参数关键字及其可选项等),如上面的address或者-p,--port.其中命令行参数如果没给定,且没有设置defualt,则出错。但是如果是选项的话,则设置为None
name:一般情况下,不要使用-或--在前面,向这样的形参传递实参时,不用加上参数关键字,直接按定义的先后顺序,依次传入参数即可,顺序 参数之前用 空格 隔开如,添加了参数解释:address 在后面紧接着又添加了参数解释:port 这时给Py文件执行时传递实参时,只需要 写成: xxxx 127.0.0.1 80 即可
使用name方式指明的形参,在add_argument时不可指定 :default值,因为这不是可选项,是必须指明实参的参数,必须要有实参传入
flags(可选项参数):一般需要 连续使用 -参数的简称关键字 和 --参数的全称关键字,如要指定 port 的参数 ,使用flags方式的话,应当是: ‘-p‘,‘--port‘ ,这时给Py文件执行时传递实参时,需要 先指定这个 参数关键字,然后再间隔空格指明 实参,写法如: xxxx -p 80 或者 xxxx --port 80
此外,在调用 这个Py文件时,使用flags方式 的参数 写在前面给它指定实参 ,使用name方式 的参数 在最后按顺序指明实参
nargs:命令行参数的个数,一般使用通配符表示,其中,‘?‘表示只用一个,‘*‘表示0到多个,‘+‘表示至少一个
nargs:命令行参数的个数,一般使用通配符表示,其中,‘?‘表示只用一个,‘*‘表示0到多个,‘+‘表示至少一个
default:默认值
type:参数的类型,默认是字符串string类型,还有float、int等类型
help:和ArgumentParser方法中的参数作用相似,出现的场合也一致
‘‘‘
#如果要添加多个可解释的参数,则应当多次使用add_argument()方法
args = parser.parse_args() #解释器(在这里是:parser对象)现在通过parse_args()方法尝试对收到的参数关键字进行解释
#此时 args对象中包含了所有解释器获取到的,可以解释并理解的全部收到的 参数内容。
port = args.port #所以这里从args对象中取出其中的参数关键字--port 参数的内容,也可能是获取到预设的默认值
‘‘‘
只有使用 name 方式指明解释的形参,才可以使用args对象.name的方式得以调用 ,因为flags表示可选择项,调用 方法 ,目前不清楚
‘‘‘
app.run(host=‘127.0.0.1‘, port=port)
‘‘‘
run函数的定义如下 :
def run(self, host=‘localhost‘, port=5000, **options):
# ......
from werkzeug import run_simple
# ......
return run_simple(host, port, self, **options)
‘‘‘
```
三、准备eth智能合约开发学习的资料搜索:
因为之前某天的学习就了解到在ETH上开发智能合约是比较容易实现的,于是决定接下来就进行这方面的尝试。
搜索到了三篇不同的博文,使用的测试环境似乎不同:
https://www.jianshu.com/p/566878a895d4?utm_source=oschina-app
https://blog.csdn.net/mongo_node/article/details/85043799
感谢博主的无私分享。
今天已经了解到以下信息:
1.ETH平台上的核心编程使用的一种以前没有听说过的编程语言:solidity
这种编程语言目测像是c系出身的语言。
2.ETH智能合约开发是可以使用Python来完成的,有了比较成熟的第三方库,而且看上去解决方案不止一种。
3.ETH智能合约开发过程中是可以在虚拟的ETH测试网络上进行的,而不必要在真正的主网上进行,这样做还可以得到测试用的假的ETH代币。
4.ETH的测试网络有多种解决方案来实现,尚不清楚哪种方案更适合于我,不过上面第二篇博文使用的狐狸钱包,这个好像比较惹人喜欢点。(关于狐狸钱包后面我也将深入研究学习。)
【学习后记】
虽然我对密码学真的零基础,但之前对区块链的通用知识的学习中还是基本了解了它的私钥签名与验签实现的简单概念,这种非对称式的加密解密方法非常玄妙。但如果不是这几天亲自花了大量精力去研究代码实现,还是不会有完全深刻的理解,尽管我仍然不算是深入理解了具体的算法实现。但其精妙的结构仍然令我着迷,同样这也就更让我深切的相信区块链宣扬的去中心化的信任体系是完全牢固的,如果不是真正对技术底层的分析,这种坚定的信仰将无法产生。
之前的阅读中不断有有钱的大佬提到,真正有钱的人都是指使懂技术的人去干活的,这当然也是事实中的真理,不过,现在我也觉得,如果不到技术底层去看一看,光是站在顶层也是无法对事物有深刻理解的。
为了追赶未来,终身学习,终身进步,我创建了【就是要学 终身成长】社群,欢迎立志于终身学习,终身成长的朋友们加入,共同交流学习。Qq群号码:646854445
或访问:www.941xue.com
【关于坚持自学的例行说明】
最后例行说明下,我为什么要坚持自学。
“如果我不曾见过太阳,我本可以忍受黑暗,然而阳光已使我的荒凉,成为更新的荒凉。”
——艾米莉·狄金森
如果要问我对自己的前半生如何看待时,我想昨天和今天的答案都将完全不同。
昨天的我,生活在荒凉的满意之中,自觉怡然自得,拿着包身包月的工资,听着仁慈的命令,过着几乎一成不变的生活;时而与周遭的人儿和睦互往,时而唇舌相抵斤斤计较,演出着生活的鸡毛蒜皮,工作的吹拉弹唱;忘我,忘我,才能融入这平和无奇的乐章中,迈着细碎的步伐,原地踏步。那时的我觉得这就是悠然自得的听天由命的平凡人生,也就是我的宿命了。
可是某一天,我见到了不一样的太阳以及太阳下不一样的人生光景——那并不荒凉。
今天的我,生活在荒凉的痛苦之中,自觉渴望改变,迈着不知所措的步伐,看着流逝的年华,睁着悔恨错失一切的双眼… …
我知道我将再无法回到过去的我,只有改变才是唯一正确的方向。
一、为什么一把年纪还在学习
放弃很多去聚餐,去HI歌,去游玩,去看电影,去追剧……的时间,然后进行着这个年纪似乎已不应当再进行的学习,引来身边人们无尽的不解与鄙夷甚至可怜……
但我不想放弃终身学习的誓言。
因为——
我对我今天的生活现状并不认同!
罗伯特清崎告诉过我们,反省自己当下的生活是不是自己想要的,这难道不是最好的动力与答案?
走过了大半生,然后才发现曾经、当下所正在进行的人生并不是自己想要的,那是一种怎样的体验?
只有心中真切的感受才能回答这个问题,而任凭再丰富的语言也是无法描绘出来的。
经历半生的跋涉,却发现走得并不正确,有多少人有勇气承认自己过去的一切都是错误的呢?
而我愿意告诉过去的我:“你错了!”
那么已经历半生错误,年岁之大又压于头顶,还有希望从这架的梯子的半端重新爬下,再蹒跚着爬上另一架梯子吗?
我宁愿相信还有希望!
这便是我为什么要继续坚持终身学习下去的全部理由。
二、这个年纪还在学这些技术有意义吗
纯的技术对这把年纪其实已没有意义。
但兴趣可以超越意义。
但技术可以引来思想的变革,这才是意义。
投资自己的头脑 ,改革自己的思想,这是最保值,更长远的投资,过去我从来没有投资过,错过太多,那就从投资自己头脑开始吧。
罗伯特清崎告诉我们,真正的富有是时间的富有;真正的自由是可以决定自己愿意做什么的自由。
因为我愿意做我兴趣所在的事,所以我希望我有自由选择的那一天,虽然今天离那一天可能还是那么遥远,但我愿意相信,每天多赶几步,离希望就更近一步。
再者,虽然我可能再已无法完全完整的掌握这些技术了,但技术本身却可以启迪心的觉醒,激发灵感,那么只要多了解一点,我相信我将离那个正离我而去跑得越来越快的未来更近一点,不至于被未知的那个未来抛弃得太远。
于是我怎能放弃追逐求索的步伐?
我要坚信:感觉太迟的时候,也许还不算太迟。
感谢一直以来关注我,鼓励我的你!
若不嫌弃这一个到了高龄才长大的可笑可叹的我,请不吝赐教。
我的q号是:578652607,敬候你的指点。
为了追赶未来,终身学习,终身进步,我创建了【就是要学 终身成长】社群,欢迎立志于终身学习,终身成长的朋友们加入,共同交流学习。Qq群号码:646854445
或访问:www.941xue.com
【同步语音笔记】
https://www.ximalaya.com/keji/19103006/262055419
【学习过程屏幕录屏】
https://www.bilibili.com/video/av92826326/
原文:https://www.cnblogs.com/lhghroom/p/12398189.html