资讯详情

用C#从零实现区块链:核心原理、工作量证明与可视化实战

📅 2026/10/14 5:19:44 | 华诺云谱 👁 阅读
用C#从零实现区块链:核心原理、工作量证明与可视化实战
1. 项目思路为什么用 C# 从零写一条区块链1.1 这个项目到底要解决什么问题我最初写这个项目的动机特别朴素市面上讲区块链原理的文章很多但大部分停留在区块链 区块 哈希 共识这种概念层面。概念谁都懂可真要自己动手把一条链跑起来才会发现一堆逃不掉的问题——区块数据怎么序列化工作量证明的难度怎么动态调整交易签名用什么算法多个节点之间怎么同步这些问题不实操根本不会暴露。所以这个项目选定的目标非常明确用 C# 从零实现一条可运行的区块链包含区块、交易、挖矿、验证、API 接口最后再给它套上一个可视化界面让每个区块的产出、每笔交易的流动都能实时看到。它不是要去对标任何生产级的公链而是把区块链最核心的机制用 C# 完整落地一遍适合两种人一是想深入理解区块链原理的开发者二是想学习 C# 服务端 客户端完整链路但缺一个像样项目的同学。为什么选 C#核心原因有三个。第一C# 的标准库对加密算法的支持非常顺手SHA256、ECDSA 签名都有现成实现不需要引第三方库。第二从命令行到 Web API 再到 WPF 界面全程只用一种语言不需要在 Python、JavaScript 之间来回切换认知负担小很多。第三微软的开发工具链对断点调试和性能分析的支持很到位挖矿循环这种 CPU 密集型的逻辑调起性能分析器来非常直观。1.2 技术选型和架构考量整个项目我分了三层来设计这个分层决定了后期加界面时没有推倒重来。第一层是区块链核心引擎C# 类库项目包含 Block区块、Transaction交易、Blockchain链、Wallet钱包这些核心类。这一层不允许依赖任何外部框架这样核心逻辑可以单独做单元测试也方便将来换 UI 技术栈。第二层是服务层用 ASP.NET Core 提供 REST API外加 SignalR 做实时推送让外部客户端能查询链上数据、提交交易也能在新区块产生时立刻收到通知。第三层是表现层也就是最终那个炫酷界面我用的是 WPF。这三层的依赖方向是单向的表现层依赖服务层的 API服务层依赖核心引擎的类库。好处是核心引擎可以独立验证——命令行版本其实就是直接调用第一层的类库写了个控制台程序后面加界面时核心代码一行没改。这一点是我后来最庆幸的决策如果一开始就把业务逻辑写死在控制台里后面做界面就得重写。命令行版本长什么样就是一个循环模拟产块和挖矿的过程每产出一个块就在控制台打印区块高度、哈希、前哈希、随机数和挖矿耗时。虽然简陋但它承担了最重要的工作验证核心逻辑正确。我当时定了一条纪律命令行版本必须把链的完整性校验跑通之后才开始做 API 和界面。事实证明这条纪律帮我省了大量返工时间。2. 区块链核心区块、哈希与工作量证明的实现细节2.1 区块结构设计与序列化区块是整个链条的基本单元。我设计的 Block 类包含以下字段Index区块高度、TimestampUnix 时间戳、Transactions交易列表、PreviousHash前一个区块的哈希、Hash当前区块的哈希、Nonce随机数、Difficulty当时挖矿难度。这里有个容易忽略的点Hash 不能作为输入参与自身计算否则就循环依赖了。正确做法是先把除了 Hash 之外的所有字段序列化成一个字符串对这个字符串做 SHA256得到的结果就是 Hash。序列化格式我选的是 JSON当时图省事直接用了 System.Text.Json。后来发现序列化性能确实是个瓶颈——区块里装着交易列表每挖一次矿就要把整个区块序列化一遍算哈希交易量大时这个开销很可观。如果重新做一遍我会考虑用手动拼接字符串的方式替代 JSON 序列化尤其是生产环境对 TPS 有要求的话序列化格式必须仔细设计。区块类的大致结构是这样public class Block { public int Index { get; set; } public long Timestamp { get; set; } public ListTransaction Transactions { get; set; } public string PreviousHash { get; set; } public string Hash { get; set; } public int Nonce { get; set; } public int Difficulty { get; set; } public string BuildHashInput() { var txData string.Join(|, Transactions.Select(t t.GetHashInput())); return ${Index}-{Timestamp}-{txData}-{PreviousHash}-{Nonce}-{Difficulty}; } }这里我踩过一个比较隐蔽的坑交易列表的顺序如果没有约定同一批交易在不同节点上可能排列顺序不同导致算出来的区块哈希不一致链就分叉了。解决方法是打包前对交易先排序我按交易的哈希值排序简单粗暴但有效。2.2 哈希计算与链式校验区块哈希用 SHA256这是整个项目最基础也最重要的函数。C# 里实现起来很直接public static string CalculateHash(string input) { using var sha256 SHA256.Create(); var bytes Encoding.UTF8.GetBytes(input); var hashBytes sha256.ComputeHash(bytes); return Convert.ToHexString(hashBytes).ToLowerInvariant(); }注意 Convert.ToHexString 是 .NET 5 之后才有的 API如果你还在用 .NET Core 3.1 或更老版本得自己写字节转十六进制的逻辑。哈希算法的输入输出都是字符串这样做的好处是整个系统里数据交换格式统一坏处是性能有损耗后面性能调优部分我会细说。链式校验的逻辑就是创世块之后每个块的前哈希必须等于前一个块的哈希public bool IsValidChain() { for (int i 1; i Blocks.Count; i) { var current Blocks[i]; var previous Blocks[i - 1]; if (current.PreviousHash ! previous.Hash) return false; var rehash CalculateHash(current.BuildHashInput()); if (rehash ! current.Hash) return false; } return true; }另一个初始化考点创世块Genesis Block没有前哈希我约定它的 PreviousHash 固定为全零字符串 0。这个约定在命令行版本和 GUI 版本里必须保持一致因为界面上会显示创世块的特殊标识我给它做了一个地球诞生的小图标效果后面前端部分再讲。2.3 挖矿工作量证明的 C# 实现工作量证明PoW的实现是整个项目里最需要耐心调的部分。逻辑本身不复杂找一个 Nonce使得整个区块的哈希值前 N 位为 0N 就是当前难度值。每尝试一个 Nonce就要重新计算一次哈希。我用 4 位前导零做初始难度在普通笔记本上大约需要几百万次尝试才能挖出一个块。关键代码如下public void Mine(int difficulty) { var prefix new string(0, difficulty); var nonce 0; while (true) { Nonce nonce; var hash CalculateHash(BuildHashInput()); if (hash.StartsWith(prefix)) { Hash hash; break; } nonce; } }这里有个很反直觉的地方代码看起来没问题但我在递归挖连续多个块时发现内存占用一直在涨。后来定位到是字符串拼接导致的——BuildHashInput 每次都重新生成一整条长字符串在几百万次循环里产生了海量临时字符串托管堆压力巨大。优化方案是用 StringBuilder 复用同一个缓冲或者干脆用更底层的字节数组处理。难度调整机制我也实现了每产 10 个区块调整一次根据实际产块时间与目标时间的比值调整难度。目标时间我设为 10 秒一个块也就是说如果最近 10 个块用了 100 秒平均 10 秒一个就保持难度不动如果平均时间小于 10 秒难度就加 1反之减 1。这个机制在单节点上意义不大但在多节点模拟时很关键它能保证系统在算力变化时自动维持稳定的产块节奏。3. 交易、钱包与内存池的落地实现3.1 交易模型设计有区块没有交易区块链就只是链条有了交易它才成为账本。我的 Transaction 类包含From发送方地址、To接收方地址、Amount金额、Timestamp时间戳、Signature签名、TxHash交易哈希。金额我直接用 decimal 类型虽然它不是高效的存储格式但胜在精度可控UI 显示时不用做浮点转换。交易哈希的算法跟区块哈希类似把结构化字段拼成字符串做 SHA256。这里我加了个小设计交易哈希里混入了 Nonce一个自增序号防止同一发送方在同一时间发起两笔完全一样的交易时哈希碰撞导致交易 ID 重复。这个字段其实对应了以太坊的 account nonce 概念只是我把它简化了。3.2 钱包与数字签名钱包是公私钥对的封装。C# 实现 ECDSA 签名用的是 System.Security.Cryptography 里的 ECDsa 类public class Wallet { public string PublicKey { get; private set; } private ECDsa _privateKey; public Wallet() { _privateKey ECDsa.Create(ECCurve.NamedCurves.nistP256); var publicKeyBytes _privateKey.ExportSubjectPublicKeyInfo(); PublicKey Convert.ToHexString(publicKeyBytes); } public string Sign(string data) { var dataBytes Encoding.UTF8.GetBytes(data); var signature _privateKey.SignData(dataBytes, HashAlgorithmName.SHA256); return Convert.ToHexString(signature); } }签名验证是交易合法性校验的关键一环。验证时只能用公钥不能碰私钥。我在 GetHashInput 里做了一个约定交易哈希计算输入里不包含 Signature 字段否则就会出现签名还没算出来哈希需要签名参与的死锁。这个顺序问题新手特别容易卡住。3.3 交易池与区块打包未确认交易先进入内存池Mempool矿工从池子里挑交易打包成块。内存池我用 ConcurrentDictionary 实现因为 API 层会并发地向池子里添加交易而挖矿线程同时在从池子里取交易不加并发集合很容易踩到线程安全问题。打包逻辑遵循一个简单的优先级规则按交易手续费从高到低排序打包前 5 笔。手续费我没有做成真实的经济系统而是留了一个 Fee 字段用来演示高手续费优先被打包的机制。界面上专门做了一个滑块让用户可以调整交易手续费然后观察这笔交易在内存池里的排队情况——这就是让区块链活起来的一个可视化切面。创世块里我预置了 100 个代币给一个初始地址每次挖矿成功矿工也能获得 12.5 个代币的奖励。这样界面上演示转账时才有余额可用不然演示两笔就破产了。4. 让区块链开口说话API 层与数据通道4.1 ASP.NET Core Web API 设计核心引擎完成后命令行版本验证通过我就开始做 API 层。API 层解决的问题是外部客户端包括后面的 GUI怎么和区块链交互。我用了 ASP.NET Core Minimal API代码量少上手快适合这种规模的项目。接口设计遵循资源导向GET /api/blocks获取全链数据POST /api/blocks/mine触发一次挖矿POST /api/transactions提交新交易GET /api/transactions/pending查看内存池GET /api/wallets/{address}查地址余额POST /api/wallets创建新钱包这里有个设计决策我想强调一下区块的 JSON 序列化模型和内部模型是分离的。直接用 Block 类序列化的话客户端会拿到 Hash、PreviousHash 这些字段没问题但如果你将来要加字段或隐藏某些内部属性改起来很痛苦。所以我在 API 层定义了一组 DTOData Transfer Object只暴露必要的字段。这个习惯一开始可能感觉多写几个类很啰嗦但项目上了规模之后真的是救命的设计。4.2 前端如何拿到区块链数据GUI 端拿数据有两种方案一是轮询每隔几秒 GET 一次全链数据二是用 SignalR 推送。我两种都实现了但很明确轮询只用于初始加载实时更新走 SignalR。轮询的坑在于如果每次 GET /api/blocks 返回全链数据那随着链越来越长响应体越来越大GUI 端渲染也会越来越卡。我做了个分页参数让客户端可以只拉指定范围的区块GUI 端配合虚拟化列表来控制渲染量。SignalR 的设计思路是这样矿机可能是另一个进程在挖到新区块后调用一个内部接口通知 API 服务API 服务再通过 SignalR Hub 广播给所有连接的客户端。客户端收到消息后只更新界面上的最新区块卡片和链图而不是整个重刷。这个增量更新的思路是让界面流畅的关键。Hub 的代码大概长这样public class BlockchainHub : Hub { public async Task NotifyNewBlock(BlockDto block) { await Clients.All.SendAsync(NewBlock, block); } public async Task NotifyNewTransaction(TransactionDto tx) { await Clients.All.SendAsync(NewTransaction, tx); } }4.3 实时推送方案选型如果你也想做这个项目实时推送方面我有几点经验SignalR 的默认传输协议是 WebSocket网络环境不支持时会自动降级到 Server-Sent Events 或长轮询。这意味着本地开发完全没问题但如果有跨网络的演示场景要留意代理服务器对 WebSocket 的支持情况。SignalR 的消息是即发即弃的客户端掉线期间产生的消息不会主动补发。这意味着 GUI 端在断线重连后必须重新走一遍轮询全链数据否则界面上会出现一段时间断层。我在客户端重连事件里挂了全量刷新回调这个问题基本就解决了。序列化格式方面默认 JSON 是 OK 的但如果你追求极致性能可以开启 MessagePack 协议。我实测下来全量刷新时 JSON 和 MessagePack 差距不大这是因为主要开销在区块哈希计算上网络传输占比很小。所以如果不是高并发场景JSON 够用了。5. 让区块链炫起来界面设计与交互5.1 技术选型WPF 还是 Blazor界面层我最终选了 WPF而没有用 Blazor原因有几点。第一我想演示桌面应用中如何实时消费 SignalR 消息WPF 里有现成的 Dispatcher 机制处理跨线程 UI 更新。第二WPF 的动画和自定义绘制能力比网页强很多做区块节点移动、连线闪烁这类效果更顺手。第三C# 桌面端开发的经典栈里WPF 仍然是很多公司的选择做完这个项目对求职有直接帮助。选型对比可以参考方案优点缺点适合场景WPF SignalR 客户端原生桌面体验动画流畅离屏渲染好分发麻烦跨平台能力弱本地演示、教学工具、数据可视化大屏Blazor Server浏览器访问无需安装跨平台需要 WebSocket 保持会话动画能力依赖浏览器Web 端展示、远程访问场景控制台 API最简单适合验证逻辑无可视化无法直观感受区块链动态核心逻辑开发和测试如果你有 Web 开发背景不想碰 XAML选 Blazor 也不会错。但炫酷界面这个需求WPF 的表现力确实更直接。5.2 核心界面模块划分GUI 布局我划分成四个区域。左侧是区块链可视化面板也是整个界面的视觉中心。我自定义了一个 ItemsControl每个区块显示为一个圆角卡片卡片之间用贝塞尔曲线连接。卡片内容显示高度、哈希前 8 位、交易数量和挖矿时间还有一个颜色条表示该区块包含的交易数——交易越多颜色越暖。右侧上方是实时交易面板监控内存池里的未确认交易每笔交易用浮动标签展示发送方、接收方、金额和手续费。交易被打包后标签会从内存池面板飞向对应的区块卡片这个动画我后面细说。右侧下方是钱包面板列出当前所有钱包地址和余额余额变化时数字会有一个轻微放大的动画提示用户注意。最底部是控制台区域保留纯文本日志输出记录每一次挖矿、交易、同步事件。这个区域是我调试时的救命稻草因为动画再炫最终排查问题还是要看日志。5.3 动画效果与实时刷新的实现细节活起来的核心在于动画。我做了三个关键动画第一挖矿过程的动态反馈。触发挖矿后界面顶部出现一个进度环形控件显示当前难度和已尝试的哈希次数。由于 C# 的挖矿循环是 CPU 密集的同步操作必须把它放到后台线程里跑否则 UI 会整个卡死。我用了 async/await Task.Run每计算 10000 次哈希通过 IProgress 接口回报一次进度UI 层更新环形控件。第二交易飞入区块的动画。用 WPF 的 DoubleAnimation 控制 TranslateTransform让交易的视觉元素从内存池面板移动到目标区块卡片的位置。这里需要计算起点和终点的屏幕坐标我的做法是让两个面板的内容替换为它们的视觉坐标然后用一个临时覆盖层承载动画元素动画结束后把元素移除。踩过的坑是如果用 Viewbox 缩放布局坐标计算会偏离因为 Viewbox 改变了元素的渲染尺寸但实际坐标仍按逻辑尺寸计算。后来我统一用固定像素布局解决了这个问题。第三新块出现的脉冲效果。当一个新区块被广播过来它会以透明度和缩放从零变为正常的动画方式出现在链的末端同时之前最后一个区块的连接线会从虚线变成实线。实现方式是监听 SignalR 的 NewBlock 消息在 Dispatcher 线程中触发 Storyboard而所有数据绑定更新在 ViewModel 层完成。这个分离也很重要UI 线程只做视觉呈现数据刷新状态由 ViewModel 管理。接口层我用了 MVVM 模式。SignalR 客户端收到消息后在 ViewModel 中更新 ObservableCollection。注意 ObservableCollection 的线程亲和性只能在 UI 线程操作它所以 SignalR 回调里要包装一层 Dispatcher.Invoke。不做这一步界面会随机崩溃或出现诡异的数据错乱。6. 踩坑实录与性能调优6.1 哈希计算性能的坑前面提到过最开始的哈希函数用字符串拼接导致的直接后果是挖矿速度极慢。我测过一次难度 4 的情况下挖一个块大约需要 4~6 秒但其中 70% 的时间花在了字符串分配和 GC 上。优化方案有三个层次。第一是把 BuildHashInput 改为复用 StringBuilder。第二是把字符串哈希改成字节数组哈希省掉 UTF8 编码转换。第三是引入 Span 和 stackalloc对小数据块避免堆分配。我实际做了前两层优化后挖矿速度提升了大约 3 倍第三层因为代码可读性下降太多我放弃了毕竟这个项目的目的不是造一台疯狂挖矿机。另一个性能瓶颈是区块数据反序列化后重新算哈希做校验。如果每次都反序列化整个链再遍历校验链越来越长后性能会肉眼可见地下降。优化方案是缓存每个区块的哈希字段只有收到新区块或本地修改区块时才重新校验一次平时直接信任已验证过的链。6.2 并发访问与锁问题API 服务和挖矿服务同时运行后我遇到了经典的并发问题两个线程同时调用 Blockchain.AddBlock导致链上出现两个相邻区块指向前一个区块的哈希相同的情况这在单条链的模型里是不允许的。解决办法是给链的添加操作加把锁private readonly object _chainLock new object(); public void AddBlock(Block block) { lock (_chainLock) { var lastBlock Blocks[^1]; if (block.PreviousHash ! lastBlock.Hash) throw new InvalidOperationException(区块的前哈希不匹配); if (!IsValidProof(block)) throw new InvalidOperationException(工作量证明不合法); Blocks.Add(block); } }锁的粒度需要仔细控制——只能在 AddBlock 或者打包插入这个关键路径上加锁不能把整个 Chain 的读写都包进锁里否则 API 的查询性能会大打折扣。实际上我是用锁保护读操作和写操作的一致性的读操作读取 Blocks 时也拿同一个锁避免读到一半链被修改导致引用错乱。6.3 常见问题与排查速查表整理一下我在开发过程中遇到的典型问题很多都是另一开发者让我帮忙排查时也反复踩到的症状可能原因排查思路新区块哈希始终验证失败序列化字段顺序不一致或遗漏字段打印 BuildHashInput 的输入串和计算哈希时的输入逐字符对比UI 卡死但控制台正常挖矿在 UI 线程执行确认调用链里的异步方法加了 ConfigureAwait(false)并且后台计算在 Task.Run 中SignalR 客户端收不到消息Hub 路由注册遗漏或客户端连接未建立打开浏览器开发者工具看不到 WebSocket 连接检查服务端 Startup 里 MapHub 的路径交易签名验证失败签名的输入串和验证时不一致复查 GetHashInput 方法里字段的拼接顺序注意时间戳精度是否被截断内存池交易越积越多挖矿打包逻辑没去池子取交易检查打包时取出的交易是否从 ConcurrentDictionary 中移除建议用 TryRemove 而不是先 TryGetValue 再 Remove还有一个非常隐蔽的 Bug 值得单独说交易金额的 decimal 类型在序列化时如果客户端提交的是一个浮点数比如 12.5反序列化到 decimal 时可能因为精度问题变成 12.5000000000导致交易哈希变化签名验证失败。我的方案是 API 层把金额字段限制为字符串服务端自己用 decimal.Parse 解析彻底避开浮点传输。6.4 命令行版本到 GUI 的迁移经验最后分享一点从命令行版本迁移到 GUI 版本的心得。迁移过程中我几乎没有改动核心引擎代码只做了两件事把控制台调用换成了 API 调用把原来分散在 Main 方法里的调试逻辑收拢成服务类。这说明一开定的分层架构确实起了作用。但迁移中也暴露了一个问题原来命令行版本里为了演示方便好几个方法都是静态的比如区块链实例只有一个全局单例。到 GUI 版本时如果界面上要同时查看两条不同难度参数的链做对比全局单例就不够用了。解决办法是把这些静态方法改成了实例方法区块链实例由依赖注入容器管理每个实例有独立的锁对象、独立的挖矿线程。这个改动虽然不算大但牵一发动全身——所有引用静态方法的地方都要同步改。如果你的项目也打算先做命令行再做 GUI建议从一开始就养成用依赖注入的习惯把区块链实例注册成 Singleton 或 Scoped后面加界面时就能省很多事。7. 让链持续长大配套工具与扩展方向7.1 区块浏览器式的数据展示界面做出来后我又加了一个类似区块浏览器的界面页可搜索区块高度和交易哈希点击区块卡片能看到该区块包含的所有交易详情。这个功能最核心的价值在于验证数据可追溯这个区块链关键特性。搜索功能我用简单的前缀匹配实现因为私链数据量小不需要引入数据库索引直接内存字典缓存哈希和区块位置的映射就够了。区块详情页里我把交易列表与签名信息做了展开用户可以直观看到一笔交易的输入输出和验证状态。这个功能在演示时特别有说服力——你能清楚地告诉观众区块链是如何保证数据不可篡改的改任何一笔交易哈希变化链全部失效。7.2 多节点模拟的取舍本来计划做 P2P 多节点同步但后来我砍掉了这个功能只保留了 API 层的节点注册机制故意没有实现真正的点对点网络。原因是单机模拟多节点时网络层做假没有意义不如把资金投入到更成熟的示例场景里。如果你一定要做多节点建议从节点间同步区块开始用 SignalR 广播新区块而不是重新实现一套 TCP 协议。等核心同步逻辑跑通后再考虑替换成真实的 P2P 传输。先把业务逻辑做对再做传输优化这个顺序是稳的。7.3 面向教学的轻量化演示模式做完整个项目后我发现它对教学的帮助很大。我整理了一个演示模式难度自动降低到 2挖矿速度极快视觉上区块一个接一个地产生观众马上能感受到区块链的生命力。同时界面上的内存池面板塞满了待打包的交易每个交易的飞行动画连续不断整体效果宛如一条缓慢流淌的河流。这个模式之所以重要是因为真实的私链挖矿速度其实很慢观众很容易看困。演示模式通过参数化控制产块速度让整个系统处于最动感的状态。这也是一个通用经验做可视化项目时节奏控制比功能齐全更影响演示效果。8. 关于这个项目我的几点实在话项目做完后有几个人问我为什么不去做一个基于已有框架的快速区块链应用非要自己从零写。我的答案是只有自己实现过一遍区块链你才能分辨哪些宣传是真的哪些是噱头。比如你亲手写过工作量证明就会理解为什么算力即权力你亲手实现过交易签名就会理解为什么私钥丢失等于资产丢失。技术层面C# 做这个项目的体验比我预想的好。从命令行到 GUI 的完整链路全程一个语言一套 IDE 工具调试体验一致这在其他语言栈里不容易做到。如果你正在学 C#又需要一个能同时体现算法、并发、网络、UI 的项目区块链是一个相当完整的练习场。最后再分享一个小技巧开发过程中给每个区块生成一个随机的主题色然后让区块卡片的背景色随主题色渐变链的长度增加时颜色呈彩虹状推进。这个小改动不仅好看还能让人一眼看出链的增长节奏视觉上特别讨喜。你也完全可以在这个方向继续发挥比如加上 3D 视角的音乐可视化效果或者接入真实的时间轴回放功能。这个项目后续我打算加的内容有两个方向一是把共识机制做一次抽象让工作量证明和权益证明可以切换这样能直观对比两种共识的性能差异二是加一个完整的测试覆盖把区块链的异常场景全部用自动化测试保护起来毕竟链一旦跑起来靠人工排查真的会头大。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑