赞
踩
在本项目中,我通过定义一个`Blockchain`类来模拟区块链。在类的构造方法中,我将调用已封装的创建区块方法`new_block()`,生成一个创世区块。同时,我设置该区块的前一个哈希值为1,以表明其为创世区块。此外,我使用列表来保存当前交易和链,并采用集合(set)来存储分布式的节点(node)。
为了模拟分布式,我将注册节点的方法封装到register_node()方法中;为了实现共识算法,我将“检查一条链是否有效”的方法和“找出最长连”的方法分别封装为valid_chain() 和 resolve_conflicts()。
然后创建区块和创建新的交易分别对应new_block() 方法和 new_transaction()方法。
模拟工作量证明时,我将会计算前一个区块的hash值,这部分内容我封装在了 proof_of_work() 方法中。
鉴于我的项目采用Web架构,并需运用Flask框架,在处理请求方法时,调用`Blockchain`类中的方法以模拟交易。
本项目是一个基于Python的山寨比特币交易系统,实现了区块链中的部分基本算法,如共识算法等,并具备创建交易、打包区块、工作量证明等核心功能。通过模仿区块链的分布式特性,实现了高性能和安全性。该项目采用Web架构,并以Python的Flask框架为基础。通过定义一个模拟区块链操作的类,实现了与Flask的交互。在测试过程中,我们使用了Postman这款API测试工具。
在Blockchain的构造函数中,初始化当前交易,当前链,以及当前节点,同时创建一个创世区块,设置previous_hash 的值为1表示当前区块为创世区块。
为的是模拟比特币的分布式交易系统,系统中可能总是存在多个节点,这些节点都知道对方的存在。在这里我们通过将Web服务启动在不同的端口,来模拟分布式交易系统中的多个不同节点。
让一个节点知道其他节点的存在,这里我们使用的方法是将另一个节点注册到当前节点中来,即将另一个节点的信息存放在当前节点Blockchain类中的 nodes 属性中。
我们这里模拟注册节点的方法是register_node() 函数,我们需要传入另一个节点的url地址,然后解析得到服务器路径,再存入nodes集合中。
有了一个创世区块之后,我们就可以在这个创世区块中进行交易了,我们使用new_transaction() 函数来模拟一次交易,传入的参数:sender表示发送者的地址,recipient表示接受者的地址,amount表示数量,该函数的返回值是本次交易的index索引,这些信息最终都会被打包添加到下一个区块中。
我们知道,区块链中的每一个新的区块都来自于工作量证明,工作量证明的目标是计算出一串解决问题的数字,这个结果是十分难计算的,但是却十分容易验证,网络上的任何人都可以验证这个结果。
这里我们也用Python代码实现一个简单的工作量证明算法,我们的规则是:找到一个数字P,使得它与前一个区块的proof拼接而成的字符串的Hash值以4个零开头。
这个过程我们需要模拟“挖矿”,通过Flask服务端,我们先运行一个工作量证明算法,得到下一个证明。
找到一个工作量证明之后,我们将给我们的“矿工”一定数量的“仿比特币”作为奖励,然后将当前所有的交易打包并生成一个新的区块,同时将存储当前交易的变量 current_transaction 重置,方便进行一下次交易。
这里我们将分别展示Flask服务端的代码以及Blockchain类中创建区块的代码。
ØFlask服务端的代码
ØBlockchain类中的代码
由于我们的仿比特币交易系统是分布式的,所以我们必须保证所有的节点都运行在同一条链上,当一个节点与另一个节点有不同时就会存在冲突,为了解决这个问题,我们遵循最长链原则,使用共识算法,让网络中的节点达成共识。
共识算法的第一部分,我们需要检查一条链是否是有效链,
确保已经安装 Python3.9、pip、Flask、requests等相关环境。同时还需要一个 HTTP 客户端Postman。
我们需要构造一个创世块(没有前区块的第一个区块),之后需要给它加上一个工作量证明。每个区块都需要经过工作量证明,俗称挖矿
使用register_node() 函数创建一个节点,作为我们连入区块链的服务器
向列表中添加一个交易记录,并返回该记录将被添加到的区块(下一个待挖掘的区块)的索引,等下在用户提交交易时会有用。
通过逐个添加方式,将区块一个个链接起来,返回整条链
通过新增一个交易授予矿工(自己)一个币
构造新区块并将其添加到链中
当一个节点与另一个节点有不同的链时,就会产生冲突。 为了解决这个问题,我们将制定最长的有效链条是最权威的规则。换句话说就是:在这个网络里最长的链就是最权威的。 我们将使用这个算法,在网络中的节点之间达成共识。
关于Blockchain类的一些实现具体功能的函数,在4.1小结已经基本展示完毕,这里展示一些Flask服务端的代码。
Ø实例化Flask结点,Blockchain对象
Ø创建一个新的交易
Ø返回整条链
Ø添加相邻节点
Ø共识机制
Ø启动服务,指定端口
- <!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>区块链演示</title>
- <style>
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background-color: #f4f4f9;
- color: #333;
- margin: 0;
- padding: 0;
- }
- header {
- background-color: #8A2BE2;
- color: #fff;
- padding: 20px 0;
- text-align: center;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- }
- .container {
- width: 80%;
- margin: 20px auto;
- overflow: hidden;
- }
- .content {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- }
- .card {
- background: #fff;
- margin: 10px;
- padding: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- border-radius: 8px;
- flex: 1 1 calc(48% - 40px);
- box-sizing: border-box;
- transition: transform 0.3s ease;
- }
- .card:hover {
- transform: translateY(-5px);
- }
- .card h3 {
- margin-top: 0;
- color: #8A2BE2;
- }
- .card form {
- display: flex;
- flex-direction: column;
- }
- .card form input, .card form button {
- padding: 10px;
- margin: 5px 0;
- font-size: 16px;
- border: 1px solid #ddd;
- border-radius: 4px;
- }
- .card form button {
- background-color: #8A2BE2;
- color: #fff;
- border: none;
- cursor: pointer;
- transition: background-color 0.3s ease;
- }
- .card form button:hover {
- background-color: #7a22c4;
- }
- .chain, .transactions, .node-chain {
- margin: 20px 0;
- }
- .block, .transaction {
- background: #fff;
- margin: 10px 0;
- padding: 10px;
- border-left: 5px solid #8A2BE2;
- border-radius: 4px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- .block p, .transaction p {
- margin: 5px 0;
- }
- h2 {
- color: #8A2BE2;
- }
- input[type="text"], input[type="number"] {
- width: calc(100% - 22px);
- }
- button {
- width: calc(100% - 22px);
- }
- </style>
- </head>
- <body>
- <header>
- <h1>区块链演示</h1>
- </header>
- <div class="container">
- <div class="content">
- <div class="card">
- <h3>在5000端口挖掘新的区块</h3>
- <button onclick="mineBlock('5000')">挖掘</button>
- </div>
- <div class="card">
- <h3>在5000端口创建新的交易</h3>
- <form id="transactionForm5000">
- <input type="text" id="sender5000" placeholder="发送方">
- <input type="text" id="recipient5000" placeholder="接收方">
- <input type="number" id="amount5000" placeholder="金额">
- <button type="button" onclick="createTransaction('5000')">创建交易</button>
- </form>
- </div>
- <div class="card">
- <h3>在其他端口挖掘新的区块</h3>
- <input type="text" id="portMine" placeholder="端口号">
- <button onclick="mineBlock(document.getElementById('portMine').value)">挖掘</button>
- </div>
- <div class="card">
- <h3>在其他端口创建新的交易</h3>
- <form id="transactionFormOther">
- <input type="text" id="portTransaction" placeholder="端口号">
- <input type="text" id="senderOther" placeholder="发送方">
- <input type="text" id="recipientOther" placeholder="接收方">
- <input type="number" id="amountOther" placeholder="金额">
- <button type="button" onclick="createTransaction(document.getElementById('portTransaction').value)">创建交易</button>
- </form>
- </div>
- <div class="card">
- <h3>查看5000端口交易记录</h3>
- <button onclick="fetchTransactions('5000')">查看</button>
- </div>
- <div class="card">
- <h3>查看其他端口交易记录</h3>
- <input type="text" id="portTransactions" placeholder="端口号">
- <button onclick="fetchTransactions(document.getElementById('portTransactions').value)">查看</button>
- </div>
- <div class="card">
- <h3>查看节点区块链</h3>
- <form id="nodeChainForm">
- <input type="text" id="nodeChain" placeholder="节点URL">
- <button type="button" onclick="fetchNodeChain()">查看</button>
- </form>
- </div>
- </div>
- <div class="chain" id="chain"></div>
- <div class="transactions" id="transactions"></div>
- <div class="node-chain" id="node-chain"></div>
- </div>
- <script>
- const apiBase = 'http://localhost';
- async function fetchChain() {
- const response = await fetch(`${apiBase}:5000/chain`);
- const data = await response.json();
- const chainDiv = document.getElementById('chain');
- chainDiv.innerHTML = '<h2>当前区块链</h2>';
- data.chain.forEach(block => {
- const blockDiv = document.createElement('div');
- blockDiv.className = 'block';
- blockDiv.innerHTML = `
- <p><strong>区块:</strong> ${block.index}</p>
- <p><strong>时间戳:</strong> ${block.timestamp}</p>
- <p><strong>交易:</strong> ${JSON.stringify(block.transactions)}</p>
- <p><strong>工作量证明:</strong> ${block.proof}</p>
- <p><strong>上一个区块哈希:</strong> ${block.previous_hash}</p>
- `;
- chainDiv.appendChild(blockDiv);
- });
- }
- async function mineBlock(port) {
- try {
- const response = await fetch(`${apiBase}:${port}/mine`);
- const data = await response.json();
- alert(data.message);
- if (port === '5000') {
- fetchChain();
- }
- } catch (error) {
- alert(`无法连接到端口 ${port}: ${error.message}`);
- }
- }
- async function createTransaction(port) {
- const sender = document.getElementById(`sender${port === '5000' ? '5000' : 'Other'}`).value;
- const recipient = document.getElementById(`recipient${port === '5000' ? '5000' : 'Other'}`).value;
- const amount = document.getElementById(`amount${port === '5000' ? '5000' : 'Other'}`).value;
- try {
- const response = await fetch(`${apiBase}:${port}/transactions/new`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ sender, recipient, amount })
- });
- const data = await response.json();
- alert(data.message);
- } catch (error) {
- alert(`无法连接到端口 ${port}: ${error.message}`);
- }
- }
- async function fetchTransactions(port) {
- try {
- const response = await fetch(`${apiBase}:${port}/chain`);
- const data = await response.json();
- const transactionsDiv = document.getElementById('transactions');
- transactionsDiv.innerHTML = `<h2>${port}端口交易记录</h2>`;
- data.chain.forEach(block => {
- block.transactions.forEach(transaction => {
- const transactionDiv = document.createElement('div');
- transactionDiv.className = 'transaction';
- transactionDiv.innerHTML = `
- <p><strong>发送方:</strong> ${transaction.sender}</p>
- <p><strong>接收方:</strong> ${transaction.recipient}</p>
- <p><strong>金额:</strong> ${transaction.amount}</p>
- `;
- transactionsDiv.appendChild(transactionDiv);
- });
- });
- } catch (error) {
- alert(`无法连接到端口 ${port}: ${error.message}`);
- }
- }
- async function fetchNodeChain() {
- const node = document.getElementById('nodeChain').value;
- try {
- const response = await fetch(`http://${node}/chain`);
- const data = await response.json();
- const nodeChainDiv = document.getElementById('node-chain');
- nodeChainDiv.innerHTML = '<h2>节点区块链</h2>';
- if (data.error) {
- alert(data.error);
- } else {
- data.chain.forEach(block => {
- const blockDiv = document.createElement('div');
- blockDiv.className = 'block';
- blockDiv.innerHTML = `
- <p><strong>索引:</strong> ${block.index}</p>
- <p><strong>时间戳:</strong> ${block.timestamp}</p>
- <p><strong>交易:</strong> ${JSON.stringify(block.transactions)}</p>
- <p><strong>工作量证明:</strong> ${block.proof}</p>
- <p><strong>上一个区块哈希:</strong> ${block.previous_hash}</p>
- `;
- nodeChainDiv.appendChild(blockDiv);
- });
- }
- } catch (error) {
- alert(`无法连接到节点: ${error.message}`);
- }
- }
- document.addEventListener('DOMContentLoaded', fetchChain);
- </script>
- </body>
- </html>
运行代码,启动我们的服务
分别查看链的具体内容,可以看到,初始化一个 BlockChain 对象之后,将会创建一个 “创世区块”,此时,区块链中只有一个区块,这个区块中还没有任何一笔交易。
调用接口,创建一笔交易,添加到下一个区块中。(这里我们只使用 5000 端口的节点测试)。
这里我们发送 Post 请求,将发送者地址,接收者地址,交易金额作为参数传入。由着3个参数构成一笔交易的具体内容。
这里为了方便辨认,我们发送者和接收者采用易于辨识的名字。
然后我们这里创建2笔交易,交易发送者的名字分别为 “孔孟群” 和 “智慧小孔”。
我们再次查看5000端口节点的完整链
但是我们发现,链的长度并没有发生改变;这是因为,我们只是进行了交易,但是这些交易并没有被打包成为一个新的区块,我们还要调用 BlockChain 的 new_block() 方法,将交易打包成为一个新的区块。
将交易打包成一个新的区块,这里我们调用 new_block ,打包生成一个区块,这里我们依旧使用5000 端口的节点进行测试。简单介绍一个这个方法:
在打包形成一个新的区块之前,所有的交易都存储在 BlockChain 类的 current_transactions 属性里面,我们只需要将这个属性作为 block 属性的一个子属性即可,然后再重置一下 current_transactions 的值即可,方便进行一下次交易。
可以看到,在第2个区块中,除了我们手动添加的2笔交易,发现还有一笔交易。实际上,这笔交易是我们在代码中手动添加的,这个 sender 为 0 表示这个结点已经挖掘出新的星币,然后这笔交易就是为了奖励那个找到工作量证明的那个用户。
打包生成一个区块后,我们的链理论上应该是有2个区块了。我们调用接口来查看一下我们的链的具体内容。
可以发现,我们的链的长度为2,的确是有2个区块,验证了我的猜测,代码没有问题。
经过上面的测试过程,我们单一节点的区块链需要具备的功能基本完成了,但是区块链是一个分布式的系统,所以我们还需要进行分布式测试。
我们在本机上使用不同的端口模拟不同的节点,第1个结点我们部署在 5000 端口,第2个结点我们部署在 5001 端口。
l5000端口的节点
l5001端口的节点
分别查看两条链的具体内容。
l5000端口节点
l5001端口节点
可以看到,2条链都初始化成功,各自具有一个创世区块。
比特币是分布式的,这里我们为了模仿比特币,启动多个不同端口的节点来模拟分布式。
同时,我们还需要让一个节点知道其相邻结点的存在,即分布式系统中的每一个节点,都需要存储该系统中的其他节点的记录。
这里我们通过 BlockChain 类的 register_node 方法来实现识别相邻节点的功能要求。
register_node 方法需要我们传入一个节点的地址,然后将这个地址添加到 BlockChain 类的 nodes 属性中,这个 nodes 是一个 set 集合。
可以看到,这里我们调用 5000 端口节点的 register_node() 方法,将 5001 端口的地址传入,返回结果显示新的结点已经被创建,说明我们的 5000 端口的节点已经知道 5001 端口节点的存在了。
因为之前我们已经将 5001 端口的节点注册到了 5000 端口的节点中,所以 5000 端口的结点时已经知道了 5001 端口节点的存在的,现在只要在 5001 端口的节点上挖掘一些新的区块,使得 5001 端口节点的链的长度要比 5000 端口节点链的长度更长,然后在调用 5000 端口结点的 resolve_conflicts() 函数,如果发现 5000 端口的链被替换成立 5001 端口的链,这说明我们的共识算法是有效的。
好,下面开始我们的测试。
l我们首先在 5001 端口多打包生成一些区块
挖掘区块,使得5001端口有8个区块,即链的长度是8。
l接下来,调用5000端口节点的resolve_conflicts()方法,解决节点间的冲突
调用之前,我们的 5000 端口链的具体内容是这样的,只有1个区块:
l调用 resolve_conflicts() 方法
调用之后显示我们的链已经被替换了
l再次查看 5000 端口链的详细内容
发现链的长度变为了8,验证了我们刚才的猜想,说明我的共识算法是有效的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。