简介这是一份基于Python与区块链实现“时间胶囊”功能的完整项目源码适合有一定Python基础、希望学习去中心化存储与智能合约开发的开发者。项目以Solidity智能合约为核心定义时间锁与解锁规则结合Vue.js前端和JavaScript辅助脚本实现信息加密上链、定时解密、记录查询以及本地元数据管理并提供连接以太坊网络的部署配置与迁移流程。资源包共24个文件包含6个Vue组件、9个JavaScript脚本、2个Solidity合约另有JSON配置、HTML入口和说明文档等整体仅159KB结构精简、模块划分清晰便于快速定位代码和二次开发。目前已有203人浏览学习可作为区块链时间戳服务、加密存储等场景的参考范例帮助开发者深入理解区块链的不可篡改性、共识机制以及智能合约从编写到上线的完整设计思路。1. Python-区块链上的时间胶囊一条链锁住未来才能读到的消息你有没有这种时刻想给一年后的自己留段话又怕半个月就忍不住翻出来看想在某个确定时间向全网公开一段数据又不信任第三方服务器替你保守秘密。这个 Python 项目把时间胶囊的核心逻辑搬上了一条从零手写的极简区块链——正文用 AES 加密存在本地节点内容哈希与解锁时间戳写进链上区块只有链上时间越过解锁点节点才肯交出解密密钥。整套代码不到十个文件适合对区块链存证、加密存储与定时解密有好奇心的开发者拿来当一个可跑的实验台正合适。2. 整体架构链上做公证链下做保险箱2.1 设计取舍为什么不在链上存正文我第一版做过把正文直接写进区块的行为后面一算账就改主意了PoW 出块要时间区块体积涨得快而且链上数据一旦写入就不能改连撤回一封写错的消息都做不到。于是定了现在这套链上做公证链下做保险箱的方案——加密后的正文存在 SQLite链上每个区块只记录一个交易摘要胶囊 ID、正文哈希、解锁时间戳、创建者地址。这个设计的思路在真实存证类项目里也很常见链上放证据链下放内容。这样设计有三个好处。第一正文不落链链上体积稳定出块速度不会被大文本拖垮。第二哈希上链保证了正文不可抵赖地存在过一旦有人改了本地密文重算哈希对不上链上证据立刻暴露。第三时间锁被做成链上状态的一部分解锁判断依赖区块时间戳而不是某个中心服务器的系统时间这在单机演示下虽然只是多了一层形式但为以后接多节点留了余地。技术选型上我一开始纠结过要不要用现成的区块链框架。最后决定自己实现极简链原因是时间胶囊业务对链的核心诉求只有两个不可篡改哈希、可验证时间戳这两个点用一条百来行的链就能讲清楚。用重型框架反而把共识、网络、合约全搅在一起读者很难分清哪一层是时间胶囊的功劳。提示项目演示的是链上公证 链下存储的常见组合不是把完整消息塞进区块的玩具写法。2.2 目录结构与数据模型整个项目我保持精简方便你按图索骥。目录长这样time-capsule-on-chain/ ├── app.py # Flask 入口注册 API 路由 ├── blockchain.py # 区块定义、区块链管理与 PoW ├── capsule_service.py # 时间胶囊业务逻辑创建/解锁 ├── crypto_utils.py # AES-CBC 加密与解密工具 ├── storage.py # SQLite 持久化密文、密钥、胶囊元数据 ├── config.py # 难度目标、数据库路径、服务端口 ├── requirements.txt # flask / pycryptodome └── init_db.py # 初始化数据库建表storage.py 里只放了一张胶囊表字段不多但每个都有用。表结构如下CREATE TABLE capsules ( capsule_id TEXT PRIMARY KEY, message_cipher BLOB NOT NULL, -- AES 加密后的密文 IV key BLOB NOT NULL, -- 解密密钥仅教学演示生产环境需要拆分保管 unlock_at REAL NOT NULL, -- 解锁时间戳单位秒 sender TEXT NOT NULL, created_at REAL NOT NULL, tx_hash TEXT NOT NULL -- 链上对应交易的哈希 );这张表是整条链的内容仓库链负责证明这条消息在某个时间点存在过表负责保存打开这颗胶囊需要的一切。key 直接明文落库是刻意为之——先跑通流程再谈怎么把密钥拆成碎片的托管方案。如果你做生产级版本加密后的密钥应该拆开异地保存这事我在避坑章节再展开。unlock_at 统一用秒为单位的时间戳避免毫秒、微秒混用。2.3 区块与交易时间锁在账本上长什么样区块结构不炫技按最常见字段实现。核心代码# blockchain.py import hashlib import json import time class Block: def __init__(self, index, timestamp, transactions, prev_hash, nonce0): self.index index self.timestamp timestamp self.transactions transactions self.prev_hash prev_hash self.nonce nonce def compute_hash(self): block_string json.dumps({ index: self.index, timestamp: self.timestamp, transactions: self.transactions, prev_hash: self.prev_hash, nonce: self.nonce, }, sort_keysTrue) return hashlib.sha256(block_string.encode()).hexdigest()compute_hash 把区块关键字段序列化后做 SHA-256序列化时强制 sort_keys保证不同节点对同一区块算出的哈希一致。后续加自定义字段时记得把新字段也加进 block_string否则改了字段但哈希不变链上的人一眼看出你做了手脚。交易本身是字典胶囊业务里有两类# 创建胶囊交易 { type: CREATE_CAPSULE, capsule_id: 018f..., # 胶囊唯一 ID content_hash: ab12..., # 密文 密钥的 SHA-256 unlock_at: 1800000000, # 解锁时间戳秒 sender: dev_user_01 } # 心跳交易用于推进时间让测试环境能等到解锁那一刻 { type: HEARTBEAT, node: node_1, ts: 1717812345.6 }content_hash 是密文 密钥整体哈希上链那一刻正文指纹就被钉死。unlock_at 写在交易里而不是取区块 timestamp这样胶囊的解锁时间和区块何时被打包解耦——你可以把一条消息锁到三年后而区块只记录今天写入。这两个时间混为一谈时间胶囊就变成出块即解锁的假胶囊了。3. 核心链路拆解从加密到解锁的完整旅程3.1 创建胶囊AES-CBC 加密与密钥保存创建胶囊的服务代码是最好懂的一块# capsule_service.py import os import hashlib import time import uuid from crypto_utils import encrypt_message from storage import insert_capsule def create_capsule(message: str, unlock_at: float, sender: str): # 1. 生成 32 字节随机密钥 key hashlib.sha256(os.urandom(32)).digest() # 2. 用 AES-CBC 加密正文输出 iv ciphertext sealed encrypt_message(message, key) # 3. 计算上链用的内容哈希 content_hash hashlib.sha256(sealed key).hexdigest() capsule_id str(uuid.uuid4()) # 4. 密文、密钥写本地库 insert_capsule(capsule_id, sealed, key, unlock_at, sender, content_hash) # 5. 构造交易并放入待打包池等待出块 tx { type: CREATE_CAPSULE, capsule_id: capsule_id, content_hash: content_hash, unlock_at: unlock_at, sender: sender, } return capsule_id, tx对应的加密工具在 crypto_utils.py# crypto_utils.py import os from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad def encrypt_message(message: str, key: bytes): iv os.urandom(16) # 每次随机生成 IV cipher AES.new(key, AES.MODE_CBC, iviv) padded pad(message.encode(utf-8), AES.block_size) ciphertext cipher.encrypt(padded) return iv ciphertext # 密文 iv 前缀 密文主体 def decrypt_message(sealed: bytes, key: bytes): iv sealed[:16] ciphertext sealed[16:] cipher AES.new(key, AES.MODE_CBC, iviv) plaintext_padded cipher.decrypt(ciphertext) return unpad(plaintext_padded, AES.block_size).decode(utf-8)参数说明key 固定 32 字节对应 AES-256。IV 是 16 字节CBC 模式下每次加密都必须重新生成如果复用同一个 IV两个密文的前 16 字节之间存在可推算关系这是密码学里的老坑避坑章节我再讲。sealed 的格式是iv ciphertext解密时按长度切分不额外占字段。第 5 步只是把交易丢进池子此刻链上还没有这条时间胶囊的存在证明。真正让它生效的是下一步PoW 打包。3.2 打包上链PoW 与时间戳推进节点把池子里攒下的交易铸成新区块# blockchain.py def proof_of_work(block, difficulty): target 0 * difficulty while not block.compute_hash().startswith(target): block.nonce 1 return block.compute_hash() class Blockchain: def __init__(self, difficulty3): self.chain [] self.pending_transactions [] self.difficulty difficulty self._create_genesis_block() def _create_genesis_block(self): genesis Block(0, time.time(), [], 0) genesis.hash genesis.compute_hash() self.chain.append(genesis) def last_block(self): return self.chain[-1] def mine_pending(self): new_block Block( indexself.last_block().index 1, timestamptime.time(), transactionsself.pending_transactions, prev_hashself.last_block().hash, ) new_block.hash proof_of_work(new_block, self.difficulty) self.pending_transactions [] self.chain.append(new_block) return new_blockproof_of_work 每次尝试更新 nonce直到 block_string 的 SHA-256 以 difficulty 个零开头。difficulty 不是算力调节器它直接决定期望尝试次数难度 3 平均要试 4096 次单机跑几毫秒难度 5 要试一百万次足够把演示接口卡成看起来坏了。教学项目用 3 比较合适既能证明 PoW 存在感又不会让测试等太久。difficulty 在 config.py 读取。心跳矿工是我为了让单机演示不尴尬专门加的线程它每隔一秒挖一个只含 HEARTBEAT 交易的新区块。链上的 last_block().timestamp 会持续前进解锁时间设在十秒后的胶囊服务端等的时候才能准时打开# app.py 内部线程 import threading import time def heartbeat_miner(app): while True: tx {type: HEARTBEAT, node: app.config[NODE_NAME], ts: time.time()} app.blockchain.pending_transactions.append(tx) app.blockchain.mine_pending() time.sleep(1)没有这个线程链上时间会停在最后一个交易区块的时间戳上。如果这颗胶囊恰好就是最后那个交易它的时间戳等于创建时刻永远满足不了区块时间 unlock_at测试会直接卡死。心跳线程在单节点模型下模拟了网络中持续有新块产生的状态是能跑通演示的关键。3.3 解锁读取时间门槛的校验与解密解锁执行的是反向流程先读链再比时间时间到了才允许取 key 解密密文# capsule_service.py def try_unlock(capsule_id, blockchain): capsule get_capsule(capsule_id) if capsule is None: raise ValueError(capsule not found) latest_time blockchain.last_block().timestamp # 核心门槛链上最新区块时间必须晚于等于解锁时间 if latest_time capsule[unlock_at]: raise PermissionError( ftime not reached, {latest_time:.1f} {capsule[unlock_at]:.1f} ) # 门槛通过用库里保存的密钥解开正文 plaintext decrypt_message(capsule[message_cipher], capsule[key]) return plaintext逻辑很直观取链上最新区块时间作为当前链上时间和创建时写入的 unlock_at 比较没到就抛 PermissionError到了就解密返回。这里有个值得注意的点——我比较的是最新区块时间而不是本地系统时间因为链上时间是所有节点都能验证的公共状态本地时间可以被单方篡改。但如果你整条链都在一台机器上跑链上时间来源还是本地系统时钟这层防线就退化成形式了。解锁成功之后我建议顺手写一条 VISIT 交易上链作为某时刻已读的存证。这个设计在数字存证场景里很有用不需要额外出块append 进 pending_transactions 即可。4. 本地跑通十分钟启动一条时间胶囊链4.1 环境准备依赖安装与数据库初始化先准备环境项目依赖只有两个flask3.0.3 pycryptodome3.20.0我用的是 Python 3.10低于 3.8 的版本建议先升级f-string 和类型注解会让你改到手软。安装依赖并建表cd time-capsule-on-chain python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python init_db.pyinit_db.py 做的事情就是 connect 到 capsules.db执行建表 SQL。如果数据库文件已存在重跑会提示表结构未变不用删库。4.2 启动服务与心跳矿工启动入口python app.py看到Running on http://127.0.0.1:5000就说明服务起来了。app.py 的启动顺序是初始化区块链加载创世块启动心跳矿工线程最后跑 Flask。你可以通过GET /chain观察链高度每过一秒应该多一个心跳块curl http://127.0.0.1:5000/chain | python3 -m json.tool返回的 chain 数组持续变长说明心跳正常。如果高度纹丝不动大概率是心跳线程抛异常最常见的是 SQLite 数据库锁问题避坑章节我会给思路。4.3 用 curl 走完整流程创建、提前解锁失败、到点解锁成功项目对外暴露四个核心接口我先列一张表方便对照方法路径作用返回GET/chain查看整条链区块数组POST/capsule/create创建时间胶囊capsule_idGET/capsule/查询胶囊元数据状态与剩余时间POST/capsule/ /unlock尝试解锁明文或 403第一步创建一个解锁时间设在 10 秒后的胶囊curl -X POST http://127.0.0.1:5000/capsule/create \ -H Content-Type: application/json \ -d {message: 给未来的自己你已经升职了, unlock_at: 1800000000}回包大致是这个样子{ capsule_id: a1b2c3d4, unlock_at: 1800000000, status: created }注意创建前执行date %s确认unlock_at 填的是当前秒数加 10别把毫秒整进来否则就是一颗永远打不开的胶囊。第二步立刻尝试解锁预期被拒绝curl -X POST http://127.0.0.1:5000/capsule/a1b2c3d4/unlock会拿到 HTTP 403响应 JSON 里的 message 字段就是 try_unlock 抛出的时间差值提示。第三步等十秒后再执行同样请求就会看到明文正文。两次调用同一个接口只是因为链上时间跨过了门槛返回就从错误变成内容这正是整个项目想表达的核心体验。如果不想干等可以临时把心跳线程的 sleep(1) 改成 sleep(0.2)或者把胶囊的 unlock_at 设成当前时间戳减 1 立刻解锁。但测试完记得把时间校验的真实性补回——绕过时间为难自己时间逻辑就测不到了。手写 curl 验证容易漏细节我更习惯用脚本批量走冒烟# smoke_test.py import requests import time base http://127.0.0.1:5000 capsule_id a1b2c3d4 resp requests.post(f{base}/capsule/{capsule_id}/unlock) print(early unlock:, resp.status_code, resp.json()) time.sleep(11) resp requests.post(f{base}/capsule/{capsule_id}/unlock) print(late unlock:, resp.status_code, resp.json())如果第二次仍返回 403把报错文本里的两个时间戳差值打出来看看链上时间是不是被心跳推到足够靠后。这个脚本在二次开发时可以直接复用改参数就能测新功能。5. 常见问题与避坑时间戳、难度和加密的翻车现场调试这个项目时我花在解 bug 上的时间比写代码多一倍。把几个典型问题整理出来你少走点弯路。5.1 时间戳被偷跑解锁提前发生现象unlock_at 设成当前时间加 3600结果一分钟后尝试解锁就成功了按理说还有一个小时的门槛。 原因区块时间戳来自time.time()矿工节点可以随意调整本机时间生成一个未来的区块链上时间因此被伪造。单节点调试时把系统时钟往后拨链上的 last_block 时间戳立刻超过 unlock_at门槛形同虚设。 解决把解锁校验从最新区块时间改成最近 N 个区块时间的加权中位数遍历self.chain[-10:]取时间戳排序后的中位数。单一矿工只能影响一个块拉不动整体中位数这是主流公链常见的思路。def median_time_past(self, n10): timestamps sorted(b.timestamp for b in self.chain[-n:]) return timestamps[len(timestamps) // 2]解锁接口里把latest_time换成median_time_past()整体侵入很小。从那以后我凡涉及时间门槛一律先问一句这个时间是单点给的还是多点共识出来的。5.2 毫秒和秒对不上永远等不到解锁现象创建胶囊接口一直正常但解锁永远是 403把错误信息里的两个时间打出来看一个带小数另一个是整数差了上千倍。 原因测试脚本用了Date.now()取毫秒时间戳后端约定的是秒。毫秒值进入 unlock_at 之后链上秒级区块时间永远追不上成为一颗永远不会解开的胶囊。 解决入口处做时间规范化。我在 create_capsule 函数第一行加了归一化如果 unlock_at 大于 100000000000约 公元 5138 年就自动除以 1000。def normalize_timestamp(ts): return ts / 1000.0 if ts 100000000000 else ts这个坑太常见了前端、接口、数据库四处都有可能埋雷。建议接口层统一收一发时间转换别指望每个调用方都守规矩。5.3 AES 解密报错 ValueError: Padding is incorrect现象某些胶囊创建后立即解锁报 padding 错误或者密文被篡改一比特后解密照样失败。 原因CBC 模式不提供真实性校验密文任何一比特被改解出来的明文 padding 都大概率不合法。另一个高频原因是 IV 复用——我改版时把 IV 固定成常量结果相同明文在相同 IV 下产生了相同密文前缀这属于密码学上的信息泄露。 解决IV 每次随机生成并随密文存储。如果还要防篡改把 AES 模式从 CBC 换成 GCMGCM 自带认证标签密文被改一比特都能在校验时发现。教学项目里我保留了 CBC 但强制随机 IVGCM 可以作为你的第一个升级方向去练手。5.4 难度配太高接口像死了一样现象启动后 curl 创建胶囊请求挂起一百多秒才回来然后整个 Flask 假死。 原因我把难度从 3 改到 6 做压测每次出块要尝试约 1600 万次 nonce。Flask 默认单线程请求被挖矿卡死心跳线程也跟着堵塞。 解决不要把 Flask 请求线程和 PoW 计算绑在一起。临时方案是难度回滚到 3或者创建胶囊接口单独用难度 2正式方案是把挖矿丢给后台进程接口只负责往交易池塞数据。你如果坚持高难度验证至少先确认请求不是同步等挖矿返回。5.5 多节点分叉后同一胶囊出现两个版本现象把它扩展成双节点时遇到的——两个节点各自在本地挖出了高度相同的块分叉后各自包含同一个胶囊 ID 的不同 content_hash解锁时读到的版本取决于哪个节点响应。 原因单机演示项目没有对等网络同步和分叉处理两个节点天然以自己本地链为准。最直接的 last_block 读取方式在分叉场景下不可信。 解决加一条最长链优先规则同步时比较双方链上累计难度只保留累计难度大的一方在胶囊创建后记录所在区块高度解锁时沿这个高度向上游走确认没有分叉回退。这是完整的改造工作量想做多节点建议先加这条规则再加通信。6. 二次开发把时间胶囊改造成定时红包最有意思的改造方向是把时间胶囊的解锁正文换成解锁转账做成一条链上的定时红包。两者底层逻辑完全一致都是时间锁 条件释放。6.1 把解锁动作换成账户转账胶囊里原来存的是密文和密钥现在换成 recipient 地址和 amount 金额。CREATE_CAPSULE 交易改成{ type: CREATE_TBOX, tbox_id: uuid, amount: 100, recipient: addr_xxx, sender: addr_yyy, unlock_at: 1800000000 }6.2 加余额账本和 TRANSFER 交易存储层加一张 accounts 表保存余额。try_unlock 变成 try_transfer时间到后从 sender 余额扣 amount给 recipient 加上并写一条 TRANSFER 交易进链。注意余额扣减必须先校验时间否则红包可以提前拆开。def try_transfer(tbox_id, blockchain): if blockchain.median_time_past() tbox[unlock_at]: raise PermissionError(time not reached) transfer_or_create_transaction( sendertbox[sender], recipienttbox[recipient], amounttbox[amount], reasontbox_id, )6.3 回归验证与个人习惯改完先跑四组用例未到期转账拒绝、到点转账成功、余额不足拒绝、同一红包解两次幂等。测试定时红包时把解锁时间设在time.time() 2配合 sleep 等它到点比手动改系统时间靠谱得多因为改系统时间会把 PoW 的时间戳也带偏。如果你想把时间锁玩出更多花样把这套源码拉到本地跑一遍冒烟脚本改起来比想象中快。从那以后我每次写时间敏感逻辑都会强制过一遍三问时间单位是秒还是毫秒、源头是链上时间还是本机时间、解锁条件用的是中位数还是单块时间。希望帮到你。本文还有配套的精品资源点击获取