ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

区块链技术与应用:Python从0搭建最小链并解析比特币数据

区块链技术与应用:Python从0搭建最小链并解析比特币数据 简介这份PDF是面向高校学生与区块链入门学习者的课后测试备考资料围绕区块链技术与应用课程的核心考点整理适合用于章节复习、考前自测与知识点查漏。包内共1个PDF文件压缩包约19KB篇幅精简便于随时翻阅。内容以单选、多选、判断题形式呈现并附带正确答案覆盖区块链的定义与历史、联盟链与以太坊的区别、共识层这一核心组件、资产证券化发行所属的业务协同类应用、创新扩散理论中的采纳人群划分、区块链的三个关键点、新技术从过热期到成熟期的四阶段演进、数字票据与第三方存证等数字资产类案例、区块链在提效降本与监管协作方面的价值以及其分布式架构与冗余度等易错辨析点。目前已有2108人学习下载可帮助读者快速定位薄弱环节、对照答案理解命题思路完成从概念记忆到解题应用的自测闭环。1. 区块链技术和应用从课后题的正确答案倒推要考什么期末复习区块链技术和应用很多人靠背课后测试的题干和答案过关代价是被追问「为什么改掉第 3 个区块里的一笔交易第 9 个区块就作废」时答不上来。这门课的测试题其实绕着四个点打转哈希指针串成的链式结构、Merkle 树的验证路径、工作量证明与共识规则、区块链在存证与溯源场景里的适用边界。把它们还原成能跑的实验比背答案稳得多用几十行 Python 从0开始搭建一个区块链平台亲手调难度位和最长链规则再拉一段真实的比特币区块链数据用区块头字段去对课本定义。后面按数据结构、最小链实现、链上数据查询、自测验证四步展开每步都落到命令、代码和参数上。2. 哈希指针与 Merkle 树区块链技术和应用的数据结构底座这一章解决的是「课本上画的那些方框和箭头落到字节层面到底长什么样」。数据结构没吃透后面调共识参数会全程靠猜。2.1 哈希指针怎么把区块串成不可篡改的链哈希指针不是普通指针。普通指针只记录地址哈希指针同时记录地址和那块数据的哈希值取值前先校验。区块头里的prev_block_hash就是这么一个东西它既是上一块的标识也是上一块内容的摘要。推论很直接改动高度为 3 的区块里任何一个字节第 3 块的哈希就变第 4 块头部存的仍是旧哈希校验立刻失败想让链重新自洽就得把第 4 块到最新块全部重挖一遍。这就是「历史越久越难改」的全部来源跟分布式、跟节点数量都没关系纯粹是密码学上的连锁。import hashlib, json def sha256_hex(data: bytes) - str: 返回 32 字节摘要的十六进制字符串 return hashlib.sha256(data).hexdigest() def block_hash(index: int, prev_hash: str, txs: list, nonce: int) - str: # sort_keysTrue 保证字段顺序固定ensure_asciiFalse 避免中文被转义 payload json.dumps( {index: index, prev_hash: prev_hash, txs: txs, nonce: nonce}, sort_keysTrue, ensure_asciiFalse, ).encode(utf-8) return sha256_hex(payload) h0 block_hash(0, 0 * 64, [genesis], 0) h1 block_hash(1, h0, [A-B:1], 42) h2 block_hash(2, h1, [B-C:1], 7) print(h1 , h1) print(h2 , h2) # 篡改第 1 块里的转账金额重算它的哈希 h1_bad block_hash(1, h0, [A-B:100], 42) print(h1 是否改变:, h1 ! h1_bad) # True print(h2 存的 prev_hash 是否还对得上:, h2 block_hash(2, h1_bad, [B-C:1], 7)) # Falseindex是区块高度prev_hash是上一块的 32 字节摘要nonce是挖矿时不断试的数。关键参数是sort_keys和编码方式序列化必须确定同一份内容每次算出的摘要才一样。自己实现时最常见的坑就是字典乱序或者中文被转义导致同样的交易算出两个哈希链在校验环节莫名其妙断掉。提示摘要比较用等值判断就够了但如果做的是密钥比较这类场景要用恒定时间比较函数别用。2.2 Merkle 树的构造规则与 SPV 验证路径长度一棵区块里通常有几百到几千笔交易全塞进区块头会让头变得无比臃肿。Merkle 树的做法是把交易两两配对求哈希逐层向上最后得到一个 32 字节的merkle_root放进区块头。轻节点只要拿到区块头和某笔交易的一条路径每层一个兄弟哈希就能验证这笔交易确实在块里通信量跟总交易数无关。交易数 n树高验证路径长度需要提供的兄弟哈希数4228331024101040961212路径长度就是ceil(log2(n))这也是课后题里出现频率极高的一个计算题。比特币还有一个容易踩的细节某一层节点数为奇数时把最后一个节点复制一份参与配对而不是把它直接提升到上一层。import hashlib def h(x: str) - str: return hashlib.sha256(x.encode(utf-8)).hexdigest() def merkle_root(leaves: list) - str: if not leaves: return h() level [h(x) for x in leaves] # 叶子层先各自做一次哈希 while len(level) 1: if len(level) % 2 1: # 奇数个节点复制最后一个 level.append(level[-1]) level [h(level[i] level[i 1]) for i in range(0, len(level), 2)] return level[0] txs [tx0, tx1, tx2, tx3, tx4] # 刻意用奇数笔 print(merkle_root(txs))leaves传的是交易原始数据的字符串形式实际实现里应当是交易的字节序列。每一轮while把节点数减半复杂度 O(n)但由于哈希函数对长度不敏感验证单笔交易只需 O(log n) 个兄弟哈希。想验证奇数复制规则是否生效把txs改成 4 笔再改成 5 笔看树高是否都等于 3。2.3 区块头 80 字节结构和字段解析比特币的区块头固定 80 字节字段顺序和长度是死规定。拿真实数据解析一遍比对着课本表格看十遍管用。字段字节数说明version4版本号小端整数prev_block_hash32前一区块哈希内部为小端字节序merkle_root32交易 Merkle 根同样小端time4出块时间戳Unix 秒bits4目标难度的紧凑编码nonce4工作量证明的计数器# 取指定区块的头部verbosetrue 返回 JSON 字段 bitcoin-cli getblockheader block_hash true # 只取原始 80 字节头部返回十六进制串适合自己解析 bitcoin-cli getblockheader block_hash falseimport struct raw bytes.fromhex(80字节头部hex) version, prev, merkle, ts, bits, nonce struct.unpack(I32s32sIII, raw) print(version:, version) print(prev:, prev[::-1].hex()) # 展示时反转内部小端人看的大端 print(merkle:, merkle[::-1].hex()) print(time:, ts, bits:, hex(bits), nonce:, nonce)struct.unpack的格式串I32s32sIII里表示小端I是无符号 4 字节整数32s是定长 32 字节。总共 43232444 80 字节解析结果长度对不上就说明数据截断了。两个 32 字节字段内部按小端存储所以打印给人看时要[::-1]反转这是初学者最常写错的一处bits是紧凑编码的难度目标需要按公式还原成 256 位整数才能做范围比较。3. 从0开始搭建一个区块链平台最小可行链与共识参数真正动手才会发现卡人的不是密码学而是「先做什么、后做什么」和「难度参数设多少」。这一章把一条能跑的最小区块链拆开讲。3.1 最小可行链的选型和演进顺序一上来就搭以太坊私链或者引入复杂共识通常的结果是环境没配好就放弃了。比较靠谱的路线是逐层加功能每一层都能单独验证。阶段做什么为什么先做这个一单机内存链区块结构 哈希指针校验先把数据结构跑通出问题好定位二加工作量证明难度位可配感受 nonce 和目标值的关系三落盘到 SQLite加高度和哈希唯一约束处理持久化与重复区块四加 HTTP 接口多节点互相同步暴露分叉与最长链规则五加交易校验和余额模型从「存数据」走到「记账」选 SQLite 而不是直接上 PostgreSQL是因为最小链的写入量只有几千行SQLite 免运维、单文件、事务语义完整。等要跑多节点并发同步再迁到 PostgreSQL 也不迟。3.2 工作量证明的难度位怎么设PoW 的本质是找一个 nonce让区块头的哈希小于目标值。本地练习常用「前导零个数」近似表达难度难度位 20 表示哈希的十六进制串前 5 个字符必须是 0。import hashlib, json, time def mine(index: int, prev_hash: str, txs: list, difficulty_bits: int, max_nonce: int 2 ** 32): target 0 * (difficulty_bits // 4) # 每 4 bit 对应一个十六进制字符 nonce, t0, hashes 0, time.time(), 0 while nonce max_nonce: payload json.dumps([index, prev_hash, txs, nonce], sort_keysTrue).encode() digest hashlib.sha256(payload).hexdigest() hashes 1 if digest.startswith(target): return {nonce: nonce, hash: digest, hashes: hashes, elapsed: round(time.time() - t0, 3)} nonce 1 raise RuntimeError(nonce 用尽需要调整难度或引入 extranonce) print(mine(1, 0 * 64, [A-B:1], 20))difficulty_bits每加 1期望尝试次数翻倍也就是 2 的 bits 次方。16 位在普通笔记本上通常是毫秒级20 位在百万次量级24 位就会明显卡顿。max_nonce是安全阀nonce 只有 4 字节真实系统里挖不到会同时递增时间戳或 coinbase 里的额外随机数extranonce来重开一轮。这里返回的hashes可以用来验证期望值——多跑几次平均值应该落在 2^bits 附近偏差过大多半是目标值判断写错了。难度位期望哈希次数建议用途16约 6.5 万单元测试、演示20约 105 万本地单机练习24约 1680 万观察出块间隔和难度调整注意真实比特币不用「前导零个数」而是用bits字段编码一个 256 位目标值再按每 2016 个区块调整一次难度把平均出块时间拉回 10 分钟。本地实现用近似法就够了但要知道这两者的差别考试里也常拿它出对比题。3.3 链状态落盘与最长链规则有了区块和 PoW接下来要解决「分叉了听谁的」。规则是累计工作量最大的那条链为主链实践中近似为最长链。import sqlite3, json DDL CREATE TABLE IF NOT EXISTS blocks ( chain_id TEXT NOT NULL, -- main / fork标记候选链 height INTEGER NOT NULL, prev_hash TEXT NOT NULL, block_hash TEXT NOT NULL UNIQUE, -- 防重复插入 merkle_root TEXT NOT NULL, nonce INTEGER NOT NULL, ts INTEGER NOT NULL, txs TEXT NOT NULL, -- JSON 字符串最小实现够用 PRIMARY KEY (chain_id, height) ); def init_db(path: str chain.db): conn sqlite3.connect(path) conn.executescript(DDL) conn.commit() return conn def append_block(conn, chain_id, height, prev_hash, block_hash, merkle_root, nonce, ts, txs): conn.execute( INSERT INTO blocks(chain_id,height,prev_hash,block_hash,merkle_root,nonce,ts,txs) VALUES (?,?,?,?,?,?,?,?), (chain_id, height, prev_hash, block_hash, merkle_root, nonce, ts, json.dumps(txs, ensure_asciiFalse)), ) conn.commit()block_hash上的 UNIQUE 约束很关键节点重复广播同一个块时插入会直接失败天然去重。PRIMARY KEY (chain_id, height)让同一个高度可以存在多条候选链方便做重组。分叉后要切主链第一步是找共同祖先-- 找出主链和分叉链上最高的相同区块即分叉点 SELECT a.height, a.block_hash FROM blocks a JOIN blocks b ON a.block_hash b.block_hash WHERE a.chain_id main AND b.chain_id fork ORDER BY a.height DESC LIMIT 1;JOIN的条件放在block_hash上而不是高度上是因为只有哈希才能证明两边是同一块内容。拿到分叉点高度之后把主链上高于该高度的记录删掉再把分叉链的区块按序重放进来状态就切换完成。真实链上还要同时回滚受影响的 UTXO 和余额这也是很多自研链在重组时数据对不上的根源。4. 比特币区块链数据的查询与解析从区块高度到 UTXO跑通了自建链再回头看真实链上数据会发现字段定义、字节序、单位这些细节全都被放大。这一章全是可执行的命令和解析代码。4.1 用 RPC 命令把区块和交易拉下来本地节点起来之后bitcoin-cli就是取数的主入口。命令关键参数返回内容getblockcount无当前主链高度getblockhash高度该高度的区块哈希getblock哈希、verbosity0 原始 hex1 头部 JSON2 含交易详情getrawtransactiontxid、true已索引交易的完整结构gettxouttxid、vout、true该输出的花费状态和金额bitcoin-cli getblockcount bitcoin-cli getblockhash 800000 # verbosity2 一次性拿到区块头和全部交易做统计最省事 bitcoin-cli getblock block_hash 2 block.jsonverbosity是最容易忽略的参数设成 0 拿到的是一整段十六进制适合自己写解析器设成 2 拿到的是带vout、vin的嵌套 JSON适合直接做统计。写批量拉取脚本时用 2做底层字节解析练习时用 0。4.2 解析交易输入输出并手算 UTXO 变化一笔交易的输入总和减去输出总和就是手续费这也是课后题里最常见的计算题。手工算一遍UTXO 模型就再也不会记混。SAT 100_000_000 # 1 BTC 1e8 satoshi def parse_tx(tx: dict) - dict: # 输出侧所有 vout 的金额之和 total_out sum(o[value] for o in tx[vout]) * SAT total_in 0 for vin in tx[vin]: if coinbase in vin: # 创币交易没有输入 continue prev vin.get(prevout) if prev: total_in int(round(prev[value] * SAT)) return { txid: tx[txid], in_sat: total_in, out_sat: int(round(total_out)), fee_sat: total_in - int(round(total_out)), } with open(block.json, r, encodingutf-8) as f: block json.load(f) rows [parse_tx(t) for t in block[tx]] print(交易数:, len(rows)) print(区块手续费合计:, sum(r[fee_sat] for r in rows)) print(交易数是否为 0:, len(rows) 0)SAT是单位换算因子RPC 返回的value单位是 BTC 浮点数做统计前必须转成 satoshi 整数否则几千笔累加后会出现分位误差。coinbase in vin用来跳过创币输入这类交易没有前序输出费用为负不是错误而是正常现象。round是因为浮点表示本来就不精确先四舍五入再转整数比直接int()截断更安全。字段含义单位注意vin[].txid/vout引用的前序输出—这就是 UTXO 的定位坐标vin[].prevout.value被花掉的金额BTC需转 satoshivout[].value新生成的输出金额BTC含找零输出vout[].n输出序号—与 txid 共同构成 UTXO提示从getblock取到的prevout是否齐全取决于节点的-txindex配置。批量解析失败时先查这一项别急着怀疑代码。4.3 批量同步的落库设计与断点续传一次拉一个块太慢需要边拉边落库并且能从上次中断处继续。CREATE TABLE IF NOT EXISTS blocks ( height INTEGER PRIMARY KEY, block_hash TEXT NOT NULL UNIQUE, prev_hash TEXT NOT NULL, ts INTEGER NOT NULL, tx_count INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS tx ( txid TEXT PRIMARY KEY, block_height INTEGER NOT NULL, fee_sat INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS tx_out ( txid TEXT NOT NULL, vout INTEGER NOT NULL, value_sat INTEGER NOT NULL, address TEXT, spent_by TEXT, -- NULL 表示尚未花费即当前 UTXO PRIMARY KEY (txid, vout) ); CREATE INDEX IF NOT EXISTS idx_tx_block ON tx(block_height); CREATE INDEX IF NOT EXISTS idx_out_addr ON tx_out(address);tx_out表用(txid, vout)做联合主键天然对应一个 UTXOspent_by在面对后续交易时被更新成花费它的 txid查询余额就是SELECT address, SUM(value_sat) FROM tx_out WHERE spent_by IS NULL GROUP BY address。断点续传只需要一句SELECT COALESCE(MAX(height), 0) FROM blocks。两个索引分别服务于「按高度回溯区块」和「按地址查流水」两类最常见的查询。需要留意的是重组处理如果同步过程中发生了链重组低于分叉点的新块会覆盖已有的高度主键导致插入冲突。稳妥做法是检测到prev_hash与本地最高块的block_hash不一致时先按高度删除若干行再重放并同步回滚tx_out.spent_by。5. 区块链技术应用的进阶验证把课后测试题改写成可执行断言背答案的效率远不如把每道题变成一条断言。写完一遍错在哪、为什么错比对着答案本红笔勾选清楚得多。下面这段自测脚本覆盖了四类高频考点可以直接当成复习清单用。import hashlib, math def sha(x: str) - str: return hashlib.sha256(x.encode(utf-8)).hexdigest() def test_hash_pointer_breaks(): 考点改动历史区块后续哈希指针全部失效 h1 sha(block1| sha(block0)) assert sha(block1| sha(block0-tampered)) ! h1 def test_merkle_path_len(): 考点验证路径长度 ceil(log2(n))奇数交易会补节点 assert math.ceil(math.log2(4)) 2 assert math.ceil(math.log2(5)) 3 def test_pow_expectation(): 考点难度每加 1 位期望尝试次数翻倍 for bits in (16, 20, 24): assert (1 bits) 2 ** bits def test_utxo_conservation(): 考点输入 输出 手续费单位必须统一到 satoshi vin_sat, vout_sat, fee_sat 100_000, 97_500, 2_500 assert vin_sat vout_sat fee_sat四条断言的意图各不相同。第一条锁死「哈希指针的连锁效应」改动内容必然导致摘要变化第二条针对 Merkle 路径长度的计算题log2上取整这个动作不能少第三条把难度位和期望次数的关系固定成等式避免把「哈希率」和「难度」混为一谈第四条守住 UTXO 守恒浮点误差是这类计算题最常见的失分点。高频错题点容易记错的答案正确的理解与验证方式篡改难度改早期区块只需重挖该块后续所有块都要重挖用第 2 章代码验证Merkle 树高按交易数减去 1 计算取ceil(log2(n))奇数节点复制最后一个PoW 目标难度越高哈希越大难度越高目标值越小哈希必须小于目标UTXO 与账户余额直接存在地址上余额由未花费输出求和得出共识规则节点多就安全安全性来自累计工作量与节点数量无直接关系把这张表里的每一条都跑过一遍代码之后再回头看课后测试会发现题干换多少种说法考的都是同一批结论。想再往前一步就把本地链的难度位设成 16跑一遍挖矿脚本记录本机每秒能算多少次哈希再用 2^16 除一下看估算出的出块时间和实测差多少。本文还有配套的精品资源点击获取
返回列表