├── .vscode
└── settings.json
├── pic
├── tx.png
├── cover.png
├── gui.png
├── image.png
├── xhs1.jpg
├── xhs2.jpg
├── image2.png
├── image_3.png
├── image_4.png
├── cover.svg
├── hash_function_diagram.svg
├── merkle.svg
├── merkle_tree.svg
├── blockchain.svg
├── asymmetric_encryption.svg
├── blockchain_2.svg
└── digital_signature.svg
├── example_code
├── 2_hash.py
├── 0_simple_ecc.py
├── 3_address.py
└── 1_ecc.py
├── README.md
├── Tutor
├── 0_intro.md
├── 6_Electronic_Cash.md
├── 4_pow.md
├── 3_p2p_network.md
├── 5_whoami.md
├── 1_simple_blockchain.md
├── 2_Merkle_Tree.md
└── 7_Transaction.md
├── Transaction.py
├── Wallet.py
├── APP.py
├── MerkleTree.py
├── Block.py
├── BlockChain.py
└── Node.py
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "marscode.chatLanguage": "cn"
3 | }
--------------------------------------------------------------------------------
/pic/tx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/tx.png
--------------------------------------------------------------------------------
/pic/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/cover.png
--------------------------------------------------------------------------------
/pic/gui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/gui.png
--------------------------------------------------------------------------------
/pic/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/image.png
--------------------------------------------------------------------------------
/pic/xhs1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/xhs1.jpg
--------------------------------------------------------------------------------
/pic/xhs2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/xhs2.jpg
--------------------------------------------------------------------------------
/pic/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/image2.png
--------------------------------------------------------------------------------
/pic/image_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/image_3.png
--------------------------------------------------------------------------------
/pic/image_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koabula/MiniCoin/HEAD/pic/image_4.png
--------------------------------------------------------------------------------
/example_code/2_hash.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 |
3 | data = "hello blockchain"
4 |
5 | data_digest = hashlib.sha256()
6 | data_digest.update(data.encode())
7 |
8 | print(data_digest.hexdigest())
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MiniCoin
2 |
3 | 
4 |
5 | ## 什么是区块链
6 |
7 | 你或许听说过比特币,自从这个东西诞生以来,江湖上就一直有它的传说。有人因为它一夜暴富,跨越阶级;有人因为它耗尽家财,负债累累;也有的人同时有这两段经历,大起大落。有的国家将它作为法定货币,有的地区将它视为违法犯罪,更多时候它作为一种灰色产业出现。但是无论你见与不见,它一直在那里。
8 |
9 | 所以让我们来了解一下区块链这个东西吧
10 |
11 | ### 区块链为何而生
12 | 有人认为比特币的诞生就是一个骗局,最终这些数据的价值都会归零。但其实,比特币给出了一个现实问题的解决方案,而这个问题今天仍会困扰我们。那就是当我们网上购物时,如何信任一个在网线另一端的陌生人?在比特币诞生以前,还有现在的大多数时候,我们选择依靠一个可信任的第三方来处理电子支付,比如银行、微信、支付宝。但是,有人有不同的想法。
13 |
14 |
15 | 2008年,一个网名为中本聪(Satoshi Nakamoto)的人发布了一篇论文:***"Bitcoin: A Peer-to-Peer Electronic Cash System"*** ,在这里第一次提出了区块链这个概念。他提出,要建立一个"基于密码学原理而不是信任的电子支付系统"。
16 |
17 | 这里,我将通过几篇文章,向你一步步展示一个区块链是怎么运作的。
18 |
19 | 不需要你有很高深的数学知识,只要你会写helloworld就能看懂这几篇小文章。
20 |
21 | > 目录
22 |
23 | 0. [引子](./Tutor/0_intro.md)
24 | 1. [一个最简单的区块链](./Tutor/1_simple_blockchain.md)
25 | 2. [Merkle树](./Tutor/2_Merkle_Tree.md)
26 | 3. [P2P网络](./Tutor/3_p2p_network.md)
27 | 4. [工作量证明](./Tutor/4_pow.md)
28 | 5. [Who am I?](./Tutor/5_whoami.md)
29 | 6. [电子货币](./Tutor/6_Electronic_Cash.md)
30 | 7. [交易](./Tutor/7_Transaction.md)
--------------------------------------------------------------------------------
/pic/cover.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example_code/0_simple_ecc.py:
--------------------------------------------------------------------------------
1 | from Crypto.PublicKey import ECC
2 | from Crypto.Cipher import AES
3 | import os
4 |
5 | def encrypt(message: str, public_key: ECC.EccKey) -> tuple:
6 | """
7 | 简单的ECC加密
8 | """
9 | # 生成一次性密钥对
10 | ephemeral_key = ECC.generate(curve='P-256')
11 |
12 | # 计算共享密钥点
13 | shared_point = ephemeral_key.d * public_key.pointQ
14 | # 使用x坐标作为AES密钥
15 | shared_key = shared_point.x.to_bytes(32, byteorder='big')
16 |
17 | # 使用AES加密消息
18 | cipher = AES.new(shared_key, AES.MODE_CBC)
19 | # 对消息进行填充
20 | padded_message = message.encode('utf-8')
21 | padded_message += b' ' * (16 - len(padded_message) % 16)
22 |
23 | # 返回临时公钥、IV和密文
24 | return (ephemeral_key.public_key(), cipher.iv, cipher.encrypt(padded_message))
25 |
26 | def decrypt(encrypted_data: tuple, private_key: ECC.EccKey) -> str:
27 | """
28 | 简单的ECC解密
29 | """
30 | ephemeral_public_key, iv, ciphertext = encrypted_data
31 |
32 | # 计算共享密钥点
33 | shared_point = private_key.d * ephemeral_public_key.pointQ
34 | # 使用x坐标作为AES密钥
35 | shared_key = shared_point.x.to_bytes(32, byteorder='big')
36 |
37 | # 使用AES解密
38 | cipher = AES.new(shared_key, AES.MODE_CBC, iv)
39 | decrypted = cipher.decrypt(ciphertext)
40 |
41 | # 去除填充并返回
42 | return decrypted.strip().decode('utf-8')
43 |
44 | def main():
45 | # 生成密钥对
46 | private_key = ECC.generate(curve='P-256')
47 | public_key = private_key.public_key()
48 |
49 | # 测试消息
50 | message = "hello ecc"
51 | print(f"原始消息: {message}")
52 |
53 | # 加密
54 | encrypted = encrypt(message, public_key)
55 | print(f"加密后的数据: {encrypted[2].hex()}")
56 |
57 | # 解密
58 | decrypted = decrypt(encrypted, private_key)
59 | print(f"解密后的消息: {decrypted}")
60 |
61 | if __name__ == "__main__":
62 | main()
--------------------------------------------------------------------------------
/pic/hash_function_diagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tutor/0_intro.md:
--------------------------------------------------------------------------------
1 | ## 引子
2 |
3 | 你或许听说过比特币,自从这个东西诞生以来,江湖上就一直有它的传说。有人因为它一夜暴富,跨越阶级;有人因为它耗尽家财,负债累累;也有的人同时有这两段经历,大起大落。有的国家将它作为法定货币,有的地区将它视为违法犯罪,更多时候它作为一种灰色产业出现。但是无论你见与不见,它一直在那里。
4 |
5 | 所以让我们来了解一下区块链这个东西吧
6 |
7 | ### 区块链为何而生
8 | 有人认为比特币的诞生就是一个骗局,最终这些数据的价值都会归零。但其实,比特币给出了一个现实问题的解决方案,而这个问题今天仍会困扰我们。那就是当我们网上购物时,如何信任一个在网线另一端的陌生人?在比特币诞生以前,还有现在的大多数时候,我们选择依靠一个可信任的第三方来处理电子支付,比如银行、微信、支付宝。但是,有人有不同的想法。
9 |
10 |
11 | 2008年,一个网名为中本聪(Satoshi Nakamoto)的人发布了一篇论文:***"Bitcoin: A Peer-to-Peer Electronic Cash System"*** ,在这里第一次提出了区块链这个概念。他提出,要建立一个"基于密码学原理而不是信任的电子支付系统"。
12 |
13 | 后面,我将通过几篇文章,向你一步步展示一个区块链是怎么运作的。
14 |
15 | ### 密码学
16 | 要想理解区块链,你最好有一些密码学知识。
17 |
18 |
19 | 生活中密码一词往往被视为登录网页的密码,事实上这个"密码"或许被叫做口令更为合适。今天我们这里讨论的密码指的是Cryptography,最初代表的意思是信息保密地保存和传输。
20 |
21 | 你可能听说过凯撒加密,凯撒在传递命令之前,将书信中的每一个字母替换成字母表中后几位的字母,比如文章中的a全部换成c,b换成d,而到了结尾将y换为a,z换成b。这样敌人截获了书信也不知道命令具体是什么。这就是最早的密码学。
22 |
23 | 这里我们简单介绍一些区块链中使用到两个最重要的密码学算法,哈希算法和非对称加密。
24 |
25 | ### 哈希算法(Hash)
26 | 你可能知道哈希表,那便是哈希算法的一个应用。哈希算法将一个任意长度的输入转化为一个固定长度的输出。显然这是一种不可逆的运算,我们不能通过哈希值还原出输入。
27 | 
28 |
29 | 一个简单的Hash:
30 |
31 | ```python
32 | def simple_hash(x):
33 | return x % 1024
34 | ```
35 |
36 | 这个函数将输入对1024取模,使得一个输入转化为了一个10位的输出。显然,对1024同模的数会产生同样的哈希值,这就是哈希碰撞
37 |
38 | 在区块链中,哈希算法作为一个数据摘要的实现,我们希望使用的哈希函数具有抗碰撞性,即没有一个高效的方法制造哈希碰撞。任何方法都和从0开始遍历的效率一样,这便实现了工作量证明。
39 |
40 | 我们可以使用python中的hashlib来调用一些哈希函数,比如说比特币使用的SHA-256
41 |
42 | ```python
43 | import hashlib
44 |
45 | data = "hello blockchain"
46 |
47 | data_digest = hashlib.sha256()
48 | data_digest.update(data.encode())
49 |
50 | print(data_digest.hexdigest())
51 |
52 | ```
53 |
54 | 这会输出data="hello blockchain"的哈希值"`cf55026ba78c889dbdaf0c32701cdb4d662f3d3ea4460110d3ed2edd0d753e72`
55 |
56 | 如果我们把data改为"hello blockchain!"的话,我们发现输出的哈希值变成了"`108be1bede687534d56a8229d4deabccfd9ee83358b15e3e95286915b8a4f648`"
57 | 我们只是增加了一个符号,哈希值便发生了巨大的改变
58 |
59 | ### 非对称加密
60 |
61 | 首先我们说说对称加密,这是指加密和解密的密钥相同的加密算法。比如说你和你的同桌上课传纸条,你们约定把每个字母向后偏移1个位置来加密,也就是说a->b,d->e,z->a,那么1就是加密和解密的密钥,这便是对称加密。
62 |
63 | 非对称加密是一种使用一对密钥的加密方式——公钥和私钥。公钥可以公开分享,而私钥需要保密。使用公钥加密的信息只能用对应的私钥解密,反之亦然。
64 |
65 | 比如说,你想通过你女神的闺蜜给女神送礼物,但是你不想让中间人知道你送的时什么,于是你从女朋友那里拿了一个锁 ( 公钥 ) ,用这把锁把礼物锁起来再送出去。女朋友拿到时,使用自己的钥匙 ( 私钥 ) 打开锁,就安全拿到了礼物。这便是非对称加密。
66 |
67 | 
68 |
69 | 除了用来加密传递消息,非对称加密还可以用来电子签名。
70 |
71 | 你现在写了一篇情书想让女神的闺蜜送给她。你可以通过使用女神的公钥加密使得闺蜜无法偷窥到你的小秘密。但是如果闺蜜觉得你不够信任她,于是把你的情书进行了篡改(虽然她也不知道篡改后会变成什么样子,但是满足了她的破坏欲)。或者更坏,她写了一篇小作文以你的名义给了女神。这样你的好事彻底泡汤了。
72 |
73 | 那么,我们便需要使用数字签名。在发出消息之前,我们将消息进行哈希,然后将哈希值使用我们的私钥进行加密。将消息和这个被加密的哈希值一起发给女神。女神收到消息后,将自己计算一次消息的哈希,然后使用你的公钥对传来的加密过的哈希进行解密,如果两个哈希一致,那么说明这真是你写的情书,而且内容没有被闺蜜修改。
74 |
75 | 
--------------------------------------------------------------------------------
/pic/merkle.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/example_code/3_address.py:
--------------------------------------------------------------------------------
1 | import os
2 | import ecdsa
3 | import hashlib
4 | import base58
5 |
6 | # 生成私钥
7 | def generate_private_key():
8 | return os.urandom(32)
9 |
10 | # 生成公钥
11 | def generate_public_key(private_key):
12 | sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
13 | vk = sk.verifying_key
14 | return b'\x04' + vk.to_string()
15 |
16 | # 生成比特币地址
17 | def generate_btc_address(public_key):
18 | # 1. SHA-256哈希
19 | sha256_bpk = hashlib.sha256(public_key).digest()
20 | # 2. RIPEMD-160哈希
21 | ripemd160_bpk = hashlib.new('ripemd160', sha256_bpk).digest()
22 | # 3. 添加网络字节
23 | network_byte = b'\x00' + ripemd160_bpk
24 | # 4. 双SHA-256哈希
25 | sha256_nbpk = hashlib.sha256(network_byte).digest()
26 | sha256_sha256_nbpk = hashlib.sha256(sha256_nbpk).digest()
27 | # 5. 取前4个字节作为校验和
28 | checksum = sha256_sha256_nbpk[:4]
29 | # 6. 拼接校验和
30 | binary_address = network_byte + checksum
31 | # 7. Base58编码
32 | address = base58.b58encode(binary_address)
33 | return address
34 |
35 | # 签名消息
36 | def sign_message(private_key, message):
37 | # 创建签名对象
38 | sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
39 | # 对消息进行签名
40 | # 首先对消息进行哈希处理,这是比特币签名的标准做法
41 | message_hash = hashlib.sha256(message.encode()).digest()
42 | signature = sk.sign(message_hash)
43 | return signature
44 |
45 | # 验证签名
46 | def verify_signature(public_key, message, signature):
47 | try:
48 | # 从完整公钥中移除前缀0x04
49 | public_key_bytes = public_key[1:] if public_key[0] == 0x04 else public_key
50 | # 创建验证密钥对象
51 | vk = ecdsa.VerifyingKey.from_string(public_key_bytes, curve=ecdsa.SECP256k1)
52 | # 对消息进行相同的哈希处理
53 | message_hash = hashlib.sha256(message.encode()).digest()
54 | # 验证签名
55 | return vk.verify(signature, message_hash)
56 | except:
57 | return False
58 |
59 | # 测试代码
60 | def main():
61 | # 生成密钥对和地址
62 | private_key = generate_private_key()
63 | public_key = generate_public_key(private_key)
64 | btc_address = generate_btc_address(public_key)
65 |
66 | print("私钥:", private_key.hex())
67 | print("公钥:", public_key.hex())
68 | print("比特币地址:", btc_address.decode())
69 |
70 | # 签名示例
71 | message = "Hello, Bitcoin!"
72 | print("\n原始消息:", message)
73 |
74 | # 签名
75 | signature = sign_message(private_key, message)
76 | print("签名:", signature.hex())
77 |
78 | # 验证签名
79 | is_valid = verify_signature(public_key, message, signature)
80 | print("签名验证结果:", is_valid)
81 |
82 | # 尝试验证被篡改的消息
83 | tampered_message = "Hello, Bitcoin!!"
84 | is_valid_tampered = verify_signature(public_key, tampered_message, signature)
85 | print("篡改消息后的验证结果:", is_valid_tampered)
86 |
87 | if __name__ == "__main__":
88 | main()
--------------------------------------------------------------------------------
/Tutor/6_Electronic_Cash.md:
--------------------------------------------------------------------------------
1 | ## Electronic Cash 电子货币
2 |
3 | 好的,上一节我们实现了一个能够在区块链中能识别我们的身份并进行数字签名的钱包。这里我们开始向这个钱包里塞点钱。那么,我们的电子货币应该是什么样的呢?这一节我们先不推进我们的代码,让我们好好构思一下如何实现一个电子货币体系。
4 |
5 | ---
6 |
7 | ### 货币是什么样的
8 |
9 | 我们的货币是什么样的?我们每天都在花钱,但是你花钱的时候有留意过你花的钱是什么样的吗。
10 |
11 | 今天我们大部分人都使用微信或者支付宝这样的电子支付,当你在手机上输入支付密码(现在花钱越来越方便了,你或许只需要刷一下你的脸甚至是你的掌纹,就可以把钱花掉),背后的一系列机构公司就将你的账户余额减去一定的数字,然后店家方账户余额增加一定的数字,这样就实现了交易。
12 | 你一定也用过现金,你从街上大爷那里拿一根糖葫芦,然后把一张五元的钞票给他,你们就快速实现了一次交易。
13 |
14 | 这两个交易你都花了钱,但是显然给你的感受是不一样的。首先,我们使用钞票时每一张钞票上都有独一无二的编号,然而线上支付时只有账户上数字的变化,这是最本质的区别。我们在买糖葫芦时,可以直接拿一张5元的纸币给大爷,但是如果我们只有一张10元纸币,那么大爷就需要拿出一张5元找零给我们。但是在线上支付时我们只需要在程序中填一个数字(比如3.14)就可以实现支付。
15 |
16 | 而对应到我们的加密货币中,这分别代表了我们的两个模型,一个是以比特币为代表的UTXO模型(Unspent Transaction Outputs),一个是以以太坊为代表的账户模型。
17 |
18 | 不过这两种模型只不过是实现方式的不同,归根结底无论是现金还是微信支付,最终实现的是一个状态转移,经过这个交易你的钱转移到了店家手里。所以无论是哪个模型,都是一个状态转移系统。
19 |
20 | ---
21 | ### UTXO模型
22 |
23 | 我们先看看中本聪在比特币白皮书中是怎么描述的:
24 |
25 | > We define an electronic coin as a chain of digital signatures. Each owner transfers the coin to the next by digitally signing a hash of the previous transaction and the public key of the next owner and adding these to the end of the coin. A payee can verify the signatures to verify the chain of ownership.
26 |
27 | 
28 |
29 |
30 | 中本聪将一枚电子货币定义为一条数字签名链。每笔交易有输入有输出,而新的交易将之前没有被花费的输出(UTXO)作为输入进行消费,这样将原本属于你的UTXO转移到了店家的地址下。我们看上图中间的那个交易,所有者1(花钱的)将之前的一笔交易和所有者2(收钱的)的公钥(或者地址)进行哈希,然后使用自己的私钥进行签名,这样就组成了一笔交易。然后将这个交易广播到区块链网络中达到共识,就实现了将钱转给所有者2的步骤。
31 |
32 | 同时我们也会发现,在这个模型下我们的账户余额不是直接储存在区块链里的,我们需要将链上所有的属于我们的UTXO加起来才构成了我们的余额。而我们的支付也不是钱包表现的那么简单,支付时需要在钱包那么多的UTXO中选出能够支付目标的组合,然后将这个组合作为输入。输出也不是只有指向收款方一个那么简单,我们在比特币链上的支付需要向打包的节点支付gas费,如果我们的UTXO组合比较大,还会产生一个输出返回我们的钱包作为找零。
33 |
34 | **双花攻击:** 对于这种现金模式的模型有一种攻击方式叫双花攻击。比如现实中我有一张百元大钞,这是一张真钱,于是我将这个钱复制一份,一张拿去吃饭,另一张拿去交学费,这就是双花。现实中我们选择提高复制现金的难度来解决这个问题,使得普通人无法实现复制这一步。但是在数字世界复制是一件再容易不过的事了,如果比特币是一个txt文件,我们可以简单地ctrl+c复制一份,一个拿去买房,一个拿去买车。
35 | 比如上面这个图片中,我作为所有者1,可以使用所有者2的公钥和我的上一笔交易产生的UTXO创建转账给所有者2的交易,但同时我也可以使用同样的UTXO和我的另外一个钱包作为接收方创建另外一个交易,同时把两笔交易提交,是不是在实现买东西的同时还没有损失我的钱?
36 |
37 | 这在比特币中是行不通的,因为其中一笔交易被节点接收后,你原本的UTXO就被花费了,在节点收到另外一个交易时交易的输入已经不是一个合法的UTXO了,那么交易就会失败。
38 |
39 | ---
40 |
41 | ### 账户模型
42 |
43 | 人们谈到以太坊可能以为它只是在比特币的基础上增加了更强大的智能合约,但其实在数字货币的实现上它们也使用了不同的方式。以太坊上使用账户的概念,一个以太坊帐户包含四个字段:
44 |
45 | - nonce,用于确保每笔交易只能处理一次的计数器
46 | - 帐户当前的以太币余额
47 | - 帐户的合约代码(若有)
48 | - 帐户的存储(默认为空)
49 |
50 | 账户分为由私钥控制的外部帐户以及由其合约代码控制的合约帐户,合约的事我们先搁下不表。也就是说一个账户维护一个nonce变量(这个名字取得不好,容易和我们前面区块pow中的随机数混淆)和当前的余额,每当进行一个交易时,nonce变量自增,同时余额进行相应修改。这样的实现在逻辑上仿佛更符合我们对虚拟货币的想象。
51 |
52 | **重放攻击:** 对于这种账户模式的转账也有一种攻击叫做重放攻击。想象你现在在便利店买水,你扫码给老板后打算离开,老板说刚刚没扫上,让你再来一次,这样你的钱就被花了两次。而一个人通过以太坊链转账给我,我是否能够将这个交易复制一遍重新广播给区块链,实现扣对面两次钱呢?
53 |
54 | 答案自然是不行,这正是以为以太坊中维护了一个nonce变量,每次交易后这个变量会加一,而交易时会同时加入当前账户的nonce值进行提交。如果我们重放这个交易,网络会因为这个nonce已经过期而拒绝再次执行这个交易。
55 |
56 | ---
57 |
58 | 今天整理了一下我们关于实现电子货币的思路,下一节我们将开始构建一个UTXO模型的电子货币体系,添加到我们的区块链中
59 |
60 | **插一嘴:白皮书**
61 | 比特币和以太坊的白皮书可以说是入门区块链的必读内容,在它们的官方网站上都可以查看,也有中文翻译版本(虽然存在一些错误)。同时它们的技术文档也是很好的阅读材料。
--------------------------------------------------------------------------------
/example_code/1_ecc.py:
--------------------------------------------------------------------------------
1 | from Crypto.PublicKey import ECC
2 | from Crypto.Cipher import AES
3 | from Crypto.Protocol.KDF import HKDF
4 | from Crypto.Hash import SHA256
5 | import os
6 |
7 | class Sender:
8 | def __init__(self):
9 | # 生成发送方的密钥对
10 | self.private_key = ECC.generate(curve='P-384')
11 | self.public_key = self.private_key.public_key()
12 |
13 | def encrypt_message(self, message: str, receiver_public_key: ECC.EccKey) -> dict:
14 | # 生成一次性密钥对
15 | ephemeral_key = ECC.generate(curve='P-384')
16 |
17 | # 计算共享密钥
18 | shared_point = self.private_key.d * receiver_public_key.pointQ
19 | shared_key = shared_point.x.to_bytes()
20 |
21 | # 生成salt并保存
22 | salt = os.urandom(16)
23 |
24 | # 使用HKDF导出AES密钥
25 | aes_key = HKDF(
26 | master=shared_key,
27 | key_len=32,
28 | salt=salt,
29 | hashmod=SHA256,
30 | context=b'encryption'
31 | )
32 |
33 | # 生成随机IV
34 | iv = os.urandom(16)
35 |
36 | # 使用AES-GCM加密
37 | cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv)
38 | ciphertext, tag = cipher.encrypt_and_digest(message.encode())
39 |
40 | return {
41 | 'ephemeral_public_key': ephemeral_key.public_key(),
42 | 'iv': iv,
43 | 'ciphertext': ciphertext,
44 | 'tag': tag,
45 | 'salt': salt
46 | }
47 |
48 | def get_public_key(self):
49 | return self.public_key
50 |
51 | class Receiver:
52 | def __init__(self):
53 | # 生成接收方的密钥对
54 | self.private_key = ECC.generate(curve='P-384')
55 | self.public_key = self.private_key.public_key()
56 |
57 | def decrypt_message(self, encrypted_data: dict, sender_public_key: ECC.EccKey) -> str:
58 | # 计算共享密钥
59 | shared_point = self.private_key.d * sender_public_key.pointQ
60 | shared_key = shared_point.x.to_bytes()
61 |
62 | # 使用发送方提供的salt
63 | aes_key = HKDF(
64 | master=shared_key,
65 | key_len=32,
66 | salt=encrypted_data['salt'],
67 | hashmod=SHA256,
68 | context=b'encryption'
69 | )
70 |
71 | # 使用AES-GCM解密
72 | cipher = AES.new(aes_key, AES.MODE_GCM, nonce=encrypted_data['iv'])
73 | plaintext = cipher.decrypt_and_verify(
74 | encrypted_data['ciphertext'],
75 | encrypted_data['tag']
76 | )
77 |
78 | return plaintext.decode()
79 |
80 | def get_public_key(self):
81 | return self.public_key
82 |
83 | def main():
84 | # 创建发送方和接收方
85 | sender = Sender()
86 | receiver = Receiver()
87 |
88 | # 要发送的消息
89 | message = "hello ecc"
90 | print(f"原始消息: {message}")
91 |
92 | try:
93 | # 发送方加密消息
94 | encrypted_data = sender.encrypt_message(
95 | message,
96 | receiver.get_public_key()
97 | )
98 | print(f"加密后的数据: {encrypted_data['ciphertext'].hex()}")
99 |
100 | # 接收方解密消息
101 | decrypted_message = receiver.decrypt_message(
102 | encrypted_data,
103 | sender.get_public_key()
104 | )
105 | print(f"解密后的消息: {decrypted_message}")
106 |
107 | except Exception as e:
108 | print(f"错误: {str(e)}")
109 |
110 | if __name__ == "__main__":
111 | main()
--------------------------------------------------------------------------------
/pic/merkle_tree.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Tutor/4_pow.md:
--------------------------------------------------------------------------------
1 | ## 工作量证明
2 |
3 | 前面我们搭建了一个能够相互传输数据的p2p网络,现在我们希望使用这个网络维护一个区块链。我们现在应该让每个节点产生数据(比特币中的交易)后,发送给其他节点,然后一群数据打包为一个区块,将这个区块发送到网络中让大家都接受它,最后所有节点都将这个区块放入本地的区块链。
4 |
5 | 但是,由谁来打包这个区块呢?为什么要接收节点A发送的区块而不是B的呢?这有很多种共识算法,我们下面介绍一下工作量证明(Proof Of Work),有时候人们也叫这个过程挖矿。
6 |
7 | ### 工作量证明(POW)
8 |
9 | 工作量证明的核心思想是通过计算一个复杂的数学问题,来证明某节点在区块链网络中进行了足够的工作,从而获得记账权(打包区块发给其他节点的)。这样说还是有些抽象,我们看一下工作量证明的基本流程:
10 |
11 | 1. 计算一个难题:网络中的节点需要解决一个复杂的数学难题,这个难题一般没有什么优化的解法,必须投入一定的算力才能解决,但是一旦有了答案便可以简单地验证
12 | 2. 竞争记账权:每个节点都在竞争解决这个问题,第一个解决的节点获取记账权
13 | 3. 打包和广播:获取记账权的节点将区块打包,广播给其他节点
14 | 4. 验证区块:收到区块的节点验证这个难题的解,以及这个区块的有效性
15 | 5. 奖励:成功打包区块的节点会获得一定的奖励(挖矿奖励)
16 |
17 | 我们回顾一下我们的区块,每个区块会由SHA256算法生成一个256位的哈希值。我们的工作量证明算法选择在每个区块中加入一个随机数(nonce)共同计算哈希值,寻找一个nonce使得区块的哈希值前n位都是0(即哈希值小于某个数)。由于哈希算法由很强的随机性,求一个符合条件的nonce的任何算法都和从0开始遍历效率是一样的。所以这是一个依赖于算力解决的问题。
18 |
19 | 如果某个节点想要攻击某个区块,那么它必须重做这个工作量证明。而且改变了一个区块,后续的区块都需要改变,也就是说它需要产生一条新的分叉。在网络中节点选择承认最长的一条链,所以需要这条恶意分叉长度超过诚实分叉时攻击才成功。我们可以看到,工作量证明实际是一种基于算力投票的机制,最长的链代表了有最多的算力投入这条链。如果区块链中多数的算力被诚实节点掌握,那么诚实的链就会成为最长的一条链被承认。而如果某个组织的算力占据了链上算力51%以上,那么便产生安全风险(51%攻击)
20 |
21 | 下面我们在我们的区块链上实现pow:
22 |
23 | 1. 首先我们需要修改`block`类,在区块头中加上nonce
24 | ```py
25 | class Block:
26 | def __init__(
27 | self, index, merkle_tree: MerkleTree, previous_hash, nonce, timestamp=None
28 | ):
29 | self.index = index
30 | self.timestamp = time.time() if timestamp is None else timestamp # 当前时间戳
31 | self.merkle_tree = merkle_tree
32 | self.merkle_root = merkle_tree.get_root_hash()
33 | self.previous_hash = previous_hash
34 | self.nonce = nonce
35 | self.hash = self.calculate_hash()
36 | ```
37 |
38 | 2. 我们需要修改`blockchain`类,添加difficulty变量,也就是区块的哈希值前多少位需要为0。
39 | 需要注意的是,在这里我们将difficulty直接写定为5,但实际上比特币中这个值是可变的,比特币使用移动平均数法来确定每小时生成区块的平均数,如果区块生成太快就加大difficulty。这样可以适应算力的不断增长。
40 | 这里我们还做出了一个修改,在创建创世区块时将时间戳也固定下来,使得不同节点有一个相同的创世区块
41 |
42 | ```py
43 | class BlockChain:
44 | def __init__(self):
45 | self.chain = [self.create_genesis_block()]
46 | self.height = 1
47 | self.difficulty = 5
48 |
49 | def create_genesis_block(self):
50 | return Block(0, MerkleTree(["Genesis Block"]), "0", 0, timestamp=0)
51 | ```
52 |
53 | 3. 最大的修改是节点类,下面是"挖矿"方法,我们开始挖这个区块时将所有数据打包到MerkleTree中,然后清空数据队列。我们将数据队列第一个数据初始化为节点自己的地址(对应比特币中第一笔交易是产生新的比特币转给创建者)。然后从0开始尝试nonce,如果挖矿成功,将这个区块接入本地区块链同时广播给其他节点。(这里的发送代码详见原代码文件,需要实现区块的序列化和反序列化)如果在成功之前便收到其他节点传来的有效区块,则结束这个循环,开始挖下一个区块。
54 | ```py
55 | def mine(self):
56 | with self.data_lock:
57 | merkle_tree = MerkleTree(self.data_queue)
58 | self.data_queue = [f"Created by {self.address}",]
59 | nonce = 0
60 | timestamp = time.time()
61 | while not self.getBlock:
62 | block = Block(self.blockchain.height, merkle_tree, self.blockchain.get_latest_block().hash, nonce, timestamp)
63 | if self.blockchain.is_block_valid(block):
64 | with self.blockchain_lock:
65 | self.send_block(block)
66 | self.blockchain.append_block(block)
67 | print(f"Mined block {block.index} :{block.hash}")
68 | break
69 | nonce += 1
70 |
71 | def mine_thread(self):
72 | while True:
73 | self.mine()
74 | with self.signal_lock:
75 | self.getBlock=False
76 | ```
77 |
78 | 4. 下面我们进行测试,通过终端输入信息广播给网络,然后画出前5个区块,查看效果如下,可以看到节点成功上传了数据并进行了挖矿
79 | 
80 |
81 | 
82 |
83 | > 为什么节点要打包数据?
84 |
85 | 我们可以发现,在区块链里,如果我们的节点在Merkle树中只储存第一个数据(也就是挖矿奖励),那么挖矿成功也可以将这个只有一个数据的区块加入区块链,那么为什么还要浪费带宽和算力来接收其他节点的数据呢?当然,如果所有节点都这样的话,那今天的比特币或许早就无法正常运作了。
86 |
87 | 这就要说到我们常说的gas费了,当你在web3钱包里转账时,你常常会发现你转账前后的金额有损失,这就是消费了gas费。当一个交易提交时,会向打包成功的节点支付一定比例的gas费作为激励,这样便使得节点愿意处理这些输入,毕竟有没有加这些数据对挖矿难度没有影响。
88 |
89 | 而我们常说比特币是"电子黄金",因为它的总量是不变的。这是由于每个区块的挖矿奖励会随着时间递减,每隔一段时间比特币会进行一次"减半",也就是挖矿奖励变成之前的一半,这样一直到挖矿奖励比最小不可分割单位(聪)还小时彻底没有新的比特币产生。之后的挖矿激励全部变成了支付的gas费。
--------------------------------------------------------------------------------
/Tutor/3_p2p_network.md:
--------------------------------------------------------------------------------
1 | ## P2P网络
2 |
3 | 我们前面谈到的区块链仿佛都运行在同一个设备上,但实际上区块链是由许多节点组成的分布式系统。每个节点都是网络的参与者,节点之间通过P2P进行通信和同步。
4 |
5 | 区块链节点的功能有:储存数据,验证交易,参与共识,网络通信等
6 | 而根据功能和权限的不同,区块链节点也有很多分类,不过在这里我们简化为两类:全节点(Full Node)和轻节点(Simple Node),全节点储存了整个区块,而轻节点只储存了每个区块的区块头
7 |
8 | 下面我们讨论一下节点的合作
9 |
10 | ### P2P网络
11 | P2P(Peer to Peer)网络是区块链的基础架构,它允许节点之间直接进行通信,而不需要中心服务器。每个节点既是服务器也是客户端,可以向其他节点请求数据,也可以为其他节点提供数据。
12 |
13 | 在我们的区块链中,我们需要节点之间交换数据,将数据打包成区块然后组成一条有共识的区块链。所以节点之间需要有网络通讯,当一个新节点加入网络时,它需要:
14 | 1. 连接到已知节点(种子节点)
15 | 2. 从这些节点获取其他节点的地址
16 | 3. 同步数据
17 |
18 | > 什么是**种子节点**?
19 |
20 | 一个新节点想要加入这个区块链网络,首先需要和其他节点建立连接,同步当前的区块链。那么这第一步便产生了问题,要到哪里去找其他节点?
21 | 于是,我们在这个网络中设置了一类叫做种子节点的节点,它们的地址被硬编码在节点代码里。当新节点起步时会首先连接这些种子节点,然后从它们那里获取其他节点的地址
22 |
23 | > 如何更新节点状态?
24 |
25 | 如果网络运行中一个节点离线了,或者新加入了一个节点,如何在其他节点中更新其状态呢?
26 | 我们可以使用心跳机制,让节点每隔一段时间向所有节点发送一条简短的信息(心跳)。如果接收到新节点的心跳就将其添加到节点列表中,如果一段时间后没有接收到某节点的心跳就视作该节点已经故障或离线
27 |
28 | 下面我们实现一个可以进行p2p通信的简单节点网络。
29 |
30 | 我们先思考一下我们p2p网络的结构应该怎么设计:(这只是一个简单的示例,现实中的实现不一定是这样的)
31 | 1. 节点建立时,向硬编码的种子节点发出加入网络的请求。种子节点将新节点加入列表,同时向所有节点介绍这个新节点
32 | 2. 节点收到一个介绍信息后,将其加入节点列表中,同时向该节点发送hello消息
33 | 3. 新节点接收到hello消息后,将发送方加入自己的节点列表中,这样便实现了节点列表的更新
34 | 4. 每隔一段时间节点会广播hello消息,接收到hello消息后节点要储存当前时间,如果长时间未接收到某节点的hello消息则在节点列表中删除
35 |
36 | 下面是形而下的实现
37 |
38 | 1. 我们首先建立节点类,初始化节点地址。其中,`peers`储存已知网络中的节点地址,初始化填入了种子节点;`hello_dict`储存上一次接收到其他节点hello消息的时间。使用`send_join`向种子节点发出加入网络的请求
39 |
40 | ```py
41 | class Node:
42 | def __init__(self, ip):
43 | self.address = ip
44 | self.peers = {"127.0.0.1"} # 种子节点
45 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
46 | self.socket.bind((ip, 5000)) # 监听5000端口
47 | self.socket.listen(5)
48 | self.hello_dict = {}
49 | while True:
50 | try:
51 | self.send_join() # 发送加入消息
52 | break
53 | except:
54 | time.sleep(1)
55 | self.handle_thread = threading.Thread(target=self.handle_connection) # 处理连接
56 | self.handle_thread.start()
57 | self.helloloop_thread = threading.Thread(target=self.helloloop) # hello消息检测
58 | self.helloloop_thread.start()
59 | self.mainloop_thread = threading.Thread(target=self.mainloop) # 主循环
60 | self.mainloop_thread.start()
61 | ```
62 |
63 | 2. 然后我们定义一系列消息传递方法,定义了一系列的通信方式。
64 |
65 | ```py
66 | def send_msg(self, msg):
67 | for peer in self.peers:
68 | if peer == self.address:
69 | continue
70 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
71 | try:
72 | s.connect((peer, 5000))
73 | s.sendall(msg.encode("utf-8"))
74 | except:
75 | print(f"Failed to send message to {peer}")
76 |
77 | def send_data(self, data):
78 | self.send_msg(f"@DATA{data}")
79 |
80 | def send_hello(self):
81 | self.send_msg(f"@HELLO{self.address}")
82 |
83 | def send_join(self):
84 | self.send_msg(f"@JOIN{self.address}")
85 |
86 | def send_intro(self,addr):
87 | self.send_msg(f"#INTRO{addr}")
88 | ```
89 |
90 | 3. 下面定义`handle_connection`方法接收消息并处理,`mainloop`执行一系列任务,`helloloop`处理hello信息
91 | ```py
92 | def handle_connection(self):
93 | while True:
94 | conn, addr = self.socket.accept()
95 | data = conn.recv(1024).decode("utf-8")
96 | if data.startswith("@DATA"):
97 | print(data[5:])
98 | elif data.startswith("@HELLO"):
99 | formaddr = data[6:]
100 | self.peers.update([formaddr])
101 | self.hello_dict[formaddr] = time.time()
102 | elif data.startswith("@JOIN"):
103 | formaddr = data[5:]
104 | self.send_intro(formaddr)
105 | self.peers.update([formaddr])
106 | print(f"Join {formaddr}")
107 | elif data.startswith("#INTRO"):
108 | formaddr = data[6:]
109 | self.peers.update([formaddr])
110 | print(f"Intro {formaddr}")
111 | conn.close()
112 |
113 | def mainloop(self):
114 | while True:
115 | self.send_data(f"This is {self.address}")
116 | time.sleep(1)
117 |
118 | def helloloop(self):
119 | while True:
120 | self.send_hello()
121 | for peer in self.hello_dict:
122 | if time.time() - self.hello_dict[peer] > 10:
123 | self.peers.discard(peer)
124 | time.sleep(5)
125 | ```
126 |
127 | 4. 我们进行简单的测试:
128 | ```py
129 | if __name__ == "__main__":
130 | ip = input("Enter IP address: ")
131 | node = Node(ip)
132 | ```
133 | 
134 |
135 | 开启3个终端建立3个节点,节点之间可以相互通信。
136 |
137 |
138 | > 种子节点这么重要,会不会产生中心化?
139 |
140 | 有人或许会说,每个节点刚开始时都要首先和种子节点打交道,这会不会违背了去中心化的初衷。实际上我们可以发现,当新节点接入网络后,种子节点和其他节点并没有区别,如果种子节点离线了也不会对现在的网络造成影响。而接入网络时,我们可以设置多个种子节点来增加稳定性。甚至我们可以选择添加熟人节点来启动新节点,也就是通过其他途径,获取接入网络的某些节点地址,用它们来替代种子节点。
141 | 在实际运行的区块链中,这个网络会更加复杂,节点的类型也不止这些。
142 |
143 | 这一节我们建立了多个节点,并且把这些节点接入了一个p2p网络,下面我们将聊聊怎么让这些节点合作维护一个区块链
144 |
--------------------------------------------------------------------------------
/Transaction.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import ecdsa
3 | import json
4 | import base64 # 用于编码字节类型数据
5 |
6 | class UTXO:
7 | def __init__(self, tx_hash, output_index, amount, recipient_address):
8 | self.tx_hash = tx_hash # 交易的哈希
9 | self.output_index = output_index # 输出在交易中的索引
10 | self.amount = amount # 金额
11 | self.recipient_address = recipient_address # 接收者的地址
12 |
13 | def to_json(self):
14 | return {
15 | "tx_hash": self.tx_hash,
16 | "output_index": self.output_index,
17 | "amount": self.amount,
18 | "recipient_address": self.recipient_address
19 | }
20 |
21 | @classmethod
22 | def from_json(cls, json_data):
23 | return cls(
24 | tx_hash=json_data["tx_hash"],
25 | output_index=json_data["output_index"],
26 | amount=json_data["amount"],
27 | recipient_address=json_data["recipient_address"]
28 | )
29 |
30 | def __repr__(self):
31 | return f"UTXO(tx_hash={self.tx_hash}, output_index={self.output_index}, amount={self.amount}, recipient={self.recipient_address})"
32 |
33 | class Transaction:
34 | def __init__(self):
35 | self.inputs = [] # 交易输入
36 | self.outputs = [] # 交易输出
37 | self.tx_hash = None # 交易哈希
38 | self.signature = None # 交易签名
39 | self.sender_public_key = None # 发送者的公钥
40 | self.block_index = None # 区块索引
41 |
42 | def to_json(self):
43 | return {
44 | "inputs": [utxo.to_json() for utxo in self.inputs],
45 | "outputs": [utxo.to_json() for utxo in self.outputs],
46 | "tx_hash": self.tx_hash,
47 | "signature": base64.b64encode(self.signature).decode('utf-8') if self.signature else None,
48 | "sender_public_key": base64.b64encode(self.sender_public_key).decode('utf-8') if self.sender_public_key else None,
49 | "block_index": self.block_index
50 | }
51 |
52 | @classmethod
53 | def from_json(cls, json_data):
54 | tx = cls()
55 | tx.inputs = [UTXO.from_json(utxo_data) for utxo_data in json_data["inputs"]]
56 | tx.outputs = [UTXO.from_json(utxo_data) for utxo_data in json_data["outputs"]]
57 | tx.tx_hash = json_data["tx_hash"]
58 | tx.signature = base64.b64decode(json_data["signature"]) if json_data["signature"] else None
59 | tx.sender_public_key = base64.b64decode(json_data["sender_public_key"]) if json_data["sender_public_key"] else None
60 | tx.block_index = json_data["block_index"]
61 | return tx
62 |
63 | def add_input(self, utxo):
64 | self.inputs.append(utxo)
65 |
66 | def add_output(self, amount, recipient_address):
67 | output = UTXO(tx_hash=self.tx_hash, output_index=len(self.outputs), amount=amount, recipient_address=recipient_address)
68 | self.outputs.append(output)
69 |
70 | def calculate_hash(self):
71 | # 计算交易的哈希值
72 | tx_content = self.get_tx_content()
73 | sha = hashlib.sha256()
74 | sha.update(tx_content.encode('utf-8'))
75 | self.tx_hash = sha.hexdigest()
76 | return self.tx_hash
77 |
78 | def sign(self, private_key):
79 | # 使用私钥对交易进行签名
80 | sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
81 | message_hash = hashlib.sha256(self.tx_hash.encode('utf-8')).digest()
82 | self.signature = sk.sign(message_hash)
83 | self.sender_public_key = b'\x04' + sk.verifying_key.to_string() # 存储发送者的公钥
84 |
85 | def verify_signature(self):
86 | try:
87 | vk = ecdsa.VerifyingKey.from_string(self.sender_public_key[1:], curve=ecdsa.SECP256k1)
88 | message_hash = hashlib.sha256(self.tx_hash.encode('utf-8')).digest()
89 | return vk.verify(self.signature, message_hash)
90 | except ecdsa.BadSignatureError:
91 | return False
92 |
93 | def get_tx_content(self):
94 | # 返回交易内容的字符串表示
95 | return "".join([
96 | f"{utxo.tx_hash}:{utxo.output_index}" for utxo in self.inputs
97 | ]) + "".join([
98 | f"{output.amount}:{output.recipient_address}" for output in self.outputs
99 | ]) + f"{self.block_index}"
100 |
101 | def __repr__(self):
102 | return f"Transaction(inputs={self.inputs}, outputs={self.outputs}, tx_hash={self.tx_hash})"
103 |
104 | if __name__ == "__main__":
105 | mining_reward = Transaction()
106 | reward_output = UTXO(
107 | tx_hash=None, # 这里会在calculate_hash后被更新
108 | output_index=0,
109 | amount=50,
110 | recipient_address="0x123"
111 | )
112 | mining_reward.outputs.append(reward_output)
113 | mining_reward.tx_hash = mining_reward.calculate_hash()
114 | print(mining_reward)
115 | print(mining_reward.to_json())
116 | print(Transaction.from_json(mining_reward.to_json()))
117 | # tx = Transaction()
118 | # tx.add_input(UTXO(tx_hash="123", output_index=0, amount=100, recipient_address="0x123"))
119 | # tx.add_output(amount=100, recipient_address="0x123")
120 | # tx.calculate_hash()
121 | # tx.sign(private_key=b'\x01' * 32)
122 | # print(tx)
123 | # print("-"*100)
124 | # print(tx.to_json())
125 | # print("-"*100)
126 | # print(Transaction.from_json(tx.to_json()))
127 |
128 |
--------------------------------------------------------------------------------
/pic/blockchain.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pic/asymmetric_encryption.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Wallet.py:
--------------------------------------------------------------------------------
1 | import random
2 | import ecdsa
3 | import hashlib
4 | import base58
5 | from Transaction import Transaction, UTXO
6 |
7 | # 生成公钥
8 | def generate_public_key(private_key):
9 | sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
10 | vk = sk.verifying_key
11 | return b'\x04' + vk.to_string()
12 |
13 | # 生成比特币地址
14 | def generate_btc_address(public_key):
15 | # 1. SHA-256哈希
16 | sha256_bpk = hashlib.sha256(public_key).digest()
17 | # 2. RIPEMD-160哈希
18 | ripemd160_bpk = hashlib.new('ripemd160', sha256_bpk).digest()
19 | # 3. 添加网络字节
20 | network_byte = b'\x00' + ripemd160_bpk
21 | # 4. 双SHA-256哈希
22 | sha256_nbpk = hashlib.sha256(network_byte).digest()
23 | sha256_sha256_nbpk = hashlib.sha256(sha256_nbpk).digest()
24 | # 5. 取前4个字节作为校验和
25 | checksum = sha256_sha256_nbpk[:4]
26 | # 6. 拼接校验和
27 | binary_address = network_byte + checksum
28 | # 7. Base58编码
29 | address = base58.b58encode(binary_address)
30 | address = "0x" + address.hex()
31 | return address
32 |
33 | # 验证签名
34 | def verify_signature(public_key, message, signature):
35 | try:
36 | # 从完整公钥中移除前缀0x04
37 | public_key_bytes = public_key[1:] if public_key[0] == 0x04 else public_key
38 | # 创建验证密钥对象
39 | vk = ecdsa.VerifyingKey.from_string(public_key_bytes, curve=ecdsa.SECP256k1)
40 | # 对消息进行相同的哈希处理
41 | message_hash = hashlib.sha256(message.encode()).digest()
42 | # 验证签名
43 | return vk.verify(signature, message_hash)
44 | except:
45 | return False
46 |
47 | class Wallet:
48 | def __init__(self):
49 | self.private_key = random.randbytes(32) # 256位私钥
50 | self.public_key = generate_public_key(self.private_key)
51 | self.address = generate_btc_address(self.public_key)
52 | self.utxo_pool = [] # 钱包中的UTXO集合
53 |
54 | def get_address(self):
55 | return self.address
56 |
57 | # 签名消息
58 | def sign_message(self, message):
59 | sk = ecdsa.SigningKey.from_string(self.private_key, curve=ecdsa.SECP256k1)
60 | # 首先对消息进行哈希处理
61 | message_hash = hashlib.sha256(message.encode()).digest()
62 | signature = sk.sign(message_hash)
63 | return signature
64 |
65 | def create_transaction(self, recipient_address, amount):
66 | # 1. 选择足够的UTXO
67 | selected_utxos = []
68 | total_amount = 0
69 | for utxo in self.utxo_pool:
70 | selected_utxos.append(utxo)
71 | total_amount += utxo.amount
72 | if total_amount >= amount:
73 | break
74 |
75 | if total_amount < amount:
76 | raise ValueError("Don't have enough coins")
77 |
78 | # 2. 创建交易
79 | tx = Transaction()
80 | for utxo in selected_utxos:
81 | tx.add_input(utxo)
82 |
83 | # 3. 创建交易输出
84 | tx.add_output(amount, recipient_address)
85 |
86 | # 4. 找零
87 | change = total_amount - amount
88 | if change > 0:
89 | tx.add_output(change, self.address)
90 |
91 | # 5. 计算交易哈希
92 | tx.calculate_hash()
93 |
94 | # 6. 签名交易
95 | tx.sign(self.private_key)
96 |
97 | # 7. 更新UTXO池
98 | # self.utxo_pool = [utxo for utxo in self.utxo_pool if utxo not in selected_utxos]
99 |
100 | return tx
101 |
102 | def receive_transaction(self, transaction: Transaction):
103 | # 1. 验证交易签名
104 | if not transaction.verify_signature():
105 | raise ValueError("Invalid transaction signature")
106 |
107 | # 2. 更新UTXO池
108 | for output in transaction.outputs:
109 | if output.recipient_address == self.address:
110 | new_utxo = UTXO(transaction.tx_hash, transaction.outputs.index(output), output.amount, output.recipient_address)
111 | self.utxo_pool.append(new_utxo)
112 |
113 | def add_utxo(self, utxo):
114 | self.utxo_pool.append(utxo)
115 |
116 | def remove_utxo(self, utxo):
117 | self.utxo_pool.remove(utxo)
118 |
119 | def __repr__(self):
120 | return f"Wallet(address={self.address}, utxo_pool={self.utxo_pool})"
121 |
122 | if __name__ == "__main__":
123 | # 创建发送方钱包
124 | sender_wallet = Wallet()
125 | recipient_wallet = Wallet()
126 | recipient_private_key, recipient_public_key = recipient_wallet.private_key, recipient_wallet.public_key
127 | recipient_address = recipient_wallet.address
128 |
129 | # 添加UTXO到发送方钱包
130 | utxo = UTXO(tx_hash="previous_tx_hash", output_index=0, amount=20, recipient_address=sender_wallet.address)
131 | sender_wallet.add_utxo(utxo)
132 | utxo = UTXO(tx_hash="previous_tx_hash", output_index=0, amount=20, recipient_address=sender_wallet.address)
133 | sender_wallet.add_utxo(utxo)
134 |
135 | # 验证接收方钱包的UTXO池
136 | print(f"Recipient Wallet UTXO Pool: {recipient_wallet.utxo_pool}")
137 | print(f"Sender Wallet UTXO Pool: {sender_wallet.utxo_pool}")
138 |
139 | # 创建交易
140 | tx = sender_wallet.create_transaction(recipient_address, 30)
141 |
142 | # 创建接收方钱包
143 | recipient_wallet = Wallet()
144 | recipient_wallet.address = recipient_address # 设置接收方地址
145 |
146 | # 接收交易
147 | recipient_wallet.receive_transaction(tx)
148 | sender_wallet.receive_transaction(tx)
149 |
150 | print("转账后")
151 | print("--------------------------------")
152 |
153 | # 验证接收方钱包的UTXO池
154 | print(f"Recipient Wallet UTXO Pool: {recipient_wallet.utxo_pool}")
155 | print(f"Sender Wallet UTXO Pool: {sender_wallet.utxo_pool}")
--------------------------------------------------------------------------------
/APP.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import ttk, messagebox
3 | import socket
4 | from Node import Node
5 | import threading
6 |
7 | class BlockchainApp:
8 | def __init__(self, root,ip):
9 | self.root = root
10 | self.root.title("Koala Wallet")
11 |
12 | # 添加线程列表用于跟踪
13 | self.threads = []
14 |
15 | # 添加关闭窗口的处理
16 | self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
17 |
18 | # 标记程序是否正在运行
19 | self.running = True
20 |
21 | # 获取本机IP
22 | self.ip = ip
23 |
24 | # 创建节点
25 | self.node = None
26 | self.init_node()
27 |
28 | # 创建界面
29 | self.create_widgets()
30 |
31 | # 定期更新余额
32 | self.update_balance()
33 |
34 | def init_node(self):
35 | # 在新线程中初始化节点
36 | def node_thread():
37 | self.node = Node(self.ip)
38 |
39 | thread = threading.Thread(target=node_thread)
40 | thread.daemon = True # 设置为守护线程
41 | self.threads.append(thread) # 添加到线程列表
42 | thread.start()
43 |
44 | def create_widgets(self):
45 | # IP地址显示
46 | ttk.Label(self.root, text="节点IP地址:").pack(pady=5)
47 | ttk.Label(self.root, text=self.ip).pack()
48 |
49 | # 钱包地址显示框架
50 | wallet_frame = ttk.Frame(self.root)
51 | wallet_frame.pack(pady=5, fill="x", padx=10)
52 |
53 | ttk.Label(wallet_frame, text="钱包地址:").pack(side="left", padx=(0,5))
54 | self.wallet_label = ttk.Entry(wallet_frame, state='readonly')
55 | self.wallet_label.pack(side="left", fill="x", expand=True)
56 |
57 | copy_btn = ttk.Button(wallet_frame, text="复制",
58 | command=lambda: self.copy_to_clipboard(self.wallet_label.get()))
59 | copy_btn.pack(side="left", padx=(5,0))
60 |
61 | # 余额显示
62 | ttk.Label(self.root, text="当前余额:").pack(pady=5)
63 | self.balance_label = ttk.Label(self.root, text="等待节点初始化...")
64 | self.balance_label.pack()
65 |
66 | # 转账框架
67 | transfer_frame = ttk.LabelFrame(self.root, text="转账", padding=10)
68 | transfer_frame.pack(pady=10, padx=10, fill="x")
69 |
70 | # 接收地址输入
71 | ttk.Label(transfer_frame, text="接收地址:").pack()
72 | self.address_entry = ttk.Entry(transfer_frame, width=50)
73 | self.address_entry.pack(pady=5)
74 |
75 | # 金额输入
76 | ttk.Label(transfer_frame, text="转账金额:").pack()
77 | self.amount_entry = ttk.Entry(transfer_frame)
78 | self.amount_entry.pack(pady=5)
79 |
80 | # 转账按钮
81 | self.transfer_btn = ttk.Button(transfer_frame, text="转账", command=self.transfer)
82 | self.transfer_btn.pack(pady=5)
83 |
84 | def on_closing(self):
85 | """处理窗口关闭事件"""
86 | self.running = False
87 |
88 | # 关闭节点的socket连接
89 | if self.node and hasattr(self.node, 'socket'):
90 | try:
91 | self.node.socket.close()
92 | except:
93 | pass
94 |
95 | # 等待所有线程结束
96 | for thread in self.threads:
97 | if thread.is_alive():
98 | thread.join(timeout=1.0) # 等待最多1秒
99 |
100 | # 销毁窗口
101 | self.root.destroy()
102 |
103 | def copy_to_clipboard(self, text):
104 | """复制文本到剪贴板"""
105 | self.root.clipboard_clear()
106 | self.root.clipboard_append(text)
107 |
108 | def update_balance(self):
109 | if not self.running:
110 | return
111 |
112 | if self.node and self.node.wallet:
113 | # 更新钱包地址
114 | self.wallet_label.configure(state='normal')
115 | self.wallet_label.delete(0, tk.END)
116 | self.wallet_label.insert(0, self.node.wallet.address)
117 | self.wallet_label.configure(state='readonly')
118 |
119 | # 计算并更新余额
120 | total_balance = sum(utxo.amount for utxo in self.node.wallet.utxo_pool)
121 | self.balance_label.config(text=f"{total_balance} coins")
122 |
123 | if self.running:
124 | self.root.after(1000, self.update_balance)
125 |
126 | def transfer(self):
127 | if not self.node:
128 | messagebox.showerror("错误", "节点尚未初始化完成")
129 | return
130 |
131 | try:
132 | recipient = self.address_entry.get()
133 | amount = float(self.amount_entry.get())
134 |
135 | if not recipient or amount <= 0:
136 | messagebox.showerror("错误", "请输入有效的接收地址和转账金额")
137 | return
138 |
139 | # 创建交易
140 | transaction = self.node.wallet.create_transaction(recipient, amount)
141 |
142 | # 广播交易
143 | self.node.send_transaction(transaction)
144 |
145 | messagebox.showinfo("成功", "转账交易已发送")
146 |
147 | # 清空输入框
148 | self.address_entry.delete(0, tk.END)
149 | self.amount_entry.delete(0, tk.END)
150 |
151 | except ValueError as e:
152 | messagebox.showerror("错误", str(e))
153 | except Exception as e:
154 | messagebox.showerror("错误", f"转账失败: {str(e)}")
155 |
156 | if __name__ == "__main__":
157 | ip = input("请输入节点IP地址:")
158 | root = tk.Tk()
159 | app = BlockchainApp(root,ip)
160 | root.mainloop()
161 |
162 |
--------------------------------------------------------------------------------
/Tutor/5_whoami.md:
--------------------------------------------------------------------------------
1 | ## Who am I ?
2 |
3 | 前面我们实现了一个p2p的区块链网络,节点之间将网络中的数据打包到一个区块中,然后通过工作量证明构建一个区块链。我们后面会逐渐将这些数据换成交易信息,将这个区块链改造成一个数字货币系统。
4 |
5 | 我们要实现交易,首先就要对每个节点进行识别。在现实生活中,我们有很多识别自己的方法,比如说身份证号,银行卡号,学校里的学号等等。但是在区块链上,我们应该如何识别每一个节点呢?你可能听说过web3钱包会有一个地址,那这个地址是什么呢?
6 |
7 | ---
8 |
9 | ### 区块链中的身份
10 |
11 | 我们现在整理一下我们的需求,我们希望有一个地址能够唯一标识自己,所有想给我们打钱的人只需要知道这个地址就可以打钱给我们。同时我们要能证明这个地址是我的,我们可以把这个地址上的钱给花出去。你可能想到了第0节时我们谈到的数字签名,我们可以将公钥公布出去,其他人将支付的数字货币标记上我们的公钥;在我们想要花这笔钱时,通过私钥来证明这笔钱属于我们。这样就让我们在区块链中有了自己的身份。
12 |
13 | 我们通过一个"钱包",来实现这些功能,当我们进入web3世界时,我们都需要一个web3钱包。比较有名的web3钱包是metamask,它的图标是一只小狐狸,支持以太坊等许多区块链的钱包,你可以在你的手机或者chrome浏览器上下载使用。如果你用过类似的web3钱包,你可能发现它们和刚刚说的想法好像不一样,这主要是因为下面这两点:
14 |
15 | 1. **地址**
16 | 刚刚我们提到了使用公钥作为我们的标识,但实际中我们往往使用一个更短的数字来作为我们的钱包地址(钱包软件中那个0x开头的串),这个地址是我们的公钥通过哈希和一些变换产生的。我们使用的哈希函数有抗碰撞性,所以该地址依旧可以唯一标识我们的钱包。
17 | 为什么要多此一举?首先,通过哈希等一系列变换,地址的长度比公钥会更短,这在使用的时候更方便;同时,生成地址时会使用校验位,避免用户将数字货币转账到一个错误的地址;并且,使用地址避免了公钥的暴露,增加了隐私保护。
18 |
19 | **插一嘴:量子计算**
20 |
21 | 量子计算机时这些年比较火的概念,有人认为研究量子计算这群人是在炒概念骗经费,也有人认为量子计算机会是一个无所不能的存在。但不可否认,量子计算对很多加密技术有着很大的冲击。比特币中使用的椭圆曲线密码(ECC)基于椭圆曲线离散对数问题,而量子计算可以高效求解这个问题。也就是说借助量子计算,我们可以通过你的公钥去破解你的私钥。但是我们的实现中只暴露了一个地址,地址是公钥经过哈希产生的,所以这中间出现了信息的损失,我们是无法通过地址去恢复公钥信息的。这样从某些方面抵御了未来可能出现的风险。
22 |
23 | 总有人认为量子计算出现后基于密码学的区块链就会轰然崩塌,但实际上安全性是区块链设计时的一大考量,密码学界有人在积极研究“后量子加密”,或许在量子计算大规模应用之前,我们已经设计好了对抗量子计算的区块链。而传统的线上交易也依赖了很多加密技术,量子计算对这些传统交易的冲击或许会更大。说不准到时候,区块链才是线上交易的最优解。
24 |
25 |
26 | 2. **助记词**
27 | 我们回到钱包的实现上来。在使用metamask创建钱包时,你似乎并没有接触到公钥和私钥,而是钱包软件给了你12个(或者24个)英文单词,这些单词是助记词,软件会提醒你千万不要把这些单词告诉别人。这些单词是什么东西呢?
28 | 我们刚刚提到,使用公钥和私钥来构建一个钱包。我们需要随机选取一个私钥,然后依据私钥生成公钥,最后通过公钥产生地址。问题出在第一步:如何选取一个私钥?我们知道目前我们计算机上产生的随机数都是伪随机,是从一个**种子**开始通过伪随机算法生成了一系列看上去随机的数。也就是说如果我们知道了算法和种子,就可以重复生成这一随机数序列。这里的助记词就是起到了种子的作用。钱包程序首先随机产生一个助记词序列,通过这些助记词生成一个种子,再使用种子产生私钥。所以如果你的助记词泄露,那么你的私钥就被攻击者获取了。
29 | 通过使用助记词,我们一般的用户就不需要自己记录那些复杂的私钥和公钥,只需要记住几个英文单词就可以掌握自己的钱包。助记词的产生遵循一些标准,最常用的是BIP-39 标准,从2048 个单词 中选出12个或24个单词组成的有序单词序列作为助记词。
30 |
31 | 下面是建立一个钱包的流程图
32 |
33 | ```mermaid
34 | graph LR
35 | A[助记词] --作为种子--> B[私钥]
36 | B --椭圆加密算法--> C[公钥]
37 | C --哈希等变换--> D[地址]
38 | ```
39 |
40 | ### web3世界的"永动机"——私钥碰撞机
41 |
42 | 永动机作为一个反科学的研究方向,一直有人为之努力,有的人是为了忽悠客户,有的人是自己真的信了。但是不可否认的,在反驳各类永动机时我们的科学也有所进步。
43 |
44 | 现在,我们web3世界也有自己的永动机了!从比特币诞生以来,一直有人问这么一个问题:私钥/助记词就那么几位,我多试试是不是就能试出个有着巨款的钱包?这就是下面图片里展示的私钥/助记词碰撞。
45 |
46 | 
47 |
48 | 
49 |
50 | 这钱赚的真这么容易吗?真的像他们说的靠多开和时间就能出货吗?让我们简单算笔账:
51 |
52 | 按照前面提到的BIP-39 标准,我们从2048个单词中选12个单词作为助记词,那么一共有 $2048^{12}=2^{132}$ 个可能的地址(实际中可能有其他约束,但数量级是一致的)。而目前以太坊上拥有1k美元以上余额的钱包地址约为32000个,那么每尝试一个地址成功的概率约为 $p=2^{-117}$ 。我们设第一次成功时的尝试次数为 $N$ ,那么 $N$ 的期望就是 $E(N)=\frac{1}{p}=2^{117}$ ,按照第二个截图说的每秒2000次尝试,期望需要 $E(T)=\frac{E(N)}{2000*3600*24} \approx 2^{90}$天。而又要多开多少个进程,才能在一个人短短的三万天左右中碰撞出一个有巨款的钱包来呢?我觉得还是每天坚持买彩票更加值得一试。
53 |
54 | > 以太坊中1k美元以上余额钱包数量数据来源于https://www.theblock.co/data/on-chain-metrics/ethereum/ethereum-addresses-with-balance-over-x
55 |
56 | 但是,就和传统的永动机一样,这个永动机也不是完全的一无是处。首先,我们的钱包软件可能存在漏洞,比如私钥/助记词的产生并不是真随机,如果使用了不良的随机算法可能会产生漏洞,使得攻击者发现了某些规律减小了遍历空间。这些碰撞机的尝试会促使钱包软件检查自己的实现是否存在漏洞。其次,这些尝试也验证了区块链的可靠安全,他们在尝试中使用不同的算法优化,探索各种加速的方法也可能为技术做出了贡献。
57 |
58 | ### 实现我们的wallet类
59 | 下面我们实现我们的wallet类并测试,可以看到成功地生成了私钥公钥和地址,进行签名和验证。同时我们将`Node.py`进行修改,在节点初始化时建立一个钱包对象。
60 |
61 | ```py
62 | import random
63 | import ecdsa
64 | import hashlib
65 | import base58
66 |
67 | # 生成公钥
68 | def generate_public_key(private_key):
69 | sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
70 | vk = sk.verifying_key
71 | return b'\x04' + vk.to_string()
72 |
73 | # 生成比特币地址
74 | def generate_btc_address(public_key):
75 | # 1. SHA-256哈希
76 | sha256_bpk = hashlib.sha256(public_key).digest()
77 | # 2. RIPEMD-160哈希
78 | ripemd160_bpk = hashlib.new('ripemd160', sha256_bpk).digest()
79 | # 3. 添加网络字节
80 | network_byte = b'\x00' + ripemd160_bpk
81 | # 4. 双SHA-256哈希
82 | sha256_nbpk = hashlib.sha256(network_byte).digest()
83 | sha256_sha256_nbpk = hashlib.sha256(sha256_nbpk).digest()
84 | # 5. 取前4个字节作为校验和
85 | checksum = sha256_sha256_nbpk[:4]
86 | # 6. 拼接校验和
87 | binary_address = network_byte + checksum
88 | # 7. Base58编码
89 | address = base58.b58encode(binary_address)
90 | address = "0x" + address.hex()
91 | return address
92 |
93 | # 验证签名
94 | def verify_signature(public_key, message, signature):
95 | try:
96 | # 从完整公钥中移除前缀0x04
97 | public_key_bytes = public_key[1:] if public_key[0] == 0x04 else public_key
98 | # 创建验证密钥对象
99 | vk = ecdsa.VerifyingKey.from_string(public_key_bytes, curve=ecdsa.SECP256k1)
100 | # 对消息进行相同的哈希处理
101 | message_hash = hashlib.sha256(message.encode()).digest()
102 | # 验证签名
103 | return vk.verify(signature, message_hash)
104 | except:
105 | return False
106 |
107 | class Wallet:
108 | def __init__(self):
109 | self.private_key = random.randbytes(32) # 256位私钥
110 | self.public_key = generate_public_key(self.private_key)
111 | self.address = generate_btc_address(self.public_key)
112 |
113 | def get_address(self):
114 | return self.address
115 |
116 | # 签名消息
117 | def sign_message(self, message):
118 | sk = ecdsa.SigningKey.from_string(self.private_key, curve=ecdsa.SECP256k1)
119 | # 对消息进行签名
120 | message_hash = hashlib.sha256(message.encode()).digest()
121 | signature = sk.sign(message_hash)
122 | return signature
123 |
124 | if __name__ == "__main__":
125 | wallet = Wallet()
126 | print("private_key: ", wallet.private_key.hex())
127 | print("public_key: ", wallet.public_key.hex())
128 | print("address: ", wallet.get_address())
129 | signature = wallet.sign_message("Hello, world!")
130 | print("signature: ", signature.hex())
131 | print("verify_signature: ", verify_signature(wallet.public_key, "Hello, world!", signature))
132 | ```
133 |
134 | 具体实现比较形而下,只要了解大致的原理便已足够。
135 |
136 | **插一嘴:多签钱包**
137 |
138 | 我们这里的钱包通过一个私钥便可以控制,但是有时候我们希望要有多个私钥同时签名的情况下才批准一个交易。比如说公司要批准一笔款项,需要2/3以上的股东授权才执行这笔转账,这样我们就需要多签钱包。多签钱包要求多个密钥中的一定数量(或全部)来共同签署交易才执行这个交易。目前有许多人使用多签钱包进行一种低级骗术,他们在各种讨论区块链的文章视频下面留言,说自己玩加密货币多久,现在不想玩了,把助记词截图放在下面。当你使用这些助记词获取钱包后,发现钱包里真的还有几碗猪脚饭的钱,于是尝试把它转走。但是转账时发生了失败,就会有人认为(或者被引导认为)是缺少gas费,于是转入一些gas费进来。发现还是转账失败。这是因为该钱包是多签钱包,这个钱包本身没有转出的权限。
--------------------------------------------------------------------------------
/Tutor/1_simple_blockchain.md:
--------------------------------------------------------------------------------
1 | ## 一个最简单的区块链
2 |
3 | ### 区块链的构成
4 | 我们先看一下区块链长什么样,下面是一个简化的比特币区块链示意图(图片来自 bitcoin.org)
5 |
6 | 
7 |
8 | 首先,区块链由一个个区块(block)构成,图片中的区块从左到右按顺序产生,右边为最新的区块。
9 |
10 | 区块分为区块头和区块体,区块头中储存了前一个区块的hash值,一个Merkle树(默克尔树)的根,区块体中储存了这个区块中的交易信息。
11 |
12 | 关于默克尔树和交易信息我们先暂且不表,下面我们简单实现一个只包含一个字符串数据的区块链
13 |
14 |
15 | 1. 下面是区块类,由索引,时间戳,数据,前一个区块的哈希组成,创建时计算区块的哈希值储存下来
16 | ```py
17 | class Block:
18 | def __init__(self, index, data, previous_hash):
19 | self.index = index
20 | self.timestamp = time.time() # 当前时间戳
21 | self.data = data
22 | self.previous_hash = previous_hash
23 | self.hash = self.calculate_hash()
24 |
25 | def calculate_hash(self):
26 | # 计算区块的hash值
27 | # 这里我们使用sha256算法
28 | sha = hashlib.sha256()
29 | sha.update(
30 | str(self.index).encode("utf-8")
31 | + str(self.timestamp).encode("utf-8")
32 | + str(self.data).encode("utf-8")
33 | + str(self.previous_hash).encode("utf-8")
34 | )
35 | return sha.hexdigest()
36 |
37 | def __str__(self):
38 | return f"Block(index={self.index},\n timestamp={self.timestamp},\n data={self.data},\n previous_hash={self.previous_hash},\n hash={self.hash})"
39 | ```
40 |
41 | 我们在这里选择SHA256作为哈希算法,它有以下特性:
42 |
43 | 1. **安全性强**:SHA256是目前最安全的哈希算法之一,至今没有发现有效的碰撞攻击方法。这意味着:
44 | - 很难找到两个不同的输入产生相同的哈希值
45 | - 即使修改输入中的一个比特,输出的哈希值也会发生显著变化
46 |
47 | 2. **输出固定长度**:无论输入数据多大,SHA256始终产生256位(32字节)的输出,这个长度:
48 | - 足够长,可以有效防止碰撞
49 | - 又不会过长,便于存储和传输
50 |
51 |
52 |
53 | 2. 下面是区块链类,使用一个列表储存所以区块,同时记录区块高度,开始时创建一个区块(一般被称为创世区块),可以通过`add_block`方法添加一个区块。我们还设置了一个`is_chain_valid`方法,通过遍历区块链检查哈希值是否算错以及储存的前一个区块哈希值是否正常,来判断区块链是否合法。
54 |
55 | ```py
56 | class BlockChain:
57 | def __init__(self):
58 | self.chain = [self.create_genesis_block()]
59 | self.height = 1
60 |
61 | def create_genesis_block(self):
62 | return Block(0, "Genesis Block", "0")
63 |
64 | def get_latest_block(self):
65 | return self.chain[-1]
66 |
67 | def add_block(self, data):
68 | previous_block = self.get_latest_block()
69 | new_block = Block(self.height, data, previous_block.hash)
70 | self.chain.append(new_block)
71 | self.height += 1
72 |
73 | def is_chain_valid(self):
74 | for i in range(1, len(self.chain)):
75 | current_block = self.chain[i]
76 | previous_block = self.chain[i - 1]
77 | if current_block.hash != current_block.calculate_hash():
78 | return (
79 | False,
80 | f"The hash of the {current_block.index} block is not equal to the calculated hash",
81 | )
82 | if current_block.previous_hash != previous_block.hash:
83 | return (
84 | False,
85 | f"The previous hash of the {current_block.index} block is not equal to the hash of the {previous_block.index} block",
86 | )
87 | return True
88 |
89 | ```
90 |
91 | 3. 下面我们测试一下我们的mini区块链能否运行
92 | ```py
93 | if __name__ == "__main__":
94 | blockchain = BlockChain()
95 | time.sleep(1)
96 | blockchain.add_block("First Block")
97 | time.sleep(1)
98 | blockchain.add_block("Second Block")
99 | time.sleep(1)
100 | blockchain.add_block("Third Block")
101 | print("blockchain height: "+str(blockchain.height))
102 | print("-"*100)
103 | print(blockchain.chain[0])
104 | print("-"*100)
105 | print(blockchain.chain[1])
106 | print("-"*100)
107 | print(blockchain.chain[2])
108 | print("-"*100)
109 | print(blockchain.chain[3])
110 | ```
111 |
112 | 测试程序创建了一个区块链,然后添加了三个区块,然后打印相关信息,输出如下。我们可以看到每一个区块储存的 previous_hash 和前一个区块的hash值相同。
113 |
114 | ```
115 | blockchain height: 4
116 | ----------------------------------------------------------------------------------------------------
117 | Block(index=0,
118 | timestamp=1731656026.8961995,
119 | data=Genesis Block,
120 | previous_hash=0,
121 | hash=528b8cb76d09cc178ef8cdcbbbc1b5ff45ecb2639edecdb786e9b850742eb79d)
122 | ----------------------------------------------------------------------------------------------------
123 | Block(index=1,
124 | timestamp=1731656027.8971088,
125 | data=First Block,
126 | previous_hash=528b8cb76d09cc178ef8cdcbbbc1b5ff45ecb2639edecdb786e9b850742eb79d,
127 | hash=17a5ece381d2d4439ca20c7531ad3f372b8d22435495e47b728ea2e50284558c)
128 | ----------------------------------------------------------------------------------------------------
129 | Block(index=2,
130 | timestamp=1731656028.8973522,
131 | data=Second Block,
132 | previous_hash=17a5ece381d2d4439ca20c7531ad3f372b8d22435495e47b728ea2e50284558c,
133 | hash=f808c10d4e843a38d39d51fa5e1a2842e738576475202b795ef2254da7772e68)
134 | ----------------------------------------------------------------------------------------------------
135 | Block(index=3,
136 | timestamp=1731656029.897595,
137 | data=Third Block,
138 | previous_hash=f808c10d4e843a38d39d51fa5e1a2842e738576475202b795ef2254da7772e68,
139 | hash=79038d89726b2777536c7251f6f3c5735eef84e170610aeff8dad7836b4e96aa)
140 | ```
141 |
142 | 如果我们试图对区块链进行篡改,把第二个区块的数据换为"Changed Block",我们检查区块链合法性就会发现区块链被篡改了。
143 | 这是因为区块二虽然接上了区块一,但是区块三中仍然储存了未篡改的区块二的哈希值,这样检查时会发现区块二被修改了
144 |
145 | 那么我们应该如何篡改一个区块链而不被发现呢?目前我们可以想到两种方法:
146 |
147 | 1. 我们在修改区块二后将区块三的previous_hash也同时修改,这样区块三就不会发现区块二被篡改了。但是,在计算区块的哈希时我们将previous_hash也纳入了计算,所以区块三的哈希值就会变化。这样我们必须将区块二后的全部区块进行修改才可以不被发现
148 | 2. 我们在修改区块二时,想办法使得篡改的区块二哈希值和之前一样,那样区块三就不会发现任何异常。实际上,这就是在尝试实现一次哈希碰撞,而我们目前还没有已知的有效方法可以对SHA256进行哈希碰撞攻击,SHA256仍然是一个安全的哈希算法
149 |
150 | ```py
151 | if __name__ == "__main__":
152 | blockchain = BlockChain()
153 | time.sleep(1)
154 | blockchain.add_block("First Block")
155 | time.sleep(1)
156 | blockchain.add_block("Second Block")
157 | time.sleep(1)
158 | blockchain.add_block("Third Block")
159 | blockchain.chain[2] = Block(2, "Changed Block", blockchain.chain[1].hash)
160 | print(blockchain.is_chain_valid())
161 |
162 | 输出
163 | (False, 'The previous hash of the 3 block is not equal to the hash of the 2 block')
164 | ```
165 |
166 | 我们这里实现了一个迷你的区块链,但是一个区块只能储存一条数据。下一节我们将会使用Merkle树将多条数据储存到区块中。
--------------------------------------------------------------------------------
/pic/blockchain_2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/MerkleTree.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | class MerkleNode:
4 | def __init__(self, left=None, right=None, data=None):
5 | self.left = left
6 | self.right = right
7 | self.data = data
8 | self.hash = self.calculate_hash()
9 |
10 |
11 | def calculate_hash(self):
12 | if self.data is not None:
13 | sha = hashlib.sha256()
14 | sha.update(str(self.data).encode("utf-8"))
15 | return sha.hexdigest()
16 | else:
17 | sha = hashlib.sha256()
18 | sha.update(str(self.left.hash).encode("utf-8") + str(self.right.hash).encode("utf-8"))
19 | return sha.hexdigest()
20 |
21 | def to_dict(self):
22 | return {
23 | 'left': self.left.to_dict() if self.left else None,
24 | 'right': self.right.to_dict() if self.right else None,
25 | 'data': self.data,
26 | 'hash': self.hash
27 | }
28 |
29 | class MerkleTree:
30 | def __init__(self, data):
31 | self.leaves = [MerkleNode(data=d) for d in data]
32 | self.root = self.build_tree(self.leaves)
33 |
34 | def to_json(self):
35 | def node_to_dict(node):
36 | if not node:
37 | return None
38 | return {
39 | 'left': node_to_dict(node.left),
40 | 'right': node_to_dict(node.right),
41 | 'data': node.data,
42 | 'hash': node.hash
43 | }
44 |
45 | tree_dict = {
46 | 'root': node_to_dict(self.root)
47 | }
48 | return json.dumps(tree_dict, ensure_ascii=False)
49 |
50 | def __str__(self):
51 | def print_node(node, level=0):
52 | if not node:
53 | return []
54 | result = [" " * (level * 4) + f"Hash: {node.hash}"]
55 | if node.data is not None:
56 | result.append(" " * (level * 4) + f"Data: {node.data}")
57 | if node.left:
58 | result.extend(print_node(node.left, level + 1))
59 | if node.right:
60 | result.extend(print_node(node.right, level + 1))
61 | return result
62 |
63 | return "\n".join(print_node(self.root))
64 |
65 | def get_root_hash(self):
66 | return self.root.hash
67 |
68 | def build_tree(self, nodes):
69 | while len(nodes) > 1:
70 | temp_nodes = []
71 | for i in range(0, len(nodes), 2):
72 | left = nodes[i]
73 | right = nodes[i + 1] if i + 1 < len(nodes) else nodes[i]
74 | parent = MerkleNode(left=left, right=right)
75 | temp_nodes.append(parent)
76 | nodes = temp_nodes
77 | return nodes[0] if nodes else None
78 |
79 | def save_svg(self,filename):
80 | # 设置基本参数
81 | node_width = 120 # 节点宽度
82 | node_height = 50 # 节点高
83 | level_height = 120 # 层间距
84 |
85 | # 计算树的层数和叶子节点数
86 | def get_tree_height(node):
87 | if not node:
88 | return 0
89 | return 1 + max(get_tree_height(node.left), get_tree_height(node.right))
90 |
91 | def count_leaves(node):
92 | if not node:
93 | return 0
94 | if not node.left and not node.right:
95 | return 1
96 | return count_leaves(node.left) + count_leaves(node.right)
97 |
98 | tree_height = get_tree_height(self.root)
99 | leaf_count = count_leaves(self.root)
100 |
101 | # 根据树的大小动态计算画布尺寸,减小宽度
102 | width = max(1200, leaf_count * node_width * 1.5) # 减小宽度倍数
103 | height = max(800, (tree_height + 1) * level_height * 1.5)
104 |
105 | # 生成SVG头部
106 | svg = [f'']
107 | svg.append(f'')
148 |
149 | # 将SVG内容写入文件
150 | with open(filename, 'w') as f:
151 | f.write('\n'.join(svg))
152 |
153 | def merkle_tree_from_json(json_data):
154 | def dict_to_node(node_dict):
155 | if not node_dict:
156 | return None
157 | node = MerkleNode(
158 | left=dict_to_node(node_dict['left']),
159 | right=dict_to_node(node_dict['right']),
160 | data=node_dict['data']
161 | )
162 | node.hash = node_dict['hash']
163 | return node
164 |
165 | data = json.loads(json_data)
166 | tree = MerkleTree([]) # 初始化一个空的 MerkleTree
167 | tree.root = dict_to_node(data['root'])
168 |
169 | # 初始化 leaves
170 | def collect_leaves(node):
171 | if not node:
172 | return []
173 | if not node.left and not node.right:
174 | return [node]
175 | return collect_leaves(node.left) + collect_leaves(node.right)
176 |
177 | tree.leaves = collect_leaves(tree.root)
178 |
179 | return tree
180 |
181 | if __name__ == "__main__":
182 | data = ["a", "b", "c","d","e","f",]
183 | tree = MerkleTree(data)
184 | # print(tree)
185 | # print(tree.to_json())
186 | tree = merkle_tree_from_json(tree.to_json())
187 | print(tree)
188 | # tree.save_svg("merkle_tree.svg")
189 |
--------------------------------------------------------------------------------
/Tutor/2_Merkle_Tree.md:
--------------------------------------------------------------------------------
1 | ## Merkle树
2 |
3 | 上篇文章中我们展示的简化区块链中,每个区块中储存了一个Merkle Root,这是这个区块下Merkle树的根。这一节我们将从这个Merkle Tree开始讲起。
4 | 
5 |
6 | ### 什么是Merkle树
7 | Merkle Tree(默克尔树)也叫哈希树,它是一个树形结构,树的叶子节点是数据块的哈希值,非叶子节点是其对应子节点串联字符串的哈希值。
8 |
9 | 下面是一个简单的Merkle树结构,我们有A,B,C三个数据,分别计算哈希值作为叶子节点,然后两两配对,计算哈希值之和的哈希值,作为两个节点的父节点,父节点继续两两配对直到根节点。
10 |
11 | 我们注意到,我们如果在某一层只有奇数个节点时就无法两两配对(比如下图中就只有三个叶子节点)。我们选择将没有配对的节点与它的副本进行哈希处理。
12 |
13 | 
14 |
15 | ### 为什么用Merkle Tree
16 |
17 | 在比特币中,每个区块都包含了一个Merkle树,树的叶子节点是这个区块中所有交易的哈希值。这样做有以下好处:
18 |
19 | 1. **防止篡改**:如果有人修改了Merkle树中的某个数据,那么其哈希值会发生变化,导致Merkle Root发生改变。而区块头中包含了Merkle Root,所以该区块的哈希值也会变化从而被检测。
20 |
21 | 2. **提供简化验证**:我们的区块链网络中存在不保存整个区块而只保存区块头的**轻节点**(对应的,储存了所有数据的节点叫**全节点**),通过Merkle树,我们可以向轻节点提供简化的验证服务(我们下一节细说)
22 |
23 | ### 实现一个简单的Merkle Tree
24 |
25 | 1. 我们首先定义一个默克尔树节点类
26 | 如果输入左右孩子,则为中间节点,计算左右孩子哈希之和的哈希
27 | 如果输入data,则为叶子节点,计算data的哈希值
28 |
29 | ```py
30 | class MerkleNode:
31 | def __init__(self, left=None, right=None, data=None):
32 | self.left = left
33 | self.right = right
34 | self.data = data
35 | self.hash = self.calculate_hash()
36 |
37 | def calculate_hash(self):
38 | if self.data is not None:
39 | sha = hashlib.sha256()
40 | sha.update(str(self.data).encode("utf-8"))
41 | return sha.hexdigest()
42 | else:
43 | sha = hashlib.sha256()
44 | sha.update(str(self.left.hash).encode("utf-8") + str(self.right.hash).encode("utf-8"))
45 | return sha.hexdigest()
46 | ```
47 |
48 | 2. 然后构建Merkle Tree类:
49 | 输入一个data列表,首先生成叶子节点,然后构建Merkle树
50 | 我们着重看一下`build_tree(self, nodes)`这个方法。对于输入的节点列表,我们遍历该列表,两两配对生成新的节点,如果没有配对的使用自己的副本配对。一个循环结束后,如果只剩一个节点则就是根节点,如果节点数大于1还需要继续循环两两配对。
51 |
52 | ```py
53 |
54 | class MerkleTree:
55 | def __init__(self, data):
56 | self.leaves = [MerkleNode(data=d) for d in data]
57 | self.root = self.build_tree(self.leaves)
58 |
59 | def __str__(self):
60 | def print_node(node, level=0):
61 | if not node:
62 | return []
63 | result = [" " * (level * 4) + f"Hash: {node.hash}"]
64 | if node.data is not None:
65 | result.append(" " * (level * 4) + f"Data: {node.data}")
66 | if node.left:
67 | result.extend(print_node(node.left, level + 1))
68 | if node.right:
69 | result.extend(print_node(node.right, level + 1))
70 | return result
71 |
72 | return "\n".join(print_node(self.root))
73 |
74 | def get_root_hash(self):
75 | return self.root.hash
76 |
77 | def build_tree(self, nodes):
78 | while len(nodes) > 1:
79 | temp_nodes = []
80 | for i in range(0, len(nodes), 2):
81 | left = nodes[i]
82 | right = nodes[i + 1] if i + 1 < len(nodes) else nodes[i]
83 | parent = MerkleNode(left=left, right=right)
84 | temp_nodes.append(parent)
85 | nodes = temp_nodes
86 | return nodes[0] if nodes else None
87 | ```
88 |
89 | 3. 我们测试一下这个Merkle树代码
90 |
91 | ```py
92 | if __name__ == "__main__":
93 | data = ["a", "b", "c","d","e","f",]
94 | tree = MerkleTree(data)
95 | print(tree)
96 | tree.save_svg()
97 | ```
98 |
99 | 输出如下,可以见到和我们前面例子中的Merkle树结构类似。e,f的父节点没有配对对象,于是产生了一个副本进行配对
100 | ```
101 | Hash: 5269ef76f0df413ef337130ed716dd53e73e92905b25a533210e9142edfda01b
102 | Hash: 58c89d709329eb37285837b042ab6ff72c7c8f74de0446b091b6a0131c102cfd
103 | Hash: 62af5c3cb8da3e4f25061e829ebeea5c7513c54949115b1acc225930a90154da
104 | Hash: ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb
105 | Data: a
106 | Hash: 3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d
107 | Data: b
108 | Hash: d3a0f1c792ccf7f1708d5422696263e35755a86917ea76ef9242bd4a8cf4891a
109 | Hash: 2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6
110 | Data: c
111 | Hash: 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4
112 | Data: d
113 | Hash: 38e21934b1ddf3c5f90417378522702c447f4ab79ca6d62843e071b775948035
114 | Hash: 1b3dae70b4b0a8fd252a7879ec67283c0176729bfebc51364fb9e9fb0598ba9e
115 | Hash: 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea
116 | Data: e
117 | Hash: 252f10c83610ebca1a059c0bae8255eba2f95be4d1d7bcfa89d7248a82d9f111
118 | Data: f
119 | Hash: 1b3dae70b4b0a8fd252a7879ec67283c0176729bfebc51364fb9e9fb0598ba9e
120 | Hash: 3f79bb7b435b05321651daefd374cdc681dc06faa65e374e38337b88ca046dea
121 | Data: e
122 | Hash: 252f10c83610ebca1a059c0bae8255eba2f95be4d1d7bcfa89d7248a82d9f111
123 | Data: f
124 | ```
125 |
126 | `save_svg()`方法将这个树转化为svg图片,代码有些长就不放到文章里了,可以在github上获取文章相关代码
127 |
128 | 这个树的输出图片如下
129 | 
130 |
131 |
132 | ### 修改我们的区块链
133 | 我们已经实现了一个Merkle Tree,下面让我们把它应用到我们的区块链上
134 |
135 | 1. 首先修改我们的`Block`类,用merkle树替代输入的data
136 | ```py
137 | class Block:
138 | def __init__(self, index, merkle_tree: MerkleTree, previous_hash):
139 | self.index = index
140 | self.timestamp = time.time() # 当前时间戳
141 | self.merkle_tree = merkle_tree
142 | self.merkle_root = merkle_tree.get_root_hash()
143 | self.previous_hash = previous_hash
144 | self.hash = self.calculate_hash()
145 |
146 |
147 | def calculate_hash(self):
148 | # 计算区块的hash值
149 | # 这里我们使用sha256算法
150 | sha = hashlib.sha256()
151 | sha.update(
152 | str(self.index).encode("utf-8")
153 | + str(self.timestamp).encode("utf-8")
154 | + str(self.merkle_root).encode("utf-8")
155 | + str(self.previous_hash).encode("utf-8")
156 | )
157 | return sha.hexdigest()
158 | ```
159 |
160 | > 为什么计算哈希值时不把整个merkle树放进去?
161 | 我们在`calculate_hash`函数中没有把整个树结构放进去,这是因为merkle_root已经有了整个树的信息摘要,如果树发生了变化那么它的根也会变化,所以没必要将整个树计算哈希增加工作量
162 |
163 | 2. 对于`BlockChain`类,只需要修改对应输入的数据格式即可适应这个改变
164 |
165 | ```py
166 |
167 | class BlockChain:
168 | def __init__(self):
169 | self.chain = [self.create_genesis_block()]
170 | self.height = 1
171 |
172 | def create_genesis_block(self):
173 | return Block(0, MerkleTree(["Genesis Block"]), "0")
174 |
175 | def get_latest_block(self):
176 | return self.chain[-1]
177 |
178 | def add_block(self, data: list):
179 | previous_block = self.get_latest_block()
180 | new_block = Block(self.height, MerkleTree(data), previous_block.hash)
181 | self.chain.append(new_block)
182 | self.height += 1
183 | ```
184 |
185 | 3. 下面我们测试一下这个使用了Merkle树的区块链,最后使用的`save_svg`将该区块链可视化为svg图片,代码太长不在文章中展示。
186 |
187 | ```py
188 | if __name__ == "__main__":
189 | blockchain = BlockChain()
190 | time.sleep(1)
191 | blockchain.add_block(["First Block","hello world","yes"])
192 | time.sleep(1)
193 | blockchain.add_block(["Second Block","BlockChain","I like money"])
194 | blockchain.save_svg("blockchain.svg")
195 | ```
196 | 输出图片如下
197 | 
198 |
199 |
200 | 这一节我们聊了聊Merkle树,我们提到了轻节点和全节点,下一节我们将谈谈区块链中节点的分工合作。
--------------------------------------------------------------------------------
/pic/digital_signature.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Block.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import time
3 | import json
4 | from MerkleTree import MerkleTree, merkle_tree_from_json
5 |
6 |
7 | class Block:
8 | def __init__(
9 | self, index, merkle_tree: MerkleTree, previous_hash, nonce, timestamp=None, miner_address=None
10 | ):
11 | self.index = index
12 | self.timestamp = time.time() if timestamp is None else timestamp # 当前时间戳
13 | self.merkle_tree = merkle_tree
14 | self.merkle_root = merkle_tree.get_root_hash()
15 | self.previous_hash = previous_hash
16 | self.nonce = nonce
17 | self.miner_address = miner_address # 添加矿工节点的IP地址
18 | self.hash = self.calculate_hash()
19 |
20 | def calculate_hash(self):
21 | # 计算区块的hash值
22 | # 这里我们使用sha256算法
23 | sha = hashlib.sha256()
24 | sha.update(
25 | str(self.index).encode("utf-8")
26 | + str(self.timestamp).encode("utf-8")
27 | + str(self.merkle_root).encode("utf-8")
28 | + str(self.previous_hash).encode("utf-8")
29 | + str(self.nonce).encode("utf-8")
30 | )
31 | return sha.hexdigest()
32 |
33 | def to_json(self):
34 | # 自定义序列化过程
35 | block_dict = {
36 | 'index': self.index,
37 | 'timestamp': self.timestamp,
38 | 'merkle_tree': self.merkle_tree.to_json(),
39 | 'previous_hash': self.previous_hash,
40 | 'nonce': self.nonce,
41 | 'hash': self.hash,
42 | 'miner_address': self.miner_address
43 | }
44 | return json.dumps(block_dict, ensure_ascii=False)
45 |
46 | def __str__(self, show_merkle_tree=False):
47 | if show_merkle_tree:
48 | return f"Block(index={self.index},\n timestamp={self.timestamp},\n merkle_root={self.merkle_root},\n previous_hash={self.previous_hash},\n hash={self.hash}) \n merkle tree: \n{self.merkle_tree.__str__()}"
49 | else:
50 | return f"Block(index={self.index},\n timestamp={self.timestamp},\n merkle_root={self.merkle_root},\n previous_hash={self.previous_hash},\n hash={self.hash})"
51 |
52 | def draw_svg(self):
53 | # 设置基本参数
54 | node_height = 50 # 节点高度
55 | level_height = 120 # 层间距
56 |
57 | # 计算Merkle树的层数和叶子节点数
58 | def get_tree_height(node):
59 | if not node:
60 | return 0
61 | return 1 + max(get_tree_height(node.left), get_tree_height(node.right))
62 |
63 | def count_leaves(node):
64 | if not node:
65 | return 0
66 | if not node.left and not node.right:
67 | return 1
68 | return count_leaves(node.left) + count_leaves(node.right)
69 |
70 | # 计算叶子节点宽度
71 | def get_node_width(node):
72 | if not node:
73 | return 120 # 默认宽度
74 | if node.data is not None:
75 | # 根据数据长度调整宽度,最小120
76 | return max(120, len(str(node.data)) * 15)
77 | return 120 # 非叶子节点使用默认宽度
78 |
79 | tree_height = get_tree_height(self.merkle_tree.root)
80 | leaf_count = count_leaves(self.merkle_tree.root)
81 |
82 | # 计算画布尺寸
83 | max_leaf_width = max(get_node_width(leaf) for leaf in self.merkle_tree.leaves)
84 | width = max(1200, leaf_count * max_leaf_width * 1.5)
85 | height = max(
86 | 800, (tree_height + 3) * level_height * 1.5
87 | ) # 增加高度以容纳区块头
88 |
89 | # 生成SVG头部
90 | svg = [f'']
91 | svg.append(
92 | f'")
181 |
182 | return "\n".join(svg)
183 |
184 | def save_svg(self, filename):
185 | svg_content = self.draw_svg()
186 | with open(filename, "w") as f:
187 | f.write(svg_content)
188 |
189 | def block_from_json(block_json):
190 | # 将JSON字符串解析为字典
191 | block_data = json.loads(block_json)
192 |
193 | # 从字典中提取构造Block对象所需的参数
194 | index = block_data['index']
195 | previous_hash = block_data['previous_hash']
196 | nonce = block_data['nonce']
197 | timestamp = block_data.get('timestamp', None)
198 |
199 | # 反序列化MerkleTree
200 | merkle_tree_data = block_data['merkle_tree']
201 | merkle_tree = merkle_tree_from_json(merkle_tree_data)
202 |
203 | # 创建并返回Block对象
204 | block = Block(index, merkle_tree, previous_hash, nonce, timestamp)
205 | block.hash = block_data['hash'] # 直接使用已保存的哈希值
206 | block.miner_address = block_data['miner_address']
207 | return block
208 |
209 |
210 | if __name__ == "__main__":
211 | block = Block(0, MerkleTree(["a", "b", "c"]), "0", 0)
212 | print(block)
213 | print(block.merkle_tree)
214 | print("--------------------------------")
215 | block = block_from_json(block.to_json())
216 | print(block)
217 | print(block.merkle_tree)
218 | print([leaf.data for leaf in block.merkle_tree.leaves])
219 |
--------------------------------------------------------------------------------
/BlockChain.py:
--------------------------------------------------------------------------------
1 | from Block import Block,block_from_json
2 | import time
3 | from MerkleTree import MerkleTree
4 | import json
5 |
6 | class BlockChain:
7 | def __init__(self):
8 | self.chain = [self.create_genesis_block()]
9 | self.height = 1
10 | self.difficulty = 5
11 |
12 | def create_genesis_block(self):
13 | return Block(0, MerkleTree(["Genesis Block"]), "0", 0, timestamp=0)
14 |
15 | def get_latest_block(self):
16 | return self.chain[-1]
17 |
18 | def add_block(self, data: list):
19 | previous_block = self.get_latest_block()
20 | new_block = Block(self.height, MerkleTree(data), previous_block.hash,0)
21 | self.chain.append(new_block)
22 | self.height += 1
23 |
24 | def append_block(self,block:Block):
25 | self.chain.append(block)
26 | self.height += 1
27 |
28 | def is_chain_valid(self):
29 | for i in range(1, len(self.chain)):
30 | current_block = self.chain[i]
31 | previous_block = self.chain[i - 1]
32 | if current_block.hash != current_block.calculate_hash():
33 | return (
34 | False,
35 | f"The hash of the {current_block.index} block is not equal to the calculated hash",
36 | )
37 | if current_block.previous_hash != previous_block.hash:
38 | return (
39 | False,
40 | f"The previous hash of the {current_block.index} block is not equal to the hash of the {previous_block.index} block",
41 | )
42 | if not current_block.hash.startswith("0" * self.difficulty):
43 | return (
44 | False,
45 | f"The hash of the {current_block.index} block does not start with {self.difficulty} zeros",
46 | )
47 | return True
48 |
49 | def is_block_valid(self,block:Block):
50 | if block.index != self.height:
51 | return False
52 | if not block.hash.startswith("0" * self.difficulty):
53 | return False
54 | if block.previous_hash != self.chain[-1].hash:
55 | return False
56 | if block.hash != block.calculate_hash():
57 | return False
58 | return True
59 |
60 | def to_json(self):
61 | return json.dumps([block.to_json() for block in self.chain[1:]], ensure_ascii=False)
62 |
63 | def draw_svg(self):
64 | # 设置基本参数
65 | block_width = 400 # 区块宽度
66 | block_height = 150 # 区块头高度
67 | arrow_size = 20 # 箭头大小
68 | node_height = 50 # Merkle树节点高度
69 | level_height = 120 # Merkle树层间距
70 |
71 | # 计算每个区块的Merkle树高度
72 | def calculate_tree_height(node):
73 | if not node:
74 | return 0
75 | return 1 + max(
76 | calculate_tree_height(node.left), calculate_tree_height(node.right)
77 | )
78 |
79 | # 计算每个区块的间隔
80 | block_spacings = []
81 | for block in self.chain:
82 | tree_height = calculate_tree_height(block.merkle_tree.root)
83 | block_spacing = max(200, tree_height * level_height)
84 | block_spacings.append(block_spacing)
85 |
86 | # 计算总宽度和高度
87 | total_width = sum(block_width + spacing for spacing in block_spacings)
88 | total_height = block_height + 800 # 额外高度用于显示Merkle树
89 |
90 | # 生成SVG头部
91 | svg = ['']
92 | svg.append(
93 | f'") # 确保SVG标签闭合
202 | return "\n".join(svg)
203 |
204 | def save_svg(self, filename):
205 | svg_content = self.draw_svg()
206 | with open(filename, "w") as f:
207 | f.write(svg_content)
208 |
209 | def block_chain_from_json(block_chain_json):
210 | block_chain_dict = json.loads(block_chain_json)
211 | block_chain = BlockChain()
212 | for block in block_chain_dict:
213 | block_chain.append_block(block_from_json(block))
214 | return block_chain
215 |
216 | if __name__ == "__main__":
217 | blockchain = BlockChain()
218 | time.sleep(1)
219 | blockchain.add_block(["First Block", "hello world", "yes"])
220 | time.sleep(1)
221 | blockchain.add_block(["Second Block", "BlockChain", "I like money"])
222 | # blockchain.save_svg("blockchain.svg")
223 | print(blockchain.to_json())
224 | print([blockchain.chain[i].hash for i in range(len(blockchain.chain))])
225 | print([block_chain_from_json(blockchain.to_json()).chain[i].hash for i in range(len(block_chain_from_json(blockchain.to_json()).chain))])
226 |
--------------------------------------------------------------------------------
/Tutor/7_Transaction.md:
--------------------------------------------------------------------------------
1 | ## Transaction
2 |
3 | 我们上一节对我们的数字货币的实现方式进行了梳理,这里我们开始构建一个UTXO模型的电子货币体系,并把它加入我们的区块链网络中。
4 |
5 | 上一节我们说到,UTXO模型中所有的交易都至少有一个UTXO输入和一个UTXO输出,并且输入和输出的金额应该是一致的。就和我们使用现金消费一样,你拿出一叠总金额大于商品价格的现金组合给老板,老板将钱收下后计算多余的金额然后进行找零。原来你们两个一起共有一百块,交易后还是一百块。那么,钱一开始从哪里来呢?
6 |
7 | 在现实中,我们的人民币是从印钞厂印制出来,然后通过银行发行到全国。而在我们的区块链中则大不相同。回想我们POW那一节,我们说到对于拿到记账权的节点会将区块的第一笔交易设置为一个特殊交易,凭空产生一定的货币转到自己的地址中。这是比特币采用的方法,这个"一定的货币"的量随时间递减,称为比特币的**减半**,所有比特币的总量是一定的,为2100万枚。而以太坊的发行量是动态的,是由协议规则、社区共识、EIP提案以及网络经济模型共同决定的。机制比较复杂,这里先按下不表。
8 |
9 | ---
10 |
11 | ### 交易和UTXO
12 |
13 | 下面我们开始实现Transaction和UTXO的实现
14 |
15 | 对于一个UTXO,我们需要记录的有:
16 | - 产生这个UTXO的交易哈希值 : 比特币白皮书中说"我们将一枚电子货币定义为一条数字签名链",我们便是通过这个给UTXO一路过来经历的交易将这些签名串到一起。而我们记录了交易的哈希值就可以找到这个交易,这个原理在本节最后再说
17 | - 这个UTXO在交易输出中的索引 : 交易可以有多个输出,所以我们需要记录它的索引才可以找到它
18 | - UTXO的金额 : 这个不用解释了
19 | - 接收者的地址 : 也就是目前这个UTXO拥有者的地址
20 |
21 | ```py
22 | class UTXO:
23 | def __init__(self, tx_hash, output_index, amount, recipient_address):
24 | self.tx_hash = tx_hash # 交易的哈希
25 | self.output_index = output_index # 输出在交易中的索引
26 | self.amount = amount # 金额
27 | self.recipient_address = recipient_address # 接收者(拥有者)的地址
28 | ```
29 |
30 | 而对于一个交易,我们需要记录的是:
31 | - 交易的输入与输出 : 两个列表,储存输入和输出的UTXO
32 | - 交易的哈希 : 这个交易的哈希值,用来唯一识别这个交易
33 | - 交易签名 : 交易发起者(付钱的)对这个交易哈希的签名
34 | - 发送者的公钥 : 用于验证这个签名
35 |
36 | ```py
37 | class Transaction:
38 | def __init__(self):
39 | self.inputs = [] # 交易输入
40 | self.outputs = [] # 交易输出
41 | self.tx_hash = None # 交易哈希
42 | self.signature = None # 交易签名
43 | self.sender_public_key = None # 发送者的公钥
44 | ```
45 |
46 | 具体的一些函数实现就不放在这里了,可以在github仓库找到
47 |
48 | ### 使我们的Wallet类可以适配交易
49 |
50 | 我们主要需要添加两个方法:创建一个交易(付钱),接收到一个交易后更新余额(收钱)
51 |
52 | 要注意:收钱不需要收款方的操作,当付款人提交交易到链上达到共识后,这笔钱已经在你的地址上了。我们这里是要实现在钱包这边更新一下显示的余额,而不是实现收款。
53 |
54 | 我们的`create_transaction`方法首先选择足够的UTXO添加进交易,如果钱不够就报错退出(没钱就是没钱,再怎么算都付不了钱)。然后在交易中添加需要转账的部分,之后计算找零,添加到输出中。最后计算哈希值并签名,更新自己的utxo池
55 |
56 | 而`receive_transaction`方法便是先验证一次交易的签名,然后遍历所有的输出,将输出到自己地址下的UTXO记录下来。
57 |
58 | ```py
59 | class Wallet:
60 | def __init__(self):
61 | self.private_key = random.randbytes(32) # 256位私钥
62 | self.public_key = generate_public_key(self.private_key)
63 | self.address = generate_btc_address(self.public_key)
64 | self.utxo_pool = [] # 钱包中的UTXO集合
65 | # ...省略其他代码 (i_i)
66 |
67 | def create_transaction(self, recipient_address, amount):
68 | # 1. 选择足够的UTXO
69 | selected_utxos = []
70 | total_amount = 0
71 | for utxo in self.utxo_pool:
72 | selected_utxos.append(utxo)
73 | total_amount += utxo.amount
74 | if total_amount >= amount:
75 | break
76 |
77 | if total_amount < amount:
78 | raise ValueError("Don't have enough coins")
79 |
80 | # 2. 创建交易
81 | tx = Transaction()
82 | for utxo in selected_utxos:
83 | tx.add_input(utxo)
84 |
85 | # 3. 创建交易输出
86 | tx.add_output(amount, recipient_address)
87 |
88 | # 4. 找零
89 | change = total_amount - amount
90 | if change > 0:
91 | tx.add_output(change, self.address)
92 |
93 | # 5. 计算交易哈希
94 | tx.calculate_hash()
95 |
96 | # 6. 签名交易
97 | tx.sign(self.private_key)
98 |
99 | # 7. 更新UTXO池
100 | self.utxo_pool = [utxo for utxo in self.utxo_pool if utxo not in selected_utxos]
101 |
102 | return tx
103 |
104 | def receive_transaction(self, transaction: Transaction):
105 | # 1. 验证交易签名
106 | if not transaction.verify_signature():
107 | raise ValueError("Invalid transaction signature")
108 |
109 | # 2. 更新UTXO池
110 | for output in transaction.outputs:
111 | if output.recipient_address == self.address:
112 | new_utxo = UTXO(transaction.tx_hash, transaction.outputs.index(output), output.amount, output.recipient_address)
113 | self.utxo_pool.append(new_utxo)
114 |
115 | ```
116 |
117 | ### 修改我们的节点
118 | 接下来我们要修改我们的节点类。我们现在可以通过wallet类来创建交易以及接收交易修改自己的余额显示,但是交易的进行需要在节点打包区块时实现。节点在p2p网络中收集各个用户发出的交易,然后对交易在本地进行检查合法性,对合法的交易进行打包。然后进行挖矿,就是我们前面pow一节提过的内容了。要注意的是,我们钱包中检查交易只验证了交易的签名,但是我们节点中不仅要检查签名,还需要检查这个操作是否合法,交易花的UTXO是否存在,有没有出现双花的情况等等。
119 |
120 | 我们还需要修改挖矿方法,每个区块的第一笔交易应该是一个没有输入只有一个输出给节点自己钱包地址的交易作为挖矿奖励,这里我们将奖励设置为50。简单起见,我们也不实现比特币的减半操作,以及交易gas费了。
121 |
122 | 我们首先修改Node的变量,添加了交易池和UTXO池,前者储存收集到需要打包的交易,后者储存整个区块链中的UTXO。注意到utxo池使用的是一个哈希表(字典)结构,它的key是交易哈希以及UTXO在交易输出中的索引。我们提到可以使用tx_hash唯一确定交易,而输出索引也是独一无二的,因此可以使用这个作为key来定位UTXO。
123 |
124 | ```py
125 | class Node:
126 | def __init__(self, ip):
127 | # ...省略和这里无关的代码
128 | self.wallet = Wallet()
129 | self.mempool = [] # 交易池
130 | self.mempool_lock = threading.Lock() # 交易池的锁
131 | self.global_utxo_pool = {} # 全局UTXO池,格式: {tx_hash:output_index: UTXO}
132 | self.utxo_pool_lock = threading.Lock() # 全局UTXO池的锁
133 | ```
134 |
135 | 接着,我们创建一个检验这个区块中所有交易的函数,这里并不实现交易而是单纯的检验。如果是第一笔交易,检查金额是否为约定的50后便通过。
136 | 对于其他交易,首先验证签名,然后验证输入的UTXO是否存在合法,最后验证金额是否正确。最后函数返回这些交易是否合法,以及不合法的交易列表。
137 |
138 | ```py
139 | def verify_block_transactions(self, block):
140 | """验证区块中的所有交易,返回(是否全部有效, 无效交易列表)"""
141 | transactions = []
142 | invalid_transactions = []
143 |
144 | for leaf in block.merkle_tree.leaves:
145 | try:
146 | tx = Transaction.from_json(json.loads(leaf.data))
147 | transactions.append(tx)
148 | except:
149 | continue # 跳过非交易数据
150 |
151 | # 临时存储已使用的UTXO
152 | used_utxos = set()
153 |
154 | with self.utxo_pool_lock:
155 | for tx in transactions:
156 | # 如果是挖矿奖励交易(第一个交易),直接验证通过
157 | if transactions.index(tx) == 0:
158 | if tx.outputs[0].amount != 50:
159 | print(f"Invalid mining reward transaction: {tx.tx_hash}")
160 | invalid_transactions.append(tx)
161 | continue
162 |
163 | try:
164 | # 1. 验证交易签名
165 | if not tx.verify_signature():
166 | print(f"Invalid transaction signature: {tx.tx_hash}")
167 | invalid_transactions.append(tx)
168 | continue
169 |
170 | # 2. 验证输入UTXO
171 | total_input = 0
172 | valid_inputs = True
173 |
174 | for utxo in tx.inputs:
175 | # 检查UTXO是否已被使用(防止双重支付)
176 | utxo_key = f"{utxo.tx_hash}:{utxo.output_index}"
177 | if utxo_key in used_utxos:
178 | print(f"Double spending detected: {utxo_key}")
179 | valid_inputs = False
180 | break
181 |
182 | # 在全局UTXO池中查找这个UTXO
183 | if utxo_key in self.global_utxo_pool:
184 | total_input += self.global_utxo_pool[utxo_key].amount
185 | used_utxos.add(utxo_key)
186 | else:
187 | print(f"UTXO not found in global pool: {utxo_key}")
188 | valid_inputs = False
189 | break
190 |
191 | if not valid_inputs:
192 | invalid_transactions.append(tx)
193 | continue
194 |
195 | # 3. 验证输入输出金额
196 | total_output = sum(output.amount for output in tx.outputs)
197 | if total_input != total_output:
198 | print(f"Input/output amount mismatch: {tx.tx_hash}")
199 | invalid_transactions.append(tx)
200 | continue
201 |
202 | except Exception as e:
203 | print(f"Error verifying transaction {tx.tx_hash}: {e}")
204 | invalid_transactions.append(tx)
205 | continue
206 |
207 | return len(invalid_transactions) == 0, invalid_transactions
208 | ```
209 |
210 | 继续我们实现了一个执行交易的函数,它会修改本地的UTXO池等状态变量
211 |
212 | ```py
213 |
214 | def process_block_transactions(self, block):
215 | """处理区块中的所有交易,更新UTXO池"""
216 | transactions = []
217 | for leaf in block.merkle_tree.leaves:
218 | try:
219 | # 解析JSON字符串
220 | tx_data = json.loads(leaf.data)
221 | tx = Transaction.from_json(tx_data)
222 | transactions.append(tx)
223 | except Exception as e:
224 | continue
225 |
226 | with self.utxo_pool_lock:
227 | for tx in transactions:
228 | # 如果是挖矿奖励交易(第一个交易),直接添加输出到UTXO池
229 | if transactions.index(tx) == 0:
230 | for i, output in enumerate(tx.outputs):
231 | utxo_key = f"{tx.tx_hash}:{i}"
232 | self.global_utxo_pool[utxo_key] = output
233 | if output.recipient_address == self.wallet.address:
234 | self.wallet.add_utxo(output)
235 | continue
236 |
237 | # 对于普通交易,直接更新UTXO池
238 | # 1. 移除已使用的UTXO
239 | for utxo in tx.inputs:
240 | utxo_key = f"{utxo.tx_hash}:{utxo.output_index}"
241 | self.global_utxo_pool.pop(utxo_key, None)
242 |
243 | # 2. 添加新的UTXO
244 | for i, output in enumerate(tx.outputs):
245 | utxo_key = f"{tx.tx_hash}:{i}"
246 | self.global_utxo_pool[utxo_key] = output
247 | # 如果是发给自己的,也添加到钱包的UTXO池
248 | if output.recipient_address == self.wallet.address:
249 | self.wallet.add_utxo(output)
250 |
251 | ```
252 |
253 |
254 | 然后我们修改挖矿线程,原理和之前POW中谈到的一样,但是将第一个数据修改为了挖矿激励交易。在进行打包前,我们先对交易池中的所有交易进行检查,删去非法的交易后再使用合法的交易构建Merkle树,继续进行挖矿操作。
255 |
256 | ```py
257 |
258 | def mine_thread(self):
259 | while True:
260 | with self.data_lock:
261 | # 创建挖矿奖励交易
262 | mining_reward = Transaction()
263 | reward_output = UTXO(
264 | tx_hash=None,
265 | output_index=0,
266 | amount=50,
267 | recipient_address=self.wallet.address
268 | )
269 | mining_reward.outputs.append(reward_output)
270 | mining_reward.tx_hash = mining_reward.calculate_hash()
271 |
272 | # 从交易池中选择交易
273 | selected_transactions = []
274 | with self.mempool_lock:
275 | selected_transactions = self.mempool
276 |
277 | # 将挖矿奖励交易放在第一位
278 | all_transactions = [mining_reward] + selected_transactions
279 |
280 | # 创建merkle树(注意这里需要先将交易转换为JSON字符串)
281 | merkle_tree = MerkleTree([json.dumps(tx.to_json()) for tx in all_transactions])
282 |
283 | # 创建区块并验证交易
284 | block = Block(self.blockchain.height, merkle_tree,
285 | self.blockchain.get_latest_block().hash,
286 | 0, time.time(), self.address)
287 |
288 | # 验证交易并获取无效交易列表
289 | is_valid, invalid_txs = self.verify_block_transactions(block)
290 |
291 | # 如果有无效交易,从交易池和选中的交易中移除它们
292 | if invalid_txs:
293 | with self.mempool_lock:
294 | self.mempool = [tx for tx in self.mempool if tx not in invalid_txs]
295 | selected_transactions = [tx for tx in selected_transactions if tx not in invalid_txs]
296 |
297 | # 重新创建merkle树(包含奖励交易和有效交易)
298 | all_transactions = [mining_reward] + selected_transactions
299 | merkle_tree = MerkleTree([json.dumps(tx.to_json()) for tx in all_transactions])
300 | block = Block(self.blockchain.height, merkle_tree,
301 | self.blockchain.get_latest_block().hash,
302 | 0, time.time(), self.address)
303 |
304 | # 尝试挖矿
305 | nonce = 0
306 | while not self.getBlock:
307 | block.nonce = nonce
308 | block.hash = block.calculate_hash()
309 | if self.blockchain.is_block_valid(block):
310 | with self.blockchain_lock:
311 | self.send_block(block)
312 | self.blockchain.append_block(block)
313 | # 处理新区块中的交易
314 | self.process_block_transactions(block)
315 | # 从交易池中移除已确认的交易
316 | with self.mempool_lock:
317 | self.mempool = [tx for tx in self.mempool
318 | if tx not in selected_transactions]
319 | print(f"Mined block {block.index} :{block.hash}")
320 | break
321 | nonce += 1
322 |
323 | with self.signal_lock:
324 | self.getBlock = False
325 |
326 | ```
327 |
328 | 这里便将交易功能加入我们的区块链了,我们会在后面使用这个区块链进行测试。需要一提的是,我们在接收其他节点传来的区块链时,也需要检查区块链中所有交易是否合法,如果存在非法交易则拒绝接收这个区块链,而如果该区块链合法,则需要遍历一遍区块链建立UTXO池。
329 |
330 | ---
331 |
332 | ### 建立一个图形化界面
333 |
334 | 我们接下来使用tkinter建立一个简单的图形化界面(这个库python自带,不需要pip),界面如下
335 |
336 | 
337 |
338 | 在初始化绑定ip地址之后(实际中应该是绑定自己主机的公网ip,但是这里在本地演示我们使用本地的回环地址),程序开始运行节点,并跟踪显示余额。可以通过输入接收者的钱包地址和转账金额实现转账。
339 |
340 | 如果你在本地运行几个节点,你可能会发现某个钱包一开始挖到了一个区块获得了50个coin的奖励,但是过了一会这50个coin的奖励不见了。这便是区块链**分叉**之后又重新形成了共识。我们的区块链网络中,每个全节点都在努力地挖矿,如果挖到一个区块就添加到本地的区块链后广播给其他节点。但是由于网络延迟(或者故意为之),这个网络中可能维护了两条区块链。在我们这个例子中,这个节点就是维护了一个和其他一部分节点不同的区块链,在进行通信后发现本地的区块链长度比另外一条链更短,于是更新接收这条长链,重新达到共识,这奖励的50coin也就消失了。
341 |
342 | 代码没什么实在的东西,可以到github仓库获取。
343 |
344 | ---
345 |
346 | 这里回头来填前面的坑
347 |
348 | **为什么我们记录了交易的索引就可以找到交易?**
349 | 在回答这个问题之前,我再问一个问题**我们的区块链是如何储存的?**
350 | 区块链既然是个链,那么很多人可能就会觉得应该顺序储存就行了,我们前面的实现也全部是在内存中使用一个列表记录,没有储存到磁盘中。但是实际上比特币的储存使用的leveldb是一个key-value的数据库(你可以理解为一个哈希表或者字典),每个区块的key是该区块的哈希值。也就是说我们获取一个区块的哈希值就可以找到这个区块的数据,通过从最新的区块出发,访问它的previous_hash位置就可以获取上一个区块,这样一直向后查找就可以实现区块链的遍历。而我们的交易也存在哈希值,所以交易的储存也使用了同样的方式。
351 |
352 | 如果你知道哈希表,那你可能会问:这样不会发生哈希碰撞吗?你或许听过: "一个屋子里只要超过23个人,那么就有50%的可能性有两个人同一天出生" ,这个数学结论(生日攻击)在这里就不适用了吗?
353 |
354 | 这就要谈到SHA256的抗碰撞性了,下面我们简单算笔账:
355 |
356 | 首先我们看一下生日攻击,如果取值范围为N,需要取k个值,那么没有碰撞的概率为 $ P_{没有碰撞}=\frac{N(N-1)...(N-k+1)}{N^k} $ 那么发生碰撞的概率就是
357 |
358 | $ P_{碰撞} = 1-\frac{N(N-1)...(N-k+1)}{N^k} = 1- \Pi_{i=0}^{k-1}(1-\frac{i}{N})$
359 |
360 | 当x较小时$ ln(1-x) \approx -x $
361 |
362 | 计算$ ln(\Pi_{i=0}^{k-1}(1-\frac{i}{N})) = \sum_{i=0}^{k-1}ln(1-\frac{i}{N}) \approx - \frac{\sum_{i=0}^{k-1} i}{N} = -\frac{k(k-1)}{2N}$
363 |
364 | 所以去掉对数放回概率中,$P_{碰撞}=1-e^{-\frac{k(k-1)}{2N}}$
365 |
366 |
367 | 我们的tx_id是一个256位的哈希值,那么它一共有 $ 2^{256} $ 个可能值,计算得到大约需要 $ 2^{128} $ 这个数量级碰撞概率才会达到50%,而比特币网络运行到现在只产生了约1,130,074,839笔交易(数据来自https://btc.tokenview.io/cn),这个数字只是 $ 2^{30} $ 数量级,带入刚刚的公式,碰撞的概率是$ P_{碰撞}= 1-e^{-\frac{1}{2^{197}}} \approx 5 \times 10^{-60} $ 这绝对是一个小概率事件,在找到攻破SHA256算法的方法之前,这就像是在宇宙中两次随机捡起同一粒沙子。
368 |
369 |
370 |
371 | ---
372 |
373 | 目前为止,我们已经实现了一个初具雏形的区块链了!这个专栏的主要内容告一段落,在后面可能会基于这个区块链添加一些复杂一点的功能(比如SPV,gas费之类的),或者会更新一些区块链其他方面的内容。
--------------------------------------------------------------------------------
/Node.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import threading
3 | import time
4 | from BlockChain import BlockChain, block_chain_from_json
5 | from Block import Block, block_from_json
6 | from MerkleTree import MerkleTree, merkle_tree_from_json
7 | import json
8 | from Wallet import Wallet
9 | from Transaction import Transaction,UTXO
10 |
11 | class Node:
12 | def __init__(self, ip):
13 | self.wallet = Wallet()
14 | self.node_ip = ip
15 | self.peers = {"127.0.0.1"} # 种子节点
16 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
17 | self.socket.bind((ip, 5000)) # 监听5000端口
18 | self.socket.listen(5)
19 | self.hello_dict = {}
20 | self.blockchain = BlockChain()
21 | self.data_queue = [f"Created by {ip}",]
22 | self.getBlock=False
23 | self.blockchain_lock = threading.Lock() # 添加线程锁
24 | self.signal_lock = threading.Lock()
25 | self.data_lock = threading.Lock()
26 | self.mempool = [] # 交易池
27 | self.mempool_lock = threading.Lock() # 交易池的锁
28 | self.global_utxo_pool = {} # 全局UTXO池,格式: {tx_hash:output_index: UTXO}
29 | self.utxo_pool_lock = threading.Lock() # 全局UTXO池的锁
30 | while True:
31 | try:
32 | self.send_join() # 发送加入消息
33 | break
34 | except:
35 | time.sleep(1)
36 | self.handle_thread = threading.Thread(target=self.handle_connection) # 处理连接
37 | self.handle_thread.start()
38 | self.helloloop_thread = threading.Thread(target=self.helloloop) # 心跳检测
39 | self.helloloop_thread.start()
40 | self.mine_thread = threading.Thread(target=self.mine_thread)
41 | self.mine_thread.start()
42 | self.send_data_thread = threading.Thread(target=self.send_data_thread)
43 | self.send_data_thread.start()
44 |
45 | # 初始化UTXO池
46 | self.init_utxo_pool()
47 |
48 | # 启动UTXO同步线程
49 | self.sync_thread = threading.Thread(target=self.sync_utxo_thread)
50 | self.sync_thread.daemon = True
51 | self.sync_thread.start()
52 |
53 | def send_msg(self, msg):
54 | for peer in list(self.peers):
55 | if peer == self.node_ip:
56 | continue
57 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
58 | try:
59 | s.connect((peer, 5000))
60 | s.sendall(msg.encode("utf-8"))
61 | except:
62 | print(f"Failed to send message to {peer}")
63 |
64 | def send_data(self, data):
65 | self.send_msg(f"@DATA{data}")
66 |
67 | def send_hello(self):
68 | self.send_msg(f"@HELLO{self.node_ip}")
69 |
70 | def send_join(self):
71 | self.send_msg(f"@JOIN{self.node_ip}")
72 |
73 | def send_intro(self,addr):
74 | self.send_msg(f"#INTRO{addr}")
75 |
76 | def send_block(self,block:Block):
77 | self.send_msg(f"@ONEBLOCK{block.to_json()}")
78 |
79 | def verify_block_transactions(self, block):
80 | """验证区块中的所有交易,返回(是否全部有效, 无效交易列表)"""
81 | transactions = []
82 | invalid_transactions = []
83 |
84 | for leaf in block.merkle_tree.leaves:
85 | try:
86 | tx = Transaction.from_json(json.loads(leaf.data))
87 | transactions.append(tx)
88 | except:
89 | continue # 跳过非交易数据
90 |
91 | # 临时存储已使用的UTXO
92 | used_utxos = set()
93 |
94 | with self.utxo_pool_lock:
95 | for tx in transactions:
96 | # 如果是挖矿奖励交易(第一个交易),直接验证通过
97 | if transactions.index(tx) == 0:
98 | if tx.outputs[0].amount != 50:
99 | print(f"Invalid mining reward transaction: {tx.tx_hash}")
100 | invalid_transactions.append(tx)
101 | continue
102 |
103 | try:
104 | # 1. 验证交易签名
105 | if not tx.verify_signature():
106 | print(f"Invalid transaction signature: {tx.tx_hash}")
107 | invalid_transactions.append(tx)
108 | continue
109 |
110 | # 2. 验证输入UTXO
111 | total_input = 0
112 | valid_inputs = True
113 |
114 | for utxo in tx.inputs:
115 | # 检查UTXO是否已被使用(防止双重支付)
116 | utxo_key = f"{utxo.tx_hash}:{utxo.output_index}"
117 | if utxo_key in used_utxos:
118 | print(f"Double spending detected: {utxo_key}")
119 | valid_inputs = False
120 | break
121 |
122 | # 在全局UTXO池中查找这个UTXO
123 | if utxo_key in self.global_utxo_pool:
124 | total_input += self.global_utxo_pool[utxo_key].amount
125 | used_utxos.add(utxo_key)
126 | else:
127 | print(f"UTXO not found in global pool: {utxo_key}")
128 | valid_inputs = False
129 | break
130 |
131 | if not valid_inputs:
132 | invalid_transactions.append(tx)
133 | continue
134 |
135 | # 3. 验证输入输出金额
136 | total_output = sum(output.amount for output in tx.outputs)
137 | if total_input != total_output:
138 | print(f"Input/output amount mismatch: {tx.tx_hash}")
139 | invalid_transactions.append(tx)
140 | continue
141 |
142 | except Exception as e:
143 | print(f"Error verifying transaction {tx.tx_hash}: {e}")
144 | invalid_transactions.append(tx)
145 | continue
146 |
147 | return len(invalid_transactions) == 0, invalid_transactions
148 |
149 | def parse_block(self, block_json):
150 | if not block_json.strip():
151 | print("Received empty block JSON")
152 | return
153 | try:
154 | block = block_from_json(block_json)
155 | except json.JSONDecodeError as e:
156 | print(f"Failed to decode block JSON: {e}")
157 | return
158 | if not block.merkle_tree or not block.merkle_tree.leaves:
159 | print("Merkle tree or leaves are not initialized")
160 | return
161 |
162 | if block.index > self.blockchain.height + 1:
163 | # 直接使用区块中记录的矿工节点IP地址
164 | self.send_blockchain_request(block.miner_address)
165 | print(f"Request blockchain from {block.miner_address}")
166 | else:
167 | with self.blockchain_lock:
168 | if self.blockchain.is_block_valid(block):
169 | # 先验证区块的所有交易
170 | if not self.verify_block_transactions(block):
171 | print(f"Rejected block {block.index}: invalid transactions")
172 | return
173 |
174 | # 如果所有交易都验证通过,接受区块并处理交易
175 | with self.signal_lock:
176 | self.getBlock = True
177 | self.blockchain.append_block(block)
178 | # 处理新区块中的交易
179 | self.process_block_transactions(block)
180 | print(f"Accepted block {block.index} :{block.hash}")
181 |
182 | def send_blockchain_request(self,addr):
183 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
184 | try:
185 | s.connect((addr, 5000))
186 | s.sendall(f"@BLOCKCHAIN{self.node_ip}".encode("utf-8"))
187 | except:
188 | print(f"Failed to send blockchain request to {addr}")
189 |
190 | def send_blockchain(self, addr):
191 | blockchain_json = self.blockchain.to_json()
192 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
193 | try:
194 | s.connect((addr, 5000))
195 | s.sendall(f"#BLOCKCHAIN{blockchain_json}".encode("utf-8"))
196 | except:
197 | print(f"Failed to send blockchain to {addr}")
198 |
199 | def send_transaction(self, transaction):
200 | # 广播交易
201 | self.mempool.append(transaction)
202 | self.send_msg(f"@TRANSACTION{json.dumps(transaction.to_json())}")
203 |
204 | def handle_transaction(self, transaction_json):
205 | try:
206 | # 解析交易
207 | transaction = Transaction.from_json(json.loads(transaction_json))
208 |
209 | # 验证交易
210 | if not transaction.verify_signature():
211 | print(f"Invalid transaction signature: {transaction.tx_hash}")
212 | return
213 |
214 | # 添加到交易池
215 | with self.mempool_lock:
216 | if transaction not in self.mempool:
217 | self.mempool.append(transaction)
218 | print(f"Added transaction to mempool: {transaction.tx_hash}")
219 |
220 |
221 | except Exception as e:
222 | print(f"Error handling transaction: {e}")
223 |
224 | def handle_connection(self):
225 | while True:
226 | conn, addr = self.socket.accept()
227 | data = ""
228 | while True:
229 | part = conn.recv(1024).decode("utf-8")
230 | if not part:
231 | break
232 | data += part
233 | # 处理接收到的数据
234 | if data.startswith("@DATA"):
235 | with self.data_lock:
236 | self.data_queue.append(data[5:])
237 | print(f"Received data: {data[5:]}")
238 |
239 | elif data.startswith("@HELLO"):
240 | formaddr = data[6:]
241 | self.peers.update([formaddr])
242 | self.hello_dict[formaddr] = time.time()
243 |
244 | elif data.startswith("@JOIN"):
245 | formaddr = data[5:]
246 | self.send_intro(formaddr)
247 | self.peers.update([formaddr])
248 | print(f"Join {formaddr}")
249 |
250 | elif data.startswith("#INTRO"):
251 | formaddr = data[6:]
252 | self.peers.update([formaddr])
253 | print(f"Intro {formaddr}")
254 |
255 | elif data.startswith("@ONEBLOCK"):
256 | self.parse_block(data[9:])
257 |
258 | elif data.startswith("@BLOCKCHAIN"):
259 | addr = data[11:]
260 | self.send_blockchain(addr)
261 |
262 | elif data.startswith("#BLOCKCHAIN"):
263 | blockchain_json = data[11:]
264 | try:
265 | new_blockchain = block_chain_from_json(blockchain_json)
266 | except json.JSONDecodeError as e:
267 | print(f"Failed to decode blockchain JSON: {e}")
268 | continue
269 |
270 | if len(new_blockchain.chain) > len(self.blockchain.chain):
271 | if new_blockchain.is_chain_valid():
272 | # 验证区块链中的所有交易
273 | if self.verify_blockchain_transactions(new_blockchain):
274 | with self.blockchain_lock:
275 | self.blockchain = new_blockchain
276 | # 重新初始化UTXO池
277 | self.init_utxo_pool()
278 | print(f"Synchronized blockchain and updated UTXO pool")
279 | else:
280 | print("Rejected blockchain: invalid transactions")
281 | else:
282 | print("Rejected blockchain: invalid chain")
283 |
284 | elif data.startswith("@TRANSACTION"):
285 | # 处理新的交易
286 | self.handle_transaction(data[12:])
287 |
288 | conn.close()
289 |
290 | def mainloop(self):
291 | while True:
292 | self.send_data(f"This is {self.node_ip}")
293 | time.sleep(1)
294 |
295 | def helloloop(self):
296 | while True:
297 | self.send_hello()
298 | for peer in list(self.hello_dict.keys()):
299 | if time.time() - self.hello_dict[peer] > 10:
300 | self.peers.discard(peer)
301 | del self.hello_dict[peer]
302 | print(f"Discard {peer}")
303 | time.sleep(5)
304 |
305 | def mine(self):
306 | with self.data_lock:
307 | merkle_tree = MerkleTree(self.data_queue)
308 | self.data_queue = [f"Created by {self.node_ip}",]
309 | nonce = 0
310 | timestamp = time.time()
311 | while not self.getBlock:
312 | block = Block(self.blockchain.height, merkle_tree, self.blockchain.get_latest_block().hash, nonce, timestamp)
313 | if self.blockchain.is_block_valid(block):
314 | with self.blockchain_lock:
315 | self.send_block(block)
316 | self.blockchain.append_block(block)
317 | if block.index == 5:
318 | self.blockchain.save_svg("blockchain.svg")
319 | print(f"Mined block {block.index} :{block.hash}")
320 | break
321 | nonce += 1
322 |
323 | def mine_thread(self):
324 | while True:
325 | with self.data_lock:
326 | # 创建挖矿奖励交易
327 | mining_reward = Transaction()
328 | reward_output = UTXO(
329 | tx_hash=None,
330 | output_index=0,
331 | amount=50,
332 | recipient_address=self.wallet.address
333 | )
334 | mining_reward.outputs.append(reward_output)
335 | mining_reward.block_index = self.blockchain.height
336 | mining_reward.tx_hash = mining_reward.calculate_hash()
337 | reward_output.tx_hash = mining_reward.tx_hash
338 |
339 | # 从交易池中选择交易
340 | selected_transactions = []
341 | with self.mempool_lock:
342 | selected_transactions = self.mempool
343 |
344 | # 将挖矿奖励交易放在第一位
345 | all_transactions = [mining_reward] + selected_transactions
346 |
347 | # 创建merkle树(注意这里需要先将交易转换为JSON字符串)
348 | merkle_tree = MerkleTree([json.dumps(tx.to_json()) for tx in all_transactions])
349 |
350 | # 创建区块并验证交易
351 | block = Block(self.blockchain.height, merkle_tree,
352 | self.blockchain.get_latest_block().hash,
353 | 0, time.time(), self.node_ip)
354 |
355 | # 验证交易并获取无效交易列表
356 | is_valid, invalid_txs = self.verify_block_transactions(block)
357 |
358 | # 如果有无效交易,从交易池和选中的交易中移除它们
359 | if invalid_txs:
360 | with self.mempool_lock:
361 | self.mempool = [tx for tx in self.mempool if tx not in invalid_txs]
362 | selected_transactions = [tx for tx in selected_transactions if tx not in invalid_txs]
363 |
364 | # 重新创建merkle树(包含奖励交易和有效交易)
365 | all_transactions = [mining_reward] + selected_transactions
366 | merkle_tree = MerkleTree([json.dumps(tx.to_json()) for tx in all_transactions])
367 | block = Block(self.blockchain.height, merkle_tree,
368 | self.blockchain.get_latest_block().hash,
369 | 0, time.time(), self.node_ip)
370 |
371 | # 尝试挖矿
372 | nonce = 0
373 | while not self.getBlock:
374 | block.nonce = nonce
375 | block.hash = block.calculate_hash()
376 | if self.blockchain.is_block_valid(block):
377 | with self.blockchain_lock:
378 | self.send_block(block)
379 | self.blockchain.append_block(block)
380 | # 处理新区块中的交易
381 | self.process_block_transactions(block)
382 | # 从交易池中移除已确认的交易
383 | with self.mempool_lock:
384 | self.mempool = [tx for tx in self.mempool
385 | if tx not in selected_transactions]
386 | # 同步钱包UTXO
387 | self.sync_wallet_utxo()
388 | print(f"Mined block {block.index} :{block.hash}")
389 |
390 | # 打印钱包状态
391 | self.print_wallet_status()
392 |
393 | break
394 | nonce += 1
395 |
396 | with self.signal_lock:
397 | self.getBlock = False
398 |
399 | def send_data_thread(self):
400 | while True:
401 | data=input()
402 | self.send_data(data)
403 | self.data_queue.append(data)
404 | print(f"Sent data: {data}")
405 |
406 | def init_utxo_pool(self):
407 | """初始化UTXO池,遍历区块链中的所有交易"""
408 | self.global_utxo_pool = {}
409 | with self.blockchain_lock:
410 | for block in self.blockchain.chain:
411 | self.process_block_transactions(block)
412 |
413 | def process_block_transactions(self, block):
414 | """处理区块中的所有交易,更新UTXO池"""
415 | transactions = []
416 | for leaf in block.merkle_tree.leaves:
417 | try:
418 | # 解析JSON字符串
419 | tx_data = json.loads(leaf.data)
420 | tx = Transaction.from_json(tx_data)
421 | transactions.append(tx)
422 | except Exception as e:
423 | continue
424 |
425 | with self.utxo_pool_lock:
426 | for tx in transactions:
427 | # 如果是挖矿奖励交易(第一个交易),直接添加输出到UTXO池
428 | if transactions.index(tx) == 0:
429 | for i, output in enumerate(tx.outputs):
430 | utxo_key = f"{tx.tx_hash}:{i}"
431 | self.global_utxo_pool[utxo_key] = output
432 | # 确保将奖励交易的输出添加到钱包的UTXO池
433 | # if output.recipient_address == self.wallet.address:
434 | # self.wallet.add_utxo(output)
435 | continue
436 |
437 | # 对于普通交易,直接更新UTXO池
438 | # 1. 移除已使用的UTXO
439 | for utxo in tx.inputs:
440 | utxo_key = f"{utxo.tx_hash}:{utxo.output_index}"
441 | self.global_utxo_pool.pop(utxo_key, None)
442 | # if utxo.recipient_address == self.wallet.address:
443 | # self.wallet.remove_utxo(utxo)
444 |
445 | # 2. 添加新的UTXO
446 | for i, output in enumerate(tx.outputs):
447 | utxo_key = f"{tx.tx_hash}:{i}"
448 | self.global_utxo_pool[utxo_key] = output
449 | # 如果是发给自己的,也添加到钱包的UTXO池
450 | # if output.recipient_address == self.wallet.address:
451 | # self.wallet.add_utxo(output)
452 |
453 | def verify_blockchain_transactions(self, blockchain):
454 | """验证区块链中所有区块的所有交易"""
455 | print("Verifying all transactions in the blockchain...")
456 |
457 | # 保存当前的UTXO池状态
458 | original_utxo_pool = self.global_utxo_pool.copy()
459 | self.global_utxo_pool = {} # 临时清空UTXO池
460 |
461 | try:
462 | # 按顺序验证每个区块
463 | for block in blockchain.chain:
464 | is_valid, invalid_txs = self.verify_block_transactions(block)
465 | if not is_valid:
466 | print(f"Invalid transactions found in block {block.index}")
467 | return False
468 |
469 | # 如果验证通过,处理这个区块的交易(更新UTXO池)
470 | self.process_block_transactions(block)
471 |
472 | print("All transactions in the blockchain are valid")
473 | return True
474 |
475 | except Exception as e:
476 | print(f"Error during blockchain verification: {e}")
477 | return False
478 |
479 | finally:
480 | # 恢复原始UTXO池状态
481 | self.global_utxo_pool = original_utxo_pool
482 |
483 | def print_wallet_status(self):
484 | """打印钱包状态"""
485 | total_balance = sum(utxo.amount for utxo in self.wallet.utxo_pool)
486 | print(f"\n=== Wallet Status for {self.node_ip} ===")
487 | print(f"Total Balance: {total_balance}")
488 | print("UTXOs:")
489 | for utxo in self.wallet.utxo_pool:
490 | tx_hash_display = utxo.tx_hash[:8] if utxo.tx_hash else "None"
491 | print(f" - Amount: {utxo.amount}, From TX: {tx_hash_display}...")
492 | print("=====================================\n")
493 |
494 | def sync_wallet_utxo(self):
495 | """与全局UTXO池同步钱包的UTXO"""
496 | with self.utxo_pool_lock:
497 | # 清空钱包的UTXO池
498 | self.wallet.utxo_pool = []
499 |
500 | # 从全局UTXO池中找出属于本钱包的UTXO
501 | for utxo_key, utxo in self.global_utxo_pool.items():
502 | if utxo.recipient_address == self.wallet.address:
503 | self.wallet.add_utxo(utxo)
504 |
505 | # 打印同步后的钱包状态
506 | # self.print_wallet_status()
507 |
508 | def sync_utxo_thread(self):
509 | """定期同步UTXO的线程"""
510 | while True:
511 | time.sleep(1) # 每1秒同步一次
512 | self.sync_wallet_utxo()
513 |
514 | if __name__ == "__main__":
515 | ip = input("Enter IP address: ")
516 | node = Node(ip)
--------------------------------------------------------------------------------