<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>smallyu的博客</title>
  <icon>https://smallyu.net/favicon.png</icon>
  <subtitle>smallyu的博客</subtitle>
  <link href="https://smallyu.net/atom.xml" rel="self"/>
  <link href="https://pubsubhubbub.appspot.com/" rel="hub"/>
  <link href="https://smallyu.net/"/>
  <updated>2026-09-06T09:08:50.722Z</updated>
  <id>https://smallyu.net/</id>
  
  <author>
    <name>smallyu</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Virae 项目的最新进展和困境</title>
    <link href="https://smallyu.net/2026/09/06/Virae%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%9C%80%E6%96%B0%E8%BF%9B%E5%B1%95%E5%92%8C%E5%9B%B0%E5%A2%83/"/>
    <id>https://smallyu.net/2026/09/06/Virae%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%9C%80%E6%96%B0%E8%BF%9B%E5%B1%95%E5%92%8C%E5%9B%B0%E5%A2%83/</id>
    <published>2026-09-06T08:49:24.000Z</published>
    <updated>2026-09-06T09:08:50.722Z</updated>
    
    <content type="html"><![CDATA[<p>距离《<a href="https://smallyu.net/2026/08/09/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA%E7%94%9F%E6%80%81%E7%9A%84%E9%A1%B9%E7%9B%AE%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9A%BE%E5%81%9A/">预测市场生态的项目为什么难做</a>》过去了接近一个月的时间，来记录和总结一下这个月的项目进展情况。</p><h3 id="Paper-Trading-比赛"><a href="#Paper-Trading-比赛" class="headerlink" title="Paper Trading 比赛"></a>Paper Trading 比赛</h3><p>接着上篇文章的末尾，Virae 原本计划好的模拟盘交易大赛，最后怎么样了呢？最后延迟了两周开始启动、找 KOL 转发、开始。</p><p>也就是说，原定 8月10日开始的推广和测试，推迟到了 8月21日开始。</p><p>这中间两周的时间干了什么呢？主要就是网页上增加了一个面向新手用户的 Guide 模式，一步一步引导操作那种。有人认为原本的网页和界面比较 Pro，只能针对熟悉预测市场的用户，新手用户会用不明白、觉得复杂。</p><p>结果怎么样了呢，历史总是重复上演，无论是原本的专业版本、还是新做的引导版本、都没有真实用户使用。</p><p>我来拆解一下这里面的两个逻辑问题。</p><ol><li>Agent 方向和模拟盘交易比赛不冲突</li></ol><p>我们需要新做引导模式的起因，是某个人在项目上面没用明白、认为项目过于复杂导致他亏了钱，所以要求整个项目做改进。</p><p>问题在于，这为什么要阻塞模拟盘交易比赛的活动？模拟盘顾名思义用的不是真钱、是平台给发的一个体验金。无论整个项目的 Agent 做的如何晦涩难懂或者容易让人亏钱，为什么要阻塞这个比赛的进行？</p><p>当然你可以认为，如果模拟盘比赛转化、引起一些参与人员真的投入了钱、亏了钱呢？这是再一次狼来了的故事。其实我们已经不止一次发生狼来了的故事，“假设会有很多很多用户来使用，我们应该拿出怎么样的诚意和产品质量、才可以达到可推广的水平？”</p><p>而模拟盘想要达到的目的是什么呢？是判断是否有用户会对预测市场的 Agentic Trading 感兴趣，还没有到真正大规模想要转化用户的阶段。</p><ol start="2"><li>即使有真实用户使用</li></ol><p>即使有真实用户使用，如何判断，用户是喜欢用 Pro 版本、还是喜欢用 Guide 版本？Giude 版本的成功能实锤 Pro 版本的失败吗？反之，Guide 版本的失败，能证明 Pro 版本也会失败吗？</p><p>这不是一个个人倾向的问题，而是对照实验的问题。我们小学课本就学过的对照实验，也许被这些专业的赚过钱的大人们都抛之脑后了。如果最终项目成功了，是这两周时间投入做了 Guide 版本的功劳、还是因为这个项目方向本来就有吸引力？</p><p>总之现在这个 Paper Trading 在被推迟两周后、预计明天 9月7日结束，也是第一次真正验证转化率的时候，看看有没有人会因为模拟盘的收益而真实尝试实盘交易。</p><ol start="3"><li>我自己的行动变化</li></ol><p>增加 Guide 版本这个事情，也给我自己的工作行为带来了变化。以前我在改动什么功能后，都会发到群里，或者直接让 ChatGPT 给我出一份每日总结发到群里。</p><p>但是某个人不但在 Virae 上因为没用明白而亏了钱、还延缓了整个项目的节奏、让我们付出多余的时间和精力去做改进，这让我深刻明白，发在群里的功能变动，是没有人看的。一开始就是他说要发功能变动的、发了又不看。</p><p>所以后来我不再群里发汇总，我最后一次发功能变动汇总消息到群里，是 8月10日。</p><p>同时我自己做汇总和记录的习惯没有停，这个网页包含了日期从 7月12日开始、9月2日结束的、我所做的 Virae 项目的功能变动。</p><p><a href="https://smallyunet.github.io/virae-logs/">https://smallyunet.github.io/virae-logs/</a></p><p>从 9月2日之后汇总就真的停了，因为 Virae 的功能改进也停了。</p><h3 id="DM-KOL"><a href="#DM-KOL" class="headerlink" title="DM KOL"></a>DM KOL</h3><p>如果你看过《<a href="https://smallyu.net/2026/06/29/%E4%B8%BA%E4%BB%80%E4%B9%88DM-KOL%E7%9A%84GTM%E7%AD%96%E7%95%A5%E4%B8%80%E5%AE%9A%E6%98%AF%E2%80%9C%E5%AF%B9%E2%80%9D%E7%9A%84/">为什么 DM KOL 的 GTM 策略一定是 “对” 的</a>》，就知道我其实不那么看好DM KOL这种策略。</p><p>8月22日到 8月28日的一周，是团队比较迷茫的一周，此时所谓的 Guide 版本也加上了、模拟盘交易大赛也交给运营同事去推广了。开发和产品方面进入一种不知道干什么的状态。</p><p>8月28日（周五），开会的时候说，我们可以继续每个人都做一下 DM KOL 这件事情，设定的计划是用一周的时间联系 50 个KOL。</p><p>经过周末两天的无所事事，8月31日（周一），我提出可以直接什么事情都不干、产品不要设计产品了、技术不要开发功能了、直接全部人全力去做 DM KOL 这件事情、目标设定为用两周时间 DM 500 个 KOL。（虽然我不看好、但是既然大家都说有用、那么就试试）</p><p>解释一下我的动机和逻辑：我们设定一条止损线，看能否用一个足够多的数量和足够长的时间内，判断现在 Virae 的项目方向是否正确。数量是指联系过KOL的数量，联系过多少个人就意味着足以判断用户群体和市场是否对 Virae 的产品方向感兴趣了。时间是指老板能够接受用多长时间来干这个事情、验证项目方向是否正确这件事情。</p><p>当然这里面也涉及到几个问题。</p><ol><li>DM KOL的方法</li></ol><p>为了避免由于我个人在 DM KOL 的经验、方法、沟通技巧上的不足，我一直在提议用矩阵的方式去联系 KOL。也就是同一个 KOL，每个人都分别按照自己的时间和话术去联系。这样可以弥补个人的不足。如果 KOL 不理某一个人，那可能是那个人的原因。如果 KOL 10 个人都不理，那就是 KOL 的问题。</p><ol start="2"><li>能不能反向证明项目方向不可行</li></ol><p>假如联系 500 个 KOL、得到的结果是没有人对 Virae 感兴趣，那么能证明 Virae 的项目方向错了吗？其实不能。但是至少能得到，投入和结果之间的性价比数据。这好理解吧。</p><ol start="3"><li>项目需要止损线吗</li></ol><p>如果你说，这个项目未来有一天会成功，但是不知道是哪天。那么我们去努力、尝试各种各样的方法、坚持，都很正常，甚至每个人都乐于做这种事情。</p><p>如果是对于一个销售人员，假如 500 个人里面有 1 个人是目标用户，那么作为销售人员，从这 500 个人里面找出那 1 个目标用户，就是职责所在。只要找到这 1 个用户，工作就完成了，工资就拿到了，奖金就拿到了，这个逻辑是闭环的。</p><p>但是对于站在项目方考虑的角度，一个项目的方向是否正确，需要判断吗？假如真的得到一份数据，市场中目标用户的比例是 500:1，对整个项目有什么意义？</p><ul><li>需要判断吗？</li></ul><p>最近短视频里，有一个勇哥餐饮很火，只要原地 360 度转一圈，他就能判断出一家餐饮店能不能继续开下去。他说过一句有道理的话，很多餐饮店，在开的时候就已经倒闭了，只是坚持了几个月才关。</p><p>同样的道理，我在担心的就是，假如项目方向从一开始就是错的，那么这个项目启动的时候，就已经失败了，只是坚持了几个月、努力了几个月，才停止和放弃。</p><ul><li>500:1 的用户比例有什么意义？</li></ul><p>Polymarket整个生态的用户数量，是链上公开的，月活跃用户大概 100 万个地址。今年 7 月份的世界杯数据表明，使用 Polymarket 官网和第三方项目的用户比例，是 8:2。那么也就是说，第三方项目方的月活跃用户总计大概有 20 万个。这样就知道 500:1 的比例数据能有什么用了吧。</p><ol start="4"><li>我为什么要站在项目方立场考虑问题</li></ol><p>我只是普通员工而已，角色上也只是挣个工资，为什么要考虑项目能不能成功的问题？</p><ul><li>项目的成败对以后的职业生涯有影响</li></ul><p>除非一个项目能让你财富自由足够养老，否则你以后还是需要继续工作。而在招聘市场上，成功的项目经历和失败的项目经历天壤之别。</p><p>失败的项目经历中，即使你做了什么有技术含量的事情，别人也不会在乎，而且解释成本非常高。而在成功的、知名项目中，即使你做的工作没什么内容，也很容易得到认可。这是我在过往很多年反复求职的经历中，明确感受到的差异。不是眼红别人容易得到认可那种，而是我自己的项目经历就有不成功的和不那么失败的。</p><p>所以一段失败的项目经历，一定程度上也是在消耗以后的职场机会。</p><ul><li>不关心项目成败，与我以往的行为模式不符</li></ul><p>多年以来我都是频繁跳槽的状态，每一次都在说这样那样的原因，而本质上就是看透了项目无法成功。</p><p>如果我一直以来的行为模式是安于挣个工资，也许我的职业经历会稳定很多、负债情况会减轻、履历会漂亮。但是显然我没有走那样的路径。</p><p>如果现在让我安心于挣个工资、不去考虑和判断项目能成功或者失败，与我个人以往的行动模式是冲突的。如果现在安于工资了，那以往不稳定的付出算什么？</p><ul><li>年龄焦虑</li></ul><p>难道你们不担心吗？</p><ol start="5"><li>我 DM KOL 的数据结果</li></ol><p>现在过去了一周，我的进度相对快，已经处理接近 400 个目标。我直接总结一下我这一周以来、样本数量接近 400 条的结果：</p><table><thead><tr><th>指标</th><th align="right">人数</th><th align="right">比例</th></tr></thead><tbody><tr><td>实际成功私信联系人</td><td align="right">284</td><td align="right">—</td></tr><tr><td>私信后获得首次回复</td><td align="right">27</td><td align="right">9.51%（27&#x2F;284）</td></tr><tr><td>首次回复后，发送第二轮消息</td><td align="right">22</td><td align="right">81.48%（22&#x2F;27）</td></tr><tr><td>第二轮消息再次获得回复</td><td align="right">9</td><td align="right">40.91%（9&#x2F;22）</td></tr><tr><td>私信回复者中，明确提到付费合作或报价</td><td align="right">6</td><td align="right">22.22%（6&#x2F;27）</td></tr><tr><td>评论触达联系人（共 103 条评论）</td><td align="right">103</td><td align="right">—</td></tr><tr><td>评论获得回复</td><td align="right">5</td><td align="right">4.85%（5&#x2F;103）</td></tr><tr><td>真正体验 Virae 的人数</td><td align="right">0</td><td align="right">0</td></tr></tbody></table><p>我倒是也明白，也许有某种心理暗示，我从一开始就抱着结果不会很好的心态，并且寄希望于团队其他成员能做的比我更好。不过再回头想想，其他人能做的多好呢？虽然我确实希望其他人做的更好。</p><ol start="6"><li>设立止损线更深层次的原因</li></ol><p>如果你属于性格敏感或者共情能力强的人，你可能看出来了，设立止损线有一个重要的心理动机：对项目方向没信心。</p><p>作为项目的开发者，对项目方向没有信心，这意味着什么呢？要么是眼光差、看不懂项目的深远用意、要么就是……这里面有很多个人偏好的因素在，也许市场环境和未知的用户群体，真的吃这一套、愿意玩这种项目、并且为此付费呢？你可以不喜欢赌博，但是如果你有机会开赌场能赚钱，你肯定会开，对吧？只要有人喜欢赌博就行。</p><p>我们现在，Virae 项目，就是在寻找赌徒，寻找愿意赌博的人。</p><p>而我自己（仅仅是我），不赌。CZ 赌不赌呢，徐明星赌不赌呢，孙宇晨赌不赌呢，他们开着世界上最大的赌场，他们不赌。</p><h3 id="下一阶段"><a href="#下一阶段" class="headerlink" title="下一阶段"></a>下一阶段</h3><p>至少到目前为止项目还没有结束，还有至少两件事情需要验证：</p><ol><li>DM KOL 原定两周时间，现在只过去了一周，等第二周结束，看看所有人的数据，然后看看下一步的行动计划、要不要继续</li><li>Paper Trading 的比赛属于活动，而这只是第一次活动，按照之前的说法，活动的转化率数据是很重要的，我猜至少得举办 3 次类似的撸毛活动、综合看 3 次的转化数据，才能得出可靠的结论。一次活动持续 2 周、两次活动之间至少间隔一周，光是这种活动都得再持续 6 周时间。所以 Virae 这个项目大概率不会在 6 周内停止</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;距离《&lt;a</summary>
        
      
    
    
    
    
    <category term="工作" scheme="https://smallyu.net/tags/%E5%B7%A5%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>预测市场生态的项目为什么难做</title>
    <link href="https://smallyu.net/2026/08/09/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA%E7%94%9F%E6%80%81%E7%9A%84%E9%A1%B9%E7%9B%AE%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9A%BE%E5%81%9A/"/>
    <id>https://smallyu.net/2026/08/09/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA%E7%94%9F%E6%80%81%E7%9A%84%E9%A1%B9%E7%9B%AE%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9A%BE%E5%81%9A/</id>
    <published>2026-08-09T15:14:31.000Z</published>
    <updated>2026-08-09T15:14:12.911Z</updated>
    
    <content type="html"><![CDATA[<p>前情提要：</p><ul><li>《<a href="https://b.smallyu.net/2026/05/27/%E6%9C%80%E8%BF%91%E5%A4%A7%E5%8D%8A%E5%B9%B4%E7%9A%84%E5%B7%A5%E4%BD%9C/">最近大半年的工作</a>》2026-05-27</li><li>《<a href="/2026/06/29/%E4%B8%BA%E4%BB%80%E4%B9%88DM-KOL%E7%9A%84GTM%E7%AD%96%E7%95%A5%E4%B8%80%E5%AE%9A%E6%98%AF%E2%80%9C%E5%AF%B9%E2%80%9D%E7%9A%84/">为什么 DM KOL 的 GTM 策略一定是 “对” 的</a>》2026-06-29</li><li>《<a href="/2026/07/10/%E5%81%9APolymarket%E9%A1%B9%E7%9B%AE%E8%B8%A9%E7%9A%84%E5%9D%91/">做 Polymarket 项目踩的坑</a>》2026-07-10</li></ul><p>我会总结分析一下最近半年来，我们团队做项目遇到的种种困难。总的来说，现在项目仍然没有找到 PMF、没有达到用户自增长的状态。与以前的项目经历不同，我可以毫不掩饰地说这个项目的官网是：<a href="http://virae.ai/">http://virae.ai/</a></p><p>在这个项目上，团队一直在以最少的人员配置 + 最低的硬件成本去做。这种低成本主要也是为了试错。至少我是这么理解的。</p><h3 id="数据服务-VS-交易工具"><a href="#数据服务-VS-交易工具" class="headerlink" title="数据服务 VS 交易工具"></a>数据服务 VS 交易工具</h3><p>一开始想要切入预测市场、做一个预测市场生态上的项目时，我们就看到了一个<a href="https://x.com/pmx_trade/status/2008997887678820862?s=46">新闻</a>，一个叫 <a href="https://www.polycule.trade/">Polycule</a> 的 TG Bot 项目，算是 Polymarket 生态比较头部和知名的一个项目，因为服务器被黑客攻击、一度损失资金甚至传闻跑路，目前也已经改名换姓、整个产品换了方向。</p><p>当时忌惮这种托管私钥方面的安全问题，团队对于项目的约束是，不要做这种托管私钥类型的交易工具。</p><p>那么不做交易工具做什么呢？主要在这几个方向上徘徊：</p><ol><li>Alerting 类型，比如市场动态方面的告警、仓位提醒等，典型项目是 <a href="http://cryptocurrencyalerting.com/">Cryptocurrying Alerting</a>、<a href="https://polyalerthub.com/">PolyAlertHub</a></li><li>DexScreener 类型，聚合不同预测市场（Polymarket、Predict.fun、Kalshi 等）的事件、并且提供相似事件的对比分析，典型项目是 <a href="https://dexscreener.com/">DexScreener</a></li><li>Lookonchain 类型，分析挖掘链上数据、讲故事，典型项目是 <a href="https://x.com/lookonchain">Lookonchain</a></li></ol><p>另外还有一个团队自己发掘出来的方向：预测市场事件的语义搜索。因为无论是从 news 文本-&gt; 提醒，还是从链上数据-&gt; 提醒，都绕不开一个关键环节：怎么精确找到自己想找的事件？我看到一个特朗普的新闻，但是并不知道与这条新闻相关的预测市场事件是哪个、去哪里下注。</p><p>然后我告诉你为什么这几个方向没做起来：</p><ol><li>语义搜索方面，做了官网、试着做推广但是没下文、没有人用。服务器方面的成本，大概需要索引40 GB 左右的市场数据到数据库，包括 embedding 数据等用于检索。</li><li>DexScreener 方面，不同的预测市场平台会刻意避开事件相同、结算规则相同，以避免跨平台套利的行为发生。这一点和 Crypto Dex 的逻辑是不一样的，同一个 Crypto Token 会尽可能去多个平台上流通，而预测市场的事件都在各自的平台上维护。</li><li>Lookonchain 数据挖掘方面，需要很强的讲故事能力。看到数据不是重点，重点是需要把信息关联起来讲故事，这一点既需要数据分析能力，也需要小编来写内容，而不是纯程序分析出来的信号可以做到的。</li><li>Alerting 服务方面，看似轻量级，但是需要高速且不限速的 API 接口，或者自己索引链上数据。而如果自己索引链上数据，就免不了需要自建节点或者自建 SubGraph。</li></ol><p>我一开始提到我们在硬件投入上也是维持在低成本模式。有多低呢，现在整个 Virae 的后端服务器，一共用了 3 vCPU、6 GB 内存，100 GB 数据，在 AWS 上。</p><p>我并不是说这样的配置不够用，而是在这样的成本约束下，自建节点、自建 SubGraph 一类的服务自然是做不了的。举个直观的对比，运行一个以太坊节点的配置，是 8 vCPU、32 GB 内存、1 TB 磁盘、且要 IOPS 大于 10k 的固态硬盘。</p><h3 id="信息聚合终端"><a href="#信息聚合终端" class="headerlink" title="信息聚合终端"></a>信息聚合终端</h3><p>这是在可以做交易工具之后、首先比较看好的方向，比如用户需要在 NBA 相关赛事下注，需要同时打开 NBA 比赛以及 Polymarket 的事件下注页面。我们能不能做一个交易终端，同时展示体育赛事的信息、以及聚合 Polymarket 的下注交易能力？</p><p>这个方向上有明确的竞品，包括 Crypto 领域的 <a href="https://gmgn.ai/">GMGN</a>、<a href="https://axiom.trade/">Axiom</a>，预测市场领域的 <a href="https://betmoar.fun/">Betmoar</a>、<a href="https://ares.pro/">Areas</a>、<a href="https://www.stand.trade/">stand.trade</a>。</p><p>结果就是没有人用。</p><p>至于为什么没有人用，是我们一直都在反思但是不知道答案的问题。</p><p>顺便提一下，所有在 Polymarket 生态的项目，都在 Polymarket 的 Builder 榜单上。Polymarket 官方提供的榜单只有交易量总额，没有更多信息。因此我做了一个辅助网站 <a href="https://poly.dark20.xyz/">https://poly.dark20.xyz/</a> ，这些数据都是从 Polymarket 官方的公开接口拿到的，列出了每一个项目方的交易额、交易人数、甚至具体的每一笔交易的详情。由于交易接口方面的约束，同赛道的项目方一定都在这个榜单上，反之，不在这个榜单上的项目方一定不是 Polymarket 生态的项目。</p><h3 id="Copy-Trading-Bot"><a href="#Copy-Trading-Bot" class="headerlink" title="Copy Trading Bot"></a>Copy Trading Bot</h3><p>在 Crypto 领域有很多 TG Bot，提供 snipe 代币的功能，或者很简便的贴一个代币的 CA 地址就可以开始交易。那么在预测市场是否需要一种 copy trading bot 来一键复制别人的交易行为呢？</p><p>比较头部的竞品很多，做的也都不错，除了之前提到的 <a href="https://www.polycule.trade/">Polycule</a>、还有 <a href="https://polycop.ai/">PolyCop</a>、<a href="https://kreo.app/">Kreo</a>、<a href="https://polybot.trading/">PolyBot</a> 等。</p><p>结果也是没有人用。</p><p>至于为什么没有人用，能找到一些原因：</p><ol><li>如果要做到0区块延迟的跟单速度，需要自建节点、监听 mempool 里的交易、然后跟单。上文提到，自建节点需要一笔硬件成本的投入。</li><li>马太效应，越是头部的 bot 越有人用，越是小的 bot 越没有人用。</li><li>运营能力，我们没有人具备运营 X 账号、其他平台发广告推广的能力。</li></ol><h3 id="电脑端-VS-手机端"><a href="#电脑端-VS-手机端" class="headerlink" title="电脑端 VS 手机端"></a>电脑端 VS 手机端</h3><p>世界杯期间，项目准备了一些预算做活动，交易就给钱那种，来了不少来自印度的撸毛用户。当时有一个发现，大多数用户都是在手机上操作，要么在手机浏览器里打开网页，要么在 TG bot 里操作。很少有用电脑直接操作。</p><p>这一点和我们的预期有很大偏差，因为一直以来，都是在做专业的交易终端，而交易终端一定是电脑网页，也因此几乎没有刻意做什么移动端的适配。</p><p>再后来，又关注到一个用户数量很多的项目 <a href="https://polym.trade/">polymtrade</a>，是一个手机 App，在苹果商店能搜到那种。这个项目页面非常简陋、而且不支持高频交易，但是月活跃交易用户在1000以上、交易额在百万美元以上。我调查过，这个项目没有融资背景、只有 3 个开发者。</p><p>所以我们后来一方面优化移动端网页、另一方面适配了 TG Mini App，希望用户可以直接在 TG 上面打开 Mini App、就能拥有完整的体验。</p><p>结果也是依然没有人用。</p><h3 id="高频交易-VS-低频交易"><a href="#高频交易-VS-低频交易" class="headerlink" title="高频交易 VS 低频交易"></a>高频交易 VS 低频交易</h3><p>高频交易就是给专业交易员操作用的，比如现在下单后、看 K 线、发现几秒钟的动量、马上操作卖出，或者同时挂很多订单、止盈止损、频繁操作。这种就要求界面操作要流畅、下单速度要快、交易反馈要准确。与之相应的，技术难度高。</p><p>低频交易是指那些佛系散户，比如今天看到什么新闻、想去某个事件上下一单、明天想起来就又下一单、下单之后也不看 K 线也不看流动性、甚至不操作不卖出，就纯粹等半年后的结算。</p><p>这两种交易频率的不同，也是产品方向上的分歧。高频交易注定使用者专业、数量少、交易量大。低频交易也许用户量多、但是交易量会很少。</p><p>有研究报告指出，整个 Polymarket 有80%的散户交易总额在100美元以下。而且这是整个 Polymarket 生态的，其中 Polymarket 官网的交易人数和交易量占了90%左右，留给第三方项目方的交易量只有10%。（研究报告原文找不到了，大概是这个数字，你不相信的话可以让 AI 重新调研一次。）</p><p>这个数据如果靠谱的话，那么 Polymarket 生态的项目会过得比较惨。看起来预测市场的交易量和收入年年新高，但是跟第三方项目方有什么关系呢？</p><h3 id="再做一个官网"><a href="#再做一个官网" class="headerlink" title="再做一个官网"></a>再做一个官网</h3><p>随着项目功能越做越多，后来逐渐发现一个误区：再做一个官网。</p><p>当时正是世界杯，Polymarket 和 Kalshi 都出了 combo 交易的功能，我们就在看能不能从接口接入这种 combo 交易的能力。后来发现官方提供的 API 不支持。</p><p>这就导致一个尴尬的现象：专业的交易终端本应该是功能增强版本的 Polymarket，结果 Polymarket 出了新功能、第三方交易终端跟不上！</p><p>这也突然带来一个警醒：Polymarket 是中心化平台、不是去中心化协议！</p><p>CLOB v1 版本的时候也许还能看到一点去中心化的影子，自从 CLOB v2 之后，就实锤 Polymarket 是中心化平台了，毫无疑问。</p><p>也因此后来 Polymarket 出了 perps 交易的功能，我们根本不去看能不能接入了。接入了也没人用。</p><p>再后来，Kalshi 还出了官网版本的交易终端 Kalshi Pro，杀死比赛。</p><p>总之再做一个官网绝对是行不通的方向。</p><h3 id="Agentic-Trading"><a href="#Agentic-Trading" class="headerlink" title="Agentic Trading"></a>Agentic Trading</h3><p>后来看 Agentic Trading 的方向比较火，很多交易所相继出了这方面的功能，我们也开始往自动交易、代理交易方向转型。转型的结果就是，目前平台上支持 BTC&#x2F;ETH 的 15m&#x2F;1h 市场的尾盘策略，可以配置参数一键启动。这个方向上目前看到最直接的竞品是 <a href="https://pm.evplus.ai/">EVPLUS</a>。</p><p>这里有一个考量在于，是做执行工具还是做策略？执行工具就是用户自己设定策略参数、我们只管执行；做策略就是我们找到稳健的策略或者策略参数、推荐给用户运行。对于用户来说，肯定是希望直接给策略，而对于项目方来说，因为考虑到策略风险，只能是做工具比较稳妥。</p><p>并且我诚实的告诉你，我们的项目也没有 edge；不但我们的项目没有，而且我可以负责任的告诉你，我们的竞品 EVPLUS 也没有，因为它的策略源码是公开的。</p><p>然后为了推广现在的这个 Auto Trade 功能，还开发了针对 Auto Trade 的 Paper Trading 账户和排行榜，希望做一场活动、让用户免费参与、最终 PNL 高的用户可以获得 100 USD 的奖励、培养用户习惯。</p><p>原本计划明天周一开始这个模拟盘交易大赛活动的，后来一个团队外部的成员，以命令式的语气、在没有经过团队讨论的情况下、就直接告诉运营人员推迟这个模拟盘交易大赛的活动。也因此我现在甚至不知道这个活动还搞不搞、什么时候开始。（你会不会没看懂，什么叫团队外的 “成员”？）</p><h3 id="单一领域-VS-多领域聚合"><a href="#单一领域-VS-多领域聚合" class="headerlink" title="单一领域 VS 多领域聚合"></a>单一领域 VS 多领域聚合</h3><p>事实证明经过了预测市场的大热点世界杯，我们在预测市场辅助工具这个方向上没有做起来。后来又开始考虑做更多领域的集成，比如前段时间看 Robinhood Chain 比较火，就集成了 Robinhood 的 Memecoins 交易。</p><p>再后来又看到 Robinhood Chain 上有一个叫 <a href="https://slvr.fun/">SLVR</a> 的链上游戏很火、交易量很高，我就直接把他们公开的合约源码拿下来、自己开发前后端、直接出了一个类似的仿盘出来 <a href="https://c.dark20.xyz/">VORE</a>（只是演示，千万不要玩）。这个 VORE 是为了证明我们能做出和 SLVR 一样的链上游戏。同时也在考虑 SLVR 这种游戏的玩法有没有什么 Virae 能够借鉴的。</p><p>总之就是这个阶段已经不局限于预测市场，而是开始打 Memecoins 或者其他链上热点项目的主意了。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>为什么我现在可以做这样一个总结呢，因为你能看到，我们尝试了各种各样的产品方向，结果都不太尽人意。我作为开发者的角度，已经没有灵感下一步还能做什么了。因为很明显，不管做什么新功能，都不会有更多人来用，这已经不是开发层面的问题。</p><p>我其实之前反复跟团队说一个道理，如果 100 个用户进来了、用着不好用、功能有 bug、走了，那是产品或者开发的问题，我们就可以去优化产品、修复 bug、提高用户留存率。但事实上是，根本没有 100 个用户进来，连来都没有来。所以问题在哪里？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;前情提要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《&lt;a</summary>
        
      
    
    
    
    
    <category term="工作" scheme="https://smallyu.net/tags/%E5%B7%A5%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>PySonar2 如何帮助 AI 节省 Token</title>
    <link href="https://smallyu.net/2026/08/03/PySonar2%E5%A6%82%E4%BD%95%E5%B8%AE%E5%8A%A9AI%E8%8A%82%E7%9C%81Token/"/>
    <id>https://smallyu.net/2026/08/03/PySonar2%E5%A6%82%E4%BD%95%E5%B8%AE%E5%8A%A9AI%E8%8A%82%E7%9C%81Token/</id>
    <published>2026-08-02T16:32:47.000Z</published>
    <updated>2026-08-09T04:36:03.375Z</updated>
    
    <content type="html"><![CDATA[<h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>今天有一个 “重大” 发现想要分享：<a href="https://github.com/yinwang0/pysonar2">PySonar2</a> 可以大幅度降低 AI 在执行 Python 项目修改的时候的 Token 消耗。</p><p>这是 ChatGPT 运行出来的 benchmark 结果：</p><img src="1.png" width="80%"><p>补充一下这个 benchmark 任务的定义：</p><img src="2.png" width="55%"><p>具体任务有 2 个：</p><img src="3.png" width="100%"><p>概括来说，就是基于一个测试专用的大型 Python 项目，设定两个方向的任务，一个任务横跨多个目录和文件，另一个任务目标明确、不需要跨文件定义，然后分别用 skill 和不用 skill 执行。</p><p>结果如你所见，使用了 skill 之后，速度略微有提高，token 消耗量则直接减少 30%！</p><h3 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h3><p>说明一下，刚才提到的 skill，是指基于 PySonar2 源码封装的、符合 AI Skills 规范的 skill，你可以直接告诉 ChatGPT：</p><blockquote><p>帮我安装这个 skill 到用户级目录：<a href="https://github.com/smallyunet/pysonar2">https://github.com/smallyunet/pysonar2</a></p></blockquote><p>就可以了。</p><p>这个 PySonar2 的仓库是 fork 出来的，因为原版的 PySonar2 仓库是王垠 13 年前的项目，已经有一些工程和特性方面的 “过时”，所以做了一点点新的工程方面的包装。当然 PySonar2 最核心的词法分析、类型推断是没有过时的。</p><p>除了这个 AI skill，我还把 PySonar2 以 VS Code 插件的形式发布了，安装后可以在 VS Code 上打开 Python 代码之后，鼠标 hover 上去看到 PySonar2 的类型推断：</p><ul><li>插件链接：<a href="https://marketplace.visualstudio.com/items?itemName=smallyu.pysonar2-code-intelligence">PySonar2 Code Intelligence</a></li></ul><h3 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h3><p>解释一下为什么 PySonar2 可以帮助 AI 节省。</p><p>如果你是程序员并且经常让 AI 写代码，一定会看到 AI 会使用大量的 <code>rg</code>，每一次任务都会把各种代码文件看一遍，然后继续执行代码修改。而 PySonar2 可以提供一种基于代码语义的、基于 AST 结构分析出来的这种内容：</p><pre><code class="json">&#123;  &quot;symbol&quot;: &quot;User&quot;,  &quot;definitions&quot;: [    &#123;&quot;file&quot;: &quot;models.py&quot;, &quot;startLine&quot;: 12&#125;  ],  &quot;references&quot;: [    &#123;&quot;file&quot;: &quot;service.py&quot;, &quot;startLine&quot;: 48&#125;,    &#123;&quot;file&quot;: &quot;tests/test_service.py&quot;, &quot;startLine&quot;: 31&#125;  ],  &quot;affectedFiles&quot;: [    &quot;models.py&quot;,    &quot;service.py&quot;,    &quot;tests/test_service.py&quot;  ]&#125;</code></pre><p>直接在一个文件里把每个变量定义的来龙去脉、相关源码文件总结出来。相当于本地建立了一个索引，AI 直接看索引，而不需要每次看一遍全部代码。</p><p>因为任何 AI 工具目前都有两方面限制：</p><ol><li>使用 AI 要花费 Token，Token 就是钱。省 Token 就是省钱</li><li>所有基于 Transformer 架构的 AI 模型，都有上下文限制。这种建立索引的做法可以节省上下文空间</li></ol><h3 id="对比"><a href="#对比" class="headerlink" title="对比"></a>对比</h3><p>那么 PySonar2 现在是否还具有某些独特的价值呢？答案是有：</p><img src="4.png" width="90%"><p>很多同类型的、号称节省 Token 的项目，都是基于关键词匹配、甚至 embedding 的匹配，与 PySonar2 的语义分析不在一个层面。</p><p>不过毕竟已经过去了 13 年，PySonar2 已经 13 年没有在学术理论上继续更新，要说 PySonar2 仍然处于世界前沿是不可能的。但是 PySonar2 因为开源、代码轻量、逻辑清晰，在 Agent 适配方面也许会有独特的优势和潜力：</p><img src="5.png" width="90%"><h3 id="启发"><a href="#启发" class="headerlink" title="启发"></a>启发</h3><h4 id="1-Python-语言只是开始"><a href="#1-Python-语言只是开始" class="headerlink" title="1. Python 语言只是开始"></a>1. Python 语言只是开始</h4><p>既然针对 Python 项目，PySonar2 可以帮助 AI 节省大量的 Token 消耗，那么其他语言的项目呢？JavaScript 是否可以借鉴同样的思路？以及更多脚本语言？同样的思路，已经不局限于 PySonar2 这个工具了。</p><h4 id="2-帮助-AI-节省-Token"><a href="#2-帮助-AI-节省-Token" class="headerlink" title="2. 帮助 AI 节省 Token"></a>2. 帮助 AI 节省 Token</h4><p>比起让 PySonar2 这个古老的项目重新发挥价值，我更在意的事情是：在 AI 时代，还有哪些代码工具是有价值的？</p><p>因为人已经不需要写代码了，甚至不需要看代码了。但是 PySonar2 在这个场景下发挥了巨大的作用，让我看到了不一样的希望：</p><ol><li>帮助 AI 节省 Token 是一个好的工具方向。</li></ol><p>（与此同时，我还在优化另外一个项目：<a href="https://smallyu.net/2025/05/11/%E6%83%B3%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B0%8FEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/">EchoEVM</a>。基于 EchoEVM 的 EVM 解释器底座，让 AI 能够在编写 Solidity 脚本的时候、有能力看到脚本文件字节码级别的执行过程。这种 debug 工具对人类没什么用，但是对于 AI 极其有用，因为人类看不懂字节码、AI 可以）</p><ol start="2"><li>底层原理在 AI 时代不会过时。</li></ol><p>PySonar2 作为拥有学术级别原理的工具，在人类已经不需要写代码的今天，仍然能够发挥价值、让 AI 节省 Token、给使用者省钱，这是真正意义上的效率工具。</p><h3 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h3><p>如果你不了解 PySonar2 的背景，分享几篇相关文章可以看一下：</p><ul><li>《<a href="https://smallyunet.github.io/yinwang-blog/blog-cn_2013_10_06_sourcegraph_20140513.html">PySonar 的第二个用户</a>》</li><li>《<a href="https://smallyunet.github.io/yinwang-blog/blog-cn_2013_10_29_pysonar2_20161011.html">PySonar2 开源了</a>》</li><li>《<a href="https://www.yinwang.org/posts/pysonar2-sg">PySonar2 与 Sourcegraph 集成完毕</a>》</li><li>《<a href="https://smallyunet.github.io/yinwang-blog/blog-cn_2013_06_21_pysonar-slides_20130725.html">PySonar 的工作原理</a>》</li></ul><h3 id="更新（2026-08-09）"><a href="#更新（2026-08-09）" class="headerlink" title="更新（2026.08.09）"></a>更新（2026.08.09）</h3><p>要补充说明的是，原文中对于节省 40% 的 Token 消耗，仅限于某些特定的、大量跨文件、变量名重叠、语义不清晰的任务。经过后来更丰富的测试，最新的 <a href="https://github.com/smallyunet/pysonar2/blob/main/docs/agent-skill-benchmark.md">benchmark 结果</a> 显示，很多通用任务中，如果强制使用 Pysonar2，消耗的 Token 和时长反而是增加的。不过由于 PySonar2 提供的精准语义分析能力，在 benchmark 中使用了 PySonar2 的准确率能达到 100%，给 AI 提供语义级别的事实证据是 Pysonar2 目前对于 AI 最明确的增强。</p><p>至于 “先给代码建立语义索引、修改代码直接使用索引” 的方式是否能够提交效率、节省 Token，我后来创建了单独的项目 <a href="https://github.com/smallyunet/code-engram">code-engram</a> 来进一步测试。目前 code-engram 主要支持 TypeScript 以及适配了 Pysonar2，唯一正向的 benchmark 数据是，在大型项目的 rename 任务中，Token 消耗节省 60%。但是对于小型项目或者其他通用任务，Token 消耗反而是增加的。这一点和 Pysonar2 单个仓库的结果类似。所以 code-engram 这个项目也仍然在继续探索中……</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;结论&quot;&gt;&lt;a href=&quot;#结论&quot; class=&quot;headerlink&quot; title=&quot;结论&quot;&gt;&lt;/a&gt;结论&lt;/h3&gt;&lt;p&gt;今天有一个 “重大” 发现想要分享：&lt;a</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
    <category term="工具" scheme="https://smallyu.net/tags/%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>一个比特币 Taproot 脚本漏洞开始的调查</title>
    <link href="https://smallyu.net/2026/07/28/%E4%B8%80%E4%B8%AA%E6%AF%94%E7%89%B9%E5%B8%81Taproot%E8%84%9A%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%BC%80%E5%A7%8B%E7%9A%84%E8%B0%83%E6%9F%A5/"/>
    <id>https://smallyu.net/2026/07/28/%E4%B8%80%E4%B8%AA%E6%AF%94%E7%89%B9%E5%B8%81Taproot%E8%84%9A%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%BC%80%E5%A7%8B%E7%9A%84%E8%B0%83%E6%9F%A5/</id>
    <published>2026-07-27T16:50:05.000Z</published>
    <updated>2026-07-28T05:41:19.708Z</updated>
    
    <content type="html"><![CDATA[<h3 id="起因"><a href="#起因" class="headerlink" title="起因"></a>起因</h3><p>在 bitcointalk 上看到一个帖子：<a href="https://bitcointalk.org/index.php?topic=5588964.0">How to handle weak scripts?</a></p><p>这个帖子说明了一种 Taproot 脚本的漏洞，无需经过签名验证，任何人都可以花费在这个 UTXO 上的余额。在 mempool 浏览器的界面上可以清晰看到，这笔 <a href="https://mempool.space/tx/f460443ee5b2bff69b9f299e31e1258f01a023555de5a63cd2d10c4ead0a5bc2"><code>f460...5bc2</code></a> 交易转出的一方有一个 unsigned 的标识，这个标识在比特币网络上并不多见：</p><img src="1.png" width="100%"><p>帖子中描述的 Taproot 脚本的逻辑是这样，这也是 <code>f460...5bc2</code> 交易真实执行的脚本过程：</p><pre><code class="bash">OP_PUSHBYTES_32 fa9b5ec193f735c41b804fc6ace1d28e81a299fc815c0f5009dd2dd7d0293c3bOP_CHECKSIGOP_IFOP_PUSHBYTES_32 ff1275b635cd160914cfe1bc516f521abde0fc8ae3fd92ae01ca16449e4758e9OP_ENDIF</code></pre><p>可以简化为：</p><pre><code class="bash">&lt;pubkey A&gt;OP_CHECKSIGOP_IF    &lt;pubkey B&gt;OP_ENDIF</code></pre><p>比特币的 Taproot 是基于栈结构的脚本，理解起来需要绕弯，把这个脚本翻译为 JavaScript 代码的话，大概是这个意思：</p><pre><code class="js">function isTrue(...params)&#123;  var pubkeyA = params[-1];  params--;  if (checkSign(pubkeyA))  &#123;    return true;  &#125;  else  &#123;    //  &#125;  return params[-1];&#125;</code></pre><p>从代码来看，在正常使用流程下，假如输入的参数是 <code>[pubkeyA]</code>，那么 <code>checkSign(pubkeyA)</code> 为 true，最终 <code>return true</code>，这个是合理的。</p><p>假如输入的参数是 <code>[pubkeyC]</code>，那么 <code>checkSign(pubkeyA)</code> 为 <code>false</code>，下面的 else 分支什么都不做，最终 <code>return null</code>，依然不是 <code>true</code>，这个也合理。</p><p>这个脚本的漏洞在哪里呢，在于 params 是一个数组，用户未必只输入一个参数。</p><p>在漏洞逻辑下，可以输入非预期的参数组合 <code>[true, pubkeyC]</code>，那么按照后面的执行流程，当进入 else 分支什么都不做之后，<code>return params[0]</code> 会把 <code>true</code> 给返回出来。</p><p>这里面存在几个基础的问题：</p><ol><li>代码没有处理 else 分支</li><li>比特币的 Taproot 脚本没有类型检查（只有操作码）</li><li>脚本作者没有正确理解 Taproot 脚本的写法，忽略了 witness 可以提交任意数量的参数</li></ol><h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><p>所以很简单，这个所谓的脚本漏洞，实际上是作者自己写了不严谨的代码导致的。要注意这里指的不是比特币的作者，仅仅只是比特币的用户。比特币的 Taproot 相当于以太坊的智能合约，任何人都可以写一些脚本规则，来决定钱怎么控制、怎么花。只不过比特币的脚本写起来复杂，而且不是图灵完备，所以很多人忽略了比特币可以自己写脚本这件事情。</p><p>很明显这不是一个比特币协议级别的漏洞，比特币的共识只负责按照脚本规则正确执行，而脚本逻辑是否有漏洞，跟协议无关。</p><p>而且事实上无论是比特币钱包还是比特币生态的项目，如果依赖于 Taproot 的脚本逻辑，一定会经过审计公司来审核代码，这种低级错误出现在主网上的情况恐怕会非常少。</p><p>另外 Taproot 这种脚本设计的隐私性其实很强，我们只能知道比特币地址 <code>bc1p</code> 开头是 Taproot 脚本，<code>0xc0</code> 开头的是当前版本号，但是一个 UTXO 在没有被花费之前，网络上是无法看到脚本具体内容的。而在被花费的那一刻，大概率全部的钱都已经被转走了。一个 P2TR UTXO 被创建的时候，链上只保存这棵树结构的根节点哈希，任何可以解锁 root 的路径都是有效输入，这些有效输入只有在花费的时候，链上才需要根据具体的 leaf 和 key path 去验证。而且即使某一个 leaf 被公开，链上也只能看到其中一个，看不到同一个 root 下的其他 leaf。</p><p>比特币从设计之初，就提倡每一笔转账都使用一个新的地址，实际上大多数靠谱的钱包也是这么设计的，这本身就很大程度规避了漏洞脚本被再次利用的风险。</p><p>所以无论是从技术上和商业逻辑上，这种漏洞脚本的出现都是极其小概率的个例，完全就是脚本作者自己写错了。主网上存在漏洞脚本的可能性很小。</p><h3 id="调查"><a href="#调查" class="headerlink" title="调查"></a>调查</h3><h4 id="示例地址"><a href="#示例地址" class="headerlink" title="示例地址"></a>示例地址</h4><p>有没有可能网路上真的存在这种漏洞脚本，不需要验证签名就可以把钱转走的？</p><p>先从 <a href="https://mempool.space/address/bc1p78u5v2rgsu7gf79yw5c85fs3da0hfsw25ht5tr6p3mvh5wvmck6qpxcn8z"><code>bc1p...cn8z</code></a> 这个例子入手，有几个问题：</p><ol><li>假如这个地址上还有钱，要怎么转出？</li></ol><p>这显然不是钱包 App 提供的功能，而且比特币的交易比较麻烦，得把 UTXO 来源的交易和编号都作为参数才能构造出交易。这是一个示例脚本 <a href="./taproot-script-path-demo.mjs">taproot-script-path-demo.mjs</a>，可以这样来运行：</p><pre><code class="bash">TXID=&#39;0618d93bc4eef77f4b47c4b4b188011f95c7ccb929e6226df2774b88b5d4693d&#39;VOUT=&#39;0&#39;INPUT_SATS=&#39;15000&#39;DESTINATION=&#39;bc1px6jn77jd4tplp94q46svkuutc53wtn24dk34wxp4dfur6tvez89swvlcmj&#39;FEE_SATS=&#39;1000&#39;node outputs/taproot-script-path-demo.mjs \  &quot;$TXID&quot; \  &quot;$VOUT&quot; \  &quot;$INPUT_SATS&quot; \  &quot;$DESTINATION&quot; \  &quot;$FEE_SATS&quot;</code></pre><p>如果你不小心把钱打到了地址 <code>bc1p...cn8z</code> 上，就可以用这个脚本，在没有私钥的情况下，把钱转出来。但是要小心脚本里的 <code>xOnlyKey</code> 和 <code>payload</code> 都是硬编码，如果换一个目标地址，这两个值就不一样了，得用另外的值。那么这个值应该是什么呢？这个值只有在 P2TR 被花费过之后才能知道。</p><p>这也就意味着，即使我们知道链上某一个地址有余额、脚本有漏洞，因为不知道整个树结构、拿不到 xOnlyKey 和 payload 的值，我们也无法动这个地址上的钱。从这个角度看，比特币上的资产安全程度还是很高的。</p><ol start="2"><li>有没有可能这个作者把钱转给了某一个有同样漏洞的 P2TR，但是忘了转出？</li></ol><p>同样有一个示例脚本来干这个事情：<a href="./audit-taproot-neighborhood.mjs">audit-taproot-neighborhood.mjs</a>，结果就是，没有。这个作者把全部关联地址的钱都转走了，不存在这种低级漏洞。</p><h4 id="扫描全网"><a href="#扫描全网" class="headerlink" title="扫描全网"></a>扫描全网</h4><p>这个程序可以扫描整个比特币网络上类似的脚本漏洞：<a href="https://github.com/smallyunet/tapscript_weak_finder">tapscript_weak_finder</a></p><p>扫描逻辑是这样：</p><ol><li>逐个区块扫描每一笔交易，判断是不是 Taproot Script 且 version 为 <code>0xc0</code></li><li>这个程序里实现了一个非常轻量的解释器，用来处理 witness 参数</li><li>会分别用 3 种参数策略来测试脚本是否可以不需要签名也能通过</li><li>直接用 <code>[01, &lt;empty signatrue&gt;]</code> 的排列组合，处理 0 个参数、1 个参数、2 个参数的情况，一共 12 种</li><li>把 atom 从 <code>01</code>, <code>&lt;empty signatrue&gt;</code>，扩展到小整数和一些常量，比如 <code>02</code>, <code>03</code>, <code>true</code>, <code>false</code> 等</li><li>对于已经公开执行过的 witness 参数比如 <code>[[data, sigA, sigB]]</code>，尝试变换组合，比如 <code>[data, &quot;&quot;, sigB]</code>、<code>[data, sigA, &quot;&quot;]</code> 等</li><li>如果程序发现了不需要签名的脚本、一定返回 <code>OP_SUCCESS80</code>的脚本，或者目前规范还没有定义的公钥类型，会记录到数据库</li></ol><h4 id="扫描结果"><a href="#扫描结果" class="headerlink" title="扫描结果"></a>扫描结果</h4><p>当然我会告诉你运行结果。如果你运行了这个开源程序，会得到和我一样的结果和结论。我的启动参数是从区块高度 <code>709632</code> 开始，这是比特币主网启用 Taproot 的高度，到 <code>959701</code> 高度结束，也就是最新的区块高度。</p><p>一共扫描了 71037 个区块，扫描了 127119410 笔交易，扫描到 1000061 个 Taproot 地址，其中 427378 个属于当前程序没识别出问题的脚本，12 是有可能有漏洞的弱脚本。</p><p>在 12 个有可能有漏洞的脚本中，8 个属于不需要签名的弱脚本，1 个属于始终返回 <code>OP_SUCCESS80</code> 的非漏洞脚本，3 个属于包含扫描程序解释器不支持操作码的非漏洞脚本。</p><h4 id="结果分析"><a href="#结果分析" class="headerlink" title="结果分析"></a>结果分析</h4><p>来逐个分析一下扫描到的有可能是 weak script 的交易。最终输出的、未经加工的结果数据在这个文件：<a href="./result_report.json">result_report.json</a>，可以还原出我下面提到的地址和交易、脚本。</p><h5 id="1"><a href="#1" class="headerlink" title="1"></a>1</h5><p>首先扫描到的这个 taproot 地址出现在 758737 区块：<a href="https://mempool.space/address/bc1pnwu7l0wln4c2l5av9nhsz968yd4alyyr9futpr6h6yfe7pa2jxzshv2354"><code>bc1p...2354</code></a></p><p>这个地址出现了 2 笔异常交易：</p><ul><li><a href="https://mempool.space/tx/00c1af8f7d9e2bfe854d1718db078eb6740920f702bc00d4a9f205d3a084c40e"><code>00c1...c40e</code></a></li><li><a href="https://mempool.space/tx/73be398c4bdc43709db7398106609eea2a7841aaf3a4fa2000dc18184faa2a7e"><code>73be...2a7e</code></a></li></ul><p>先说结论，这 2 笔交易是非常专业的开发者在演示比特币主网的 BIP342 规范而发起的。</p><p>第一笔交易 <code>00c1...c40e</code> 包含 2 个 witness 参数，是用来测试交易的。</p><p>第二笔交易 <code>73be...2a7e</code> 就离谱了，包含 500001 个 witness 参数，<code>OP_RETURN</code> 的内容解码后得到 “you’ll run cln. and you’ll be happy.” 的文本。</p><p>TapScript 的规定是，初始栈最多只能有 1000 个元素，但是第二笔交易出现了 500001 个元素而且交易成功了，是因为 BIP342 规范定义，如果脚本中出现 <code>OP_SUCCESSx</code>，则无论脚本包含多少元素，一律视作成功。因为 <code>OP_SUCCESSx</code> 属于目前还未明确支持、但是预留给未来有可能使用的操作码，所以 BIP342 这样来规定了。</p><p>很明显第二笔交易就是用来演示这种状况的。也因为这笔交易包含大量参数，花费了超过 700 美元的手续费。</p><h5 id="2"><a href="#2" class="headerlink" title="2"></a>2</h5><p>其次扫描到的地址出现在 771740 区块；</p><ul><li><a href="https://mempool.space/address/bc1pzmc9a8uczft74n2s0lakz7jgmfn3jmgpd8wnqg2eyn8mev25cccsne648w"><code>bc1p...648w</code></a></li><li><a href="https://mempool.space/address/bc1p2y390fnxvgevhwx5v47amhzm4e347vkckksusc30z0z6zq8f62hqc2u69a"><code>bc1p...u69a</code></a></li><li><a href="https://mempool.space/address/bc1pkntn5g8mg9fwf32xwj52e0vx9gaxv5zzn4yt54saaaj8x825a43qu3n6cl"><code>bc1p...n6cl</code></a></li></ul><p>这 3 个脚本对应的交易包含了 <code>OP_CHECKSEQUENCEVERIFY</code> 操作码，扫描程序没有处理这个操作码，所以认为是异常给挑出来了。<code>OP_CHECKSEQUENCEVERIFY</code> 操作码的含义是，必须等待多少个区块，才可以使用地址里的资金。属于是典型的 TimeLock 脚本。</p><h5 id="3"><a href="#3" class="headerlink" title="3"></a>3</h5><p>在区块高度 775722，扫描到了这个地址：<a href="https://mempool.space/address/bc1p9zpp682rerqscftc98kn7g3yne08mkte27eletq3w9kdw8q3prcqqaamq6"><code>bc1p...amq6</code></a></p><p>这是一个真正有逻辑漏洞的脚本，不需要任何签名就可以转移资金。这个脚本明显是一个 Ordinals 数据结构的封装，可能是某个开发者在尝试自己开发类似 Ordinals 的协议。当然这个地址上的钱当时就被全部转走了，现在没有余额。</p><p>另外还有几个地址，数量不多，大多数是 Ordinals 的测试交易，就不一一详细解释了：</p><ul><li><a href="https://mempool.space/address/bc1p07d0z7drxkewne2z665xngf8rtnga9p32wl957zwtd96n89qja2sch3hu0"><code>bc1p...3hu0</code></a>，区块高度 777335，一个 Ordinals PNG 图片</li><li><a href="https://mempool.space/address/bc1pna0qxzm3z8vwre6h6vv5fh6m7pc5e9pk3nqtfxd77zad4jm8c25q88xgdr"><code>bc1p...xgdr</code></a>，区块高度 777617，一个加法运算，没有校验签名</li><li><a href="https://mempool.space/address/bc1p395qvu3zaehjrvek9nqjkdd785f379y7zsjesy0p8vugd7gkr94qd7gq7d"><code>bc1p...gq7d</code></a>，区块高度 777681，一个测试脚本，固定返回 true</li><li><a href="https://mempool.space/address/bc1pmqpwg3gh5dcaepz2ttuet7vtdtug23zpjz93p7e902jv582nudcqk09x3t"><code>bc1p...9x3t</code></a>，一个 Ordinals 的 Hello World 脚本</li><li>…</li></ul><h3 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h3><p>已经扫描了从 Taproot 脚本功能上线到至今为止全部的区块和交易，真的出现了少量 <a href="https://bitcointalk.org/index.php?topic=5588964.0">How to handle weak scripts?</a> 中描述的类似的脚本漏洞，不过那些地址上都已经没有余额了。</p><p>这件事情另外的启发：</p><ol><li>比特币网络是否存在更复杂类型的脚本漏洞？</li><li>以太坊的脚本漏洞是否可以用类似的手段扫描？</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;起因&quot;&gt;&lt;a href=&quot;#起因&quot; class=&quot;headerlink&quot; title=&quot;起因&quot;&gt;&lt;/a&gt;起因&lt;/h3&gt;&lt;p&gt;在 bitcointalk 上看到一个帖子：&lt;a</summary>
        
      
    
    
    
    
    <category term="比特币" scheme="https://smallyu.net/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"/>
    
  </entry>
  
  <entry>
    <title>做 Polymarket 项目踩的坑</title>
    <link href="https://smallyu.net/2026/07/10/%E5%81%9APolymarket%E9%A1%B9%E7%9B%AE%E8%B8%A9%E7%9A%84%E5%9D%91/"/>
    <id>https://smallyu.net/2026/07/10/%E5%81%9APolymarket%E9%A1%B9%E7%9B%AE%E8%B8%A9%E7%9A%84%E5%9D%91/</id>
    <published>2026-07-10T11:53:28.000Z</published>
    <updated>2026-07-10T11:53:28.685Z</updated>
    
    <content type="html"><![CDATA[<h3 id="钱包设施的选择"><a href="#钱包设施的选择" class="headerlink" title="钱包设施的选择"></a>钱包设施的选择</h3><p>现在做 Crypto 方向的项目，一般会选择 <a href="https://www.privy.io/">Privy.io</a> 或 <a href="https://www.turnkey.com/">Turnkey</a> 来作为钱包的基础设施提供商。这些钱包设施提供了注册登录方面的集成，可以用传统方式邮箱、Google 登录，也可以用 web3 一点的方式，比如 MetaMask 钱包或者 OKX 钱包登录。更重要的是这些钱包设施解决了钱包安全的问题，允许给用户创建 embedded wallet，如果不使用这些钱包服务，项目方需要自己在服务器上保存用户的私钥，这是很危险的，尤其是对于中小团队来说，安全问题是很沉重的负担。Privy 或者 Turnkey 的解决方案是，私钥由他们来保管，储存在 TEE 一类的加密硬件中，项目方只需要通过 API 来访问和使用钱包。</p><p>至于 Privy 还是 Turnkey 的取舍问题，比如我们经常在看的 <a href="https://kreopoly.app/">Kreo</a>、<a href="https://polycop.ai/">PolyCop</a>、<a href="https://polym.trade/">polymtrade</a> 等项目都在用 Privy。这些项目列表都在 <a href="https://builders.polymarket.com/">Polymarket Builder 榜单</a> 上。Polymarket 的这个 Builder 榜单上，列出了每一个 Polymarket 生态的项目方交易量排名。<a href="https://gmgn.ai/">GMGN</a> 也用的 Privy。</p><p>用 Turnkey 的项目也有，比如 <a href="https://axiom.trade/">Axiom</a>、<a href="https://www.alchemy.com/">alchemy</a> 等。我们当时用了 Turnkey。一开始是因为 Turnkey 从定位上对自动化运行的 Trading Bot 更加友善，Privy 定位上有很重的对前端界面 SDK 的依赖。后来又发现了一个 Privy 和 Turnkey 比较大的区别：是否支持 Telegram 注册登录。</p><p>Privy 支持 TG 登录，Turnkey 不支持，为什么呢？</p><p>因为 TG 没有像 Google 那样成熟的 OAuth 体系，TG 账号本身无法像 email 或者 wallet 一样给 Privy&#x2F;Turnkey 一个 identity provider。Privy 针对这种场景做了简化，你用 TG 登录，登录后 Privy 代替你去通过 API 请求钱包的操作，实际上此时 Privy 后端 “就是你”，假如 Privy 后端的服务器被黑了，你的钱包权限会完全暴露在黑客手里。而 Turnkey 比较生硬，无法提供 identity provider，就不能让你操作钱包，Turnkey 的后端不愿意代替用户承担这种权限风险。</p><p>所以这是我们发现的 Privy 和 Turnkey 之间比较大的场景支持程度上的差异。如果想要用 Turnkey 并且支持 TG 登录，就得专门做一个 TG Miniapp，登录的时候唤起 Miniapp 来完成。</p><h3 id="CLOB-V2-集成"><a href="#CLOB-V2-集成" class="headerlink" title="CLOB V2 集成"></a>CLOB V2 集成</h3><p>Polymarket 上的订单系统叫 CLOB，下单等行为都是在和 CLOB API 交互。4月28日，Polymarket 正式启用 <a href="https://docs.polymarket.com/v2-migration">CLOB V2</a>。对于我们这种下游项目来说，最大的变动是需要换掉一些合约地址信息，比如 Copy Trading 功能需要扫描链上交易，以前监听的是 V1 版本的合约，迁移后监听的是 V2 版本的合约。此外如果下游不适配也是可以的，因为大体上兼容旧版本的订单参数。</p><p>而 CLOB V2 也引入了 FAK、FOK 等 Market Order 在使用的参数，这些参数直到一个多月以后我才发现它们的差异。因为在 V1 版本订单如果没有立即成交，会把没成交的部分自动挂单为 Limit Order，但同时又要求 Limit Order 的 shares 数量必须大于 5。所以用户有时会遇到一个奇怪的错误，为什么订单 shares 数量必须最小是 5？又为什么有时候会提醒最小订单金额是 1 美元？为什么有时候能成功有时候失败了，最小金额不是固定的？用上 FAK 参数的话倒是没有这种旧的问题了。</p><h3 id="Safe-Wallet-gt-Deposit-Wallet"><a href="#Safe-Wallet-gt-Deposit-Wallet" class="headerlink" title="Safe Wallet -&gt; Deposit Wallet"></a>Safe Wallet -&gt; Deposit Wallet</h3><p>5月4日，Polymarket 发公告说启用新的 <a href="https://x.com/PolymarketDevs/status/2050992767372013922">Deposit Wallet</a>，不再使用旧版本的 Safe wallet。这是一个 breaking change，所有旧的用户依然兼容，但是新的尤其是通过 api 操作的钱包，都将使用新的 Deposit Wallet，也就是说下游项目必须兼容。</p><p>这个时间点非常微妙，因为我们原本计划项目 Beta 版本上线的日期是 5月5日。也因此当时连续两三天我紧赶慢赶去适配这个 Deposit Wallet 的迁移。这本身不是一件特别难的事情，但是由于突然升级带来的措手不及，以及上线的 delay，还是有一点点压力。</p><p>至于集成过程中遇到的唯一挫折，是 Polymarket 的 Migration 文档里没有写新的 Deposit Wallet Implementation 地址。在用 Factory 合约创建钱包的时候，需要一个 implement 合约作为参数，不同的 implement 合约出来的钱包地址是完全不一样的。而重点不只是我们用哪个地址，而是 Polymarket 的 Relayer api 用哪个，因为在新的这种 Deposit Wallet 模式下，所有的钱包操作都会通过官方的 Relayer api 了。以前用的 Safe wallet，其实我们可以自己直接去链上调合约的。Polymarket 的这个改动，反而让钱包体系更加不那么去中心化。至于正确的 implement 的值，则是需要直接从 Polymarket 发布的 SDK 里去读。而且当时还遇到个小坑就是 Polymarket 发布的不同 SDK 之间用的 implement 地址还不一样。不是同一个 SDK 不同版本，而是那种不同 SDK，比如 client-sdk, server-sdk 这种。当时 Discord 群里遇到这个集成问题的开发者非常多。</p><h3 id="Relayer-API"><a href="#Relayer-API" class="headerlink" title="Relayer API"></a>Relayer API</h3><p>刚才提到新的 Deposit Wallet 必须要通过 Polymarket 官方的 Relayer API。我们在迁移后并且项目上线一段时间、做活动，紧接着就遇到了另一个坑：Unverified 状态的 Builder API 只有每天 100次的请求额度。</p><img src="1.png" width="80%"><p>看出 Polymarket 的阴险之处了吧，先是要求所有新用户都用 Deposit Wallet，而新的 Deposit Wallet 自然必须通过 Relayer API 来操作钱包，再然后你会发现，Relayer API 的请求是有额度限制的！如果是原本的 Safe wallet 可以直接链上操作绕过 Polymarket 的 API 额度限制。现在不行了。</p><p>这个 Relayer API，每次创建新的钱包都要用、每一次从钱包里 withdraw 钱要用，所以这是一个高频率会用到的接口。</p><p>针对这个限制的缓兵之计是，Polymarket 默认用 Builder API 来对 Relayer API 授权，但是 Relayer API 却没有要求 Builder API 具体是哪一个。所以我注册了 10个账号、每一个账号都创建了 builder api，配置到服务器里，用来暂时缓解 relayer api 的额度限制问题。每个账号每天100次请求额度，也就意味着 10 个账号一天有 1000 次额度。因为 verified 从提交到审核需要两到四周的时间，是一个很长的周期。</p><p>当然最终还是通过了 Verified，现在我们的项目已经出现在 Polymarket Builder 榜单上，并且状态是 Verified。</p><h3 id="Deposit-Wallet-升级"><a href="#Deposit-Wallet-升级" class="headerlink" title="Deposit Wallet 升级"></a>Deposit Wallet 升级</h3><p>7月6日，又是 Deposit Wallet 的问题。我们发现了一个很严重的 bug，自从 7月1日以来注册的全部用户，创建钱包都失败了！而因为用户反馈少，出问题很多天我们才发现这个问题。也就是说，新注册的用户，注册完之后如果充值，会把钱充值到错误的地址上。</p><p>为什么会出现这个 bug 呢，因为 7月1日，Polymarket 直接修改了创建钱包的逻辑，用上了 beacon 的方式去创建代理钱包，而且连公告都没发，直接改变 API 接口的行为。这种改动，是需要下游项目方主动适配的。</p><img src="2.jpg" width="100%"><p>我们也有错误，我们的错误之处就在于没能及时发现这种异常，并且在程序前后端加上足够的保护性、防御措施。针对这次事故所有用户充值钱到错误地址的，全部金额都赔了，好在金额不大。</p><h3 id="Combos"><a href="#Combos" class="headerlink" title="Combos"></a>Combos</h3><img src="3.png" width="100%"><p>Polymarket 针对世界杯的市场，新出了一个叫 combos 的功能。我们就开始评估是否可以接入这个 combos 的功能。结果就是，从 <a href="https://docs.polymarket.com/market-makers/combos">combos 文档</a> 上看，API 只提供了针对做市商提交 quote 的接口，没有提供作为普通用户作为 taker 角色去下单的接口。</p><p>Polymarket 从 6月4日开始运作 combos 的接口，看起来也是在辛苦 build 中。</p><img src="4.png" width="60%"><h3 id="tick-size"><a href="#tick-size" class="headerlink" title="tick size"></a>tick size</h3><p>7月10日，用户反馈遇到了订单交易失败的问题。经过排查后发现是订单的 <a href="https://docs.polymarket.com/changelog">tick size 有了变化</a>，所有的世界杯市场都支持 0.0025 (0.25¢) 级别精度的 tick size。在此之前 tick size 只有固定 4 种，这个 0.25¢ 属于第 5 种（枚举类型）。</p><p>同样的，这个变动其实 Polymarket 早在 7月2日就上线了，但是我们直到 7月10日才发现 bug。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>所以你看到了，一直以来，我们都在不断的追随 Polymarket 的脚步，一直不断的响应 Polymarket 的功能更新和变化。</p><p>也因此，在构建项目的过程中，我们也开始逐渐意识到这样的项目误区：再做一个官网。</p><p>如果定位是 “再做一个官网”，项目方就会陷入非常被动的处境。而且 Polymarket 家大业大，官网 UI 做的也好，有功能都是第一时间上官网，再者官网自家的服务器，下单速度也快。我们的订单速度再快也不可能快过 Polymarket 官网。</p><p>这是一件尴尬的事情，如果想要做的比官网使用体验好，客观上难度是很高的。Polymarket 不是 memecoins，一般 memecoins 没有官网至少不会有官方钱包、官方 DEX，但是 Polymarket 有官网、有官方解决方案。Polymarket Builder 的定位，从来都不是说，Polymarket 是一个开放的去中心化协议，大家都来建设项目去玩。</p><p>与之相反，Polymarket 最近的功能更新上也越来越往中心化的方向走，定位更像是中心化交易所，比如 Binance 这种，Polymarket API，就是 Binance API。做 Polymarket 生态的项目，就有点像是做 Binance 的 Trading Bot 一类事情。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;钱包设施的选择&quot;&gt;&lt;a href=&quot;#钱包设施的选择&quot; class=&quot;headerlink&quot; title=&quot;钱包设施的选择&quot;&gt;&lt;/a&gt;钱包设施的选择&lt;/h3&gt;&lt;p&gt;现在做 Crypto 方向的项目，一般会选择 &lt;a</summary>
        
      
    
    
    
    
    <category term="Polymarket" scheme="https://smallyu.net/tags/Polymarket/"/>
    
  </entry>
  
  <entry>
    <title>分享一个 Polymarket 的 BTC 5分钟策略</title>
    <link href="https://smallyu.net/2026/07/05/%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%AAPolymarket%E7%9A%84BTC-5%E5%88%86%E9%92%9F%E7%AD%96%E7%95%A5/"/>
    <id>https://smallyu.net/2026/07/05/%E5%88%86%E4%BA%AB%E4%B8%80%E4%B8%AAPolymarket%E7%9A%84BTC-5%E5%88%86%E9%92%9F%E7%AD%96%E7%95%A5/</id>
    <published>2026-07-04T17:32:20.000Z</published>
    <updated>2026-08-09T04:50:13.356Z</updated>
    
    <content type="html"><![CDATA[<h3 id="免责声明"><a href="#免责声明" class="headerlink" title="免责声明"></a>免责声明</h3><ol><li>这不是无风险套利，只是普通的策略，数学上不是正期望的，存在明显的策略风险</li><li>有可能赚钱也有可能亏钱，跟随市场情况波动，过去的收益率不代表未来的收益率</li><li>策略数据方面，因为这几天一直在频繁调整策略和参数，我也不确定未来连续运行几天后真实的收益是多少，能确定的是我最近半个月一直在运行、下单超过 300 个场次，总收益微弱，但是没有亏本。至少说明这个策略不是亏钱机器。</li></ol><h3 id="代码仓库"><a href="#代码仓库" class="headerlink" title="代码仓库"></a>代码仓库</h3><ul><li><a href="https://github.com/smallyunet/poly-btc5m">https://github.com/smallyunet/poly-btc5m</a></li></ul><p>更新比较频繁，谨慎使用。是我自己实际在运行的代码，虽然策略简单但是开发量并不小，如果想尝试可以直接使用。另外如果你不相信我接下来对于策略的判断，则务必亲自尝试 🐶</p><p>顺便说一个开发体会是，X 上很多玩预测市场的人，在通过 Vibe Coding 的方式来写策略 Bot、自己执行。从我的感受来看，非程序员群体想要实现出能运行的策略，基本不可能可靠（高素质具备逻辑能力除外）。</p><p>有些策略包括我接下来说的策略，看起来和理解起来是异常简单的，但是落实到代码上，有很多具体的问题。比如重复下单的问题、退出策略是否正常执行、线上突然多了一堆订单属于策略内还是程序异常，这些都需要反复调试。不是说非程序员想不清楚，而是这个调试的过程非常磨人，得不断调参数、看效果、效果不好改策略、再看效果。这实际上就是程序开发每天在干的事情，现在虽然不需要自己动手写代码了，但是结果还得自己看。</p><h3 id="策略说明"><a href="#策略说明" class="headerlink" title="策略说明"></a>策略说明</h3><h4 id="基本策略"><a href="#基本策略" class="headerlink" title="基本策略"></a>基本策略</h4><p>对于下一场还未开始的 BTC 5分钟市场，提前直接挂单 limit order，以 45c 的价格买入 Up，同时以 45c 的价格买入 Down。如果两个挂单都成交，则结算的时候因为 0.45+0.45 &lt; 1，所以必然得到本金 10% 的收益。一旦两个订单都成交，收益必然锁定，不需要任何后续操作。</p><p>损失风险也很明确，如果开盘后进入单边行情，价格在整个 5 分钟的周期内都没有震荡超过 55%，则策略遭受 50% 本金损失。这也就意味着，每次输 1 场，需要赢 5 场才能够转亏为盈。</p><p>为了减少损失，策略也包含两个止损机制：</p><ol><li>如果开盘后单腿成交、收益达到 5%、同时另一腿没有成交，则直接止盈成交的一侧、另一侧直接取消挂单</li><li>如果最后 1 分钟内还是只有单腿成交，并且另一腿的价格低于 75c，则以当前价格买入 2 倍数量的另一腿，锁定损失</li></ol><h4 id="策略的主观部分"><a href="#策略的主观部分" class="headerlink" title="策略的主观部分"></a>策略的主观部分</h4><p>这个策略从纯粹的逻辑和量化的角度，显然是站不住脚的，风险很大也很明确，而且没有程序化的规避方式，策略本身也没有足够的回测数据做支撑。所以这个策略很大程度上是主观的，我要解释的是，为什么我主观上认为这个策略可行？</p><p>btc5m 这种市场，行情分两种，一种是这个策略双边同时开单能够获利的震荡行情，另一种就是一旦开盘就已经锁定了结算方向、价格一去不复返的单边行情。</p><p>那么反过来想，假如单边行情多于震荡行情，是否意味着，在开盘后几秒内，直接买入价格高于 55c 的一边，就可以拿着等待结算了？单次收益率超过 80%。然后结合单边行情概率上多于震荡行情这一点，只要简单执行跟风买入的操作，就是一种能稳定赚钱的策略了。这是一种 “超级尾盘” 策略，因为尾盘一般在接近结算的价格比如 99c 才入场，而现在概率告诉你，55c 就足以入场。</p><p>但是事实上，大家交易的体感上，btc5m 结算前 5 秒钟结果大反转的场次都很频繁。别说单边买入，尾盘买入都会亏的很惨。而且我也不太相信赚钱可以简单到持续单边买入就可以，过于违反直觉。</p><p>所以再正过来想，既然不可能单边行情多于震荡行情，那么就应该是震荡行情居多。</p><h4 id="已忽略的客观部分"><a href="#已忽略的客观部分" class="headerlink" title="已忽略的客观部分"></a>已忽略的客观部分</h4><p>既然刚才的判断是纯主观，那么是否可以通过判断 BTC 的价格动量，来预测 BTC 是否会在接下来 5 分钟内大幅波动呢？</p><p>这个测试我已经做过了，从 Binance 的 wss api 去获取实时价格信息、判断下一个轮次开始前 120 秒内价格是否在某一个中间线上下波动，并且还加了复杂的评分系统，需要至少上下横穿 2 次、并且价格波动幅度在 12bps 以上、信号连续持续出现 3 次、以及根据 Polymarket 上 orderbook 本场的波动情况等等各种条件，甚至还考虑拿一下 Binance 上的资金费率、Polymarket 上下一场的 Top holder 等数据来辅助判断。</p><p>也尝试过一些动态调整参数方面的优化，比如根据评分结果的高低来动态调整入场价格和入场金额，分数高一点就 42c, 1.2*amount，分数低一点就 46c, 0.8*amount 之类。</p><p>结果就是，不但能经过层层筛选的场次非常少，会导致下单的场次也非常少。而且主要是没有明显的效果。假如闭着眼睛不判断，直接下单，可能 10 场里有 9 场是 paired 状态（盈利的）、还有 1 场是 single 状态（亏损的）。而经过一系列评分系统最后高于波动率才下单的做法，可能一天下单 10 场，这 10 场里还是有 1 场 single。高的评分并没有减少 single 状态的风险，反而还大幅减少了下单的场次。</p><p>当然，你肯定会觉得，是我的量化思维不够专业，写不出足够专业有效的模型去预测下一场价格的波动情况。那我承认……</p><h4 id="收益上限"><a href="#收益上限" class="headerlink" title="收益上限"></a>收益上限</h4><p>即使这个策略真的有效可以赚钱，收益也是有上限的。从最近几场 Polymarket 上的流动性深度来看，每一次下单的金额，单腿金额在 100 美元左右还是安全的，如果到 200 美元以上，就会出现买单不够、无法成交的风险。</p><p>这一点其实违反直觉，也许你觉得 Polymarket 交易量那么大（百亿美元级别）、玩 btc5m 的人应该很多才对吧？从官网上可以直接看到每一场的 Top holder 和 Top PNL，持有者最多在几千个 shares，收益最高的在几百、几千美元的区间。</p><p>那么一天有 288 个 5 分钟，假如真的每一场都是赚的，按照 200 美元的本金算，每一场收益是 10% 也就是 20 美元，一天的收益是 5760 美元。这个是理论上限。当然不可能每一场都赢，大概率赢 9 场输 1 场，收益率减半，一旦多 1 场输，就抹平 5 场的收益，收益打一折，再然后……</p><h4 id="策略扩展"><a href="#策略扩展" class="headerlink" title="策略扩展"></a>策略扩展</h4><p>既然 btc5m 市场可以用这样的做法，那么 btc15m, btc1h, btc4h, btc1d，eth15m, sol1h…… 是不是也都可以用这样的策略？显然是可以的。</p><h3 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h3><ul><li>F：如果这个策略真的可以赚钱，为什么要分享出来？    </li><li>Q：因为真正赚钱的策略从来都是公开的秘密。比如一直反复在说的，能长期重仓投入的策略，只有定投 BTC。</li></ul><br><ul><li>F：为什么不等多运行几天、等有一份好看的策略数据出来再分享？    </li><li>Q：没耐心。即使最近几天、几个月、几年通过策略赚钱了，也许接下来几天、几个月、几年会再亏进去。我还是同样的观点，只要在牌桌上，就没有赢家。</li></ul><h3 id="相关阅读"><a href="#相关阅读" class="headerlink" title="相关阅读"></a>相关阅读</h3><ul><li>《<a href="https://crazy.smallyu.net/2026/06/30/%E4%B8%8D%E8%A6%81Copy-Trading/">不要 Copy Trading</a>》2026-06-30</li><li>《<a href="https://crazy.smallyu.net/2026/05/10/%E5%9C%A8%E5%B8%81%E5%9C%88%E4%B8%8D%E8%A6%81%E5%81%9A%E6%92%B8%E6%AF%9B%E5%85%9A/">在币圈不要做撸毛党</a>》2026-05-10</li><li>《<a href="/2022/12/03/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E4%BA%A4%E6%98%93%E6%9C%BA%E5%99%A8%E4%BA%BA/">我的加密货币交易机器人</a>》2022-12-03</li></ul><br><h3 id="更新（2026-07-17）"><a href="#更新（2026-07-17）" class="headerlink" title="更新（2026.07.17）"></a>更新（2026.07.17）</h3><p>这是一个 200 美元买来的教训。其实我早就应该明白、或者已经明白这个道理：策略不可以赚钱。但是由于工作原因，以及可能受到一些其他影响，我突发奇想开始去尝试 Polymarket 上的策略。</p><p>真正试验这个策略的过程要远比文章写出来的复杂，花费了大量的时间和精力。但是在经过这样的尝试后，反而更加佐证了这个结论：策略不可以赚钱。也许周期性可以依靠运气赚钱，但是长期来看一定是负期望。</p><p>我没有时间和耐心去写复盘，就让 ChatGPT 完整总结了一下前因后果：</p><p>《<a href="https://github.com/smallyunet/poly-btc5m/wiki/%E6%88%91%E4%BB%AC%E6%98%AF%E6%80%8E%E4%B9%88%E6%8A%8A%E4%B8%80%E4%B8%AA%E7%9C%8B%E8%B5%B7%E6%9D%A5%E8%83%BD%E8%B5%9A%E9%92%B1%E7%9A%84-BTC-5-%E5%88%86%E9%92%9F%E7%AD%96%E7%95%A5%EF%BC%8C%E8%B7%91%E5%88%B0%E6%9C%80%E7%BB%88%E5%81%9C%E6%9C%8D%E7%9A%84">我们是怎么把一个看起来能赚钱的 BTC 5 分钟策略，跑到最终停服的</a>》</p><p>最后我再解释和强调一下这里说的 “教训” 是指：不要抱有侥幸心理。当你知道 “策略不可以赚钱” 之后，也许偶然会想再试试、也许这一次会不一样、也许这样那样就可以，然后就会发现，最终不但没赚到钱，还赔了钱。这就是教训。</p><br><h3 id="更新（2026-08-09）"><a href="#更新（2026-08-09）" class="headerlink" title="更新（2026.08.09）"></a>更新（2026.08.09）</h3><p>策略还在继续维护中，只是现在会更加谨慎；结合之前急于求成的教训，现在仅运行模拟盘、然后分析回归测试出来的数据；有新的发现和结果会再更新。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;免责声明&quot;&gt;&lt;a href=&quot;#免责声明&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="策略" scheme="https://smallyu.net/tags/%E7%AD%96%E7%95%A5/"/>
    
  </entry>
  
  <entry>
    <title>为什么 DM KOL 的 GTM 策略一定是 “对” 的</title>
    <link href="https://smallyu.net/2026/06/29/%E4%B8%BA%E4%BB%80%E4%B9%88DM-KOL%E7%9A%84GTM%E7%AD%96%E7%95%A5%E4%B8%80%E5%AE%9A%E6%98%AF%E2%80%9C%E5%AF%B9%E2%80%9D%E7%9A%84/"/>
    <id>https://smallyu.net/2026/06/29/%E4%B8%BA%E4%BB%80%E4%B9%88DM-KOL%E7%9A%84GTM%E7%AD%96%E7%95%A5%E4%B8%80%E5%AE%9A%E6%98%AF%E2%80%9C%E5%AF%B9%E2%80%9D%E7%9A%84/</id>
    <published>2026-06-29T06:24:59.000Z</published>
    <updated>2026-06-29T08:15:54.321Z</updated>
    
    <content type="html"><![CDATA[<p>团队的项目在 Go to Market 环节遇到了困难，所以找了有经验的 BD（商务拓展）来提供指导意见。对方给出的策略是：每个人都去 DM KOL 谈合作。</p><p>具体做法是直接在 X 上找到相关领域的 KOL，然后跟对方聊天、套近乎、问需求、谈合作。</p><p>这个策略能够达到的目的是（转述）：</p><ol><li>最差的结果，是把产品介绍发给了对方，对方看到了、知道有这么个产品，不回复</li><li>稍微好一点的结果是，对方有回复，并且体验了产品，并且还给出了对产品的意见，这个属于很大的收获</li><li>最好的结果是，对方不但体验了产品，还对产品表达认可，愿意和项目方合作，把产品推荐给他的粉丝，以及通过返佣的方式长期合作</li></ol><p>这个策略是 BD 给出的、在他的理解中最为可行的、用户转化率、留存率最高的方案。</p><p>说实话我没有 GTM 的经验，所以我也在不断反思，这个策略好不好、对不对、为什么专业的有经验的 BD 唯一认可的是这个策略。当然我早就想明白了结论，只是现在把这个结论写出来。我来跟你解释一下为什么。</p><p>我的结论是：这个策略一定是对的，但未必是有结果的。</p><h3 id="学习的例子"><a href="#学习的例子" class="headerlink" title="学习的例子"></a>学习的例子</h3><p>很简单的道理，如果一个学生问老师，怎么提高学习成绩？老师会说，课前预习、上课好好听讲做笔记、课后认真完成作业、有不会的题要请教同学和老师。这一套学习方面的方法论，亘古不变。</p><p>但是很明显，我们都知道，同一个班级、同样的老师、同样的课本、同样的作业，学生的成绩就是天差地别。</p><p>为什么呢，因为策略方向是对的，但是具体到执行上每个学生都不一样，每个人的行为差异会被放大。</p><p>所以只要策略方向足够大、足够宽泛，策略就一定 “正确”。只是正确未必有用，因为策略需要具体的人来执行，而每一个人都是有差异的，只要不能机械化到代码程序可以重复执行，结果就肯定有差异。</p><p>也因此，你学习成绩不好不能怪老师吧？怎么有的学生人家就是成绩好呢？老师讲的东西都一样，你不能说老师讲的道理不对吧？</p><h3 id="投资的例子"><a href="#投资的例子" class="headerlink" title="投资的例子"></a>投资的例子</h3><p>李笑来在 2022 年之后就不再做投资了，专职做家庭教育、写书、卖课、讲课。我喜欢李笑来的《财富自由之路》和《韭菜的自我修养》两本书，现在也时不时留意李笑来的新书和课程。</p><p>他最近几年的观点，非常明显有一个共同点，就是非常 “基础”、非常 “简单”，简单到像跟 6 岁小孩子讲道理一样，比如我们跟孩子说要诚实、勇敢、善良等，李笑来讲的观点，也都类似这种，比如 “生产是财富的来源”、“要学会复利”、“知识是可以迁移的、只要有时间就学习”、“自我暗示非常有力量” 等等。</p><p>他讲的这些内容，感觉都简单到 “是个人知道”，他讲的每一点，简单到 “每一个普通人都可以做到”。</p><p>然而事实上，世界上只有一个李笑来，真正财富自由的只有李笑来。他的课程并没能把那些 “真理” 一样的道理教给别人、让别人变得富有。</p><p>我也经常反思这个问题，为什么呢，为什么李笑来这样经历丰富的人，最终总结出的各种道理，反而异常朴实、基础、简单？我相信他不是刻意隐瞒，而是真心实意相信自己的那套东西。</p><p>所以现在直接能串联起来了。就跟学生问老师成绩怎么变好一样。很多人问李笑来，怎么变有钱，怎么变富有？然后李笑来告诉你，要坚持，要努力，要学习，要产出，要……</p><p>明白这里面的漏洞了吗？策略方向一定对，具体执行因人而异。还是那句话，只要策略方向足够大、足够宽泛，策略就一定 “正确”。</p><h3 id="DM-KOL-的例子"><a href="#DM-KOL-的例子" class="headerlink" title="DM KOL 的例子"></a>DM KOL 的例子</h3><p>回到 DM KOL 这个策略，我来拆解两个问题：</p><h4 id="1-策略范围是不是很大？"><a href="#1-策略范围是不是很大？" class="headerlink" title="1. 策略范围是不是很大？"></a>1. 策略范围是不是很大？</h4><p>很大。从一开始转述的、期望达到的 3 个效果来看，其实包含了市场调研、功能反馈、寻找合作 3 种目的。对于大公司来说这可能是 3 件事情，只是小团队就直接聚合为一件事情、全都放在 “DM KOL” 这句话里了。</p><p>只看这几个词可能不够直观，具体来说，我们期望问 KOL：</p><ol><li>你觉得 PM 有哪些不好用的地方？</li><li>你觉得我们的产品是否好用、有什么建议？</li><li>是否愿意跟我们合作、推荐并拿返佣？</li></ol><p>当然前提是对方愿意回信息。因为实际上大多数 KOL 不会回消息，或者上来就报价。而这个 DM KOL 策略的重点，实际上在于建立关系、跟人搭讪，先建立联系、然后表述出目的。</p><p>从这个描述能听出问题所在不。这就全部依赖于个人的社交能力了，而且是通过网络打字社交。BD 也跟我们说，要持续骚扰对方到骂你为止。</p><h4 id="2-实际执行效果怎么样？"><a href="#2-实际执行效果怎么样？" class="headerlink" title="2. 实际执行效果怎么样？"></a>2. 实际执行效果怎么样？</h4><p>也很差。不过我要分享的，是我们团队面对的最大问题，以及为什么效果不好：因为执行成本高。</p><p>很多交易所，长期招聘 BD、而且 BD 没有无责底薪、说开除就开除，随便去任何一个招聘群，都能看到类似的吐槽。</p><p>很多项目方，在小红书上招聘实习生，一个月 3000块人民币，实习生就愿意辛辛苦苦任劳任怨干一个月，天天在 X 上刷流量、发广告。</p><p>而 DM KOL 的策略，属于周期长、反馈慢的策略，可能 DM 1000 个 KOL，其中能有 10 个给出反馈、有 1 个能达成合作。这还得是你花心思认真跟对方接触的情况下。</p><p>如果对这个过程没有实际感受，可以回想一下找工作的过程。比如我找工作的频率就很高，深刻理解这种状态。在找工作的时候，可能联系几百个 HR，然后面试几十家公司，最终找到一份合适的工作。</p><p>对于 BD 或者销售人员来说，DM KOL 就是他们的工作呀，他们不但能用整天的时间来干这件事情，而且有足够的时间去思考、总结相关的经验。</p><p>至于我所在的团队，所有成员中，目测时间成本没有比我低的。每个人都戴着 “金手铐”。毕竟不是专业的 BD 人员，也都有自己职业方向的事情需要做，能够用于 DM 的时间很少，更重要的是，DM 是一件反馈周期长、不确定性很高的事情。花一整天的时间去 DM，可能都什么反馈都不会得到。而在自己熟悉的道路上，总是能有一些结果的。</p><p>所以让成员兼职去 DM KOL 这件事情，如果想要得到可观的结果，难度不亚于先让成员做一次职业转型，然后再统一做长时间刻苦的执行后，才能有结果。</p><p>如果不那样，要么有人能认真把这件事情做好，要么什么都做不好。我个人是不相信，有人能顺带做一件事情，不用心也不总结，就能比专业的人做出更好的效果。</p><p>毕竟哪怕是专业的 BD 团队，同样完全正确的策略下，10 个 BD 成员里得到的结果想必也天差地别，有的人被开除了，有的人成为金牌员工了。这种事情谁说得准呢。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>所以你应该看出来了，我写这些的目的并不是在吐槽，而是在分析和分享。</p><p>重要的是我明白了一个道理：为什么看起来正确的策略，却得不到好的结果？</p><p>不知道我讲清楚了吗？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;团队的项目在 Go to Market 环节遇到了困难，所以找了有经验的 BD（商务拓展）来提供指导意见。对方给出的策略是：每个人都去 DM KOL 谈合作。&lt;/p&gt;
&lt;p&gt;具体做法是直接在 X 上找到相关领域的</summary>
        
      
    
    
    
    
    <category term="GTM" scheme="https://smallyu.net/tags/GTM/"/>
    
  </entry>
  
  <entry>
    <title>AI Agent 看板</title>
    <link href="https://smallyu.net/2026/06/18/AI-Agent%E7%9C%8B%E6%9D%BF/"/>
    <id>https://smallyu.net/2026/06/18/AI-Agent%E7%9C%8B%E6%9D%BF/</id>
    <published>2026-06-18T13:56:27.000Z</published>
    <updated>2026-06-18T14:21:51.804Z</updated>
    
    <content type="html"><![CDATA[<p>有一个给 AI Agent 设计的看板工具叫 Multica，理念是本地部署，本地运行的 AI 比如 Codex、Claude Code、Antigravity 等都以 Agent 形式接入看板，然后你就可以给 AI Agent 分配任务、管理项目进度等。</p><p>看到这个产品后有感而发。</p><p>对于这个产品本身，怎么理解这个事情呢，我觉得可以这样理解：</p><p>首先看板工具是指类似 Jira、Linear、GitHub Project、Notion 这些知名的项目管理工具。</p><p>然后先不说 AI，就说人，假如就是一些真实的人、真实的员工、真实的技术团队。有能力通过任务面板把技术团队管理明白的 leader，包括任务怎么分配、任务时长、任务难度的定义、结果怎么验收等等。对于能处理好任务、把项目进度安排的井井有条的 leader，这个 AI Agent 看板工具才有意义。</p><p>也就是说，对于已经具备能力管理好 “人” 的 leader 型人，他才需要把员工换成 AI，然后在看板工具上给 AI 分配任务这种产品。</p><p>AI 相对于人类，有优点也有弱点。优点就是效率高，你给它明确的任务，它能执行的很好。但是 AI 的弱点在于，不需要主动汇报工作、没有情绪，也不需要承担责任。如果 AI 把你的数据库删了，或者强硬汇报错误的内容、或者突然降智听不懂人话了，你能拿他怎么办呢？骂它？扣它工资？给它讲道理、画大饼让它内疚？</p><p>所以当你作为一个能管理好员工或者下属的 leader，试图通过 AI 降本增效的时候，这个 AI Agent 看板工具才能发挥作用。而与此同时，这个 leader 也得好好掂量一下能不能用好 AI。</p><p>下一个问题，AI 能提高效率是显而易见的，但是 AI 真的能降低成本吗？用 Codex 日常写代码一天三四个小时，需要开每个月 100 美元的套餐。如果更高强度使用，大概一天六七个小时运行时长的话，每个月 200 美元的套餐是必需的。</p><p>同时考虑到 Codex 有周限额和日限额，200 美元的套餐跑满肯定不够，算作一个 AI Agent 300 美元吧。然后看板工具的意义当然在于，多个 Agent 同时运行。如果只需要一个，在 Codex 里用就行，没有必要看板。</p><p>那么按照小型技术团队 3 个人的规模来算，AI Agent 的费用算作一个月 1000 美元吧。</p><p>也就是说，原本也许 1 leader + 2 senior + 1 junior 配置的技术团队，可以精简为 1 leader + 2 Codex + 1 Antigravity。要只算订阅费的话，是不是真的节省了不少成本？</p><p>不过还要考虑一个小问题，人类队伍中 leader 不需要独自承担责任，只需要验收就行。但是在 Agent 的配置形式下，leader 必然是更忙更累的，因为需要 review 所有 AI 的工作结果、手动打字（甚至不能语音和会议）来同步给 AI 工作任务、需要把任务边界和规则定义的非常清楚。相信做过技术的话就会理解，看似用 AI 代替了大量人力，但同时极大增加了 leader 的负荷。这种推演方式下 leader 一定是忙不过来的。</p><p>所以可以退一步，改为 2 leader + 2 Codex + 2 Antigravity 的模式，也就是 (1 leader + 1 Codex + 1 Antigravity)*2 的模式。这样的话 leader（或者工程师）的工作负荷勉强合理、工作任务能够合理完成。这种模式下，成本变成了多少呢？</p><p>继续下一个问题，刚才的讨论一直有一个前提，就是这个 leader 本身有能力管理好 “人”，在此基础上去使用 AI。那么如果本身这个 leader（工程师）不具备管理人类的能力呢？</p><p>结合之前提到的 AI 的弱点，那么工作一定是一团糟糕的，毫无疑问。难道我们或者你，在过往的工作经历中没有遇到过管理能力糟糕的 leader 吗？设想他们管理一堆不需要承担责任的 AI Agent，真的能让工作或者项目完成的更好吗？</p><p>总的来说，项目的任务看板工具实际上不是技术问题而是管理问题。有能力用好看板的人没那么需要 AI，没有能力用好看板的人也用不好 AI。</p><p><br><br></p><img src="summary.png" width="80%">]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;有一个给 AI Agent 设计的看板工具叫 Multica，理念是本地部署，本地运行的 AI 比如 Codex、Claude Code、Antigravity 等都以 Agent 形式接入看板，然后你就可以给 AI Agent</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>被 AI 工具裹挟的程序员</title>
    <link href="https://smallyu.net/2026/06/04/%E8%A2%ABAI%E5%B7%A5%E5%85%B7%E8%A3%B9%E6%8C%9F%E7%9A%84%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    <id>https://smallyu.net/2026/06/04/%E8%A2%ABAI%E5%B7%A5%E5%85%B7%E8%A3%B9%E6%8C%9F%E7%9A%84%E7%A8%8B%E5%BA%8F%E5%91%98/</id>
    <published>2026-06-04T09:02:57.000Z</published>
    <updated>2026-06-04T09:19:27.919Z</updated>
    
    <content type="html"><![CDATA[<p>6月1日，GitHub Copilot 换成了根据使用量计费的模式。按照我原本的使用习惯，新的计费模式下，一个月至少得花 800 美元才够用，所以不得不寻找更便宜的套餐。这也就导致最近两三天，除了关心工作内容本身，我还需要花费大量时间和精力去体验 Antigravity、CodexX 等工具和模型，以及解决不同工具之间切换带来的上下文摩擦。</p><p>当然我还是坚持一贯的偏见：不用 Claude Code 的工具和模型。因为 Claude Code 的模型太过听话，出现路灯效应的现象比较严重。</p><br><p>由此联想到的问题是，自从有了 AI，我们开始关心的话题变成了：</p><ol><li>哪个 AI 模型好用</li><li>哪个 AI 套餐便宜</li><li>哪个 AI 工具好用、prompt 工程做得好</li></ol><p>而 AI 出现以前，我们关心的问题是：</p><ol><li>哪种编程语言好用</li><li>哪种编程语言编译速度快、写起来简单、心智负担小、运行时错误可控</li><li>哪种 IDE 好用</li></ol><p>在 AI 出现以前，有个人站出来说：</p><ol><li>编程语言是不重要的，编程思想才重要</li><li>编程语言的语法是不重要的，从本质上理解语义才重要</li><li>我教给你们学会所有编程语言的方法</li><li>我的工作不是去评价现有的垃圾语言，而是去设计最先进、优雅的编程语言</li><li>……</li></ol><p>那么 AI 出现以后，会不会有个人站出来说：</p><ol><li>AI 模型是不重要的，重要的是使用 AI 的人</li><li>好的程序员应该懂得如何驾驭各种 AI 模型</li><li>理解 AI 模型的能力只是表面，要深刻理解 AI 模型的训练方法、“智力”的来源才重要</li><li>我教给你们理解所有 AI 模型的方法</li><li>我的工作不是评价现有的垃圾模型，而是设计最高效、最先进的 AI 模型</li><li>……</li></ol><p>会不会有这样的人出现呢？🐶</p><br><p>其实我有一个更深层次的焦虑：</p><p>AI 时代，我们曾经学的那些编程语言、原理，都是手工艺时代的知识和技能，是不是已经过期、过时、不重要了？你可以说它本质上重要，但是事实上确实 AI 抹平了大多数程序员的能力。</p><p>或者我换个问题，AI 时代，理解 Lambda 演算还重要吗？把图灵放到现代，他肯定不会写代码，调 API 的能力和普通程序员用 AI 写出来的差不了多少。你觉得这个观点能带来什么启发？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;6月1日，GitHub Copilot 换成了根据使用量计费的模式。按照我原本的使用习惯，新的计费模式下，一个月至少得花 800 美元才够用，所以不得不寻找更便宜的套餐。这也就导致最近两三天，除了关心工作内容本身，我还需要花费大量时间和精力去体验</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>比特币社区在讨论的抗量子计算是怎么回事</title>
    <link href="https://smallyu.net/2026/05/31/%E6%AF%94%E7%89%B9%E5%B8%81%E7%A4%BE%E5%8C%BA%E5%9C%A8%E8%AE%A8%E8%AE%BA%E7%9A%84%E6%8A%97%E9%87%8F%E5%AD%90%E8%AE%A1%E7%AE%97%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/"/>
    <id>https://smallyu.net/2026/05/31/%E6%AF%94%E7%89%B9%E5%B8%81%E7%A4%BE%E5%8C%BA%E5%9C%A8%E8%AE%A8%E8%AE%BA%E7%9A%84%E6%8A%97%E9%87%8F%E5%AD%90%E8%AE%A1%E7%AE%97%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B/</id>
    <published>2026-05-31T08:15:55.000Z</published>
    <updated>2026-05-31T08:44:55.697Z</updated>
    
    <content type="html"><![CDATA[<p>引用框里的内容都是 Gemini 生成的。我也不太喜欢这种大量 AI 生成内容的文章，但是很多专业名词和数据又依赖于 AI 给出的数据。所以还是以分享和整理信息为主吧。</p><h3 id="生成式-AI"><a href="#生成式-AI" class="headerlink" title="生成式 AI"></a>生成式 AI</h3><p>对于我们外行人来说，ChatGPT 3.5 似乎是一夜之间出现的，2023 年横空出世，一下子到达了能够通过图灵测试的地步。而实际上 GPT 已经连续多年发布有突破性的论文，让 GPT 模型不断演进：</p><blockquote><ul><li><strong>2018年（GPT-1）</strong>： 发表论文 《Improving Language Understanding by Generative Pre-Training》，正式提出了“生成式预训练”的概念。</li><li><strong>2019年（GPT-2）</strong>： 发表论文 《Language Models are Unsupervised Multitask Learners》，证明了网页大模型在不需要微调的情况下，就能做很多不同的任务</li><li><strong>2020年（GPT-3）</strong>： 发表了里程碑式的论文 《Language Models are Few-Shot Learners》。这时候模型参数达到了 1750 亿，大家突然发现，大力出奇迹，模型开始展现出惊人的理解能力。</li><li><strong>2022年 3月</strong>，OpenAI 发表了一篇极为重要的论文：《Training language models to follow instructions with human feedback》。在这篇论文中，他们引入了 RLHF（基于人类反馈的强化学习） 技术，训练出了 InstructGPT。</li><li><strong>2022年 11月</strong>，ChatGPT （基于 GPT-3.5 架构）诞生，实际上就是 InstructGPT 的 “亲兄弟” 或直接衍生版。OpenAI 将这种 “听得懂指令” 的模型，套上了一个最符合人类习惯的聊天框界面（UI），并在 2022 年 11 月底作为研究预览版免费开放。</li></ul></blockquote><h3 id="量子计算的现状"><a href="#量子计算的现状" class="headerlink" title="量子计算的现状"></a>量子计算的现状</h3><p>所以针对量子计算的担忧不是杞人忧天，那些我们未曾关注到的行业也许正在发生惊天动地的变化。</p><blockquote><p>如果把之前的量子计算比作 GPT-3 之前的“野生天才”，它空有庞大的计算潜力，但极度不稳定。目前（2026年），整个行业正在全力攻克量子计算的“InstructGPT 时刻”——<strong>量子纠错（QEC, Quantum Error Correction）</strong>。</p><p>目前行业呈现出以下几个核心进展：</p><ol><li>从 “物理比特” 走向 “逻辑比特”</li></ol><ul><li>以前媒体炒作 “IBM 达到 1000 个量子比特” 或 “谷歌实现量子霸权”，那些都是<strong>物理比特（Physical Qubits）</strong>。它们就像极其脆弱的肥皂泡，周围有一点温度波动、Wi-Fi 信号，甚至空气流动，就会发生 “退相干（Decoherence）”，导致计算崩溃。现在的行业共识是：数量不再是唯一标准，<strong>质量（纠错能力）</strong>才是。谷歌的 Willow 芯片和微软、IBM 的最新路线图都在证明一件事——通过把几十甚至上百个脆弱的 “物理比特” 绑定在一起，变成一个具有容错能力的 “逻辑比特（Logical Qubits）”。</li></ul><ol start="2"><li>多技术路线的“军阀混战”</li></ol><p>不同于大模型几乎全盘采用 Transformer 架构，量子计算目前还没有统一的硬件标准：</p><ul><li><strong>超导路线（IBM、谷歌）</strong>： 跑得最快，但需要接近绝对零度（0.015 Kelvin）的极低温冰箱，工程成本极高。</li><li><strong>拓扑量子（微软 Majorana 1）</strong>： 走纯物理学底层突破，利用新型拓扑材料自带的抗噪特性，如果成功，能在一颗芯片上塞进 100 万个比特。</li><li><strong>离子阱&#x2F;光子路线（Quantinuum、Xanadu）</strong>： 在常温或更高温度下表现更好，相干时间长。</li></ul></blockquote><h3 id="击溃加密体系需要的算力"><a href="#击溃加密体系需要的算力" class="headerlink" title="击溃加密体系需要的算力"></a>击溃加密体系需要的算力</h3><blockquote><p>要让现有的全球加密体系（如银行、政府通信、区块链使用的 RSA、ECC、DH 等非对称加密算法）彻底崩溃，传统的经典计算机和未来的量子计算机需要跨越的算力鸿沟完全不在一个维度。</p><ol><li><strong>经典计算机</strong>：需要“宇宙毁灭级”的算力</li></ol><p>目前互联网安全的基石（比如 RSA-2048 算法），其核心数学原理是“大整数质因数分解”。把两个很大的质数乘起来很容易，但要把乘积倒推拆解成这两个质数，极其困难。</p><p>要用现有的超级计算机去暴力破解一个 RSA-2048 密钥，需要多大算力？</p><ul><li><strong>算力需求</strong>： 大约需要 2^112 次运算。</li><li><strong>时间成本</strong>： 即使动用全人类目前所有的超级计算机（包括最顶级的超级计算机集群）一起不吃不喝地算，也需要<strong>几千万年甚至上亿年</strong>。</li><li><strong>能量成本</strong>： 破译一个密钥消耗的电能，可能需要抽干几个地球的能源。因此，在经典计算机的框架下，现有的主流加密体系在物理上是绝对安全的。</li></ul><ol start="2"><li><strong>量子计算机</strong>：只需要“数千个逻辑比特”的降维打击</li></ol><p>量子计算机之所以恐怖，是因为它拥有一个专属的数学武器——Shor 算法（秀尔算法）。</p><p>Shor 算法在处理“大整数质因数分解”时，把计算复杂度的量级从经典计算机的指数级（Exponential）直接降到了多项式级（Polynomial）。</p><p>在量子计算中，算力不再用“每秒运算多少次（FLOPS）”来衡量，而是看“容错逻辑比特（Logical Qubits）的数量”。</p><p>根据目前的理论和工程推算，要在一夜之间攻破 RSA-2048，量子计算机需要达到以下指标：</p><table><thead><tr><th>指标</th><th>理论需求值</th><th>为什么现在做不到？</th></tr></thead><tbody><tr><td><strong>逻辑比特数量</strong></td><td><strong>大约 4000 个</strong>完美的、高质量容错逻辑比特</td><td>目前人类最顶尖的实验室也只能勉强制造出<strong>几十个</strong>逻辑比特。</td></tr><tr><td><strong>物理比特数量</strong></td><td><strong>大约 2000 万个</strong>物理比特</td><td>因为量子极易受到干扰，平均需要几千个脆弱的“物理比特”去纠错、合成 1 个“逻辑比特”。</td></tr><tr><td><strong>破解时间</strong></td><td><strong>大约几小时到几天</strong></td><td>一旦硬件达标，量子计算机只需要喝一杯咖啡或睡一觉的时间，就能解开经典计算机算上亿年的题目。</td></tr></tbody></table></blockquote><h3 id="击溃比特币（ECC-椭圆曲线加密）需要的算力"><a href="#击溃比特币（ECC-椭圆曲线加密）需要的算力" class="headerlink" title="击溃比特币（ECC 椭圆曲线加密）需要的算力"></a>击溃比特币（ECC 椭圆曲线加密）需要的算力</h3><blockquote><p>比特币使用的是 SECP256k1 椭圆曲线加密。相比于 RSA，椭圆曲线在面对量子攻击时更加脆弱。</p><ul><li>攻破比特币的公钥私钥对，理论上只需要 2000 到 3000 个逻辑比特。</li><li>如果量子计算机拥有足够的比特数，它可以在 10 分钟内（比特币下一个区块打包出来之前）直接通过公钥反算出私钥，从而瞬间转移任何钱包里的资产。</li></ul><p>既然只需要 4000 个逻辑比特就能让世界瘫痪，那这一天离我们有多远？</p><p>目前全球的顶尖量子机构（IBM、谷歌、微软、Quantinuum等）正处于从“几百个物理比特”向“数十万个物理比特”冲刺的阶段。按照目前的工程进度预测：</p><ul><li>2026 - 2030年（<strong>安全期</strong>）： 量子计算机的逻辑比特数预计突破三位数，能够用来模拟新材料、新药物，但依然无法撼动 RSA-2048。</li><li>2030 - 2035年（<strong>高危期</strong>）： 如果量子纠错技术（QEC）发生突破，物理比特到逻辑比特的转化率大幅提升，千万级物理比特的计算机可能面世。这时候，现有的加密体系将进入真正的倒计时。</li></ul></blockquote><h3 id="比特币社区的讨论"><a href="#比特币社区的讨论" class="headerlink" title="比特币社区的讨论"></a>比特币社区的讨论</h3><blockquote><p>近期比特币社区（特别是核心开发者、Blockstream 等研究机构）关于量子计算的讨论确实骤然升温，甚至可以说是<strong>近年来争论最激烈的一次</strong>。</p><p>引发这波热烈讨论的直接导火索是 <strong>2026 年 3 月底谷歌、以太坊基金会和斯坦福大学联合发布的一篇重磅论文</strong>。论文指出：攻破比特币底层加密算法（ECDSA）所需的物理比特数量，比之前预估的要<strong>整整低了 20 倍</strong>（大约只需要不到 50 万个物理比特），并且在极端算力下，最快可能在 <strong>9 分钟</strong>内反算出私钥。由于比特币出块时间是 10 分钟，这意味着量子计算机有可能在交易发出后、被打包进区块前的这短短几分钟里，进行“拦截偷地雷”。</p><p>面对这个被大幅压缩的时间表，目前比特币社区主要在密集进行以下三件事：</p><ul><li>推进首个抗量子提案：BIP-360 (P2MR)。这是比特币历史上<strong>第一个正式写入路线图的抗量子改进提案</strong>（Bitcoin Improvement Proposal）。</li><li>激烈争论：“直接搬用美国标准（NIST）” 还是 “用哈希签名”。NIST（美国国家标准局）推荐了 Dilithium 和 Falcon 等基于格（Lattice-based）的抗量子算法。但比特币核心开发者们发现：直接搬过来会要了比特币的命。</li><li>排查“沉睡钱包”，商讨软分叉与“遗产冻结”政策。比特币网络中，真正面临迫切量子威胁的，其实是那些公开了公钥的旧地址。</li></ul></blockquote><h3 id="哪些加密算法受到威胁"><a href="#哪些加密算法受到威胁" class="headerlink" title="哪些加密算法受到威胁"></a>哪些加密算法受到威胁</h3><p>那么我们来搞清楚，到底哪些加密算法会受到量子计算机的威胁，哪些不会：</p><blockquote><p>核心结论：</p><ul><li><strong>非对称加密（RSA&#x2F;ECC）</strong> 依靠的是“精妙的数学大厦”，量子计算机有“画好的图纸（Shor 算法）”，所以一拆就倒。</li><li><strong>单向哈希（SHA&#x2F;Keccak</strong>） 依靠的是“无序的混沌黑盒”</li></ul></blockquote><h3 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h3><p>我发现这样下去不太好，文章里全是复制粘贴 AI 生成的文本，所以干脆放一些后续的问题列表，能够把这个话题延续下去。只要把这些问题复制给任何 AI，都会得到比较好的解答：</p><ul><li>为什么 SHA-256 天生抗量子？</li><li>那么 Merkle root 为什么可以代替用户的公钥来代表资产？</li><li>为什么比特币现在的资产是在公钥里，而不是在 SHA-256(公钥) 里？</li><li>你提到了 P2PK，比特币还有哪几种资产形式，分别都有没有套上 hash？</li><li>现在的 P2TR 是为了干什么来着，或者说有什么特性和优势，相比 P2WPKH？</li><li>以太坊社区推崇的 ZK 加密是在解决什么问题，ZK 加密抗量子计算吗？</li><li>我还是没能太理解，Merkle tree 在选择路径去验证某几个节点的时候，是怎么去选择的？</li><li>为什么全节点要打包给我 Hash(EFGH)？</li><li>全节点知道 EFGH 的数据吗？</li><li>在比特币的 Taproot 中，在最新的抗量子方案中，Merkle tree 是怎么发挥作用的？</li><li>所以我的疑问在于，你刚才提到一句“提供一条到 Merkle Root 的路径证明”，我想知道这个路径是怎么来的？</li><li>那么在 P2MR 的过程中，意思是我的资产是被锁在某个叶子节点上吗？</li><li>我理解了，那么 A、B、C、D 其实分别都是一把钥匙，每次都使用不同的钥匙，用来抵抗量子计算的攻击？</li><li>我还是不理解，这个 Merkle tree 默认有多少个节点？一个用户的私钥默认有多少个节点？</li><li>Merkle tree 的节点数量一旦生成，是不是就不能改了，因为 root hash 是确定性的？</li><li>我想想，P2MR 中，节点会知道我的 Merkle tree 上有几个节点吗？</li><li>那么这个使用场景，和那种轻节点从全节点拿某一个 hash 值的场景，其实是不一样的？</li></ul><p>这个问题列表反映出另外一个有意思的点，我早期的 <a href="/2025/12/02/%E6%88%91%E5%AF%B9%E4%BA%8EAI%E6%97%B6%E4%BB%A3%E7%9A%84%E7%AD%94%E6%A1%88/">观点和结论</a> 在于，AI 时代，回答问题的能力已经不重要，而提问问题的能力更加重要。这个问题列表正是印证了这一点，答案不重要，任意一个顶级 AI 模型都可以解答。而提问问题的过程和思路是重要的。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;引用框里的内容都是 Gemini 生成的。我也不太喜欢这种大量 AI 生成内容的文章，但是很多专业名词和数据又依赖于 AI 给出的数据。所以还是以分享和整理信息为主吧。&lt;/p&gt;
&lt;h3 id=&quot;生成式-AI&quot;&gt;&lt;a href=&quot;#生成式-AI&quot;</summary>
        
      
    
    
    
    
    <category term="量子计算" scheme="https://smallyu.net/tags/%E9%87%8F%E5%AD%90%E8%AE%A1%E7%AE%97/"/>
    
    <category term="比特币" scheme="https://smallyu.net/tags/%E6%AF%94%E7%89%B9%E5%B8%81/"/>
    
  </entry>
  
  <entry>
    <title>为什么买的加密货币一定要去中心化</title>
    <link href="https://smallyu.net/2026/05/16/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B9%B0%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E4%B8%80%E5%AE%9A%E8%A6%81%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96/"/>
    <id>https://smallyu.net/2026/05/16/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B9%B0%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E4%B8%80%E5%AE%9A%E8%A6%81%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96/</id>
    <published>2026-05-16T08:50:14.000Z</published>
    <updated>2026-05-18T08:32:57.121Z</updated>
    
    <content type="html"><![CDATA[<p>中心化的加密货币价格能不能涨呢？能涨，而且拉盘、控盘、砸盘更狠。大一点的像 ETH，SOL、BNB，随意一点的就是 TRUMP、DOGE 一类。但是你要相信，如果你是从二级市场开始参与，那么拉盘周期吃到肉的人，一定不会是你。要么喝汤，要么被割（大概率）。</p><p>美股能不能涨呢？能涨，虽然所有的股票都是中心化的，但是完全适合用于投资，因为有监管，有审核。英伟达、苹果、谷歌、特斯拉。</p><p>从投资者的视角，硅谷已经不从二级市场割韭菜了，从 1.5 级市场就开始割。OpenAI 和 Anthropic 万亿美元级别的估值，一旦上了二级市场，就等着小韭菜们接盘，价格会逐渐跌。但是人家融资，你有钱也投不进去。真正顶级的公司，一级市场都没机会。而不那么顶级的公司，几个 VC 互相割。不管能不能 IPO 割小韭菜，VC 的钱是割到了。</p><p>人们买加密货币是为了什么？是为了高倍的涨幅，想从中挣钱。币圈没有信仰，要有信仰也都是信仰钱。金融市场都是为了钱。没有人是为了去中心化的信仰和理想买比特币的。</p><p>回到比特币的初衷，比特币想干什么来着？比特币想要抗通胀。现代政府和金融体系要依靠负债表的不断扩张，来维持社会运转，也就意味着法币从趋势上是持续增发的。</p><p>比特币有供应量上限，正是为了应对通胀。所以 ETH 等所有没有供应量上限的公链，哪怕代币供应量是动态条件之后通缩的，也达不到抗通胀的目的。</p><p>2008 年，全球的法币总供应量大概 21 万亿美元，因此比特币的总供应量设计为 2100 万。按照当时的愿景，当比特币能代替全球的法币职能的那一天到来，一个 BTC 的价格应该等于（当时的） 100 万美元。</p><p>这不是巧合，比特币的精度是 8 位小数点，最小单位是 sat，那么如果 1 BTC &#x3D; 100 万美元，恰好 1 sat &#x3D; 1 美分，美元的最小单位就是比特币的最小单位。</p><p>2009 年，密码学家 Hal Finney 成为历史上第一个接受比特币转账的人，收到了来自中本聪 10 BTC 的转账。</p><p>2009 年，全球的家庭总财富体量大概在 100 万亿到 300 万亿之间。按照这个财富价值估算，如果比特币在全球的资产储备中占据主导地位，一个 BTC 的价格将约等于（当时的） 1000 万美元。</p><p>2027 年，全球货币总供应量大概 70 万亿美元，全球家庭财富总量大概 530 万亿美元。映射到比特币上，如果按照 M1 供应量计算，每个 BTC 的价值等于 333 万美元。如果按照财富总量计算，每个 BTC 的价值等于 2650 万美元。</p><p>这就是比特币的特殊之处，比特币不是为了创造下一个百万富翁，而是为了保护全世界人的资产不受到损失。BTC 的价格不应该再大幅暴涨，而是应该稳定逐步随着法币的增发而价值上涨。</p><p>从抗通胀的逻辑，比特币和黄金在同一个赛道，山寨币和美股在同一个赛道。</p><p>所以结论是：</p><ol><li>定投 BTC、PAXG</li><li>Alpha 机会你自己随缘，ETH、Nvidia 等，Do Your Own Research</li><li>争取一级市场机会，投资、工作、创业，你自己想办法</li></ol><p>两个负面规则：</p><ol><li>不要指望一个素不相识的人发个币让你买，就能带你起飞</li><li>炒币、炒股造就的富翁很少，大多数是做了什么事情、参与了什么项目、有什么实体企业。只要还在牌桌上，就没有赢家</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;中心化的加密货币价格能不能涨呢？能涨，而且拉盘、控盘、砸盘更狠。大一点的像 ETH，SOL、BNB，随意一点的就是 TRUMP、DOGE</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>RISC-V 虚拟机无法代替 EVM 虚拟机</title>
    <link href="https://smallyu.net/2026/04/24/RISC-V%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%97%A0%E6%B3%95%E4%BB%A3%E6%9B%BFEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/"/>
    <id>https://smallyu.net/2026/04/24/RISC-V%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%97%A0%E6%B3%95%E4%BB%A3%E6%9B%BFEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/</id>
    <published>2026-04-24T12:34:05.000Z</published>
    <updated>2026-04-24T12:41:19.686Z</updated>
    
    <content type="html"><![CDATA[<ol><li>RISC-V 太复杂，非常依赖于虚拟机沙盒环境的标准化，但是现在还没有统一的规范</li><li>EVM 经过多年百亿级别金融生态的磨练，从开发到审计已经比较成熟，切换到 RISC-V 的成本很高</li><li>RISC-V 虚拟机的冯诺伊曼税更高、确定性更脆弱</li></ol><p>Gavin Wood 当年在设计 EVM 的时候，会面临两个问题：基于栈还是基于寄存器？显然实际上的 EVM 是基于栈的，也许是出于上线迭代等外部因素影响，也许是出于 Gavin 当时的历史局限性。后来 Gavin 离开了以太坊、创建了 Polkadot 之后，一开始把 Polkadot 的虚拟机设计为 基于 WASM 虚拟机，最近又转向了基于 RISC-V 指令集的、用于实现通用计算的虚拟机。也就是说，Gavin 抛弃了基于栈的 EVM 的老路，最终转向寄存器结构的 RISC-V。</p><p>EVM 底层是 256 位的无符号数字，每一条指令、每一个数据，都会拆解为 256 位的数字来表示。至于为什么是 256 位，可能因为以太坊是从比特币演变来的，而哈希算法 Keccak-256 和签名算法 secp256k1 等都沿用了比特币的设计，它们的输出都是 256 位，为了无缝适配这些加密算法，就采用了 256 位的字长。</p><p>我们平时使用的电脑 CPU，大都是 64 位的。CPU 经历过 32 位的时代，但是由于在地址寻址的方式下，32 位最多表示 2^32 Byte &#x3D; 4 GB 的内存大小，已经跟不上使用需求，所以现在的 CPU 已经都是 64 位的。</p><p>这里有两个数字，EVM 是 256 位的，CPU 是 64 位的。这也就意味着，每一个 EVM 的指令操作，到了 CPU 上都至少需要 10 个指令来完成，模拟一个栈内存、计算低位、计算高位、把结果推回栈等，这一套动作下来，256 位的 EVM 指令在执行效率上，比使用 64 位 CPU 指令，慢大概 10 倍到 100 倍。</p><p>RISC-V 是一个精简的指令集架构标准，包含有 64 位的 RISC-V 虚拟机指令标准。这就意味着，通过 JIT 编译，RISC-V 的指令几乎可以 1:1 映射到现代 64 位的 CPU 上，执行效率接近于 CPU 本身的效率。</p><p>Arbitrum 有过一次叫 Stylus 的 <a href="https://chronicle.castlecapital.vc/p/state-arbitrum">版本升级</a>，内容是用运行于 WASM 的虚拟机性能与 EVM 虚拟机的性能进行对比，官方的宣传口径是，一个简单的加法 <code>ADD</code> 运算，在 WASM 上的 Gas 消耗比在 EVM 上少 150 倍，而这 150 倍就是指令集转换带来的额外开销。WASM 的性能比 RISC-V 差，因为 WASM 从设计之初是在浏览器环境运行一个多编程语言的虚拟机，所以合理推测，基于 RISC-V 的指令，性能上也要好过 EVM 至少 100 倍。</p><p>从指令执行的效率方面来看，RISC-V 绝对要好过 EVM 太多。</p><p>另一方面，RISC-V 非常适合做通用计算，比如 <a href="https://cartesi.io/">cartesi</a> 这个项目，相当于直接在每一个链上节点本地，运行一个小型的 Linux 系统，这个系统内能运行的，就不只是智能合约，而可以包括各种各样进程级别的程序，这能释放很大的想象空间。</p><p>很多 zkVM 也都是基于 RISC-V 在做，毕竟要给一个 256 位的虚拟机以及各种复杂的指令集开发 ZK 电路，心智负担太大了，而 RISC-V 是一个精简指令集，只有比较底层的指令，比 zkEVM 实现起来要更加容易并且通用。</p><p>至于 EVM 方面，也有在性能方面做出卓越突破的链比如 <a href="https://www.monad.xyz/">Monad</a>，实现了真正的并行 EVM。只不过 Monad 提高性能的方式并不是从比较底层的指令集层面去做，而是乐观执行、有冲突再回滚的方式，以及实现了专用的数据库而不是通用的 KV 数据库来提高磁盘 IO 效率，整体结合下来，让 EVM 的执行速度有了显著提升。</p><p>那么至此可以看到的是，基于 RISC-V 指令集的虚拟机、智能合约语言，有着种种性能上的优点。</p><p>是不是意味着 RISC-V 是替代 EVM 的最佳趋势呢？显然也不是。</p><p>生态方面的护城河当然很重要，RISC-V 属于彻底抛弃了 EVM 体系、自成一套，支持各种编译型的编程语言。那也就意味着，包括开发者和审计公司，在 EVM 上踩过的坑，需要推倒重来。这是谁都不愿意看到的事情。EVM 的简单带来了性能上的劣势，也让安全风险和执行逻辑上的排查更加可控，因为是栈结构，Solidity 语言里连指针操作都没有，确定性非常高。而 RISC-V 的寄存器模式，合约的 ABI 不再是字节码而是 ecall ABI，里面涉及到内存分配、有堆内存、有垃圾回收，复杂度非常高。</p><p>而为了解决确定性的问题，一般的做法是在 RISC-V 支持的编程语言上，再阉割一层，用一个沙盒环境来执行合约逻辑，并且禁用一些语言特性，从沙盒外部能够看到一个确定性的结果。这当然就会牵扯到，每一个项目都在定义自己的沙盒标准，没有业界统一的方案。</p><p>再就是很多项目方宣传口径上的问题，放大了 RISC-V 的性能优势，刻意在忽略 RISC-V 模式带来的冯诺伊曼税的问题。在 EVM 上，修改余额就是一个 SSTORE 指令，EVM 天然知道什么是账户、什么是余额，要去数据库的什么位置寻找数据。而 RISC-V 过于通用，如果要修改余额，可能需要先分配内存、处理语言运行时的边界检查、写入数据、释放内存等，虽然单个指令的执行效率高了，但是反而在业务逻辑上消耗了更多的 CPU 周期。</p><p>总的来说，RISC-V 虚拟机还无法代替 EVM 虚拟机。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;ol&gt;
&lt;li&gt;RISC-V 太复杂，非常依赖于虚拟机沙盒环境的标准化，但是现在还没有统一的规范&lt;/li&gt;
&lt;li&gt;EVM 经过多年百亿级别金融生态的磨练，从开发到审计已经比较成熟，切换到 RISC-V 的成本很高&lt;/li&gt;
&lt;li&gt;RISC-V</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>Polkadot 的 Existential Deposit 机制错在哪里</title>
    <link href="https://smallyu.net/2026/04/22/Polkdot%E7%9A%84Existential-Deposit%E6%9C%BA%E5%88%B6%E9%94%99%E5%9C%A8%E5%93%AA%E9%87%8C/"/>
    <id>https://smallyu.net/2026/04/22/Polkdot%E7%9A%84Existential-Deposit%E6%9C%BA%E5%88%B6%E9%94%99%E5%9C%A8%E5%93%AA%E9%87%8C/</id>
    <published>2026-04-22T09:50:48.000Z</published>
    <updated>2026-04-22T09:55:32.176Z</updated>
    
    <content type="html"><![CDATA[<p>Polkadot 有一个叫 <a href="https://support.polkadot.network/support/solutions/articles/65000168651-what-is-the-existential-deposit-">Existential Deposit (ED)</a> 的机制，含义是如果你的账户余额小于 1 DOT，区块链网络会直接删除你的账户以及余额。删除的意思是指，你的地址信息将不存在于链上，其他地址无法转账小于 1 DOT 给你，你的余额以及 Nonce 值会被直接抹除，整个网络的代币总供应量也会因此减少。</p><h3 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h3><p>我们先来理解一下为什么 ED 这样的设计会存在。Vitalik 要说代码和工程水平可能远不及他拥有的名气，Gavin Wood 是以太坊的实际开发者。Gavin 因为实际设计了以太坊的节点，所以很清楚以太坊存在状态数据会无限膨胀的问题。</p><p>以太坊节点会把整条链上所有有过交易记录的地址信息，都维护到世界状态中，用的是一种叫 MPT 的树结构，每个地址信息都是树上的一个叶子节点。那么就存在一个问题，我们平时使用以太坊钱包也会有体会，如果地址里有 0.00001 ETH 的余额，这种非常小额的余额，是永远无法被转出的，因为以太坊对于普通转账交易，固定要求 21000 Gas，无论 Gas Price 多低，以太坊账户的余额都不可能彻底清零。这种非常小的余额被称为粉尘余额，对于强迫症来说会是一种灾难。</p><p>对于以太坊节点来说，这些地址以及余额，就不仅只是视觉上强迫症的问题，而是意味着，节点需要维护的状态数据，永远都会只增加、不减少。每多一个新的地址有余额，服务器上的节点就需要多储存一份地址的键值对信息，哪怕对内存的占用量很小，也会持续累积和增加，给服务器持续带来压力。这种设计在工程上是很暴力、不负责也不合理的。</p><p>针对粉尘余额，存在两个问题，我们分别来对比：</p><ol><li>用户视角的粉尘余额是否可以消除、让余额彻底清零</li><li>节点视角的粉尘余额是否会持续给服务器增加压力</li></ol><h4 id="用户视角"><a href="#用户视角" class="headerlink" title="用户视角"></a>用户视角</h4><p>比特币是如何解决粉尘余额问题的？假如有一个 UTXO 地址里的余额非常小只有 1 sats，那么自己发起转账肯定是不行的，因为 1 sats 不够付矿工费。但是比特币的交易允许有多个 input，只要把小额的 UTXO 作为其中一个 input，再让其他有大额余额的 UTXO 作为另外的 input 来支付矿工费，整笔交易就可以成功，拥有 1 sats 的 UTXO 余额也会清零。所以是 UTXO 账户模型让比特币天然没有粉尘余额的问题。</p><p>以太坊是粉尘余额的起源地，所以目前始终无法解决。</p><p>Polkadot 为了解决粉尘余额的问题，把 Existential Deposit 作为 Day Zero 的核心设计写到了协议里。余额小于 1 DOT 就会被自动清除，自然不存在粉尘余额的问题。这种清除余额并不是指余额会转给矿工或者转给黑洞地址，而是凭空消失一样地清除。</p><h4 id="节点视角"><a href="#节点视角" class="headerlink" title="节点视角"></a>节点视角</h4><p>比特币节点也是需要 chainstate 的，会把所有有余额信息的地址信息都维护在节点的内存中。不过相比以太坊，由于比特币地址的粉尘余额是可以被清零的，而节点方面对于余额是 0 的地址不需要维护一份状态数据，如果一个地址的余额是 0，节点就可以把地址从状态数据中删掉，内存占用就可以减少一点点。所以对于比特币节点来说，状态数据并不会只增不减，而是可以根据实际情况有增有减。</p><p>以太坊节点呢，一方面因为用户余额本身就无法清零，余额再小节点也得维护状态。另一方面在于，以太坊其实有清理状态数据的机制，只是地址可以被清理的要求很苛刻，需要余额为 0，并且 Nonce 值为 0。Nonce 值这个东西，也是比特币没有的概念，以太坊为了账户-余额模型加的。也就是说，哪怕余额为 0 了，只要发出过交易，Nonce 值就不可能是 0。两方面原因叠加下来，以太坊的状态数据会只增不减，持续膨胀。</p><p>Polkadot 呢，同样是利用 Existential Deposit 的设计，会直接清理掉小于阈值的地址以及相关信息，相应的，链上节点维护状态数据的压力就会变小。</p><h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>所以 Polkadot 利用 Existential Deposit 这种机制，同时解决了粉尘余额对于用户以及对于节点的影响，在工程上是一种比较纯粹和现代化的设计。</p><h3 id="错在哪里"><a href="#错在哪里" class="headerlink" title="错在哪里"></a>错在哪里</h3><p>回到标题，Polkadot 的这种设计有错吗？</p><ol><li>太过纯粹的工程思维</li></ol><p>程序员群体在解决现实问题的时候，往往会偏好工程化的解决方式，而忽略现实用户的体验。Polkadot 给人的感觉就是，小钱不是钱，我没钱就不配用。</p><p>加密货币作为一种资产，一种货币，一种钱，链上节点像是在回收垃圾一样，把我的余额给回收掉。是不是等同于在说，小的金额就是垃圾？用编程语言的垃圾回收机制，来针对加密资产做回收，这是比较错配的行为。内存中的无效资源、悬空对象可以是垃圾，但是用户的资金不是。在比特币网络，哪怕只有 1 sats，也会安安静静躺在那里。</p><ol start="2"><li>违背了加密货币的原则</li></ol><p>Polkadot 的 Existential Deposit，直接违背了加密货币的根本原则：只有持有私钥的人，才有资产的控制权。</p><p>我们常说 “Not your keys, not your coins”，但是到了 Polkadot 这里，只有你自己持有私钥对吧，但是链上的节点可以直接抹掉你的余额，在没有私钥的情况下让地址内的余额发生了变化。这是一种非常违背原则的做法。</p><p>在区块链中，唯一有效的、对余额产生影响的，应该来自于账本数据，以太坊尚且没有做得这么过分，但是 Polkadot 做了，直接改了状态数据里的余额，在没有交易行为的情况下，引入了节点层面的收割机制。</p><ol start="3"><li>账户模型的原罪</li></ol><p>如果说 <a href="/2025/10/24/%E4%BB%A5%E5%A4%AA%E5%9D%8AAA%E9%92%B1%E5%8C%85%E7%9A%84%E8%87%B4%E5%91%BD%E9%97%AE%E9%A2%98%E6%98%AF%E4%BB%80%E4%B9%88/">AA 钱包</a> 是以太坊为了解决账户模型而打出的一个补丁，那么 Existential Deposit 就是 Polkadot 为了填补账户模型打出的另外一个补丁，试图在以太坊的基础上向前多走一步，而做出的激进设计。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Polkadot 有一个叫 &lt;a</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>利息，套利，交易策略，金融市场</title>
    <link href="https://smallyu.net/2026/03/02/%E5%88%A9%E6%81%AF%EF%BC%8C%E5%A5%97%E5%88%A9%EF%BC%8C%E4%BA%A4%E6%98%93%E7%AD%96%E7%95%A5%EF%BC%8C%E9%87%91%E8%9E%8D%E5%B8%82%E5%9C%BA/"/>
    <id>https://smallyu.net/2026/03/02/%E5%88%A9%E6%81%AF%EF%BC%8C%E5%A5%97%E5%88%A9%EF%BC%8C%E4%BA%A4%E6%98%93%E7%AD%96%E7%95%A5%EF%BC%8C%E9%87%91%E8%9E%8D%E5%B8%82%E5%9C%BA/</id>
    <published>2026-03-02T05:52:16.000Z</published>
    <updated>2026-03-21T10:25:18.055Z</updated>
    
    <content type="html"><![CDATA[<h3 id="资金费率套利"><a href="#资金费率套利" class="headerlink" title="资金费率套利"></a>资金费率套利</h3><p>资金费率套利是一种没有风险、保本的套利方式。我一开始尝试自己写脚本来运行策略，后来发现 Binance 直接提供了 bot 来进行这种操作：</p><div style="display:flex; justify-content:center; gap:12px; flex-wrap:wrap;">    <img src="1.png" style="width:35%; height:auto;">    <img src="2.png" style="width:35%; height:auto;"></div><p>这里的 Arbitrage Bot 就是资金费率套利的 Bot，点开可以看到有很多币种可以选，包括按照资金费率来排序等。而且 Binance 的 Bot 做的很智能，如果资金费率是正的，就开正向的套利策略（使用 USDT），如果资金费率是负的，就开反向的策略（使用代币）。</p><p>资金费率套利的获利空间很小，尤其是 BTC 等主流币种，APR 在 1% 以下，当前低迷的市场环境下，大概持有 300 天以上才能磨平手续费损耗。实际上是很不划算的方式。而如果按照资金费率排序，选最高的一个代币，APR 能相对高一点，也许能达到 40% 甚至更多，当然高收益也就意味着高的波动和风险：</p><div style="display:flex; justify-content:center; gap:12px; flex-wrap:wrap;">  <img src="3.png" style="width:35%; height:auto;">  <!-- <img src="4.png" style="width:35%; height:auto;"> -->  <img src="5.png" style="width:35%; height:auto;">  </div><p>资金费率套利是 Detla 中性的，假如代币突然暴涨，把 2 倍的做多合约给爆仓了，会不会导致损失一半本金呢？不会，因为现货涨了 2 倍后卖掉正好抵消了爆仓的损失，所以本金没有变化，整个过程就是赚了这段时间的资金费率。</p><p>所以根据资金费率高的代币排序，然后开套利 Bot，是我唯一尝试过想赚点利息的方式。</p><h3 id="保本赚币"><a href="#保本赚币" class="headerlink" title="保本赚币"></a>保本赚币</h3><div style="display:flex; justify-content:center; gap:12px; flex-wrap:wrap;">  <img src="5.png" style="width:35%; height:auto;">  <img src="6.png" style="width:35%; height:auto;"></div><p>Simple Earn 很简单，存到 Binance 里赚利息。问题在于 APR 并不高，USDC 也就 6%，而且有非常高的额度限制，只支持 200 USDC。几乎没什么用。</p><h3 id="双币投资"><a href="#双币投资" class="headerlink" title="双币投资"></a>双币投资</h3><p>双币投资（Dual Investment）的策略整体看下来，关键在于你心里要有一个预期的价位。比如 BTC 到 60000 就愿意买入，无论它以后会不会继续跌，都以 60000 的价格买入（卖出同理）。</p><p>双币投资更像是期货交割的逻辑，对于普通散户来说，同样没什么用。为什么双币投资的利息高呢？因为风险也高，有卖飞或者接刀子的风险。风险高，利息就高了。</p><p>如果双币投资设定一个非常离谱的价格呢，比如在 100000 的时候买入 BTC，岂不是可以达不到条件一直赚利息了？但实际上利息是由风险决定的，市场看到风险小，也就没有人愿意付高额利息，所以价格离谱的市场本身就不存在。</p><h3 id="开合约、加杠杆"><a href="#开合约、加杠杆" class="headerlink" title="开合约、加杠杆"></a>开合约、加杠杆</h3><p>别作死。</p><h3 id="Lido"><a href="#Lido" class="headerlink" title="Lido"></a>Lido</h3><img src="8.png" width="80%"><p>这 2.3% 的 APR 实在是太低了。用某个人的话来说，对于普通人，低于 5% 的 APR 根本不需要考虑，得至少 20 年才能赚钱。</p><h3 id="Aave"><a href="#Aave" class="headerlink" title="Aave"></a>Aave</h3><img src="9.png" width="80%"><p>Aave 的 APR 也很低，甚至最高也低于 2%，几乎等于没有。</p><h3 id="Uniswap"><a href="#Uniswap" class="headerlink" title="Uniswap"></a>Uniswap</h3><img src="10.png" width="80%"><p>乍一看还挺吓人的，USDC&#x2F;ETH 有 76% 的 APR。问题在于提供流动性是存在无常损失的，你投入 1000 ETH 和 1000 USDC，随着池子中不断进行交易，最终你提出来可能就剩 300 ETH 和 1700 USDC。代币的总量不变，但是 ETH 的价格在剧烈波动，你的投入就完全不划算了。而 Uniswap 上的稳定币交易对 USDT&#x2F;USDC 这种池子的收益率自然很低，在 1% 左右。</p><h3 id="BTC-5-分钟猜涨跌"><a href="#BTC-5-分钟猜涨跌" class="headerlink" title="BTC 5 分钟猜涨跌"></a>BTC 5 分钟猜涨跌</h3><img src="11.png" width="80%"><p>Polymarket 上线了 BTC 5 分钟猜涨跌的玩法。那么在这种市场有获利的机会吗？结论是没有。这是一种负期望值的游戏，散户、低频等同于纯粹的赌博。甚至可以认为所有的事件预测市场都是赌博。别提什么稳定套利的机会了。</p><h3 id="Polymarket-上的-free-money"><a href="#Polymarket-上的-free-money" class="headerlink" title="Polymarket 上的 free money"></a>Polymarket 上的 free money</h3><p>一方面，Polymarket 上有一些整活市场，比如 <a href="https://polymarket.com/event/will-jesus-christ-return-before-2027">耶稣会不会在 2027 年回归？</a>。这种市场的 no 价格大约 96.2c，意味着如果持仓一年等待事件结算后，持仓的年化收益大约 4%。非要和传统银行以及现金 APR 比的话这个收益率也还不错。</p><p>另一方面，Polymarket 有一个机制，对于某些 <a href="https://help.polymarket.com/en/articles/13364459-holding-rewards">符合要求</a> 的市场，只要 holding 就会一直分发年化 4% 的 shares。有一些市场是显然知道结局的，所以可以大胆买入，比如某个市场的 no 价格是 91.1c，当事件结算后会获得接近 9% 的年化收益，再累加上 4% 的持仓收益，实际上是一个还算不错的理财选择。</p><h3 id="MEV-攻击、链上科学家、黑暗森林"><a href="#MEV-攻击、链上科学家、黑暗森林" class="headerlink" title="MEV 攻击、链上科学家、黑暗森林"></a>MEV 攻击、链上科学家、黑暗森林</h3><p>快省省吧。</p><h3 id="金融市场"><a href="#金融市场" class="headerlink" title="金融市场"></a>金融市场</h3><p>经过这么多年的发展以及几年前的 DeFi Summer，无论是 Cex 利息还是 DeFi，随着 ETF 的推出、华尔街巨头的介入，只要市场上有低风险的套利机会，就会有几亿的资金以及高频低延迟的机器人入场。普通玩家的机会非常少。</p><p>在金融市场里怎么赚钱？</p><ol><li>拿时间换空间（撸空投）</li><li>拿本金换利息（大额本金）</li><li>拿认知、精力换超额收益（现货定投、寻找 Alpha）</li></ol><p>除此之外，别无他法。</p><p>散户之所以是韭菜，是因为：</p><ol><li>没有无限的本金</li><li>缺乏顶级的认知</li><li>缺乏极度的耐心</li></ol><p>反正什么都没有。有的只有无知，和赚钱的欲望。</p><h3 id="定投策略"><a href="#定投策略" class="headerlink" title="定投策略"></a>定投策略</h3><p>李笑来经历过那么多项目，最后总结出的终极交易策略是定投。《<a href="https://ri.firesbox.com/#/cn/">定投改变命运</a>》这本书其实写的非常好，信息密度很高。当我们对于某些高频交易策略动心的时候，不妨回顾一下这本书里提到的要点，而定投正是需要极度耐心的一种策略。</p><p>当然定投策略以及李笑来的理论，也有让人觉得扯淡的地方。比如书里到最后，给大家的建议是提高 “场外赚钱的能力”。这基本上是非常正确的废话，谁不知道场外赚钱重要……</p><p>定投是一种与人性做对抗的策略，确实很难严格落实，尤其是对于从业者来说，天天会看到行情和市场情绪，不可能做到情绪高的时候还定投，也做不到情绪低的时候管得住手。</p><p>想要赚钱，还是得做出点别人做不到的事情才行。</p><h3 id="Restaking"><a href="#Restaking" class="headerlink" title="Restaking"></a>Restaking</h3><p>刚才提到了定投，那么假如坚持定投 BTC 的话，BTC Restaking 能不能在持有 BTC 的基础上再赚点利息？</p><img src="12.png" width="80%"><p>Restaking 赛道唯一一个真正做 BTC Restaking 的项目 Babylon，提供的 APR 至高 0.87%，这确实是不值一提。毕竟 2B 的资金量压在协议里，APR 又不能凭空产生，项目收益再高，一分到 2B 的质押量上，APR 就不可能高了。所以 BTC Restaking 几乎不值得去操作。</p><h3 id="场外赚钱"><a href="#场外赚钱" class="headerlink" title="场外赚钱"></a>场外赚钱</h3><p>我在思考 “场外赚钱” 这个问题。我发现 “定投+持有” 的过程无聊至极。</p><p>金融市场里有一个很奇怪的逻辑，明明你就是场外赚钱能力有限，才想要到金融领域通过投资手段寻找高杠杆、高回报率的新机会。而投资界的大佬都会告诉你，场外赚钱的能力才是最重要的，你要有源源不断的现金流才行。</p><p>而事实上，金融领域从来只放大资金收益，而不负责基础资金的来源。在金融领域的不可能三角是：</p><ul><li>本金</li><li>收益率</li><li>时间</li></ul><p>如果你有足够的本金，即使收益率低也有很好的回报，100 万美元年化 10% 一年下来就是 10 万美元。如果你愿意承担极度高的风险，土狗币百倍币就是高收益最直接的渠道，收益率是对承担风险的回报。</p><p>那么如果本金也不够大，风险也不愿意承担呢？剩下能投入的只有时间。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;资金费率套利&quot;&gt;&lt;a href=&quot;#资金费率套利&quot; class=&quot;headerlink&quot; title=&quot;资金费率套利&quot;&gt;&lt;/a&gt;资金费率套利&lt;/h3&gt;&lt;p&gt;资金费率套利是一种没有风险、保本的套利方式。我一开始尝试自己写脚本来运行策略，后来发现 Binance</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>LayerZero 从来不挑战比特币的地位</title>
    <link href="https://smallyu.net/2026/02/12/LayerZero%E4%BB%8E%E6%9D%A5%E4%B8%8D%E6%8C%91%E6%88%98%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E5%9C%B0%E4%BD%8D/"/>
    <id>https://smallyu.net/2026/02/12/LayerZero%E4%BB%8E%E6%9D%A5%E4%B8%8D%E6%8C%91%E6%88%98%E6%AF%94%E7%89%B9%E5%B8%81%E7%9A%84%E5%9C%B0%E4%BD%8D/</id>
    <published>2026-02-12T14:16:07.000Z</published>
    <updated>2026-02-13T01:52:33.891Z</updated>
    
    <content type="html"><![CDATA[<h3 id="Zero-网络"><a href="#Zero-网络" class="headerlink" title="Zero 网络"></a>Zero 网络</h3><p>2月10日，LayerZero 公布了 Layer 1 网络 <a href="https://layerzero.network/zero">Zero</a> 的技术方案。有意思的指标有两个：</p><ol><li>兼容 EVM</li><li>TPS 达到 200万&#x2F;秒</li></ol><p>没错，号称每秒钟处理高达 200 万笔交易，是并行 EVM 链 Monad 的 200 倍，是 Solana 的 30 倍，是以太坊主网的 10 万倍，是比特币的 20 万倍。</p><p>LayerZero 的魔法在于优化了 <a href="https://jolt.a16zcrypto.com/intro.html">Jolt</a>，一个基于 RISC-V 指令集的 zkEVM，而且运行在 GPU 集群上，生成 ZK 证明的速度非常快。也就是说，当用户把大量交易提交给 Zero 网络的 Block Producers 之后，基于 GPU 集群的算力， Zero 网络可以非常快生成区块数据的证明。</p><p>Zero 网络出块的硬件要求大概是 96 核 GPU，768 GB RAM，这显然是算力中心级别的要求。那么整个网络岂不是非常中心化、被控制在少数华尔街头部公司的手里了？</p><p>由于 ZK 技术的特性，Zero 网络提供了另一个特性：验证 ZK 证明只需要非常轻量级的设备，家用电脑就可以完成对区块数据的验证。</p><p>这是一个有意思的点，为了提高 TPS，Zero 网络把出块的权力中心化，同时把验证区块的权力极度去中心化。那么 Zero 网络到底是中心化的，还是去中心化的？</p><h3 id="比特币网络"><a href="#比特币网络" class="headerlink" title="比特币网络"></a>比特币网络</h3><p>去中心化网络唯一的标杆是比特币网络。虽然比特币在协议上是去中心化的，所有节点都是全节点，但事实上出块的算力基本被几大矿池垄断，比特币网络还算是去中心化的吗？</p><p>如果单从这个出块权力的集中程度分析，还真是有点难分伯仲，这也是 Zero 网络的诞生让人迷惑的点。</p><p>我们信任比特币的去中心化，是因为每一个节点都有全量账本数据，由于区块链逐个区块哈希的设计，我们可以重放所有交易历史，以此判断新产生的区块数据是否合法。PoW 共识则是所谓一 CPU 一票，哈希碰撞难但是验算简单，以此来验证出块权。所以总体来说，对于比特币，我们相信的是加密算法。</p><p>而对于 Zero 网络，其实我们相信的也是 ZK 加密算法。我们应该相信 Block Producers 会按照电路来生成证明，然后所有的成千上万的 Verifer 都可以去验证区块数据是否合法，这整个过程也完全是加密算法来保障的。</p><p>如果说 Zero 的源代码分发也许会有风险，比如生成 ZK 证明的电路逻辑出现 bug，或者验证 ZK 证明的逻辑有漏洞，给攻击者可乘之机。那么所有的区块链都面临源代码不安全的风险，包括比特币，你并不能保障比特币的源码就是绝对安全的。所以从这个角度无法区分 Zero 网络的优劣。</p><p>回到去中心化的角度，比特币网络交易重放可以保障区块数据可靠，而 Zero 网络虽然出块权被少数节点掌握，但是验证区块的权力借助 ZK 技术，能够轻易下放到任何轻量级设备上，这是比特币都无法比拟的 “去中心化”。假如 Zero 网络的 Block Producers 作恶，所有用户都可以知道，并且反馈给结算层，如果众多 Verifier 不同意，Block Producers 的出块权会被取消……</p><p>从这几个角度去对比，是不是恍惚间发现 Zero 网络竟然可以和比特币同台竞技，甚至 ZK 技术的出现给了 Zero 超越比特币的可能性？</p><h3 id="共识"><a href="#共识" class="headerlink" title="共识"></a>共识</h3><p>当然，刚才刻意弱化了核心问题所在。面对这种区块链架构上的疑惑，如果一时间没有思路，可以回到区块链技术的圣经 —— <a href="https://bitcoin.org/bitcoin.pdf">比特币白皮书</a> 找灵感。</p><p>比特币白皮书在摘要里就提到了比特币要解决的核心问题：双花（double-spending）。</p><p>Zero 网络引入 ZK 技术确实解决了区块链数据不可篡改这个问题，但是如何解决双花问题？答案是 Zero 网络用了 PoS 共识，引入了 Validator 的概念。别管他到底是哪种 PoS，反正都是 PoS。</p><p>参考以太坊的架构去理解，以太坊有两层，执行层和共识层，执行层负责执行交易，共识层负责验证节点出块的权力。</p><p>Zero 网络的架构和以太坊有点类似，分为出块层和结算层，出块层就是用大规模 GPU 执行区块和产生 ZK 证明的那一层，而结算层，则是 PoS 质押、验证节点是否有出块权力的一层。</p><p>你也许没跟上，不是说解决双花问题吗，怎么又在讨论节点出块权力的问题了。因为假如节点出块的权力被控制，或者可以做恶，那么恶意节点就可以允许区块数据，也就是交易历史被改写，上一秒看到交易成功了，下一秒发现交易回滚了。所以节点出块的权力得到保障，双花问题也可以得到解决。</p><p>那么这下就清晰了，Zero 引入了 ZK 技术把 TPS 提高了两个数量级，确实有很大的进步。但是在共识方面，依然用了 PoS。对于一些比特币的原宗旨教徒来说，PoS 共识甚至都是不入流、不能抗审查的。那么 ZK 再牛自然也就没有意义了。</p><h3 id="EVM"><a href="#EVM" class="headerlink" title="EVM"></a>EVM</h3><p>所以，Zero 网络从一开始的竞争对手就不是比特币，根本不会动摇比特币的地位。回到 Zero 网络的 <a href="https://layerzero.network/blog/zero-the-decentralized-multi-core-world-computer">Manifesto</a>。从标题 “Multi-Core World Computer” 就能看出是奔着以太坊去的，在 Manifesto 中也直接对比了以太坊、Solana 的性能问题。当然是不可能出现比特币的。</p><p>而实际上，Zero 网络连以太坊主网的地位都不会动摇，以太坊的去中心化程度是仅次于比特币的，全节点运行在民用级设备一直是 Vitalik 的愿景 —— “如果基金会不在了，以太坊还会存在吗？”</p><p>更进一步，Zero 网络连 Solana 的地位也不会动摇，因为 Solana 独特的生态，和一些名人以及政治叙事，轻易不会被取代。</p><p>总的来说，Zero 网络仅仅只能在 EVM 的生态里做竞争，可能会直接动摇发布不久的并行 EVM 网络 Monad 的前景。其他的更大的野心，明显还做不到。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;Zero-网络&quot;&gt;&lt;a href=&quot;#Zero-网络&quot; class=&quot;headerlink&quot; title=&quot;Zero 网络&quot;&gt;&lt;/a&gt;Zero 网络&lt;/h3&gt;&lt;p&gt;2月10日，LayerZero 公布了 Layer 1 网络 &lt;a</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>默认参数引起的以太坊节点运行错误</title>
    <link href="https://smallyu.net/2026/02/03/%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0%E5%BC%95%E8%B5%B7%E7%9A%84%E4%BB%A5%E5%A4%AA%E5%9D%8A%E8%8A%82%E7%82%B9%E8%BF%90%E8%A1%8C%E9%94%99%E8%AF%AF/"/>
    <id>https://smallyu.net/2026/02/03/%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0%E5%BC%95%E8%B5%B7%E7%9A%84%E4%BB%A5%E5%A4%AA%E5%9D%8A%E8%8A%82%E7%82%B9%E8%BF%90%E8%A1%8C%E9%94%99%E8%AF%AF/</id>
    <published>2026-02-03T07:19:01.000Z</published>
    <updated>2026-02-03T07:20:39.538Z</updated>
    
    <content type="html"><![CDATA[<p>大概是两年前的事情了。在工作中，我们用以太坊节点搭建了一条网络，上线后发现网络中不能新增 Validator，也就是质押者的 32 ETH 没有生效。</p><p>原因是我们用了多种共识层客户端，包括 Prysm 和 Lighthouse 等。为什么会突然出问题呢？因为 Prysm 有一个叫 <code>--contract-deployment-block</code> 的命令行参数，其他客户端比如 Lighthouse 和 Teku，这个参数的默认值都是 <code>0</code>（参数名字在不同客户端可能不同，但是含义类似），而 Prysm 的这个参数默认值是 <code>11184524</code>，这个数字是以太坊从 PoW 转向 PoS 后质押机制开始生效的区块高度。</p><p>这个参数在干什么呢？它会从这个参数配置的区块高度开始，去扫描 Deposit Contract（主网是 <a href="https://etherscan.io/address/0x00000000219ab540356cbb839cbe05303d7705fa"><code>0x00000000219ab540356cbb839cbe05303d7705fa</code></a> ）上质押者的存款记录。我们平时说的质押 32 ETH，就是质押到这个合约里了，或者说就是把 32 ETH 转账给这个合约。这个合约在收到你的 ETH 之后，Consensus Client 比如 Prysm，就能根据合约的记录知道某一个地址确实给合约转了 32 ETH，因此认可这个人成为出块节点。</p><p>以太坊整个网络可能有几千个节点，每一个节点都在执行同样的操作，就是从 Deposit Contract 上扫描质押者列表、然后维护到自己本地的数据库状态中。直到轮到自己出块的时候，就把这个一直在维护的质押者列表的数据，计算一个总数 Deposit Count 和哈希根 Deposit Root，作为 <code>eth1_data</code> 字段的值，提交到区块数据中：</p><img src="./1.png" width="100%" /><p>这个截图来自以太坊主网的区块 <a href="https://beaconcha.in/block/24374562">24374562</a>，意味着当前以太坊一共有 204 万个质押者。要注意质押者不等于物理服务器上运行的节点数量，一个节点可以运行几千个质押者（Staker &#x2F; Validator），所以推测以太坊实际上的物理节点数量大概在几千个左右。</p><p>回到 Prysm 配置的问题，如果不配置 <code>--contract-deployment-block</code> 参数，默认值是 <code>11184524</code>，那么对于一条新启动的链来说（Chain ID 不是 1 那种），Prysm 就不去扫描 Deposit Contract 合约在块高度 <code>11184524</code> 之前的质押记录了，本地数据库里没有质押者的数据，在出块的时候自然也不会带上 <code>eth1_data</code> 的字段数据。</p><p>以太坊的 <a href="https://github.com/ethereum/consensus-specs/blob/c94138e73e0e70eb4b27f9be4d4e9325fa1aebf7/specs/phase0/beacon-chain.md#eth1-data">协议设计</a> 中要求，<code>eth1_data</code> 的数据必须要超过半数节点一致才可以生效。（注意这里的比例是 1&#x2F;2，和其他地方用到的魔法比例 2&#x2F;3 不一样）。</p><img src="./2.png" width="80%" /><p>所以如果你新启动的网络中有超过一半的节点用了 Prysm，同时这些 Prysm 节点没有可以设置 <code>--contract-deployment-block</code> 参数，网络就会异常、不能正确处理新加入的质押节点。</p><p>我们一般认为软件的默认参数是相对安全可靠的，如果刻意设置才表示我们有特殊需求。而在以太坊 Prysm 客户端的这种语境下，不刻意设置默认值反而是危险的。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;大概是两年前的事情了。在工作中，我们用以太坊节点搭建了一条网络，上线后发现网络中不能新增 Validator，也就是质押者的 32 ETH 没有生效。&lt;/p&gt;
&lt;p&gt;原因是我们用了多种共识层客户端，包括 Prysm 和 Lighthouse 等。为什么会突然出问题呢？因为</summary>
        
      
    
    
    
    
    <category term="以太坊" scheme="https://smallyu.net/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A/"/>
    
  </entry>
  
  <entry>
    <title>硬件钱包与资产安全</title>
    <link href="https://smallyu.net/2026/01/25/%E7%A1%AC%E4%BB%B6%E9%92%B1%E5%8C%85%E4%B8%8E%E8%B5%84%E4%BA%A7%E5%AE%89%E5%85%A8/"/>
    <id>https://smallyu.net/2026/01/25/%E7%A1%AC%E4%BB%B6%E9%92%B1%E5%8C%85%E4%B8%8E%E8%B5%84%E4%BA%A7%E5%AE%89%E5%85%A8/</id>
    <published>2026-01-25T08:04:40.000Z</published>
    <updated>2026-01-25T08:05:22.061Z</updated>
    
    <content type="html"><![CDATA[<p>加密货币的资金管理，从最安全到最不安全的方式排名是：</p><ol><li>家族信托</li><li>硬件钱包</li><li>MPC钱包</li><li>交易所托管</li><li>热钱包</li></ol><p>如果你不在乔布斯排行榜上，家族信托就跟你没关系。对于个人而言，硬件钱包是目前最安全的方案。</p><p>这里没有提多签钱包、助记词+Passphrase 之类的方案，因为那些方案在技术手段上复杂到你都用不明白，复杂的方案是无法长期留存的，反而会增加遗忘的风险，更不可靠。</p><p>那么有几个需要更正的对于硬件钱包的认识：</p><ol><li>硬件钱包是钥匙，不是保险柜</li><li>钱是存在助记词上，不是硬件钱包上</li><li>硬件钱包输错密码会直接清空数据</li><li>助记词不是打开保险柜的备用钥匙，助记词是保险柜本身</li></ol><p>而助记词安全也有几个有意思边界：</p><ol><li>你无法验证助记词是否已经泄露</li><li>如果你不把助记词拿出来，就无法验证助记词是否正确</li><li>你把助记词拿出来的行为本身，就增加了助记词暴露的风险</li></ol><p>所以去中心化资产在给你带来控制权的同时，也要求你自己承担资产安全的风险。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;加密货币的资金管理，从最安全到最不安全的方式排名是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;家族信托&lt;/li&gt;
&lt;li&gt;硬件钱包&lt;/li&gt;
&lt;li&gt;MPC钱包&lt;/li&gt;
&lt;li&gt;交易所托管&lt;/li&gt;
&lt;li&gt;热钱包&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你不在乔布斯排行榜上，家族信托就跟你没</summary>
        
      
    
    
    
    
    <category term="钱包" scheme="https://smallyu.net/tags/%E9%92%B1%E5%8C%85/"/>
    
  </entry>
  
  <entry>
    <title>预测市场比币圈更赌场</title>
    <link href="https://smallyu.net/2026/01/15/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA%E6%AF%94%E5%B8%81%E5%9C%88%E6%9B%B4%E8%B5%8C%E5%9C%BA/"/>
    <id>https://smallyu.net/2026/01/15/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA%E6%AF%94%E5%B8%81%E5%9C%88%E6%9B%B4%E8%B5%8C%E5%9C%BA/</id>
    <published>2026-01-15T00:43:43.000Z</published>
    <updated>2026-01-15T00:45:39.118Z</updated>
    
    <content type="html"><![CDATA[<ol><li>预测市场不存在长期主义</li><li>预测市场一定会有明确的结果</li><li>输的归零，风险不低于合约，高于现货，合约可以加仓，预测市场没商量</li><li>所以预测市场无法用于投资</li><li>预测市场的技术实现不是去中心化的，所以跟 web3 没关系，定位上类似于币安等 CEX</li><li>所以预测市场比土狗盘还残酷</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;ol&gt;
&lt;li&gt;预测市场不存在长期主义&lt;/li&gt;
&lt;li&gt;预测市场一定会有明确的结果&lt;/li&gt;
&lt;li&gt;输的归零，风险不低于合约，高于现货，合约可以加仓，预测市场没商量&lt;/li&gt;
&lt;li&gt;所以预测市场无法用于投资&lt;/li&gt;
&lt;li&gt;预测市场的技术实现不是去中心化的，所以跟</summary>
        
      
    
    
    
    
    <category term="预测市场" scheme="https://smallyu.net/tags/%E9%A2%84%E6%B5%8B%E5%B8%82%E5%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>为什么去中心化的跨链桥不可能实现</title>
    <link href="https://smallyu.net/2026/01/13/%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84%E8%B7%A8%E9%93%BE%E6%A1%A5%E4%B8%8D%E5%8F%AF%E8%83%BD%E5%AE%9E%E7%8E%B0/"/>
    <id>https://smallyu.net/2026/01/13/%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84%E8%B7%A8%E9%93%BE%E6%A1%A5%E4%B8%8D%E5%8F%AF%E8%83%BD%E5%AE%9E%E7%8E%B0/</id>
    <published>2026-01-13T04:05:36.000Z</published>
    <updated>2026-01-13T04:05:46.225Z</updated>
    
    <content type="html"><![CDATA[<p>准确来说，是 Trustless 的跨链桥不可能实现。因为一般认为去中心化就意味着 Trustless，虽然实际上去中心化是在信任一个 Permissonless 的群体。</p><p>不纠结这些具体的名词。现在的各种跨链桥方案，以及未来的跨链桥方案，一定都做不到纯粹的、工具性质的、数学证明的、轻量级运营的、去中心化的形式，能够满足物理隔离的、异步的、异构链之间的跨链。</p><p>先列举一下现在的跨链桥方案，以 BTC 到 ETH 的跨链为例：</p><ol><li><p>公证人模式，也就是中心化的模式，把 BTC 打给一个中心化机构，机构再把 ETH 打给你指定的地址。安全性依赖中心化机构，没什么可说的。现在大多数跨链桥都是这种传统的模式，最多就是机构内部多签。</p></li><li><p>TSS 密钥分片、多签，相对去中心化。把 BTC 转给一个第三方的链比如 Thorchain，在 Thorchain 中，有多个节点分别持有 ETH 链上资产的密钥分片，每个节点分别扫描和验证 BTC 的交易信息，达到一定阈值则多签的签名生效、在 ETH 链上放行资产。</p><p> 这种模式把公证人给去中心化了，有多个节点在共同验证来源交易是否正确。这是目前相对来说比较靠谱的、依赖社区博弈的方案。因为 Thorchain 本身就是一条链，想成为持有密钥分片的节点就需要质押 Token 等。TSS 的签名算法比较消耗计算资源，做不到同时分发太多的密钥分片，所以持有分片的节点数量在一定范围内。像所有的 PoS 链一样，表面上是去中心化的，实际上是 Permissioned 的，只不过是权限的门槛高低不同。</p><p> TSS 确实把跨链桥去中心化了，但是引入了 BTC 和 ETH 之外的第三条链，而第三条链的安全性又依赖于它自己的经济学博弈，本身就是风险。所以这种模式可靠但不够纯粹。</p></li><li><p>合并挖矿，特指资产的来源方是 PoW 链，让 BTC 的矿工在挖矿的时候，把跨链桥的某种证明信息给带上，一起挖到 BTC 的区块信息里。那么 BTC 的区块信息本身就包含了跨链信息。这种方式非常可靠但是不可行，因为矿工不可能配合跨链桥干这种事情。</p></li><li><p>HTLC，本质上是资金的对敲，对实时性要求比较高，必须双方同时在 BTC 和 ETH 在线交换密钥，而且 HTLC 只能做到资金的交换而不是跨链，涉及到汇率的问题。</p></li><li><p>全验证轻客户端，也就是在 ETH 上运行 BTC 的轻客户端，比如让每个 Geth 节点都可以同步和验证 BTC 的全部区块信息，那么 Geth 本身就是一个轻量级的 BTC 客户端，自然也能够实时地、无需第三方信任地把 BTC 交易信息同步到 ETH 的智能合约上。但是这样显然成本太高了，相当于一个客户端同时兼容了两条链。</p></li><li><p>ZK 跨链桥，运行一个 BTC 的轻客户端，把所有区块信息生成电路证明，然后在 ETH 上部署一个 Verifier，只需要验证很小的证明就可以相信来源信息是正确的。那么这种 ZK 模式的问题在哪里？ZK 并没有黑魔法，它的问题在于，需要在 ETH 上部署一个 Verifier。也就是说，你不需要关心是谁、是怎么生成证明的，但是你必须相信在 ETH 上的 Verifier。那么 Verifier 的代码本身、背后的开发者、来源是否可信任又是一个问题。</p></li><li><p>TEE 硬件设备签名。比如在一个 Intel 的服务器上，同步 BTC 的区块信息，然后用 TEE 内置的私钥签名，证明这个信息一定来源于这台设备。然后在 ETH 上，只需要判断来源签名是否来自于那台硬件设备，就可以证明信息是否安全。TEE 方案最大的问题自然是单点风险。</p></li></ol><p>各种方案对比下来，似乎根本找不到一种让人满意的方式。为什么会是这样？</p><ol><li><p>分布式系统中的两军问题（Two Generals’ Problem），这是计算机科学史上第一个被证明无解的问题。这个问题具体的定义不重要，重要的是它早已被科学界证实无解。</p></li><li><p>计算理论中的预言机问题（The Oracle Problem），每一条区块链本身都是一个确定性的状态机，它是封闭的、可被验证的、可被重复计算的。如果要和外部通信，就必然需要引入信任模型、经济学博弈、密码学证明等，这些内容会让跨链方案不再纯粹。</p></li></ol><p>所以去中心化的跨链桥这个问题，已经触及到了计算机科学的理论边界，不是工业界无能，而是这个问题本身无解。</p><ol start="3"><li>有趣的是，Vitalik 早在 2022 年就发表过一个 <a href="https://x.com/VitalikButerin/status/1479501366192132099">观点</a>，未来的区块链是多链的（multi-chain），而不是跨链的（corss-chain)。因为即使存在可靠的跨链工具解决了通信问题，ETH 仍然需要信任 BTC 的共识机制。假如 BTC 网络分叉，ETH 不可能跟着 BTC 的分叉去重新分配资金。这同样是一个无解的问题。</li></ol><p>这种跨链桥方面的技术边界可以带给我们启发：</p><ol><li>不存在无需信任的技术方案，只有信任转移、最小化信任</li><li>工程架构不可能完美，大多数时候需要权衡</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;准确来说，是 Trustless 的跨链桥不可能实现。因为一般认为去中心化就意味着 Trustless，虽然实际上去中心化是在信任一个 Permissonless</summary>
        
      
    
    
    
    
    <category term="跨链桥" scheme="https://smallyu.net/tags/%E8%B7%A8%E9%93%BE%E6%A1%A5/"/>
    
  </entry>
  
  <entry>
    <title>我对于 AI 时代的答案</title>
    <link href="https://smallyu.net/2025/12/02/%E6%88%91%E5%AF%B9%E4%BA%8EAI%E6%97%B6%E4%BB%A3%E7%9A%84%E7%AD%94%E6%A1%88/"/>
    <id>https://smallyu.net/2025/12/02/%E6%88%91%E5%AF%B9%E4%BA%8EAI%E6%97%B6%E4%BB%A3%E7%9A%84%E7%AD%94%E6%A1%88/</id>
    <published>2025-12-02T07:09:32.000Z</published>
    <updated>2025-12-22T01:59:40.047Z</updated>
    
    <content type="html"><![CDATA[<p>当我的工资从每天 ___人民币上升到每天 ___美元的时候，我开始思考是什么让工资增长。</p><p>得到的结论是：</p><ol><li>技术能力的提升</li></ol><p>然后我继续思考这几个问题：</p><ol><li>什么样的技术能力算是好</li><li>什么样的技术能力是稀缺、长久的</li><li>如何提高技术能力</li></ol><p>所以多年以来，贯彻我人生的思路就是：如何提高技术能力。最明显的体现是，如果工作不能让我成长，就换工作。</p><p>那么对于 “如何提高技术能力” 这个问题，我的答案是：</p><ol><li>关注底层原理和实现，而不是应用层的框架和接口</li><li>需要具备在复杂代码中找到并调试关键流程的能力</li><li>实际参与一些比较前沿的、像样子的项目</li><li>自己模仿实现一些比较底层和看起来硬核的项目</li><li>关注技术理念而不是具体实现</li><li>知道王垠课程所代表的 “计算的本质”</li></ol><br><p><strong>事实一</strong></p><p>但是紧接着，我发现了一个令人不安的事实：</p><ol><li>工资的高低、工作是否稳定，和技术能力的好坏没有必然的联系</li></ol><p>得到这个结论有这样几个原因：</p><ol><li>我不认为自己技术能力不合格，但是我遭遇的面试结果为不合适的情况，非常非常多</li><li>我不认为自己技术能力不合格，但是我亲眼看见水平很一般的人在做领导、面试官</li><li>我不认为自己技术能力不合格，但是我的工作很不稳定</li><li>有的人出生就在罗马</li></ol><p><strong>事实二</strong></p><p>与此同时，我发现另一个令人不安的事实是，AI 在改变游戏玩法：</p><ol><li>普通人和知识渊博的专家之间，差距只是一个 Gemini</li><li>程序员不再需要手写代码</li></ol><p>AI 引起的变化非常大，直接改变 “如何提高技术能力” 这个问题的答案：</p><ol><li>写代码的能力完全不重要，掌握多编程语言的能力完全不重要</li><li>在领域内的经验不再需要积累，一问 AI 全是标准答案</li></ol><p>复盘一下我之前犯的错误：</p><ol><li><a href="https://smallyu.net/2025/05/11/%E6%83%B3%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B0%8FEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/">EchoEVM</a>，在半年前，开发这样的东西似乎是有意思的。半年后的今天，AI 可以轻易开发出完整的项目。所以 EchoEVM 不再有意义。</li><li><a href="https://smallyu.net/2025/09/06/%E4%B8%80%E4%B8%AA%E9%9B%86%E6%88%90Geth%E5%92%8CCometBFT%E7%9A%84%E5%85%BC%E5%AE%B9%E5%B1%82/">EthBFT</a>，在 2 个月前，开发这样的东西也许可以看到 AI 能力的局限性。但是现在，AI 在逐渐突破以前的局限。所以 EthBFT 也不再有意义。</li></ol><p>EchoEVM 和 EthBFT 的共同特点，是偏低层、侧重技术的实现，试图用硬核项目来证明自己的技术能力。然而在拥有 AI 的今天，这种硬核的代码能力恰恰是 AI 最擅长、最先取代的。</p><br><p><strong>新的问题</strong></p><p>结合这两个令人不安的事实，需要回答的新问题是：</p><ol><li>如何提高自己的竞争力、稀缺性、硬实力、挣更多钱、不被时代淘汰</li></ol><p><strong>新的答案</strong></p><p>那么对于新的问题，我的答案是：</p><ol><li>提高发现问题的能力</li></ol><br><p><strong>对答案的解释</strong></p><p>你也许会说，这不废话吗，自古以来，发现问题的能力都是重要的。</p><p>不，这不一样，在没有 AI 的时候，你可以不需要有判断力，不需要能够发现问题，哪怕只是听产品经理的话来实现功能，也就是干好程序员的活，就可以活下去。</p><p>但是 AI 取代了这种只会听话干活的人。</p><p>“发现问题” 同时涵盖技术领域和非技术领域，在技术领域，发现代码有没有问题、功能设计是否存在漏洞、业务的边界条件是否缺少约束；在非技术领域，发现用户有哪些实际的需求，发现市场有哪些比较大的空缺。</p><p>那么为什么没有把 “提高判断力” 放到答案中？因为判断明天的股市涨跌也算判断，这种能力是无法验证以及无法通过努力提高的。</p><p><strong>进一步问题</strong></p><p>还没完，对于新的答案，有两个问题：</p><ol><li>如何提高自己发现问题的能力</li><li>与代码能力不同，发现问题的能力该如何量化、与他人比较</li></ol><p>对于 “如何提高自己发现问题的能力” 的问题：</p><ol><li>只有见过更好的，才能知道现在看到的有什么不足。所以要事事都向上看齐</li></ol><p>对于 “发现问题的能力该如何量化” 的问题：</p><ol><li>技术方面，把发现的问题落实到技术文档、设计文档、架构文档上</li><li>非技术方面，把发现的问题记录下来，比如博客、日记，文字可以记录思考的过程</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;当我的工资从每天 ___人民币上升到每天</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>不要投资任何隐私币</title>
    <link href="https://smallyu.net/2025/11/17/%E4%B8%8D%E8%A6%81%E6%8A%95%E8%B5%84%E4%BB%BB%E4%BD%95%E9%9A%90%E7%A7%81%E5%B8%81/"/>
    <id>https://smallyu.net/2025/11/17/%E4%B8%8D%E8%A6%81%E6%8A%95%E8%B5%84%E4%BB%BB%E4%BD%95%E9%9A%90%E7%A7%81%E5%B8%81/</id>
    <published>2025-11-17T14:36:29.000Z</published>
    <updated>2025-11-17T14:37:26.258Z</updated>
    
    <content type="html"><![CDATA[<ol><li>除了试图犯罪以外，真正需要协议级隐私的场景很少</li><li>协议级隐私带来的是技术上的极度复杂、使用体验差</li><li>假如 ZEC 使用 100% 的 z 地址，CEX 首先会下架 ZEC</li><li>任何代币只要全过程匿名，将要面对的不只是CEX下架，还有各国政府的封禁</li><li>大型机构绝不会也不可以投资隐私币</li><li>隐私币绝不会成为国家储备</li><li>任何代币，只要警察对你进行物理监禁，技术型隐私就毫无意义</li><li>你需要生活在真实的世界中，而不是网络中</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;ol&gt;
&lt;li&gt;除了试图犯罪以外，真正需要协议级隐私的场景很少&lt;/li&gt;
&lt;li&gt;协议级隐私带来的是技术上的极度复杂、使用体验差&lt;/li&gt;
&lt;li&gt;假如 ZEC 使用 100% 的 z 地址，CEX 首先会下架</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>区块链技术世界的三大真理</title>
    <link href="https://smallyu.net/2025/11/03/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%96%E7%95%8C%E7%9A%84%E4%B8%89%E5%A4%A7%E7%9C%9F%E7%90%86/"/>
    <id>https://smallyu.net/2025/11/03/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E4%B8%96%E7%95%8C%E7%9A%84%E4%B8%89%E5%A4%A7%E7%9C%9F%E7%90%86/</id>
    <published>2025-11-03T07:37:35.000Z</published>
    <updated>2025-11-09T09:22:13.316Z</updated>
    
    <content type="html"><![CDATA[<p>有这样两个事实：</p><ol><li>王垠是编程语言理论、计算机科学理论方面的专家</li><li>在区块链技术理论方面，目前看不到这种级别的专家，尤其是愿意公开发声、开课、讲授知识的</li></ol><p>这样的事实背后是有原因的：</p><ol><li>计算机科学经过了几十年发展，区块链大概十几年</li><li>区块链本身、从诞生之初就是工程化集成的产物，而不是理论创新</li></ol><p>这会带来不同的现象：</p><ol><li>掌握计算机科学的基础理论，lambda 演算、图灵机、计算模型，理解计算的本质后，在编程语言方面可以长久复用、不会过时，无论上层语言、框架如何变化，计算的核心不会变化</li><li>区块链工程似乎没有基础理论，没有什么技术是不会变的，也没有什么技术是需要长期积累的。从业者年龄小、新人多、工作内容以系统集成、调 SDK 为主</li></ol><p>所以区块链的技术世界中，有没有什么理论性质的 “真理”，是长久不变、可以复用、无论上层框架如何变化都不需要担心的？</p><p>区块链技术世界的三大真理：</p><ol><li>共识。如何解决拜占庭将军问题。</li><li>加密。以数学为根基的不可篡改、验证。</li><li>激励。社会学博弈引擎，让共识长久运转。</li></ol><p>掌握了这三个部分的技术，无论区块链形式上怎么推陈出新，无论行业热点如何变化，都不用担心，因为区块链本质上就是在解决这些问题。</p><p>怎么样才算是掌握了 “真理”？我看懂了、我理解了，算是我会了吗？算是我掌握了吗？</p><p>掌握真理的标准是，可以根据真理，从头构建出知识。</p><p>在计算机科学的世界里，假如世界毁灭了，给你一张纸和笔，你可以从头实现 lambda 演算、实现数据结构、实现一个解释器、实现一种编程语言，甚至构造出更多东西，不依赖于教材、框架、API，这叫掌握真理。</p><p>真理的意义在于，让你明白知识为何必须如此存在。——这也是王垠的课程在试图教会你的东西，王垠不教知识，只教 “王垠式真理”。所以我一直认为王垠的课程好、价值高。</p><p>类似的，在区块链的世界里，如果你可以从脚本写起，实现共识、加密、激励，不一定重建全部细节，但一定要理解现有系统为何那样设计，就差不多了。</p><p>要注意，智能合约的编程语言不在真理的范畴之内，无论是比特币脚本、Solidity、Move、Cairo，都只是表达交易逻辑的 DSL，都是在用不同形式，定义区块链执行交易的规则，很重要但是还没到 “真理层”。</p><p>非要说智能合约的真理层，可能可以表达为一个确定性的状态转移函数，无论语言如何变化，这个 “真理” 都始终存在：</p><pre><code>State_t+1 = f(State_t, Transaction)</code></pre><p>下一个状态来自于上一个状态加上一些交易引起的状态变化，简单吧。但我们这篇文章重点关注区块链世界中的 “王垠式真理”，所以依然是三大真理：共识、加密、激励。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;有这样两个事实：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;王垠是编程语言理论、计算机科学理论方面的专家&lt;/li&gt;
&lt;li&gt;在区块链技术理论方面，目前看不到这种级别的专家，尤其是愿意公开发声、开课、讲授知识的&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样的事实背后是有原因的：&lt;/p&gt;
&lt;ol&gt;
&lt;li</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>以太坊 AA 钱包的致命问题是什么</title>
    <link href="https://smallyu.net/2025/10/24/%E4%BB%A5%E5%A4%AA%E5%9D%8AAA%E9%92%B1%E5%8C%85%E7%9A%84%E8%87%B4%E5%91%BD%E9%97%AE%E9%A2%98%E6%98%AF%E4%BB%80%E4%B9%88/"/>
    <id>https://smallyu.net/2025/10/24/%E4%BB%A5%E5%A4%AA%E5%9D%8AAA%E9%92%B1%E5%8C%85%E7%9A%84%E8%87%B4%E5%91%BD%E9%97%AE%E9%A2%98%E6%98%AF%E4%BB%80%E4%B9%88/</id>
    <published>2025-10-23T16:55:34.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<p>到目前为止，最新的 AA（Account Abstraction）钱包规范仍然是基于 EIP-4337 实现的。</p><p>对于 AA 钱包存在的问题，像操作繁琐、Bundler 中心化、可用性低、生态支持不完善、合约安全风险高等表面上的问题就不多说了。</p><p>一句话描述 AA 钱包在干什么事情：AA 钱包能实现 “对账户资金的授权” 与 “把交易广播到链上” 这两个行为的分离。</p><h3 id="AA-钱包的功能"><a href="#AA-钱包的功能" class="headerlink" title="AA 钱包的功能"></a>AA 钱包的功能</h3><p>AA 钱包的功能，体现到具体的交易行为上，就是如果没有 AA 钱包，你得自己发交易。有了 AA 钱包，你可以只签名，不发交易，让其他人代替你把交易发到链上就行。</p><p>为什么 AA 钱包能做到这一点？因为 AA 钱包本质上就是一个合约，所以你会发现，AA 钱包的大多数 “优点”，其实是智能合约本身就具备的功能，像什么社交恢复、批量操作等。唯一能带来特殊体验的，只有 “代付手续费” 这个特性。</p><p>那为什么 AA 钱包能实现代付手续费这个功能？因为 AA 钱包的所有操作实际上不是交易，而是 UserOperation，有一个链下的 Bunlder 程序会把这些用户操作，通过发交易批量提交到链上。</p><h3 id="为什么以太坊需要-AA-钱包"><a href="#为什么以太坊需要-AA-钱包" class="headerlink" title="为什么以太坊需要 AA 钱包"></a>为什么以太坊需要 AA 钱包</h3><p>为什么以太坊需要 AA 钱包？因为以太坊的共识层协议要求，交易必须由一个 EOA 地址来发起。这个 EOA 地址，就是交易结构中的 from 字段，以太坊节点会从这个 from 地址计算手续费、扣手续费、验证交易有效性等。</p><p>合约没有私钥，交易不可能由合约发起。在这样的规则约束下，就导致以太坊所有的链上行为，都必须由某一个 EOA 地址来发起交易。</p><p>你可能觉得不对，部署一个合约，然后让合约来验证 data 里得签名数据就好了。data 里的签名数据，未必需要和发起交易的地址一致。</p><p>没错，事实上，AA 钱包的发展链路是：meta-transcations -&gt; EIP-2771 -&gt; EIP-4337。</p><p>这些方案在解决的问题本质上都是：如何让使用资金的权限，与发起链上交易的行为分离。</p><p>而引起这一系列复杂协议的根源，来自于以太坊 “交易必须由一个 EOA 地址来发起” 的规则。</p><h3 id="为什么以太坊有这个规则"><a href="#为什么以太坊有这个规则" class="headerlink" title="为什么以太坊有这个规则"></a>为什么以太坊有这个规则</h3><p>为什么以太坊要有 “交易必须由一个 EOA 地址来发起” 这个规则？</p><p>因为以太坊的账户模型，是账户-余额模型。协议必须要知道，一笔交易的手续费从哪里扣。</p><h3 id="比特币不存在这个问题"><a href="#比特币不存在这个问题" class="headerlink" title="比特币不存在这个问题"></a>比特币不存在这个问题</h3><p>比特币的 PSBT 交易格式，可以实现原生的多签。功能是多个钱包只负责签名，最终由另外一个钱包把交易广播出去就可以。</p><p>多签交易，就是典型的把对资金的授权，与广播交易行为分离开的场景。</p><p>为什么比特币不存在以太坊的这个问题？因为比特币使用的是 UTXO 模型，交易根本没有 from 地址，有的是多个输入脚本，节点只需要校验交易是否符合脚本的解锁规则，而不需要考虑手续费从哪里扣的问题。</p><h3 id="AA-钱包的定位"><a href="#AA-钱包的定位" class="headerlink" title="AA 钱包的定位"></a>AA 钱包的定位</h3><p>我们梳理一下这个链条：以太坊使用账户-余额模型 -&gt; 交易必须由 EOA 地址发起 -&gt; 需要 AA 钱包。</p><p>AA 钱包在干的事情，实际上是在给以太坊的账户模型打补丁，为了修补账户-余额模型相比 UTXO 模型的不足，才有了 AA 钱包这个东西。AA 钱包是在不涉及以太坊协议变更的前提下，诞生出的一种 workaround 方案。</p><p>从地位上来说，AA 钱包对于以太坊的地位，类似于铭文&#x2F;符文&#x2F;RGB 对于比特币的地位。在比特币生态里，因为没有图灵完备的脚本，所以在不触及比特币协议变更的前提下，搞出了铭文&#x2F;符文&#x2F;RGB 这些 workaround方案。</p><p>AA 钱包需要链下的 bundler 来提交交易，与符文需要链下的索引器来维护符文的数据状态，是不是一个意思，都严重依赖于链下的程序？</p><p>而事实上我们都知道，比特币生态的玩法，至今都还没有被主流社会认可。</p><p>假如 AA 钱包未来有一天能被社会大众认可，那也就意味着 workaround 方案在区块链世界中是可行的。对于整个生态的叙事都将引起巨大的改变。</p><h3 id="AA-钱包的未来"><a href="#AA-钱包的未来" class="headerlink" title="AA 钱包的未来"></a>AA 钱包的未来</h3><p>综上所述，我们能得出的结论是，以太坊永远不可能支持 “原生” 的 AA 的钱包（在协议层面支持）。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这些结论，对于技术人员的指导意义在于：</p><ol><li>对 AA 钱包祛魅，不要以为 AA 钱包是高级、先进的技术。</li><li>可以学习、使用、研究 AA 钱包，但是千万不要真的 “相信” AA 钱包的技术理念。</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;到目前为止，最新的 AA（Account Abstraction）钱包规范仍然是基于 EIP-4337 实现的。&lt;/p&gt;
&lt;p&gt;对于 AA 钱包存在的问题，像操作繁琐、Bundler</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>基于 AI 语义执行的 MCP 区块链的设计</title>
    <link href="https://smallyu.net/2025/10/13/%E5%9F%BA%E4%BA%8EAI%E8%AF%AD%E4%B9%89%E6%89%A7%E8%A1%8C%E7%9A%84MCP%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E8%AE%BE%E8%AE%A1/"/>
    <id>https://smallyu.net/2025/10/13/%E5%9F%BA%E4%BA%8EAI%E8%AF%AD%E4%B9%89%E6%89%A7%E8%A1%8C%E7%9A%84MCP%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E8%AE%BE%E8%AE%A1/</id>
    <published>2025-10-13T09:22:58.000Z</published>
    <updated>2025-10-13T09:32:26.965Z</updated>
    
    <content type="html"><![CDATA[<h3 id="MCP-区块链"><a href="#MCP-区块链" class="headerlink" title="MCP 区块链"></a>MCP 区块链</h3><p>MCP（Modal Content Protocol）是 AI Agent 在使用的一种协议规范，用于 AI 和外部的工具进行交互，MCP Server 则是具体执行外部交互的组件。</p><p>MCP 区块链的含义是，首先它是一条区块链，然后每个节点都原生支持 MCP 协议的 RPC 接口，可以直接接受来自 AI Agent 的请求。每一个 MCP 请求，都是一笔交易（智能合约调用），这也就意味着，区块链会记录下所有的 MCP 交互历史。</p><p>节点内部的 MCP 执行引擎，功能分两部分，对于内部交互（EVM 在干的事情），只需要维护好内部的状态转移，把结果写入本地的 KV 数据库就可以了。对于外部交互，则只记录下要执行的请求本身，先不做外部调用。那么实际上对于每一个请求，都会改变本地的状态，所以这些交易是可以重放的，也就可以根据哈希值来确认节点数据的完整性。</p><p>至于对外部的执行请求，可以由外部的执行节点（一种角色）来完成。每一个外部请求都包含一些详细的参数，比如需要几个执行者、结果如何验证。交易会把一部分手续费作为执行费用，奖励给执行节点。如果执行节点作恶，自然也会有相应的惩罚机制。</p><p>对于外部的调用，关键在于如何验证外部执行的结果，这个问题可以交给调用者来定义，比如要创建一个 GitHub 仓库，验证方式就是，可以通过 API 查询到这个仓库的信息。</p><p>这就是 MCP 区块链的大体思路。</p><h3 id="设计来源"><a href="#设计来源" class="headerlink" title="设计来源"></a>设计来源</h3><p>解释一下这个想法的来源。MCP 区块链并不是想要 “把 MCP Server 去中心化”，而是想要 “给区块链带来与 AI 交互的能力”。这两种动机是截然不同的。</p><p>这个想法背后的逻辑很简单，比特币其实有脚本，只不过是生硬的操作码形式。以太坊干了一件很厉害的事情，给操作码加上了编译器，让开发者可以用编程语言来表达操作码。那么如今的 AI，很厉害的一点在于，打通了从自然语言到编程语言的路径。也就是说，未来的区块链，也许可以做到自然语言直接与状态机交互，而不需要经过 自然语言 -&gt; 编程语言 -&gt; 状态机 这样的路径。明显编程语言是一个中间层，MCP 区块链的设想在于消除这个中间层。</p><p>另一个边界在于，让区块链完全按照自然语言的意图执行是不切实际的，因为哪怕是人类，也需要书面形式的合同这种东西，所以代码本身不会消失，状态转移也不会消失。目前能够实现自然语言和状态机直接交互的技术方式，就是 MCP。</p><h3 id="区块链的技术趋势"><a href="#区块链的技术趋势" class="headerlink" title="区块链的技术趋势"></a>区块链的技术趋势</h3><p>最近在思考一个问题，区块链的下一个技术趋势是什么？能明确的几个事实是：</p><ol><li>大家已经不再怀疑加密货币是一种支付手段这件事情</li><li>依靠开发区块链发币的路线，已经完全没有叙事空间</li><li>下一个技术趋势，绝不会是以太坊路线图的扩展</li></ol><p>行业内已经提过的方向也都不大有机会，DeAI、ZK、Layer2、DeFi、跨链、RWA、GameFi、BaaS、NFT、元宇宙、DAO、DID 等等，都是陈词滥调了。</p><p>所以区块链在技术趋势上，需要的一个原语级别的新叙事。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;MCP-区块链&quot;&gt;&lt;a href=&quot;#MCP-区块链&quot; class=&quot;headerlink&quot; title=&quot;MCP 区块链&quot;&gt;&lt;/a&gt;MCP 区块链&lt;/h3&gt;&lt;p&gt;MCP（Modal Content Protocol）是 AI Agent</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>一个集成 Geth 和 CometBFT 的兼容层</title>
    <link href="https://smallyu.net/2025/09/06/%E4%B8%80%E4%B8%AA%E9%9B%86%E6%88%90Geth%E5%92%8CCometBFT%E7%9A%84%E5%85%BC%E5%AE%B9%E5%B1%82/"/>
    <id>https://smallyu.net/2025/09/06/%E4%B8%80%E4%B8%AA%E9%9B%86%E6%88%90Geth%E5%92%8CCometBFT%E7%9A%84%E5%85%BC%E5%AE%B9%E5%B1%82/</id>
    <published>2025-09-05T16:30:00.000Z</published>
    <updated>2025-09-05T16:40:08.462Z</updated>
    
    <content type="html"><![CDATA[<h3 id="项目动机"><a href="#项目动机" class="headerlink" title="项目动机"></a>项目动机</h3><p>在对 <a href="https://paragraph.com/@smallyu/%E5%AF%B9-arc-%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%88%86%E6%9E%90">Arc 项目</a> 进行分析的过程中，发现 Arc 干了一件很有意思的事情，先是自己开发了 Rust 版本的 Tendermint 共识 malachite，接着开发了一个对接 Reth 和 malachite 的兼容层 malaketh-layered，也就是说，Arc 这条链的架构是这样：</p><pre><code class="text">Reth -&gt; malaketh-layered -&gt; malachite</code></pre><p>最终形成了一条完全以太坊等价的 PBFT 链。</p><p>那么有没有类似架构的链，直接把 Geth 和 CometBFT 给结合起来呢。是有的，<a href="https://paragraph.com/@smallyu/%E5%AF%B9-berachain-%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%88%86%E6%9E%90">Berachain</a> 开发了一个<a href="https://github.com/berachain/beacon-kit">beacon-kit</a>，干的就是这样的事情，Berachain 主网本身就是这种架构启动的。</p><p>但是 beacon-kit 有一个问题，就是代码过度 “复杂”，不但自己设计了 slot 的概念，还把 Berachain 的一些经济模型的设计、LST 质押之类的东西都放到了 beacon-kit 中。所以虽然 beacon-kit 在工程上是一个 Geth+CometBFT 可行的实践，但是它本身并不是工具性质的立场在做，夹带了不少私货。</p><p>因此我觉得需要一个通用的、工具性质的兼容层项目，目前命名为 EthBFT。这个项目的愿景是，提供简洁、开放、最小实现、工具性质的架构，达到集成 Geth 和 CometBFT 的目的。整个区块链网络的架构会是这样：</p><pre><code class="text">Geth -&gt; EthBFT -&gt; CometBFT</code></pre><p>EthBFT 主要干两件事情：</p><ol><li>通过以太坊执行层的 Engine API 拿区块数据</li><li>把区块数据通过 ABCI 接口提交到 CometBFT</li></ol><p>这里虽然用 Geth 举例，但对于其他以太坊的执行层客户端，应该也是通用的，因为以太坊的执行层和共识层客户端，本来就是互相兼容的，仅仅通过 RPC 接口通信。所以预计 EthBFT 可以兼容全部的以太坊执行层客户端。</p><p>而 EthBFT 的设计，自然不会和 Geth 或者 CometBFT 有代码层面的耦合，EthBFT 是一个独立的进程，可以单独启动，Geth 也可以单独启动，CometBFT 也可以单独启动，3 个组件之间，彼此通过 RPC 接口通信，具体的 RPC 接口地址等信息则会体现在 EthBFT 的配置文件中。</p><p>这就让 3 个组件互相之间，完全解耦了。</p><h3 id="对-PBFT-的信心"><a href="#对-PBFT-的信心" class="headerlink" title="对 PBFT 的信心"></a>对 PBFT 的信心</h3><p>我之前以为区块链技术的发展会趋于追新，也会趋于去中心化，但是发现似乎不是那样。</p><p>从前两年的 Celestia 使用了 PBFT，到 Hyperliquid 改进了 PBFT 共识，再到最近 Arc 项目自己实现了 PBFT 共识，证明在高TPS的场景下，PBFT算法还非常有活力。</p><p>PoW 和 PoS 去中心化程度高，但是不能满足高 TPS 的需求，也不能达到最终一致性的要求，这些都是 PBFT 特有的优势，尤其是企业级的应用场景下，没那么在意去中心化。</p><p>我们也许会有疑问，如果不在乎去中心化，那直接用 Server 端提供服务不就行了吗，用区块链干什么。在丢失去中心化特性的前提下，至少区块链还保留有数据公开、数据变更可追溯等特点，也是一些不错的优势。</p><p>因此，PBFT 这种诞生接近 30 年的算法，将来还会继续发光发热。也因此，去搞一个 PBFT 相关的项目，不会有太大问题。</p><h3 id="项目前景"><a href="#项目前景" class="headerlink" title="项目前景"></a>项目前景</h3><p>EthBFT 肯定不会受到市场的关注，因为大家只在乎一条链能不能发币，能不能套利，并不在乎你的技术架构是什么。</p><p>EthBFT 只是一个工具性质的项目。如果一个开发者，想要一条以太坊完备的链，同时又想要高 TPS，在没有 EthBFT 的情况下，需要怎么做呢。我懒得展开分析对比搭建链的方案了，总之我觉得 EthBFT 可以填补这部分的空缺，非侵入式那种。世界上缺一个这样的工具。</p><h3 id="对-AI-技术的怀疑"><a href="#对-AI-技术的怀疑" class="headerlink" title="对 AI 技术的怀疑"></a>对 AI 技术的怀疑</h3><p>现在 <a href="https://github.com/smallyunet/ethbft">smallyunet&#x2F;EthBFT</a> 项目已经有了基本的框架，能跑通最小版本，我把它归档为 <a href="https://github.com/smallyunet/ethbft/tree/v0.0.1">v0.0.1 版本</a>。能跑通的表现是 Geth 的区块高度会逐渐增加，CometBFT 也在正常出块，Geth 和 CometBFT 的区块高度保持同步。当然现在还属于非常早期的版本，开发时间有限，功能上肯定有不完善的地方，接下来还会继续改进。</p><p>我之前说 <a href="/2025/04/12/%E9%BC%93%E5%90%B9Cursor%E7%9A%84%E4%BA%BA%E6%8A%80%E6%9C%AF%E8%83%BD%E5%8A%9B%E9%83%BD%E5%B7%AE/">鼓吹 Cursor 的人技术能力都差</a>，因为 AI 可以放大你的能力，但是不可能代替你懂。v0.0.1 版本的 EthBFT，全部代码都是 AI 写的，没错，但是以 EthBFT 这个项目为例，现在要干的事情非常清晰，你可以试试，在不懂以太坊和 Cosmos，甚至不懂技术的情况下，完全托管给 AI，能不能搞出一个能运行的、EthBFT 这样的项目。</p><p>如果你自己对技术的理解不清晰，或者有错误，关键是 AI 不会纠正你的错误，因为 AI 并不知道你心里想要的 “正确” 是什么。AI 会非常听话地按照你的描述写代码，如果你语焉不详，AI 写出来的代码必然会跑偏，朝着错误的方向发展，而且很多时候 AI 会自己偷偷埋坑，你以为它实现了，结果它要么没写全，放了个 TODO 在那儿，要么按照自己的理解写出一大堆不需要的代码。</p><p>所以让 AI 把代码写对，其实不是一件容易的事情，首先你自己得懂，然后你得时刻盯着它干活。AI 始终只是助手而已。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;项目动机&quot;&gt;&lt;a href=&quot;#项目动机&quot; class=&quot;headerlink&quot; title=&quot;项目动机&quot;&gt;&lt;/a&gt;项目动机&lt;/h3&gt;&lt;p&gt;在对 &lt;a</summary>
        
      
    
    
    
    
    <category term="计划" scheme="https://smallyu.net/tags/%E8%AE%A1%E5%88%92/"/>
    
  </entry>
  
  <entry>
    <title>我的加密货币定投策略（二）</title>
    <link href="https://smallyu.net/2025/08/28/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%AE%9A%E6%8A%95%E7%AD%96%E7%95%A5%EF%BC%88%E4%BA%8C%EF%BC%89/"/>
    <id>https://smallyu.net/2025/08/28/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%AE%9A%E6%8A%95%E7%AD%96%E7%95%A5%EF%BC%88%E4%BA%8C%EF%BC%89/</id>
    <published>2025-08-28T10:46:21.000Z</published>
    <updated>2025-11-17T14:36:55.436Z</updated>
    
    <content type="html"><![CDATA[<p>前情提要：《<a href="/2024/08/28/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%AE%9A%E6%8A%95%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%80%EF%BC%89/">我的加密货币定投策略（一）</a>》</p><h3 id="新的定投策略"><a href="#新的定投策略" class="headerlink" title="新的定投策略"></a>新的定投策略</h3><p>现在把新的定投策略调整为这样：</p><ol><li>定投频率：每周一次</li><li>定投金额：100 美元</li><li>定投标旳：比特币（BTC）</li></ol><p>就这么简单，不再整那些花里胡哨的东西，只投比特币。至于以太坊或者其他平台币，想用的时候买就行了，手续费用不了多少，价格也高不了多少，没必要因为 “可能要用” 而提前布局。</p><p>你也许会犹豫，比特币现在的价格这么高了，还可以买吗？定投策略本身就是在消除对入场时机的顾虑，毕竟这是定投，不会一下子把钱砸进去，所以不用那么在意此时此刻的价格。关于定投策略的优点，李笑来的《定投改变命运》里已经有非常详细的解释，这里不多复述。</p><p>你也许会纠结，以太坊最近涨了那么多，接近历史新高，ETH&#x2F;BTC 汇率持续走高，BTC 市占率持续下跌，为什么只选择定投 BTC 这一个币种？我的建议是，不要 FOMO。在接下来半年到一年的时间里，像以太坊一样动人心弦的例子还会有很多，不是可能有，是一定会有，也许每一次都会让你后悔，当初怎么没选这个币？要是早点买早点定投就好了。所以还是那句老话，不要 FOMO。</p><h3 id="之前的错误"><a href="#之前的错误" class="headerlink" title="之前的错误"></a>之前的错误</h3><p>我之前犯了很严重的错误，也许是出于想体现出自己 “知道的多” 的心理，在定投组合中，选择了非常多小币种。现在差不多一年过去了，根据计算，实际上的收益效果比较差，大概在 22% 左右，这个收益率还不如全买 BTC 的涨幅。</p><p>当时有几个月的时间，我确实是按照定投组合在操作，但是后来由于这些原因，我自己都没能坚持下来定投这件事情：</p><ol><li><p>某些币种比例太小，导致金额也小，假如一天定投 10 美元，某个代币占比 5%，一天就是 0.5 美元，一个月下来一共 15 美元。这样的金额即使涨 50% 或者跌 50% 都不会有感觉。而且这种小金额的代币一直放在账户里，也会让人不舒服。所以主观感受上，天天看到这些币种很难受，会想清理掉他们。</p></li><li><p>由于我时常关注加密行情的变化，有时明明知道价格下跌了，但是由于定投计划的存在，我无法手动加仓；有时明明知道价格上涨了，但是由于定投计划的存在，无法暂停定投。这样的感觉也很难受，像是被定投计划操控一样。</p></li><li><p>对于山寨币的风险预估不足，后来逐渐认识到山寨币的价值有限，而定投计划一开始给了山寨币比较高的比例，造成我头脑中的计划调整频率很快，快到定投策略跟不上。另外由于我的投资预算的不稳定，也就是收入和支出的不稳定，经常需要动一些钱，导致定投计划不得不暂停、重新开始。</p></li></ol><p>所以不管以前怎么回事，以前的定投策略始终是有问题的，连我自己都无法好好实施。</p><h3 id="黄金"><a href="#黄金" class="headerlink" title="黄金"></a>黄金</h3><p>注意这里只是 “加密货币” 的定投策略，不包含其他资产类型。比如价格锚定黄金的 PAXG 代币，不属于 “加密货币定投策略”，但是我会买、会定投，策略也是一样的：</p><ol><li>定投频率：每周一次</li><li>定投金额：100 美元</li><li>定投标旳：黄金（PAXG）</li></ol><h3 id="美股"><a href="#美股" class="headerlink" title="美股"></a>美股</h3><p>要这么说，把 BTC 和 PAXG 两个代币列出来不就行了吗？为什么还要强调黄金是不同于加密货币的资产类型？</p><p>因为 Kraken （海妖交易所）上支持买美股了。代币是发行在 Solana 链上的。我不懂美股，现在不能制定美股的定投计划。假如认为价格锚定黄金的代币算加密货币的话，那价格锚定美股的代币，算不算加密货币呢？事情就变复杂了。</p><p>美股上链这事还挺有意思，给未来带来了很多可能性。时代在发展，社会在进步，越来越有意思了。</p><h3 id="关于卖出策略的疑问"><a href="#关于卖出策略的疑问" class="headerlink" title="关于卖出策略的疑问"></a>关于卖出策略的疑问</h3><p>人们常说，你无法同时拥有青春和对青春的体验。同样的道理，你无法同时享受拥有比特币，和比特币价格上涨带来的财富。</p><p>假如你现在还年轻，你需要钱，你要买车，你要旅游，你要体验生活。然后你把比特币卖了。等到老年之后，你会不会后悔？也许到时候会想，假如当时没卖掉，现在价值可就翻了很多倍。</p><p>要么放弃当下的享受，要么换来日后的后悔。怎么选呢？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;前情提要：《&lt;a</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>DeFi 基础: 理解 AMM 定价机制</title>
    <link href="https://smallyu.net/2025/08/20/DeFi%E5%9F%BA%E7%A1%801/"/>
    <id>https://smallyu.net/2025/08/20/DeFi%E5%9F%BA%E7%A1%801/</id>
    <published>2025-08-20T14:00:00.000Z</published>
    <updated>2026-02-28T07:26:05.930Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这是一个 DeFi 系列教程，在动手实践的过程中，学习和理解 DeFi 相关的概念与原理：</p><ol><li><a href="/2025/08/20/DeFi%E5%9F%BA%E7%A1%801/">DeFi 基础: 理解 AMM 定价机制</a></li><li><a href="/2025/08/21/DeFi%E5%9F%BA%E7%A1%802/">DeFi 基础: 预言机与报价</a></li><li><a href="/2025/08/21/DeFi%E5%9F%BA%E7%A1%803/">DeFi 基础: 借贷与清算</a></li><li><a href="/2025/08/21/DeFi%E8%BF%9B%E9%98%B61/">DeFi 进阶: 闪电贷与套利</a></li></ol></blockquote><p>AMM 的全称是 Automated Market Maker，自动做市商，作用是不需要订单簿撮合交易，就可以自动完成定价与交易。</p><p>这篇文章解释了 Uniswap V2 的核心定价逻辑，并且提供了完整的合约代码示例、命令行操作步骤、实际的链上交易现场等，作为理解 AMM 的配套参考。</p><h3 id="AMM-计算公式"><a href="#AMM-计算公式" class="headerlink" title="AMM 计算公式"></a>AMM 计算公式</h3><h4 id="基本逻辑"><a href="#基本逻辑" class="headerlink" title="基本逻辑"></a>基本逻辑</h4><p>Uniswap V2 用的定价逻辑是恒定乘积做市商（Constant Product Market Maker, CPMM），也是我们的示例 AMM 合约在用的方法。这里有一个恒等公式：</p><pre><code>x * y = k</code></pre><p>意味着池子里有两种资产 <code>x</code> 和 <code>y</code>，当 x 增多的时候，y 就应该减少，y 增多的时候，x 应该减少，k 总是保持不变。</p><p>在添加初始流动性的时候，我们第一次确定下来这个 k 的值，比如我们按照 2000 USDC &#x2F; 1 WETH 的价格注入初始流动性，会得到（不考虑精度）：</p><pre><code>k = 2000</code></pre><p>当我们想要用 USDC 换出 WETH 的时候，池子里的 USDC 增多，为了保持 k 不变，合约会计算应该保留多少 WETH，然后把相应数量的 WETH 转给我们。</p><h4 id="第一次兑换"><a href="#第一次兑换" class="headerlink" title="第一次兑换"></a>第一次兑换</h4><p>当我们想要用 USDC 换出 WETH 的时候，池子里的 USDC 增多，为了保持 k 不变，合约就会把相应数量的 WETH 转给我们了。</p><p>例如，我们试图用 500 USDC 换出 WETH，此时加上初始流动性的 2000 USDC，池子里一共 2500 USDC，那么：</p><pre><code>x = 2500y = k/x = 2000/2500 = 0.8</code></pre><p>这个 0.8 意味着，为了保证 AMM 池子里的 k 值恒定为 2000，池子需要转出 0.2 WETH。也就是说，我们会得到 0.2 个WETH。</p><h4 id="第二次兑换"><a href="#第二次兑换" class="headerlink" title="第二次兑换"></a>第二次兑换</h4><p>我们再来用 500 USDC 买一次，此时池子里一共有 2500+500&#x3D;3000 USDC，则：</p><pre><code>x = 3000y = k/x = 2000/3000 = 0.667</code></pre><p>这个恒定乘积公式计算得出池子里应该保留 0.667 个 WETH，上一轮交换后还剩 0.8 WETH，所以这一轮我们实际得到 0.8-0.667 &#x3D; 0.133 WETH。</p><p>对比来看，第一次用 500 USDC 可以换出 0.2 WETH，第二次用 500 USDC 就只能换出 0.133 WETH 了。随着池子里流动性的减少，WETH 的价格涨了。</p><h4 id="价格曲线"><a href="#价格曲线" class="headerlink" title="价格曲线"></a>价格曲线</h4><p>这就是自动做市商的核心逻辑，价格不是写死的，而是根据池子中剩余的流动性算出来的。要注意 x 和 y 的乘积是一条曲线，因为 y&#x3D;k&#x2F;x，画成图是这样：</p><img src="1.png" width="50%"><p>接下来会用实际的操作步骤与链上交互，来体验 AMM 的运作。</p><h3 id="示例合约"><a href="#示例合约" class="headerlink" title="示例合约"></a>示例合约</h3><p>合约代码源文件在仓库：<a href="https://github.com/smallyunet/defi-invariant-lab/tree/v0.0.1">smallyunet&#x2F;defi-invariant-lab@v0.0.1</a></p><p>首先准备两个合约，一个是 <a href="https://github.com/smallyunet/defi-invariant-lab/blob/v0.0.1/contracts/libs/TestERC20.sol">TestERC20.sol</a>，比起标准的 ERC-20 合约，支持自定义代币精度，以及随意 mint 一些代币。</p><p>第二个要准备的合约是 <a href="https://github.com/smallyunet/defi-invariant-lab/blob/v0.0.1/contracts/amm/SimpleAMM.sol">SimpleAMM.sol</a>，提供了对代币增加流动性、代币兑换等功能。合约代码不算很简单，我们会在接下来实际的操作用，逐步体会和理解这个合约的功能，以及解读源代码。</p><p>以下所有操作都在以太坊的测试网 Sepolia 上进行。</p><h3 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h3><p>准备好命令行工具，以及设置两个环境变量：</p><pre><code class="bash">foundryupexport RPC_URL=&quot;https://ethereum-sepolia-rpc.publicnode.com&quot;export PK_HEX=&quot;&lt;YOUR_PRIVATE_KEY_HEX&gt;&quot;</code></pre><p>下载合约仓库、进入到仓库根目录：</p><pre><code>git clone https://github.com/smallyunet/defi-invariant-lab/git switch v0.0.1cd defi-invariant-lab</code></pre><h3 id="部署代币合约"><a href="#部署代币合约" class="headerlink" title="部署代币合约"></a>部署代币合约</h3><h4 id="部署合约"><a href="#部署合约" class="headerlink" title="部署合约"></a>部署合约</h4><p>部署两个测试版本的 ERC-20 代币，一个叫 USDC，一个叫 WETH：</p><pre><code class="bash">forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args &quot;USD Coin&quot; &quot;USDC6&quot; 6</code></pre><p>部署的合约地址是：<a href="https://sepolia.etherscan.io/address/0x84637EaB3d14d481E7242D124e5567B72213D7F2"><code>0x84637EaB3d14d481E7242D124e5567B72213D7F2</code></a>。</p><pre><code class="bash">forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args &quot;Wrapped Ether&quot; &quot;WETH18&quot; 18</code></pre><p>部署的合约地址是：<a href="https://sepolia.etherscan.io/address/0xD1d071cBfce9532C1D3c372f3962001A8aa332b7"><code>0xD1d071cBfce9532C1D3c372f3962001A8aa332b7</code></a>。</p><h4 id="验证合约"><a href="#验证合约" class="headerlink" title="验证合约"></a>验证合约</h4><p>如果愿意，可以这样验证下合约：</p><pre><code class="bash">export ETHERSCAN_API_KEY=你的keycast abi-encode &quot;constructor(string,string,uint8)&quot; &quot;USD Coin&quot; &quot;USDC6&quot; 6forge verify-contract \  --chain-id 11155111 \  0x84637EaB3d14d481E7242D124e5567B72213D7F2 \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args &quot;0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000855534420436f696e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055553444336000000000000000000000000000000000000000000000000000000&quot; \  --etherscan-api-key $ETHERSCAN_API_KEYforge verify-contract \  --chain-id 11155111 \  0xD1d071cBfce9532C1D3c372f3962001A8aa332b7 \  contracts/libs/TestERC20.sol:TestERC20 \  --constructor-args $(cast abi-encode &quot;constructor(string,string,uint8)&quot; &quot;Wrapped Ether&quot; &quot;WETH18&quot; 18) \  --etherscan-api-key $ETHERSCAN_API_KEY</code></pre><h3 id="部署-AMM-合约"><a href="#部署-AMM-合约" class="headerlink" title="部署 AMM 合约"></a>部署 AMM 合约</h3><h4 id="部署合约-1"><a href="#部署合约-1" class="headerlink" title="部署合约"></a>部署合约</h4><p>这里的参数 30 指收取 0.3% 的手续费：</p><pre><code class="bash">forge create \  --rpc-url $RPC_URL \  --private-key $PK_HEX \  --broadcast \  contracts/amm/SimpleAMM.sol:SimpleAMM \  --constructor-args $USDC_ADDR $WETH_ADDR 30</code></pre><p>部署的合约地址是：<a href="https://sepolia.etherscan.io/address/0x339278aA7A09657A4674093Ab6A1A3df346EcFCF"><code>0x339278aA7A09657A4674093Ab6A1A3df346EcFCF</code></a>&#96;</p><h4 id="验证合约-1"><a href="#验证合约-1" class="headerlink" title="验证合约"></a>验证合约</h4><pre><code class="bash">forge verify-contract \  --chain-id 11155111 \  0x339278aA7A09657A4674093Ab6A1A3df346EcFCF \  contracts/amm/SimpleAMM.sol:SimpleAMM \  --constructor-args $(cast abi-encode &quot;constructor(address,address,uint16)&quot; $USDC_ADDR $WETH_ADDR 30) \  --etherscan-api-key $ETHERSCAN_API_KEY</code></pre><h3 id="mint-代币"><a href="#mint-代币" class="headerlink" title="mint 代币"></a>mint 代币</h3><p>声明钱包地址与合约地址：</p><pre><code class="bash">export MY_ADDR=0x44D7A0F44e6340E666ddaE70dF6eEa9b5b17a657export AMM_ADDR=0x339278aA7A09657A4674093Ab6A1A3df346EcFCFexport USDC_ADDR=0x84637EaB3d14d481E7242D124e5567B72213D7F2export WETH_ADDR=0xD1d071cBfce9532C1D3c372f3962001A8aa332b7</code></pre><p>挖 100 万个 USDC，精度是 6 位数：</p><pre><code class="bash">cast send $USDC_ADDR &quot;mint(address,uint256)&quot; $MY_ADDR 1000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p>挖 1000 个 WETH，精度是 18 位数：</p><pre><code class="bash">cast send $WETH_ADDR &quot;mint(address,uint256)&quot; $MY_ADDR 1000000000000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p>铸币的交易与结果可以直接在浏览器上看到，<a href="https://sepolia.etherscan.io/tx/0x7140377c3c495be8a593a97c63cfa768783923861e539d330e49f9a93a2cfacd">这个</a> 是挖 USDC 的交易，<a href="https://sepolia.etherscan.io/tx/0xeb659015a41982a3daaca481869ef4e6afc6c5b0b7a34fa656efc7bc2f9be6ad">这个</a> 是挖 WETH 的交易。</p><h3 id="给-AMM-合约授权"><a href="#给-AMM-合约授权" class="headerlink" title="给 AMM 合约授权"></a>给 AMM 合约授权</h3><p>给 AMM 授权是因为接下来想要给 AMM 添加流动性，添加流动性会调用 <a href="https://github.com/smallyunet/defi-invariant-lab/blob/v0.0.1/contracts/amm/SimpleAMM.sol#L29"><code>addLiquidity</code></a> 函数，其中用到了 <code>transferFrom</code>，所以需要先给合约授权，让合约可以动用我的 USDC 和 WETH 代币：</p><pre><code class="bash">cast send $USDC_ADDR &quot;approve(address,uint256)&quot; $AMM_ADDR &quot;0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&quot; \  --rpc-url $RPC_URL --private-key $PK_HEXcast send $WETH_ADDR &quot;approve(address,uint256)&quot; $AMM_ADDR &quot;0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff&quot; \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p>交易哈希分别是 <a href="https://sepolia.etherscan.io/tx/0x5d71669f6a5a63f439a53e6af801d21fdf23ae67cf43d17e30d34b5f66974354">USDC</a> 和 <a href="https://sepolia.etherscan.io/tx/0x1650337af02c74280b9ae1b83222a35f77e090500e3099f419cb0dc0ec7062ab">WETH</a>。</p><h3 id="添加初始流动性"><a href="#添加初始流动性" class="headerlink" title="添加初始流动性"></a>添加初始流动性</h3><p>添加流动性的 <a href="https://github.com/smallyunet/defi-invariant-lab/blob/v0.0.1/contracts/amm/SimpleAMM.sol#L29">函数</a> 比较简单，大概是合约里有两个变量 <code>reserve0</code> 和 <code>reserve1</code>，调用 <code>addLiquidity</code> 函数的时候，会向 AMM 合约转账参数数量个代币。</p><p>先以 2000 USDC &#x2F; 1 WETH 的价格，添加初始流动性：</p><pre><code class="bash">cast send $AMM_ADDR &quot;addLiquidity(uint256,uint256)&quot; 200000000000 100000000000000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p><a href="https://sepolia.etherscan.io/tx/0x060bfff6111946c7c84fe6415f883c92ffe7ff9bd694e5b87598a647b63c089f">交易</a> 完成后，可以查询到 AMM 合约剩余的代币数量：</p><pre><code class="bash">cast call $AMM_ADDR &quot;getReserves()(uint112,uint112)&quot; --rpc-url $RPC_URL# 200000000000 [2e11]# 100000000000000000000 [1e20]</code></pre><h3 id="用-USDC-换-WETH"><a href="#用-USDC-换-WETH" class="headerlink" title="用 USDC 换 WETH"></a>用 USDC 换 WETH</h3><h4 id="合约代码解读"><a href="#合约代码解读" class="headerlink" title="合约代码解读"></a>合约代码解读</h4><p>我们的合约代码 <a href="https://github.com/smallyunet/defi-invariant-lab/blob/v0.0.1/contracts/amm/SimpleAMM.sol#L39"><code>swap0For1</code></a> 是这样：</p><pre><code class="solidity">function swap0For1(uint256 amtIn) external returns (uint256 out) &#123;    require(token0.transferFrom(msg.sender, address(this), amtIn), &quot;t0in&quot;); // 把用户的 x 转进合约    uint256 r0 = token0.balanceOf(address(this)); // 查询当前 x    uint256 r1 = token1.balanceOf(address(this)); // 查询当前 y    uint256 amtInEff = (amtIn * (10_000 - feeBps)) / 10_000; //计算扣除手续费后，用户转入了多少 x    // x*y=k, solve out = r1 - k/(r0)    uint256 k = (r0 - amtInEff) * r1;   // 计算 k    out = r1 - Math.ceilDiv(k, r0);     // 计算给用户多少 y    require(token1.transfer(msg.sender, out), &quot;t1out&quot;);&#125;</code></pre><p>函数代码体现了刚才描述的关于 <code>x*y=k</code> 的恒定公式。因为 AMM 合约考虑到收手续费的问题，所以有一个 <code>amtInEff</code> 用来表示用户实际转入了多少 x。</p><h4 id="测试第-1-次交换"><a href="#测试第-1-次交换" class="headerlink" title="测试第 1 次交换"></a>测试第 1 次交换</h4><p>我们来实际发起交易，看看合约运行后的效果，先试着用 1000 USDC，看能换多少个 WETH 出来：</p><pre><code class="bash">cast send $AMM_ADDR &quot;swap0For1(uint256)&quot; 1000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p><a href="https://sepolia.etherscan.io/tx/0xf13bd1d1602d7c106c2acdf4cb3b1ec37fa42d8871a682e32cce3f2049fff5a2">交易</a> 完成后，查看一下代币余额：</p><pre><code class="bash">cast call $USDC_ADDR &quot;balanceOf(address)(uint256)&quot; $MY_ADDR --rpc-url $RPC_URLcast call $WETH_ADDR &quot;balanceOf(address)(uint256)&quot; $MY_ADDR --rpc-url $RPC_URLcast call $AMM_ADDR &quot;getReserves()(uint112,uint112)&quot; --rpc-url $RPC_URL</code></pre><p>其实区块链浏览器上能很直接的看到交换的数量，交易哈希是：<a href="https://sepolia.etherscan.io/tx/0xf13bd1d1602d7c106c2acdf4cb3b1ec37fa42d8871a682e32cce3f2049fff5a2"><code>0xf13bd1d1602d7c106c2acdf4cb3b1ec37fa42d8871a682e32cce3f2049fff5a2</code></a> </p><p>我们转出了 1000 USDC，收到了 <code>0.496019900497512437</code> 个 WETH。这里因为有 0.3% 的手续费，所以收到的 WETH 不是 0.5。</p><p>除了手续费，还存在一个价格的问题，按理来说，随着剩余 WETH 数量的减少，WETH 的价格会越来越高。</p><h4 id="测试第-2-次交换"><a href="#测试第-2-次交换" class="headerlink" title="测试第 2 次交换"></a>测试第 2 次交换</h4><p>再来用 1000 USDC 兑换一次，看能换出多少 WETH：</p><pre><code class="bash">cast send $AMM_ADDR &quot;swap0For1(uint256)&quot; 1000000000 \  --rpc-url $RPC_URL --private-key $PK_HEX</code></pre><p>这次兑换的交易哈希是：<a href="https://sepolia.etherscan.io/tx/0x1ee9ceb0707d77d78669bfb6cc1179bf9d6b31c57b868f5f52ed2f01a4127481"><code>0x1ee9ceb0707d77d78669bfb6cc1179bf9d6b31c57b868f5f52ed2f01a4127481</code></a></p><p>这一次，花费了 1000 USDC，收到了 <code>0.491116179005960297</code> 个 WETH。与上一次兑换的结果相比，收到的 WETH 真的减少了。</p><h3 id="用-WETH-换-USDC"><a href="#用-WETH-换-USDC" class="headerlink" title="用 WETH 换 USDC"></a>用 WETH 换 USDC</h3><p>可以自己测试玩一下。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这是一个 DeFi 系列教程，在动手实践的过程中，学习和理解 DeFi 相关的概念与原理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/2025/08/20/DeFi%E5%9F%BA%E7%A1%801/&quot;&gt;DeFi 基础: 理解 AMM</summary>
        
      
    
    
    
    
    <category term="DeFi" scheme="https://smallyu.net/tags/DeFi/"/>
    
  </entry>
  
  <entry>
    <title>Go 语言 GMP 调度器的原理是什么</title>
    <link href="https://smallyu.net/2025/08/18/Go%E8%AF%AD%E8%A8%80GMP%E8%B0%83%E5%BA%A6%E5%99%A8%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88/"/>
    <id>https://smallyu.net/2025/08/18/Go%E8%AF%AD%E8%A8%80GMP%E8%B0%83%E5%BA%A6%E5%99%A8%E7%9A%84%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88/</id>
    <published>2025-08-18T13:50:50.000Z</published>
    <updated>2025-08-22T08:54:30.257Z</updated>
    
    <content type="html"><![CDATA[<p>声明：我看不起 “Go 语言 GMP 调度器的原理是什么” 这种技术话题。</p><p>我平时没兴趣研究这种问题。因为在面试中被问到的频率太高了，现在想花 2 个小时的时间来了解下。一方面研究下这个问题背后到底有多大的技术含量，另一方面把这个问题的答案写下来。但是我不会让这种内容停留在我的头脑里，所以下次面试被问到，我肯定还说不会 😏</p><h3 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h3><p>GMP 是一个缩写：</p><ul><li>G（goruntine）：就是协程，代码里每 <code>go</code> 一个，G 的数量就多一个</li><li>M（Machine）：就是系统级别的线程，在其他语言里的 thread</li><li>P（Processor）：数量为 <code>GOMAXPROCS</code>，通常默认是 CPU 核心数。</li></ul><p>GMP 的意思是，启动多少个 M（线程） 来执行 G（协程），最多允许 P（核心数）个 M 并行执行。</p><h3 id="三个不变量"><a href="#三个不变量" class="headerlink" title="三个不变量"></a>三个不变量</h3><p>无聊的（简化后的）定义来了：</p><ol><li>只有拿到 P 的 M 才能执行任务</li><li>可运行的 G 只会在某个 P 的本地 runq 或者全局队列</li><li>当 M 进入阻塞状态（syscall&#x2F;cgo）时，会及时把 P 让出</li></ol><p>这几句话看着很费劲，不需要现在理解，接下来会用一些代码例子来说明他们的含义。</p><h3 id="GMP-的调试日志"><a href="#GMP-的调试日志" class="headerlink" title="GMP 的调试日志"></a>GMP 的调试日志</h3><p>这是一个最简单的代码文件，用来演示启动一个协程：</p><pre><code class="go">package mainimport (    &quot;fmt&quot;    &quot;sync&quot;)func main() &#123;    var wg sync.WaitGroup    wg.Add(1)    go func() &#123;        defer wg.Done()        fmt.Println(&quot;Hello from goroutine&quot;)    &#125;()    wg.Wait()&#125;</code></pre><p>然后带上调试参数运行一下：</p><pre><code class="bash">go build demo0.goGODEBUG=&#39;schedtrace=200,scheddetail=1&#39; ./demo0</code></pre><p>注意不要用 <code>go run</code>，因为会引入一些 Go 语言运行时的日志。这个二进制版本的日志比较干净，内容是：</p><pre><code class="bash">SCHED 0ms: gomaxprocs=10 idleprocs=7 threads=5 spinningthreads=1 needspinning=0 idlethreads=0 runqueue=0 gcwaiting=false nmidlelocked=1 stopwait=0 sysmonwait=false  P0: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P1: status=1 schedtick=0 syscalltick=0 m=2 runqsize=0 gfreecnt=0 timerslen=0  P2: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P3: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P4: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P5: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P6: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P7: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P8: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  P9: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0  M3: p=0 curg=nil mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=true blocked=false lockedg=nil  M2: p=1 curg=nil mallocing=0 throwing=0 preemptoff= locks=6 dying=0 spinning=false blocked=false lockedg=nil  M1: p=nil curg=nil mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=nil  M0: p=nil curg=nil mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=1  G1: status=1() m=nil lockedm=0  G2: status=4(force gc (idle)) m=nil lockedm=nilHello from goroutine</code></pre><p>这些日志显示了这些信息：</p><ul><li>第一行 <code>SCHED</code> 开头的是汇总信息，告诉我们程序启动了 10 个 P（gomaxprocs&#x3D;10）。</li><li>只有 <code>P1</code> 被 <code>M2</code> 拿着运行</li><li><code>P0</code> 被 <code>M3</code> 拿着处于 <code>spinning</code> 状态，也就是等待任务的状态。</li></ul><p>没看到 print 相关的 G，是因为任务运行时间太短了，没被 trace 捕获就结束了，这里主要展示 GMP 的详细信息可以用 debug 命令来看。</p><h3 id="抢占式调度"><a href="#抢占式调度" class="headerlink" title="抢占式调度"></a>抢占式调度</h3><pre><code class="go">package mainimport (    &quot;fmt&quot;    &quot;runtime&quot;    &quot;time&quot;)func busy(tag string, d time.Duration) &#123;    end := time.Now().Add(d)    x := 0    for time.Now().Before(end) &#123;        x++    &#125;    fmt.Println(tag, &quot;done&quot;, x)&#125;func main() &#123;    runtime.GOMAXPROCS(1)    go busy(&quot;A&quot;, 1500*time.Millisecond)    busy(&quot;B&quot;, 1500*time.Millisecond)&#125;</code></pre><p>这个代码的运行结果是，有时候 <code>A</code> 在 <code>B</code> 前面，有时候 <code>B</code> 在 <code>A</code> 前面。</p><p>我们已经用 <code>runtime.GOMAXPROCS(1)</code> 设定只有一个 P，但是 Go 语言的 GMP 调度器，仍然会 10ms 释放一次时间片，也就意味着，即使 <code>go busy(&quot;A&quot;)</code> 处于阻塞状态，时间片之后也会让出执行权，交给主线程去运行 <code>B</code>。</p><p>可以用这个 <code>busy</code> 的函数定义来让抢占式调度更加肉眼可见：</p><pre><code class="go">func busy(tag string, d time.Duration) &#123;    end := time.Now().Add(d)    next := time.Now()    for time.Now().Before(end) &#123;        if time.Now().After(next) &#123;            fmt.Print(tag, &quot; &quot;) // 每 ~100ms 打印一次            next = time.Now().Add(100 * time.Millisecond)        &#125;    &#125;    fmt.Println(tag, &quot;done&quot;)&#125;</code></pre><p>程序的打印结果会是 <code>B A B A B A A B A B A B A B A B A B A B A B A B B A B A B A B done</code>。这意味着不是 tag 为 <code>A</code> 的 P 一路执行到底，也不是 tag 为 <code>B</code> 的 P 一路执行到底，他们在 GMP 调度器中交替执行。</p><h3 id="P-偷活干（work-stealing）"><a href="#P-偷活干（work-stealing）" class="headerlink" title="P 偷活干（work-stealing）"></a>P 偷活干（work-stealing）</h3><p>来看这个代码示例：</p><pre><code class="go">package mainimport (    &quot;runtime&quot;    &quot;sync&quot;    &quot;time&quot;)func spin(d time.Duration) &#123;    deadline := time.Now().Add(d)    for time.Now().Before(deadline) &#123;    &#125; // 纯CPU忙等&#125;func main() &#123;    runtime.GOMAXPROCS(1) // 先让所有 G 挤到同一个 P 的本地队列    const N = 120    var wg sync.WaitGroup    wg.Add(N)    for i := 0; i &lt; N; i++ &#123;        go func() &#123; defer wg.Done(); spin(500 * time.Millisecond) &#125;()    &#125;    time.Sleep(30 * time.Millisecond) // 给点时间把队列堆满到 P0    runtime.GOMAXPROCS(4) // 突然放大并行度：P1~P3 会去“偷” P0 的一半    wg.Wait()&#125;</code></pre><p>这个代码干了什么呢，首先设定之后一个 P，然后启动 120 个 G 给这个 P 去执行。30 毫秒后，突然增大 P 的数量。</p><p>用 debug 日志能看到，运行后半段有这样的日志：</p><pre><code class="bash">P0: status=1 schedtick=46 syscalltick=2 m=0 runqsize=17 gfreecnt=0 timerslen=0P1: status=1 schedtick=58 syscalltick=0 m=4 runqsize=5 gfreecnt=15 timerslen=0P2: status=1 schedtick=60 syscalltick=0 m=2 runqsize=5 gfreecnt=18 timerslen=0P3: status=1 schedtick=42 syscalltick=0 m=3 runqsize=17 gfreecnt=0 timerslen=0</code></pre><p>也就是说，本应该 G 全在 P0 上运行，等到 P1、P2、P3 出来后，它们发现 P0 很忙，就去 P0 的队列里拿了几个任务过来执行。</p><h3 id="P-的-runq-队列和全局队列"><a href="#P-的-runq-队列和全局队列" class="headerlink" title="P 的 runq 队列和全局队列"></a>P 的 runq 队列和全局队列</h3><p>一个 P 想找活干的时候，上面的代码是偷其他 P 的示例。更严谨的流程是，P 先从本地 runq 队列找，再到全局队列找，找不到再去偷其他 P 的。</p><p>什么是 runq 队列，什么是全局队列？可以看这个代码：</p><pre><code class="go">package mainimport (    &quot;runtime&quot;    &quot;sync&quot;    &quot;time&quot;)func spin(d time.Duration) &#123;    end := time.Now().Add(d)    for time.Now().Before(end) &#123;    &#125; // 纯CPU忙等：保持 runnable&#125;func main() &#123;    runtime.GOMAXPROCS(1) // 只有 P0：所有新 G 先进入 P0 的本地 runq    const N = 600 // 让它明显超过本地 runq 容量（当前实现通常是 256）    var wg sync.WaitGroup    wg.Add(N)    for i := 0; i &lt; N; i++ &#123;        go func() &#123; defer wg.Done(); spin(800 * time.Millisecond) &#125;()    &#125;    time.Sleep(500 * time.Millisecond) // 给运行时时间把“溢出的一半”推到全局队列    runtime.GOMAXPROCS(4) // 其它 P 进场，会先从“全局队列”拿活（不是偷）    wg.Wait()&#125;</code></pre><p>debug 状态运行：</p><pre><code class="bash">go build demo4.go   GODEBUG=&#39;schedtrace=200,scheddetail=1&#39; ./demo4 &amp;&gt; demo4.log</code></pre><p>日志会比较多，日志前面几行像这样：</p><pre><code class="bash">SCHED 0ms: gomaxprocs=10 idleprocs=9 threads=2 spinningthreads=0 needspinning=0 idlethreads=0 runqueue=0 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 timerslen=0  P1: status=0 schedtick=0 syscalltick=0 m=nil runqsize=0 gfreecnt=0 timerslen=0</code></pre><p>其中首行的 <code>runqueue=0</code> 就是全局队列，P0 后面的 <code>runqsize=0</code> 是 P0 的本地队列，P1 后面的 <code>runqsize=0</code> 是 P1 的本地队列。可以看到此时的 P1 状态是 0，也就是不可运行。</p><p>随着程序的运行，P0 会启动非常多个 G，日志状态是这样：</p><pre><code class="bash">SCHED 200ms: gomaxprocs=1 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=3 runqueue=395 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=10 syscalltick=2 m=0 runqsize=204 gfreecnt=0 timerslen=1</code></pre><p>一般 P 的本地队列默认是上限是 256，达到这个峰值后，就会把任务溢出到全局队列。</p><p>再然后，P1、P2、P3 启动，开始从全局队列拿任务（全局队列有任务则不需要偷其他 P 的）：</p><pre><code class="bash">SCHED 826ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 needspinning=1 idlethreads=0 runqueue=217 gcwaiting=false nmidlelocked=0 stopwait=0 sysmonwait=false  P0: status=1 schedtick=35 syscalltick=2 m=0 runqsize=179 gfreecnt=0 timerslen=0  P1: status=1 schedtick=14 syscalltick=0 m=3 runqsize=90 gfreecnt=0 timerslen=0  P2: status=1 schedtick=14 syscalltick=0 m=4 runqsize=64 gfreecnt=0 timerslen=0  P3: status=1 schedtick=13 syscalltick=0 m=2 runqsize=46 gfreecnt=0 timerslen=0</code></pre><p>另外，当 P 依次从本地 runq、全局队列、其他 P 都找不到任务时，会再去问一下 netpoll（问一下 OS）有没有新的 G，要是有就执行，没有就自旋（待命）。这就是 P 执行任务的逻辑。</p><h3 id="阻塞-syscall-会及时让出"><a href="#阻塞-syscall-会及时让出" class="headerlink" title="阻塞 syscall 会及时让出"></a>阻塞 syscall 会及时让出</h3><p>看这个代码例子：</p><pre><code class="go">package mainimport (    &quot;fmt&quot;    &quot;runtime&quot;    &quot;time&quot;)func main() &#123;    runtime.GOMAXPROCS(2)    go func() &#123;        time.Sleep(2 * time.Second) // 类比阻塞 syscall/cgo        fmt.Println(&quot;blocking done&quot;)    &#125;()    go func() &#123;        for i := 0; i &lt; 6; i++ &#123;            time.Sleep(300 * time.Millisecond)            fmt.Println(&quot;still running&quot;, i)        &#125;    &#125;()    time.Sleep(3 * time.Second)&#125;</code></pre><p>运行结果会是：</p><pre><code class="bash">still running 0still running 1still running 2still running 3still running 4still running 5blocking done</code></pre><p>这个代码示例的含义是，第一个 G 明明会阻塞任务队列，一直占着 P 执行，但实际上第二个 G 仍然在运行。</p><p>说明 GMP 调度器不会因为某个 G 的阻塞，影响到其他 G 的执行。（其实这是协程调度器很基本的要求）</p><h3 id="关闭异步抢占"><a href="#关闭异步抢占" class="headerlink" title="关闭异步抢占"></a>关闭异步抢占</h3><p>对于这个代码示例：</p><pre><code class="go">package mainimport (    &quot;fmt&quot;    &quot;runtime&quot;    &quot;time&quot;)func spin() &#123;    for &#123; /* 紧密循环 */    &#125;&#125;func main() &#123;    runtime.GOMAXPROCS(1)    go spin()    time.Sleep(100 * time.Millisecond)    fmt.Println(&quot;I should still print unless preemption is off&quot;)&#125;</code></pre><p>可以分别用两个命令来运行，一个是</p><pre><code class="bash">go build demo7.goGODEBUG=&#39;schedtrace=1000,scheddetail=1&#39; ./demo7</code></pre><p>另一种是：</p><pre><code class="bash">go build demo7.goGODEBUG=&#39;schedtrace=1000,scheddetail=1,asyncpreemptoff=1&#39; ./demo7</code></pre><p>用 <code>asyncpreemptoff=1</code> 可以关闭异步抢占。也就是说，如果没有关闭，没有带这个参数，程序会正常运行，打印出：</p><pre><code class="bash">I should still print unless preemption is off</code></pre><p>如果关闭了异步抢占，则程序会被死循环卡住。这个例子主要可以体现 GMP 主动让出 CPU 的特点，当关闭了主动让出的能力后，GMP 就会被阻塞住了。</p><h3 id="Go-语言源码"><a href="#Go-语言源码" class="headerlink" title="Go 语言源码"></a>Go 语言源码</h3><p>我没有深入看源码，比如 G、M、P 的常量定义在 <a href="https://github.com/golang/go/blob/master/src/runtime/runtime2.go#L18"><code>src/runtime/runtime2.go</code></a> 文件：</p><img src="1.png" width="60%"><p>再比如 <a href="https://github.com/golang/go/blob/master/src/runtime/proc.go"><code>src/runtime/proc.go</code></a> 文件中的 <code>runqputslow</code> 函数，功能就是判断本地队列有没有满，如果满了就放到全局队列：</p><img src="2.png" width="60%"><h3 id="进一步深入"><a href="#进一步深入" class="headerlink" title="进一步深入"></a>进一步深入</h3><p>这篇文章肯定有不全面和不到位的地方，我不想进一步深入了，也许有人喜欢折腾这些吧。</p><p>Go 语言的 GMP，就是协程调度器的一种具体的工程化的实现，估计很多人在意的，是这种工程化实现背后的细节，比如怎么用栈结构来管理任务队列、怎么实现抢占、让出逻辑等。协程调度器的具体实现方式可以有各种各样的变化，但它们的基本原理都是 continuation。只是 Go 语言把协程作为卖点了。只要其他语言愿意，也是可以开发出自己版本的协程调度器的。</p><p>那么问题来了，那些喜欢研究 GMP 原理的人，你们有没有了解过其他语言的协程（coroutine）、虚拟线程、异步函数、Process 是怎么实现的，它们都是比线程更轻量的类似于协程的东西，和 Go 语言的 gorountine 有什么区别？横向对比一下？</p><p>如果什么时候，我的工作需要，只有我了解这些内容，才能把工作做好，那么我肯定去把这些东西搞明白。</p><h3 id="疑问"><a href="#疑问" class="headerlink" title="疑问"></a>疑问</h3><p>我之前写过一个观点：</p><p>Go 语言 “千辛万苦” 做出了自动的垃圾回收，减轻程序员对于内存管理的头脑负担。而有些面试官 “千辛万苦” 去搞明白 Go 语言 GC 的原理是什么，怎么标记怎么释放之类，不但引以为豪，而且拿来考察候选人。作为 Go 语言的教徒，你知不知道你的行为在否定 Go 语言设计者的努力？如果真的相信用头脑来管理内存的力量，为什么不去搞 Rust？好比我是一个汽车驾驶员，我要去考驾照，难道需要我搞清楚发动机的工作原理、是怎么把汽油燃烧转变为机械动力的、能量转化公式是什么？我又不是在制造汽车，也不是在开发编程语言。</p><p>同样的道理：</p><p>Go 语言为了让广大程序员能便捷简单地、用上轻量级的协程，“千辛万苦” 搞出来一个 <code>go</code> 关键字，然而有些人却费尽 “千辛万苦” 研究这个调度器是怎么实现的，懂原理则说明会 Go 语言，不懂则说明 Go 语言水平不行，这是什么道理？作为 Go 语言的教徒，你在否定 Go 语言设计者的努力，明白吗？如果这个语言需要你搞清楚协程调度的原理，才能写出好的代码，那就说明这个语言实现的不到位，偏离了设计者的初衷，没有达到设计者本来的意图。</p><p>如果你是编程语言的开发者，需要在另一种语言中借鉴、实现、优化 Go 语言的调度器，那么你就尽情研究吧，这样的工作确实需要懂 GMP 调度器的原理。如果不是那样的工作呢？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;声明：我看不起 “Go 语言 GMP 调度器的原理是什么” 这种技术话题。&lt;/p&gt;
&lt;p&gt;我平时没兴趣研究这种问题。因为在面试中被问到的频率太高了，现在想花 2</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Go" scheme="https://smallyu.net/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>Web3 项目分析计划</title>
    <link href="https://smallyu.net/2025/08/14/Web3%E9%A1%B9%E7%9B%AE%E5%88%86%E6%9E%90%E8%AE%A1%E5%88%92/"/>
    <id>https://smallyu.net/2025/08/14/Web3%E9%A1%B9%E7%9B%AE%E5%88%86%E6%9E%90%E8%AE%A1%E5%88%92/</id>
    <published>2025-08-14T15:00:00.000Z</published>
    <updated>2025-09-30T17:05:54.826Z</updated>
    
    <content type="html"><![CDATA[<p>Web3 项目分析系列文章专用的 Paragraph 频道地址是：</p><ul><li><a href="https://paragraph.com/@smallyu">https:&#x2F;&#x2F;paragraph.com&#x2F;@smallyu</a></li></ul><h3 id="计划内容"><a href="#计划内容" class="headerlink" title="计划内容"></a>计划内容</h3><p>经过几天时间的尝试，我觉得 Web3 项目分析计划是一件很有意义的事情。不清楚看到分析文章的人有没有收获，但是从我自己理解项目、学习技术的角度，是有收获的，所以我需要把这个计划继续下去，变为一件常态化的事情。</p><p>具体计划内容是，每周分析一个 Web3 行业的项目，从看白皮书开始，到理解项目的运作模式、当前商业状态等，尤其关注技术理念和技术创新方面，然后写成分析报告，不需要很专业的那种报告，大概相当于学习笔记就可以了。具体分析哪个项目是经过主观挑选的。最终的分析报告也许会有质量，有看点，但也许会比较短，没有质量。因为我并不能在一开始选定项目的时候，就知道这个项目有没有含量，尤其是技术含量。</p><h3 id="工程代码没有价值"><a href="#工程代码没有价值" class="headerlink" title="工程代码没有价值"></a>工程代码没有价值</h3><p>这个计划有点像是区块链研究员干的事情，而不是区块链程序员应该干的事情。为什么我的计划不是每天写 100 行代码，开发一个区块链小工具，或者每天积累一点，开发一个大的区块链工程？</p><p>因为工程代码如果脱离项目背景，就没有价值。我在几年的工作中写过很多代码，但是如果现在把那些代码拿出来，会发现毫无意义。工程化的代码，往往是为了完善项目的功能，而项目需要某些方面功能，是为了迎合运营和宣发的需求，一定是有商业目的的。如果需求背景不存在，代码就毫无价值。</p><p>尤其是随着 AI 的日益强大，写工程代码这件事情更是越来越廉价。AI 可以几分钟写出上万行代码，堆砌代码的能力绝对超过人类。如果我想靠每天写几百行工程代码来训练和提升自己，那我一定会失败的很惨。所以不能干这样的事情。</p><p>什么样的工程代码是有意义的呢？就是已经找准了产品需求和定位的情况下，想把功能落实跑通，然后让 AI 来干活，把代码写出来。AI 写的代码有时候会跑偏，需要手动修复一下 bug，这种情况下，手动写出的工程代码才有意义。现在的开发节奏已经应该是这样了。</p><p>以前的时代，人们喜欢说 “Talk is cheap. Show me the code”，但是现在时代变了， prompt 比 code 更有价值，也许这句话会变为 “Code is cheap. Show me the prompt”。</p><h3 id="文章更能表达思想"><a href="#文章更能表达思想" class="headerlink" title="文章更能表达思想"></a>文章更能表达思想</h3><p>不去计划每天写一些工程代码，另一个原因在于，我已经做过了一些尝试，去试图开发小的区块链工具，或者大的区块链工程。目前来看，我之前的想法，无论是做小工具的思路，还是做大工程的思路，都是没有结果的，因为需求本身也许不存在。没有任何正反馈，根本做不下去。</p><p>与代码相比，写文字、写文章、写观点更有意义一点。一个产品创意背后，可能有 100 行代码，也可以有 10000 行代码，需要付出的时间成本完全不同，但如果最终的关注量都是 0，那么结果就是一样的，9900 行代码白写了。而文字是能够体现思想的。</p><p>你也许想反驳，怎么能说工程代码没有价值呢？以太坊的客户端同一份 Spec，有五六种工程化的实现，用了不同语言、做了不同优化，市场占有率有高有低，难道不是工程化代码价值的体现吗？当然是，他们拿着以太坊基金会的赞助开着公司写着代码，而且已经有了明确的项目背景，工程代码自然是有价值的。我指的是没有项目背景的工程代码。</p><p>虽然工程代码没有价值，但教学性质的代码是很有意义的，我仍然会复习计算机课的练习题，以保证自己的代码水平。我已经是第三轮做那些练习题了，这次我严格限制自己的做题速度，一天最多做一道题。一方面是保证有足够的时间消化练习题包含的知识，相信潜意识的力量。另一方面，得分配时间到其他事情上，不能整天只反复做同样的题。而且由于做题比较慢，可以逐渐培养自己每天做题的习惯，不至于遗忘计算机课的知识。</p><h3 id="提高宏观理解能力"><a href="#提高宏观理解能力" class="headerlink" title="提高宏观理解能力"></a>提高宏观理解能力</h3><p>为什么我觉得对项目做分析是有意义的？因为其实我对区块链技术的理解，很大程度上，来自于几年前读了很多白皮书。我当时按照币种市值的排名，逐一下载了排名前几百的币种白皮书，还用 A4 纸都打印出来看。</p><p>记得几年前有人发邮件问我，如何学习区块链技术。我当时认真写了个回复，说我是从哪个网站下载的白皮书，以及看了哪些书之类。后来对方回复我说，这不是他想要学习的区块链技术，他想要学习的是如何写代码。那个时候我才意识到，不同的人，对技术的定义是不同的。</p><p>以前没有 AI，我没能认识到代码的价值，现在有了 AI，我还是认识不到代码的价值。</p><h3 id="研究能力的重要性"><a href="#研究能力的重要性" class="headerlink" title="研究能力的重要性"></a>研究能力的重要性</h3><p>在币圈，人们常说 DYOR（Do Your Own Research），这个词经常出现在 KOL 推广和夸赞某个代币的时候，用来声明不做投资建议，你要自己对自己负责。“研究能力” 一直都是非常重要的能力，如果不具备好的研究能力，你连自己的钱都管理不好。事实上什么事情都需要研究，研究如何学英语、研究如何找工作、研究假期去哪儿玩、研究写代码、研究科学技术、研究如何哄女朋友开心，等等，都是研究。Web3 项目分析计划的目标正是研究项目、锻炼研究能力。</p><p>具备好的研究能力的人，不管学习什么都会变得轻松。试想，你觉得去研究明白怎么把代码写好，尤其是工作中用的普通代码，需要多长时间？很多时候连 “研究” 都用不着！那么，你觉得能把某种技术研究明白的人，会没有能力研究清楚怎么写代码吗？</p><p>那么为什么我觉得自己可以写出分析报告？我以前没专门写过，但是有时会根据技术来对项目做横向对比，所以专注于对某个项目做技术分析，应该不是难事。我工作过的项目，假如让我写分析，肯定能写出其中的细节，只是因为项目还在，不能写。写项目分析对我来说也是一个学习和积累的过程。</p><p>实际上分析区块链项目的方法论，我早在《<a href="/2024/10/15/%E7%9C%8B%E6%87%82%E4%BB%BB%E6%84%8F%E5%8C%BA%E5%9D%97%E9%93%BE%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84/">看懂任意区块链项目的技术架构</a>》就写过了，到现在都不觉得那篇文章内容有什么问题，无非就是链上链下交互，不同项目往里面填充不同的业务逻辑而已。</p><h3 id="写作平台的选择"><a href="#写作平台的选择" class="headerlink" title="写作平台的选择"></a>写作平台的选择</h3><p>我对于博客上应该放哪些文章，是比较纠结的，我不希望一打开博客，满屏幕都是 “对 XX 项目的分析”。为了保持文章列表的简洁，这些项目分析系列的内容应该换一个平台放。最近看到 Paragraph 不错。Paragraph 是一个 Web3 领域的 Newsletter 平台，功能类似于 Web2 的 Substack，每篇文章的全部内容都会提交到 Arweave 区块链上，包括作者的名字、头像、文章正文、配图等。（这也就意味着文章一旦发布，就不可能被删除。）</p><p>为什么不选择其他平台呢？比如发到知乎、掘金，甚至是头条、百度、登链等平台，再加上 Meidum、X、Mirror 一类，文章访问量肯定可以高很多，关注量也会高很多。</p><p>因为那些充斥着低质量内容的平台，不值得去发布高质量内容的文章。那些人是看不懂的，看不懂我在写什么。看看掘金首页上有什么？10 篇文章 8 篇讲 Cursor，很难想象用户素质得多低。知乎就更不用说了，内容杂乱、商业化，关键是网页访问弹窗，不是让登陆就是让下载 APP，正经人谁去那种平台啊。我在脉脉的职言区，匿名账号下，发布过几千条帖子，总阅读量超过几千万，发的都是观点偏激、引战一类的内容。那种阅读量有意义吗？没有意义。</p><p>所以继续努力吧，等自己成为 somebody，再考虑访问量的问题。没有人会关心 nobody 写的东西。</p><h3 id="为什么要做出计划"><a href="#为什么要做出计划" class="headerlink" title="为什么要做出计划"></a>为什么要做出计划</h3><p>其实要按照我自己喜好，我觉得自己真正有价值的文章，是吐槽同事、吐槽公司、吐槽面试经历等情绪宣泄类内容。那些是包含了亲身经历、切实体会、真情实感在里面的，耗费了时间和心情才得到的、宝贵的人生体验，比技术文章有意思多了。对行业的见解、对公司的不满、对同事的吐槽，是我的文章永远超越 AI 的地方，因为 AI 没有情绪，不会生气、不会沮丧。单纯讲技术知识点，AI 一下子就能生成很多，但是 AI 永远无法体会到作为人的情感。</p><p>反正人总要做选择，要么忙着活，要么忙着死。</p><img src="1.jpg" width="60%" />]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Web3 项目分析系列文章专用的 Paragraph 频道地址是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a</summary>
        
      
    
    
    
    
    <category term="项目分析" scheme="https://smallyu.net/tags/%E9%A1%B9%E7%9B%AE%E5%88%86%E6%9E%90/"/>
    
  </entry>
  
  <entry>
    <title>对 Psyche Network 项目的分析</title>
    <link href="https://smallyu.net/2025/08/01/%E5%AF%B9Psyche-Network%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%88%86%E6%9E%90/"/>
    <id>https://smallyu.net/2025/08/01/%E5%AF%B9Psyche-Network%E9%A1%B9%E7%9B%AE%E7%9A%84%E5%88%86%E6%9E%90/</id>
    <published>2025-08-01T06:33:21.000Z</published>
    <updated>2026-08-04T11:43:17.094Z</updated>
    
    <content type="html"><![CDATA[<h3 id="项目背景"><a href="#项目背景" class="headerlink" title="项目背景"></a>项目背景</h3><p><a href="https://psyche.network/runs">Psyche Network</a> 是 AI + Web3 赛道的一个项目，由 <a href="https://nousresearch.com/">Nous Research</a> 团队研发，两个月前获得了 Paradigm 机构 5 千万美元的 <a href="https://cointelegraph.com/news/nous-research-raises-50m-paradigm-decentralized-ai-solana">A 轮融资</a>。</p><p>Psyche Network 的项目背景在 <a href="https://nousresearch.com/nous-psyche/">官方说明文章</a> 里有详细介绍。Nous Research 团队研发出了一种去中心化的算法 DeMo，这种算法能够把大语言模型（LLM）的训练，放到分布式网络里进行，不需要集群服务那种高耦合。就类似比特币挖矿的矿池一样，会把大的计算任务，拆解为小的计算任务，分发给不同的 Client 节点进行计算，计算之后再把结果汇总起来。</p><p>当然 LLM 的训练和矿池的挖矿，从算法原理上完全是两码事，这里只是想类比说明便于理解。具体 DeMo 是怎么从算法角度把任务拆解和合并的，可以看 <a href="https://blog.lambdaclass.com/introducing-demo-decoupled-momentum-optimization-for-efficient-distributed-llm-training/">官方的解释</a>，反正我没看懂，就是一堆向量、权重、loss function 什么的术语。关于怎么防止节点提交虚假数据之类，我认为也都在算法的设计范畴，后续就不多讨论算法本身的有效性了。</p><p>DeMo 的 <a href="https://arxiv.org/pdf/2411.19870">论文</a> 里用了 100 billion 的 tokens 做训练测试，得到了比较好的结果。100 B tokens 是什么概念呢，比如 <a href="https://github.com/deepseek-ai/DeepSeek-V3?tab=readme-ov-file#4-evaluation-results">DeepSeek-V3</a> 的 tokens 数量是 15 TB，可见 DeMo 在实验阶段的 tokens 数量级，距离商用产品还差很多。可以对比一些其他模型的 tokens 数量：</p><table><thead><tr><th>模型</th><th>参数量</th><th>预训练 tokens 数量</th><th>公开来源或泄露信息</th></tr></thead><tbody><tr><td>GPT-3</td><td>175 B</td><td>≈ 499 B</td><td>论文及后续综述</td></tr><tr><td>GPT-3.5</td><td>175 B</td><td>推测 ~1 T 左右</td><td>—</td></tr><tr><td>GPT-4</td><td>1.7 T</td><td>≈ 13 T tokens</td><td>SemiAnalysis &#x2F; The Decoder 报告</td></tr><tr><td>Llama 3</td><td>70 B</td><td>&gt; 15 T tokens</td><td>Meta 官方模型卡</td></tr><tr><td>DeMo OLMo</td><td>1 B</td><td>0.1 T tokens（100 B）</td><td>DeMo 论文</td></tr></tbody></table><p>Psyche Network 基于 DeMo 的算法原理，结合区块链来构建分布式网络，第一阶段的目标是训练出 40 B parameters, 20 T tokens 的模型。关于 <code>parameters</code> 和 <code>tokens</code> 这两个指标，我的理解是，<code>parameters</code> 是训练一开始就定义好的固定指标，<code>tokens</code> 则是需要不断进行计算和训练的，而 DeMo 解决的是 <code>tokens</code> 的分布式计算。Psyche Network 官网上有实时显示当前的训练进度，目前已经达到了 1 TB 的 tokens 数量：</p><img src="1.png" width="80%"><p>这个模型训练完，也许可以接近 GPT-3 的水平。对比来看虽然 tokens 数量比 GPT-3 多，但是 parameters 比 GPT-3 少，所以最终效果应该不如 GPT-3。</p><h3 id="项目结构"><a href="#项目结构" class="headerlink" title="项目结构"></a>项目结构</h3><p>Psyche Network 的 <a href="https://docs.psyche.network/explain/index.html">文档</a> 里有介绍整体的项目结构，比较好理解，有一个中心化的 Coordinator 负责创建训练任务，其余的 Client 负责接收任务、提交任务结果。在没有区块链的场景下，Coordinator 与 Client 之间的通信是通过直接的 TCP 连接完成的。而有了区块链之后，Coordinator 和 Client 之间就是通过区块链来传递消息了。</p><img src="2.png" width="40%"><p>Psyche Network 的 <a href="https://github.com/PsycheFoundation/psyche/tree/main/architectures">代码仓库</a> 里同时保留了 <code>centralized</code> 和 <code>decentralized</code> 两个版本的代码架构，这其实不太是好事，因为说明这个项目原本可以中心化运行，只是现在在做一些去中心化改造。这样的项目去中心化程度肯定是有限的。</p><p>而所谓去中心化版本的部分，Psyche Network 选择了 Solana 来作为运行智能合约的区块链平台，这也许和 Psyche Network 原本的项目就是用 Rust 语言有关。</p><p>代码仓库的 decentralized 目录下，有一些 Solana 的合约代码，这些 Solana 合约承担起了创建训练任务、计算每个 Client 节点的奖励、分发奖励的功能。</p><p>Psyche Network 目前只是测试网阶段，链上交易也都是在 Solana 的 Devnet 上进行，可以直接看合约文件里的 <code>declare_id!()</code> 语句，里面写的就是合约地址，比如 coordinator 的合约地址是 <code>HR8RN2TP9E9zsi2kjhvPbirJWA1R6L6ruf4xNNGpjU5Y</code>，能在 <a href="https://solscan.io/account/HR8RN2TP9E9zsi2kjhvPbirJWA1R6L6ruf4xNNGpjU5Y?cluster=devnet">区块链浏览器</a> 上看到频繁的交易记录。</p><p>至于奖励的计算，因为有 Coordinator 这个中心化角色的存在，所以事情比较简单，Coordinator 在收到 Client 的任务结果后进行验证，如果没问题，则发起一笔链上交易，给 Client 记分。具体代码是 <a href="https://github.com/PsycheFoundation/psyche/blob/main/architectures/decentralized/solana-coordinator/programs/solana-coordinator/src/instance_state.rs#L146-L149">这两行</a>：</p><img src="3.png" width="80%"><p>每个 Client 的分数都记录在合约里，Client 想领取奖励，就自己到 treasurer 合约上 claim，treasurer 会根据分数和汇率计算并转账代币。</p><p>那么 treasurer 分发的奖励是哪个代币呢？具体代币是 Coordinator 在创建任务的时候 <a href="https://github.com/PsycheFoundation/psyche/blob/main/architectures/decentralized/solana-treasurer/programs/solana-treasurer/src/logic/run_create.rs#L34">指定的</a>，只要是标准的 SPL 代币都可以。</p><img src="4.png" width="70%"><p>所以整体来看，Psyche Network 是利用 Solana 区块链来记录任务 Meta 信息、计算任务奖励、分发奖励等。只要 Client 的加入是 permissonless 的，Psyche Network 就确实达到了和宣传一样的效果，让 LLM 模型训练的算力去中心化。</p><p>而代币的分发和奖励虽然是区块链项目的常规操作，但是至少附加了公开透明等特性，而且不出意外的话，Psyche Network 最终会走到发币的一步，到时候任务奖励可能全用 Psyche Network 自己的代币进行，或者演变为 LLM 训练的任务平台，任何第三方都可以创建任务和分发奖励之类，像 Eigne Layer 那样。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;项目背景&quot;&gt;&lt;a href=&quot;#项目背景&quot; class=&quot;headerlink&quot; title=&quot;项目背景&quot;&gt;&lt;/a&gt;项目背景&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://psyche.network/runs&quot;&gt;Psyche Network&lt;/a&gt; 是 AI</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
    <category term="项目分析" scheme="https://smallyu.net/tags/%E9%A1%B9%E7%9B%AE%E5%88%86%E6%9E%90/"/>
    
  </entry>
  
  <entry>
    <title>continuation 教程: 理解 CPS</title>
    <link href="https://smallyu.net/2025/07/23/continuation%E6%95%99%E7%A8%8B1/"/>
    <id>https://smallyu.net/2025/07/23/continuation%E6%95%99%E7%A8%8B1/</id>
    <published>2025-07-23T04:12:12.000Z</published>
    <updated>2026-05-27T07:28:42.310Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这是一个 continuation 系列教程：</p><ol><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B1/">continuation 教程：理解 CPS</a></li><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B2/">continuation 教程：用 yield 实现协程调度</a></li><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B3/">continuation 教程：用 call&#x2F;cc 实现协程调度</a></li><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B4/">continuation 教程：用 shift&#x2F;reset 实现协程调度</a></li><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B5/">continuation 教程：体验 Racket 语言</a></li><li><a href="/2025/07/23/continuation%E6%95%99%E7%A8%8B6/">continuation 教程：实现抢占式协程调度</a></li></ol></blockquote><p>我们来由浅入深地系统学习下 continuation 的原理以及应用场景。这个系列教程的内容和王垠的 continuation 专项班无关，是我自己学习和研究的成果，所以不会有版权问题。不过当然正是因为我学习了基础班，打下了坚实的基础，才知道该如何去自学和理解 continuation 这个概念。这篇文章会少量透露出基础班学到的技能，毕竟 continuation 属于基础班的进阶内容，无法跳过基础技能去理解。</p><h3 id="递归"><a href="#递归" class="headerlink" title="递归"></a>递归</h3><p>首先用递归的形式写一个阶乘函数 <code>fact</code>，我们已经很熟悉它的写法，不需要过多解释：</p><pre><code class="js">function fact(n)&#123;  if (n === 0)   &#123;    return 1;  &#125;  else  &#123;    return n * fact(n - 1);  &#125;&#125;console.log(&quot;fact1=&quot;, fact(1)); // 1console.log(&quot;fact3=&quot;, fact(3)); // 6console.log(&quot;fact5=&quot;, fact(5)); // 120</code></pre><h3 id="尾递归"><a href="#尾递归" class="headerlink" title="尾递归"></a>尾递归</h3><p>接着把 <code>fact</code> 函数改为尾递归的形式。尾递归会比递归多一个参数，新参数用来保存每个调用计算后的值：</p><pre><code class="js">function factTail(n, prod)&#123;  if (n == 0)  &#123;    return prod;  &#125;  else  &#123;    return factTail(n-1, prod*n);  &#125;&#125;console.log(&quot;factTail1=&quot;, factTail(1, 1)); // 1console.log(&quot;factTail3=&quot;, factTail(3, 1)); // 6console.log(&quot;factTail5=&quot;, factTail(5, 1)); // 120</code></pre><h3 id="CPS-形式"><a href="#CPS-形式" class="headerlink" title="CPS 形式"></a>CPS 形式</h3><p>我们基于 <code>fact</code> 函数的尾递归形式，再新增一个参数 <code>k</code>，这个 <code>k</code> 是一个函数，<code>fact</code> 不直接返回计算后的值，而是结果值对 <code>k</code> 函数的调用，像这样：</p><pre><code class="js">function factTailCPS(n, prod, k)&#123;  if (n == 0)  &#123;    return k(prod);  &#125;  else  &#123;    return factTailCPS(n-1, prod*n, k);  &#125;&#125;factTailCPS( 1, 1, x =&gt; console.log(&quot;factTailCPS1=&quot;, x) ); // 1factTailCPS( 3, 1, x =&gt; console.log(&quot;factTailCPS3=&quot;, x) ); // 6factTailCPS( 5, 1, x =&gt; console.log(&quot;factTailCPS5=&quot;, x) ); // 120</code></pre><p>这个 k 就是 continuation，意味着告诉 <code>fact</code> 函数，你执行完了计算出结果之后，应该如何进行下一步延续。不用怀疑，这个函数完全符合 CPS（Continuation-Passing-Style）的形式。</p><h3 id="典型-CPS"><a href="#典型-CPS" class="headerlink" title="典型 CPS"></a>典型 CPS</h3><p>但是用尾递归结合 continuation 参数的形式，显然不够简洁，并不算典型的 CPS 形式。典型的 CPS 形式比较难理解，所以不需要自己思考出来，直接看这个现成的例子，我们对递归形式的 <code>fact</code> 函数改进一下：</p><pre><code class="js">function factCPS(n, k)&#123;  if (n == 0)  &#123;    return k(1);  &#125;  else  &#123;    return factCPS(n-1, r =&gt; k(n * r));  &#125;&#125;</code></pre><p>可能看着有点懵，不要慌，我们拆解一下其中的内容。首先 <code>k</code> 仍然代表 continuation，并且 <code>k</code> 是一个函数。然后我们这样来调用：</p><pre><code class="js">let factCPS1 = factCPS(0, x =&gt; x);console.log(&quot;factCPS1=&quot;, factCPS1); // 1let factCPS3 = factCPS(3, x =&gt; x);console.log(&quot;factCPS3=&quot;, factCPS3); // 6let factCPS5 = factCPS(5, x =&gt; x);console.log(&quot;factCPS5=&quot;, factCPS5); // 120</code></pre><p>关键在于调用的时候，传入函数的第二个参数是 <code>x =&gt; x</code>，如果结合函数内部的 <code>r =&gt; k(n * r)</code>，也许一下子就糊涂了。</p><p>这确实是最难理解的部分。我们以计算 2 的阶乘为例，写一个拆解 <code>factCPS</code> 函数调用步骤的过程。这里用到的技巧是在基础班第一课就学过的 <code>单步替换</code>，对于理解递归非常有帮助。如果在基础班经过训练并且打好基础，确实会有助于理解更复杂的东西，比如这里的 CPS 调用：</p><pre><code class="js">let factCPS2 = factCPS(2, x =&gt; x);console.log(&quot;factCPS2=&quot;, factCPS2); // 2// n=2, k=x=&gt;x, return factCPS(1, r =&gt; k(2 * r));  // n=1, k=r=&gt;(x=&gt;x)(2*r), return factCPS(0, r =&gt; k(1 * r));    // n=0, k=r=&gt;(r=&gt;(x=&gt;x)(2*r)(1*r)), return k(1);      // k(1) = r=&gt;(x=&gt;x)(2*r)(1*1)      //      = (x=&gt;x)(2)      //      = 2</code></pre><p>虽然我已经按照正确的思路拆解出了正确的步骤，但是从阅读者的角度，这仍然会非常难理解，可以自己拆解一下试试，逐步理解典型 CPS 的调用过程。理解这些步骤也许需要几个小时的时间，这是正常的。</p><p>总结来说，CPS 的每一次调用，都是在用闭包来储存当前步骤计算的值。尾递归是直接用参数传递值，而 CPS 是在用闭包传递给下个步骤值，就是这样的关系。当然理解这一点的前提是，知道闭包是什么，这个也是基础班学习的重点内容，尤其是会在实现解释器环节，自己实现闭包的语句，对于闭包的理解会很透彻。</p><h3 id="fib-函数的-CPS"><a href="#fib-函数的-CPS" class="headerlink" title="fib 函数的 CPS"></a>fib 函数的 CPS</h3><p>计算阶乘的函数 <code>fact</code> 特点是只在函数体内进行一次递归调用，我们再来看计算斐波那契数列的 <code>fib</code> 函数，它会在函数体内进行两次递归调用，CPS 该怎么处理这个情况。</p><p><code>fib</code> 函数的递归形式的定义是这样：</p><pre><code class="js">function fib(n)&#123;  if (n == 0)  &#123;    return 0;  &#125;  else if (n == 1)  &#123;    return 1;  &#125;  else   &#123;    return fib(n-1) + fib(n-2);  &#125;&#125;console.log(&quot;fib(2)=&quot;, fib(2)); // 1console.log(&quot;fib(5)=&quot;, fib(5)); // 5</code></pre><p>这里直接给出 <code>fib</code> 函数的 CPS，然后理解一下 <code>fib</code> 函数的运作过程：</p><pre><code class="js">function fibCPS(n, k)&#123;  if (n == 0)  &#123;    return k(0);  &#125;  else if (n == 1)  &#123;    return k(1);  &#125;  else  &#123;    return fibCPS(n-1, r1 =&gt; fibCPS(n-2, r2=&gt;k(r1+r2)) );  &#125;&#125;</code></pre><p>可以看到，对于需要两次递归调用的情况，CPS 是把另一次递归调用，写在了原本的 <code>r =&gt; k(r)</code> 函数里，让第二次内部调用成为了递归调用 <code>fib</code> 时候的子调用。这句话有点绕，可以结合代码理解一下。</p><p>CPS 形式的 <code>fib</code> 函数这样来调用：</p><pre><code class="js">let fibCPS1 = fibCPS(1, x=&gt;x);console.log(&quot;fibCPS1=&quot;, fibCPS1); // 1let fibCPS2 = fibCPS(2, x=&gt;x);console.log(&quot;fibCPS2=&quot;, fibCPS2); // 1let fibCPS4 = fibCPS(4, x=&gt;x);console.log(&quot;fibCPS4=&quot;, fibCPS4); // 3let fibCPS5 = fibCPS(5, x=&gt;x);console.log(&quot;fibCPS5=&quot;, fibCPS5); // 5</code></pre><p>我们以计算 3 的斐波那契数为例，拆解一下具体的执行步骤。要注意的是，这个过程非常复杂，比 <code>fact</code> 函数还要复杂很多，只有自己亲自写一下才能搞清楚：</p><pre><code class="js">let fibCPS3 = fibCPS(3, x=&gt;x);console.log(&quot;fibCPS3=&quot;, fibCPS3); // 1+1=2// n=3, k=x=&gt;x,        // return fibCPS(2, r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) );// n=2, k= r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)),        // return fibCPS(1, r1 =&gt; fibCPS(0, r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(r1+r2)) );// n=1, k= r1 =&gt; fibCPS(0, r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(r1+r2)),        // return k(1)       // return ( r1 =&gt; fibCPS(0, r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(r1+r2)) )(1)       // return fibCPS(0, r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(1+r2))          // n=0, k= r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(1+r2)              // return k(0)              // return ( r2 =&gt; ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) )(1+r2) )(0)              // return ( r1 =&gt; fibCPS(1, r2=&gt;(x=&gt;x)(r1+r2)) ) (1+0)              // return fibCPS(1, r2=&gt;(x=&gt;x)(1+r2))                  // n=1, k = r2=&gt;(x=&gt;x)(1+r2)                  // return k(1)                  // return (x=&gt;x)(1+1)                  // return 2</code></pre><p>那么经过了 <code>fact</code> 和 <code>fib</code> 函数的训练，我们就已经知道 CPS 的形式是什么，以及具体的执行步骤是怎样了。理解 CPS 只是开始，接下来还会利用 continuation 实现更多有趣的程序。</p><h3 id="练习题"><a href="#练习题" class="headerlink" title="练习题"></a>练习题</h3><p>已知一个递归形式的 <code>sumFrom</code> 函数，接收两个参数 <code>a</code> 和 <code>b</code>，函数的功能是计算 <code>a+(a+1)+...+(b-1)+b</code> 的值，例如参数是 <code>1</code> 和 <code>4</code>，则计算 <code>1+2+3+4</code> 的结果：</p><pre><code class="js">function sumFrom(a, b)&#123;  if (a == b)   &#123;    return a;  &#125;  else  &#123;    return b + sumFrom(a, b-1);  &#125;&#125;console.log(sumFrom(1, 3));   // 6console.log(sumFrom(2, 5));   // 14</code></pre><p>练习的内容是，将 <code>sumFrom</code> 函数修改为 CPS 形式，补充 <code>sumFromCPS</code> 函数空白处的代码，让程序可以满足测试用例中的输出结果：</p><pre><code class="js">function sumFromCPS(a, b, k)&#123;  // ____&#125;sumFromCPS(1, 3, x =&gt; console.log(&quot;sumFromCPS(1, 3)=&quot;, x));   // 6sumFromCPS(2, 5, x =&gt; console.log(&quot;sumFromCPS(2, 5)=&quot;, x));   // 14</code></pre><h3 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h3><p>我们已经体验了手动将递归程序转变为 CPS 形式的过程，实际上存在能将代码自动转变为 CPS 形式的方法，也就是传说中 “王垠 40 行代码” 在干的事情。可以参考这两个链接查看更多内容：</p><ul><li><a href="https://www.zhihu.com/question/20822815">王垠的「40 行代码」真如他说的那么厉害吗？</a></li><li><a href="https://www.yinwang.org/blog-cn/2012/07/04/dan-friedman">GTF - Great Teacher Friedman</a></li></ul><p>因为 “自动 CPS 变换” 的难度比较大，我自己并不打算深入学习和实现。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这是一个 continuation 系列教程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/2025/07/23/continuation%E6%95%99%E7%A8%8B1/&quot;&gt;continuation 教程：理解</summary>
        
      
    
    
    
    
    <category term="continuation" scheme="https://smallyu.net/tags/continuation/"/>
    
    <category term="教程" scheme="https://smallyu.net/tags/%E6%95%99%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>如何开发一个比特币符文（Runes）协议</title>
    <link href="https://smallyu.net/2025/07/15/%E5%A6%82%E4%BD%95%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%AF%94%E7%89%B9%E5%B8%81%E7%AC%A6%E6%96%87%EF%BC%88Runes%EF%BC%89%E5%8D%8F%E8%AE%AE/"/>
    <id>https://smallyu.net/2025/07/15/%E5%A6%82%E4%BD%95%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%AF%94%E7%89%B9%E5%B8%81%E7%AC%A6%E6%96%87%EF%BC%88Runes%EF%BC%89%E5%8D%8F%E8%AE%AE/</id>
    <published>2025-07-15T14:00:00.000Z</published>
    <updated>2026-08-04T11:43:16.823Z</updated>
    
    <content type="html"><![CDATA[<p>比特币符文（Runes）在技术原理上比较简单，很容易理解，实现 Runes 只需要用到比特币脚本中的 <code>OP_RETURN</code> 操作符。也是正因为简单，所以在技术特性上， Runes 相对干净一点，没有铭文和 RGB 那么复杂的链下状态。而 Runes 厉害的地方在于，能把这样一个生态给玩起来，虽然现在也凉了，但是我们不那么关心市场表现，从技术的角度，非常切实的看一下 Runes 是如何实现的，并且我们自己会一步一步开发一个简化的 Runes 协议出来。当我们真正理解了 Runes 协议，就可以直接看懂更复杂的项目，比如 Alkanes，一个在比特币上支持 WASM 智能合约的协议。</p><p>这篇文章的操作基于《<a href="/2025/07/10/%E6%AF%94%E7%89%B9%E5%B8%81%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B/">比特币脚本开发教程</a>》中的知识，如果缺少对比特币脚本的基本了解，可以先看一下基础教程。</p><h3 id="1-定义数据结构"><a href="#1-定义数据结构" class="headerlink" title="1. 定义数据结构"></a>1. 定义数据结构</h3><p>首先把 Runes 相关的操作定义为 json 格式的数据结构。用什么语言都可以，这里用的是 Rust。为了简化教程，可以省去一些实际中常用的元素，比如 <code>transfer</code> 的操作定义中，应该允许一次性转账给多个目标地址，但这里只有一个目标地址，没有用数组：</p><pre><code class="rust">struct IssueRune&#123;    op: u8,          // 固定为 0，代表发行    symbol: String,  // Rune name    supply: String,  // 发行总量&#125;struct TransferRune&#123;    op: u8,          // 固定为 1，代表转账    id: u64,         // rune_id    vout: u32,       // 转账目标    amount: String,  // 转账金额&#125;</code></pre><p>这个结构定义中，唯一可能有点迷惑的是 <code>vout</code> 字段，一般我们要转账给一个目标地址的时候，会使用目标地址的字符串作为值，但是 Runes 协议中，为了节省链上空间，使用 <code>vout</code> 也就是当前这笔交易、第几个输出的索引作为 Runes 转账的目标。因为每一笔交易的第 0 个输出会包含 <code>OP_RETURN</code> 的值，转账操作的字符全部会放到 <code>OP_RETURN</code> 的内容中，所以在一笔交易里，<code>vout</code> 只用数字就足以表明转账的目标地址是哪个。交易数据的结构大概是这样：</p><pre><code class="json">tx &#123;  vin:   [...]  vout:  [    &#123; vout: 0, scriptPubKey: OP_RETURN &lt;json数据&gt; &#125;,    &#123; vout: 1, scriptPubKey: OP_0 &lt;转账地址1&gt; &#125;,    &#123; vout: 2, scriptPubKey: OP_0 &lt;转账地址2&gt; &#125;  ]&#125;</code></pre><p>接着给数据结构定义一下序列化函数，让结构体可以转变为 json 字符串：</p><pre><code class="rust">impl IssueRune&#123;    fn toJson(&amp;self) -&gt; String    &#123;        format!(            &quot;&#123;&#123;\"op\":&#123;&#125;,\"symbol\":\"&#123;&#125;\",\"supply\":\"&#123;&#125;\"&#125;&#125;&quot;,            self.op, self.symbol, self.supply        )    &#125;&#125;impl TransferRune&#123;    fn toJson(&amp;self) -&gt; String    &#123;        format!(            &quot;&#123;&#123;\"op\":&#123;&#125;,\"id\":&#123;&#125;,\"vout\":&#123;&#125;,\"amount\":\"&#123;&#125;\"&#125;&#125;&quot;,            self.op, self.id, self.vout, self.amount        )    &#125;&#125;</code></pre><h3 id="2-发行-Runes-代币"><a href="#2-发行-Runes-代币" class="headerlink" title="2. 发行 Runes 代币"></a>2. 发行 Runes 代币</h3><p>接下来的操作都基于本地启动的 regtest 节点，所以记得先启动起来，同时验证下有没有加载钱包、钱包里有没有余额。然后准备一下要发行 Rune 的 json 数据，比如这样，发行的 Rune 叫 <code>Doge</code>，总发行量 <code>1000</code> 个：</p><pre><code class="rust">fn issue_rune()&#123;    let issue = IssueRune     &#123;        op: 0,        symbol: &quot;Doge&quot;.to_string(),        supply: &quot;1000&quot;.to_string(),    &#125;;    println!(&quot;Issue Doge JSON: &#123;&#125;&quot;, issue.toJson());&#125;</code></pre><p>运行这个函数，就会得到这样的输出，后面的 json 数据很重要，我们稍后会把这个数据发送到链上：</p><pre><code class="bash">Issue Doge JSON: &#123;&quot;op&quot;:0,&quot;symbol&quot;:&quot;Doge&quot;,&quot;supply&quot;:&quot;1000&quot;&#125;</code></pre><p>然后运行这个命令行，把 json 数据转变为 16进制字符串：</p><pre><code class="bash">echo -n &quot;&#123;&quot;op&quot;:0,&quot;symbol&quot;:&quot;Doge&quot;,&quot;supply&quot;:&quot;1000&quot;&#125;&quot; | xxd -p -c 999</code></pre><p>我得到了这样的输出：</p><pre><code class="bash">7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d</code></pre><p>这就是会放到 <code>OP_RETURN</code> 后面、用来上链的数据。注意 <code>OP_RETURN</code> 最多支持 80 个字节，所以这个数据不能太长。</p><p>接着查看并挑一笔未花费的输出，因为 Runes 所有的操作都必须绑定到 UTXO 上。用这个命令查看你的钱包有哪些 UTXO 可用，然后挑一个你喜欢的：</p><pre><code class="bash">bitcoin-cli -datadir=./ -regtest listunspent</code></pre><p>比如我要用的 UTXO 是这样：</p><pre><code class="json">&#123;  &quot;txid&quot;: &quot;8bfd524e9fc150dab11289d7e6d07860b2b5d6acb54b278a5dc1d1d7631bc8fa&quot;,  &quot;vout&quot;: 0,  &quot;address&quot;: &quot;bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw&quot;,  &quot;amount&quot;: 50.00000000,  // ...&#125;</code></pre><p>然后生成一个找零地址，用来接收比特币余额，这里用了 legacy 格式的地址，这个不是强制的，用 SegWit 的地址也不影响：</p><pre><code class="bash">bitcoin-cli -datadir=./ getrawchangeaddress legacy</code></pre><p>我生成的地址是 <code>n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV</code>，接下来可以用这个地址构建交易了：</p><pre><code class="bash">bitcoin-cli -datadir=./ createrawtransaction \  &#39;[&#123;&quot;txid&quot;:&quot;8bfd524e9fc150dab11289d7e6d07860b2b5d6acb54b278a5dc1d1d7631bc8fa&quot;,&quot;vout&quot;:0&#125;]&#39; \  &#39;[&#123;&quot;data&quot;:&quot;7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d&quot;&#125;,&#123;&quot;n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV&quot;:49.99&#125;]&#39;</code></pre><p>注意这个命令给找零地址的金额为 49.99，这种操作是不可以直接在主网使用的，需要精确计算余额和手续费的差值，然后给找零地址，不然会有很大的资金损失。这里只是懒得计算精确值。</p><p>生成交易数据后，对交易进行签名：</p><pre><code class="bash">bitcoin-cli -datadir=./ signrawtransactionwithwallet 0200000001fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000</code></pre><p>发送签名后的交易到链上：</p><pre><code class="bash">bitcoin-cli -datadir=./ sendrawtransaction 02000000000101fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac02473044022004a2553cc5348dd4521c093149b0ba5e5603fe4134d06a455e12abeac097ea19022076e72632b2488e1316e54559ed733b37de9ce7fd04119e78a59546a3d2c1faea0121020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc100000000</code></pre><p>要留意这个命令会输出一个 txid，这个 txid 比较重要，我们后续会从这个 txid 来转出 Doge 代币，所以要记得留下这个 txid 的记录，我的交易哈希是：<code>e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a</code>。挖一个区块来确认交易：</p><pre><code class="bash">bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>如果一切顺利，交易数据就应该已经上链了。那么链下的解析器在拿到这笔交易后，会看到发行 Rune 的操作，并且解析出来。如果想确认下 <code>OP_RETURN</code> 的数据是不是写对了，可以解码一下刚才广播的交易详情：</p><pre><code class="bash">bitcoin-cli -datadir=./ decoderawtransaction 02000000000101fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac02473044022004a2553cc5348dd4521c093149b0ba5e5603fe4134d06a455e12abeac097ea19022076e72632b2488e1316e54559ed733b37de9ce7fd04119e78a59546a3d2c1faea0121020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc100000000</code></pre><p>输出的结果是这样：</p><pre><code class="json">&quot;vout&quot;: [    &#123;      &quot;value&quot;: 0.00000000,      &quot;n&quot;: 0,      &quot;scriptPubKey&quot;: &#123;        &quot;asm&quot;: &quot;OP_RETURN 7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d&quot;,        // ...      &#125;    &#125;,    &#123;      &quot;value&quot;: 49.99000000,      &quot;n&quot;: 1,      &quot;scriptPubKey&quot;: &#123; //... &#125;    &#125;  ]</code></pre><p>很明显看到了在第 0 个输出中，有 <code>OP_RETURN 十六进制数据</code> 的内容，如果还是不放心，可以 decode 一下 16进制字符串：</p><pre><code class="bash">echo -n &quot;7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d&quot; | xxd -r -p</code></pre><p>应该得到：</p><pre><code class="bash">&#123;op:0,symbol:Doge,supply:1000&#125;</code></pre><h3 id="3-转账-Runes-代币"><a href="#3-转账-Runes-代币" class="headerlink" title="3. 转账 Runes 代币"></a>3. 转账 Runes 代币</h3><p>刚才创建了 <code>Doge</code> 代币，发行量是 <code>1000</code>，我们接下来通过一笔转账交易，来转出这 1000 个 Doge。</p><p>首先得计算一下 rune_id，也就是我们刚才发行的 <code>Doge</code> 的唯一 ID 是什么，因为 Symbol 字符串是有可能重复的，而且占用字符空间也多，一般会做一些工程上的折中，比如对 txid 按照字节反序，然后取前 8 字节，得到一个 u64 长度的数字，像这样：</p><pre><code class="rust">fn calc_run_idby_txid()&#123;    let txid = &quot;e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a&quot;.to_string();    // 按字节反序，然后取前 8 字节    let mut bytes = hex::decode(txid).unwrap();    bytes.reverse();    let run_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());    println!(&quot;Run ID: &#123;&#125;&quot;, run_id);&#125;</code></pre><p>这个函数运行后会得到 <code>10367542271932362826</code>，我们把这个数字作为 rune_id，去构建转账 rune 需要的 json 数据：</p><pre><code class="rust">fn transfer_rune()&#123;    let transfer = TransferRune    &#123;        op: 1,        id: 10367542271932362826,        vout: 1,        amount: &quot;1000&quot;.to_string(),    &#125;;    println!(&quot;Transfer Rune JSON: &#123;&#125;&quot;, transfer.toJson());&#125;</code></pre><p>这个参数里要留意 <code>vout</code> 的值，它是接下来构建交易的时候，要转出到某个地址的 vout 的索引，和创建代币时候的交易没有任何关系。代码运行后得到这样的结果：</p><pre><code class="bash">Transfer Doge JSON: &#123;&quot;op&quot;:1,&quot;id&quot;:10367542271932362826,&quot;vout&quot;:1,&quot;amount&quot;:&quot;1000&quot;&#125;</code></pre><p>接下来就可以重复之前的步骤，把 json 转为 16进制字符串：</p><pre><code class="bash">echo -n &quot;&#123;&quot;op&quot;:1,&quot;id&quot;:10367542271932362826,&quot;vout&quot;:1,&quot;amount&quot;:&quot;1000&quot;&#125;&quot; | xxd -p -c 999</code></pre><p>我得到 <code>7b6f703a312c69643a31303336373534323237313933323336323832362c766f75743a312c616d6f756e743a313030307d</code>。</p><p>创建一个新地址用于接收 Doge：</p><pre><code class="bash">bitcoin-cli -datadir=./ getnewaddress</code></pre><p>我的新地址是：<code>bcrt1qc250507tws9z9wkurfcv3jue2nls6npzaqt7ka</code>。</p><p>利用刚才得到的参数，组装一笔转账 Doge 的交易：</p><pre><code class="bash">bitcoin-cli -datadir=./ createrawtransaction \&#39;[&#123;&quot;txid&quot;:&quot;e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a&quot;,&quot;vout&quot;:1&#125;]&#39; \&#39;[&#123;&quot;data&quot;:&quot;7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d&quot;&#125;,&#123;&quot;bcrt1q0n2x7030x59j5ql9pp6mw0tps74ag0znrdp45r&quot;:0.01&#125;,&#123;&quot;n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV&quot;:49.9798&#125;]&#39;</code></pre><p>这里和之前的交易略有不同，包含两个输出，第一个是接收 Doge 的地址，金额随意，因为重点在于 Doge 余额，而不是 BTC 余额。第二个参数则是找零地址，我们前面的交易里用到过。</p><p>剩下的操作轻车熟路，对这笔交易签名、把交易广播出去、挖一个新区块让交易确认：</p><pre><code class="bash"># 对交易签名bitcoin-cli -datadir=./ signrawtransactionwithwallet 02000000014af05f48c4e8e08f277d61c7542343729840e7c16465ba47ee982f8b0b1d06e20100000000fdffffff0300000000000000003d6a3b7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d40420f00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c53601fe729010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000# 广播交易bitcoin-cli -datadir=./ sendrawtransaction 02000000014af05f48c4e8e08f277d61c7542343729840e7c16465ba47ee982f8b0b1d06e2010000006a47304402201437a9e83ae0c6842ebd9d355af9c7be1f6f2eaa070b5d7a6e02e13ca8f2d13102206d05753c428f526b8c6636022991591517cc7d7982badfc633519cb44715957a0121026f441e8156148d0bb4963edaff187873f9800a37bb5f0731256e38d632031283fdffffff0300000000000000003d6a3b7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d40420f00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c53601fe729010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000# 得到交易哈希：80709a25e5355d51ee6d7fb625c40e9c4c49b049afa3aca18aeaa03bc685c1f0# 确认交易bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>到这一步，转账 Doge 的交易就完成并且上链了。</p><h3 id="4-解析-Runes-交易"><a href="#4-解析-Runes-交易" class="headerlink" title="4. 解析 Runes 交易"></a>4. 解析 Runes 交易</h3><p>你也许有点纳闷，这不就是发了两笔普通的比特币交易吗，只是放了两个 json 数据到交易上。Runes 就是这样，所有的操作，就在 <code>OP_RETURN</code> 允许的那 80 个字节的空间里完成。链上只记录 Runes 的操作，而不维护 Runes 的最终状态。包括有哪些代币、代币余额等信息，全部需要链下程序根据协议进行解析，状态也全部在链下程序维护。</p><p>我们首先可以通过已知的交易哈希，获取到这两笔交易的全部详情数据。如果交易哈希不是已知，可以监听扫描全部区块的全部交易，然后按照协议约定解析就行了。这里简化一点。</p><p>在 <code>Cargo.toml</code> 文件中导入依赖包：</p><pre><code class="toml">[dependencies]hex              = &quot;0.4&quot;bitcoin          = &quot;0.31&quot;          # Script &amp; consensus decodebitcoincore-rpc  = &quot;0.18&quot;          # RPC clientserde            = &#123; version = &quot;1.0&quot;, features = [&quot;derive&quot;] &#125;anyhow           = &quot;1.0&quot;</code></pre><p>通过 RPC 查询节点上的交易数据：</p><pre><code class="rust">// 导入必要依赖use bitcoin::&#123;Transaction, Txid&#125;;use bitcoincore_rpc::&#123;Auth, Client, RpcApi&#125;;use serde::Deserialize;use std::path::PathBuf;use std::str::FromStr;fn parse_tx()&#123;    // 去启动比特币节点的数据目录下，找用来 rpc 鉴权的 cookie 文件    let mut cookie = PathBuf::from(&quot;/Users/smallyu/work/github/bitcoin-regtest&quot;);    cookie.push(&quot;regtest/.cookie&quot;);    let rpc = Client::new(        &quot;http://127.0.0.1:18443&quot;,        Auth::CookieFile(cookie),    ).unwrap();    // 我们已知的交易哈希    let issue_txid    = Txid::from_str(&quot;e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a&quot;).unwrap();    let transfer_txid = Txid::from_str(&quot;80709a25e5355d51ee6d7fb625c40e9c4c49b049afa3aca18aeaa03bc685c1f0&quot;).unwrap();    // 这里会得到完整的交易数据    let issue_hex    = rpc.get_raw_transaction_hex(&amp;issue_txid, None).unwrap();    let transfer_hex = rpc.get_raw_transaction_hex(&amp;transfer_txid, None).unwrap();    println!(&quot;Issue Hex: &#123;&#125;&quot;, issue_hex);    println!(&quot;Transfer Hex: &#123;&#125;&quot;, transfer_hex);    // 调用函数来解析交易    parse_op_return(issue_hex);    parse_op_return(transfer_hex);&#125;</code></pre><p>这个函数在运行的时候，会从链上节点，查询出真实的已经上链的交易数据。接下来可以对这两笔交易的 Runes 操作做解析：</p><pre><code class="rust">fn parse_op_return(tx_str: String)&#123;    let tx: Transaction = bitcoin::consensus::deserialize(&amp;hex::decode(tx_str).unwrap()).unwrap();    let script = tx.output[0].script_pubkey.clone();    // OP_RETURN, DATA    let mut iter = script.instructions();    let mut op_return = iter.next();    let mut data = iter.next();    // 解析数据    match op_return    &#123;        Some(Ok(op_return)) =&gt;            &#123;                match data                &#123;                    Some(Ok(data)) =&gt;                        &#123;                            match (data)                            &#123;                                bitcoin::blockdata::script::Instruction::PushBytes(bytes) =&gt;                                    &#123;                                        let json_str = std::str::from_utf8(bytes.as_ref()).unwrap();                                        println!(&quot;&#123;&#125;&quot;, json_str);                                    &#125;                                _ =&gt; panic!(&quot;Expected OP_RETURN with data&quot;),                            &#125;                        &#125;                    _ =&gt; panic!(&quot;No data found in OP_RETURN&quot;),                &#125;            &#125;        _ =&gt; panic!(&quot;No OP_RETURN found in script&quot;),    &#125;&#125;</code></pre><p>解析数据的代码部分，写法上嵌套有点多，只是因为我不喜欢用语法糖。想看起来更舒服的话，也可以在代码写法上做调整，变得更精简，不过无论写法如何，代码干的事情都一样。这个函数会从交易数据里，经过层层解析，打印出这样的结果：</p><pre><code class="bash">&#123;op:0,symbol:Doge,supply:1000&#125;&#123;&quot;op&quot;:1,&quot;id&quot;:10367542271932362826,&quot;vout&quot;:1,&quot;amount&quot;:&quot;1000&quot;&#125;</code></pre><p>这样，我们就看到了期望的两个 Runes 动作，第一个是发行 Doge，第二个是对 Doge 进行转账。</p><p>以上过程就是 Runes 协议比较核心的内容，剩下的只需要把链下程序扩充一下，记录 Runes 状态、根据 rune_id 关联 Rune 操作、储存和显示余额变更等信息就可以了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;比特币符文（Runes）在技术原理上比较简单，很容易理解，实现 Runes 只需要用到比特币脚本中的 &lt;code&gt;OP_RETURN&lt;/code&gt; 操作符。也是正因为简单，所以在技术特性上， Runes 相对干净一点，没有铭文和 RGB 那么复杂的链下状态。而 Runes</summary>
        
      
    
    
    
    
    <category term="教程" scheme="https://smallyu.net/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="BTC" scheme="https://smallyu.net/tags/BTC/"/>
    
  </entry>
  
  <entry>
    <title>比特币脚本开发教程</title>
    <link href="https://smallyu.net/2025/07/10/%E6%AF%94%E7%89%B9%E5%B8%81%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B/"/>
    <id>https://smallyu.net/2025/07/10/%E6%AF%94%E7%89%B9%E5%B8%81%E8%84%9A%E6%9C%AC%E5%BC%80%E5%8F%91%E6%95%99%E7%A8%8B/</id>
    <published>2025-07-09T16:42:10.000Z</published>
    <updated>2026-08-04T11:43:16.647Z</updated>
    
    <content type="html"><![CDATA[<p>比特币脚本有点像房间里的大象，大家都知道这个东西，但是大家都看不见，或者不在乎。这个教程将从最基本的操作开始，理解比特币脚本的原理，学会自己写比特币脚本。因为比特币脚本不是图灵完备的，所以包含很多命令行操作，以及需要观察输出结果。</p><h3 id="1-启动本地节点"><a href="#1-启动本地节点" class="headerlink" title="1. 启动本地节点"></a>1. 启动本地节点</h3><p>运行这个命令安装 <code>bitcoind</code> 的二进制，然后用 <code>bitcoind --help</code> 来测试是否安装成功：</p><pre><code class="bash">brew install bitcoin</code></pre><p>创建一个用于测试使用的目录，比如我的目录名称是 <code>bitcoin-regtest</code>：</p><pre><code class="bash">mkdir ./bitcoin-regtestcd ./bitcoin-regtest</code></pre><p>在这个目录下新建一个叫 <code>bitcoin.conf</code> 文件，复制这些配置内容进去：</p><pre><code class="conf">regtest=1txindex=1fallbackfee=0.0001</code></pre><p>这是本地节点的配置文件，后续我们的比特币脚本将基于本地启动的开发节点来测试。这个配置文件中的 <code>regtest=1</code> 比较关键，指明了节点的类型是本地开发网络，不会真的到公网上同步区块数据，本地节点的块高度将从 0 开始。另外两个配置 <code>txindex=1</code> 是指启动本地节点对所有交易的索引，方便我们后续查看交易，<code>fallbackfee=0.0001</code> 则是指明交易手续费的大小。</p><p>停留在包含配置文件的当前目录下，执行这个命令来启动节点。这里的命令行，以及后续的命令行，都会带上 <code>-datadir</code> 参数，因为我们希望节点数据是隔离的，每一个工作目录都是一份新的环境，不至于污染电脑的全局环境，而且默认环境的路径比较长，不同操作系统不一致，虽然我们在后续的命令里都需要带上这么一个参数，看起来有点麻烦，但同时也避免了很多其他问题，比如找不到系统默认目录在哪儿之类：</p><pre><code class="bash">bitcoind -datadir=./ -daemon</code></pre><p>命令成功执行会看到 <code>Bitcoin Core starting</code> 的字样。为了测试节点是否真的在运行，可以用这个命令查看节点的状，会得到一个 json 数据：</p><pre><code class="bash">bitcoin-cli -datadir=./ getblockchaininfo</code></pre><p>如果还是对节点的运行状态不放心，可以直接查看节点的日志文件。这就是我们指定了数据目录的好处，日志文件在这个位置：</p><pre><code class="bash">cat ./regtest/debug.log</code></pre><p>如果想要停掉节点，避免后台进程一直在电脑上运行，用这个命令来停止节点：</p><pre><code class="bash">bitcoin-cli -datadir=./ stop</code></pre><p>注意启动节点用的是 <code>bitcoind</code>，停止节点用的是 <code>bitcoin-cli</code>。前者属于 server 端的命令，后者属于 client 端的命令。</p><p>另外，如果在停止节点后重启节点，发现钱包（下一小节内容）不能用了，可以用这个命令来导入钱包：</p><pre><code class="bash">bitcoin-cli -datadir=./ loadwallet learn-script </code></pre><h3 id="2-创建钱包"><a href="#2-创建钱包" class="headerlink" title="2. 创建钱包"></a>2. 创建钱包</h3><p>运行这个命令来创建一个比特币钱包：</p><pre><code class="bash">bitcoin-cli -datadir=./ createwallet &quot;learn-script&quot;</code></pre><p>我们刚提到命令行中使用 <code>-datadir</code> 参数来指定数据目录，那么钱包的文件其实也会在数据目录下保存，可以看一下 <code>./regtest/wallets</code> 目录，有一个 <code>learn-script</code> 的文件夹，我们刚才创建的钱包就在这个文件夹内。</p><p>查看钱包地址的命令，比如我的地址是 <code>bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code>：</p><pre><code class="bash">bitcoin-cli -datadir=./ getnewaddress</code></pre><p>接着在本地节点上，给钱包地址挖一些钱出来，这里的参数 <code>101</code> 是指挖 101 个区块。为什么是 101 个区块呢？一般我们挖的区块数量会大于 100，因为比特币网络有 100 个区块的成熟期，也就是区块奖励需要在 100 个区块之后，才可以消费。假如我们只挖了 99 个区块，虽然理论上应该得到很多区块奖励，但实际上是不能花费的。</p><pre><code class="bash">bitcoin-cli -datadir=./ generatetoaddress 101 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>这个命令运行输出的是每个块的区块哈希。运行结束后，我们就可以查看钱包地址的余额了，余额应该是 50：</p><pre><code class="bash">bitcoin-cli -datadir=./ getbalance</code></pre><p>为什么是 50？因为比特币的区块奖励每 4 年减半，第一次减半之前的块奖励，每个区块都是 50 BTC。为什么挖了 101 个块，但只能查到 50 BTC 的余额？因为后 100 个区块的成熟期，奖励是不到账的。</p><h3 id="3-发送交易"><a href="#3-发送交易" class="headerlink" title="3. 发送交易"></a>3. 发送交易</h3><p>那么现在我们已经有了本地在运行的节点，以及有余额的钱包，接下来可以发起一笔普通的转账交易。先生成一个用于接收转账的新地址，我生成的地址是 <code>bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc</code>：</p><pre><code class="bash">bitcoin-cli -datadir=./ getnewaddress</code></pre><p>可以查看验证一下，新生成的地址余额为 0。这个命令中的参数 <code>0</code> 意味着查询结果包含未确认的交易。</p><pre><code class="bash">bitcoin-cli -datadir=./ getreceivedbyaddress bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc 0</code></pre><p>接着使用发起交易的命令，来向新生成的地址转账 0.01 BTC：</p><pre><code class="bash">bitcoin-cli -datadir=./ sendtoaddress bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc 0.01</code></pre><p>这个命令会返回交易哈希，比如我的哈希值是 <code>81be2e97507d7a274029ec4d5ce9728a54fe6d885aa0f12a13ec6f54eee66c26</code>。我们需要用这个交易哈希来查询交易结果和交易详情，像这样：</p><pre><code class="bash">bitcoin-cli -datadir=./ gettransaction 81be2e97507d7a274029ec4d5ce9728a54fe6d885aa0f12a13ec6f54eee66c26</code></pre><p>这笔交易此时就已经提交到链上了，但是也许你会注意到，查询交易详情返回的交易状态中，有一个 <code>&quot;confirmations&quot;: 0</code>，意味着交易还没有被确认，而且区块高度还停留在 <code>lastprocessedblock: 101</code> 上。因为比特币不会自动出块，这个时候查询接收地址的余额，能看出差异：</p><pre><code class="bash"># 查询到余额是 0.01bitcoin-cli -datadir=./ getreceivedbyaddress bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc 0# 查询到余额是 0bitcoin-cli -datadir=./ getreceivedbyaddress bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc 1</code></pre><p>因为我们之前有说明，最后一个参数是 0 代表包含未确认的交易，否则只查询确认的交易。我们刚刚发送的交易就还没有确认。如果想确认下来，就得用之前的 <code>generatetoaddress</code> 命令再挖一个区块出来：</p><pre><code class="bash">bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>现在再去查询交易状态，无论是确认数还是钱包余额，就都符合预期了。</p><h3 id="4-查看交易脚本"><a href="#4-查看交易脚本" class="headerlink" title="4. 查看交易脚本"></a>4. 查看交易脚本</h3><p>我们刚才发送的是一笔 P2WPKH 交易，因为现在比特币客户端默认使用原生 SegWit 的地址格式。</p><p>先了解一下 P2PKH 是什么，全称是 Pay to Public-Key Hash，我们使用的比特币地址本身就是一个公钥的子集，而 P2PKH 交易以账户地址为接收参数，所以命名为 P2PKH。我们常说的比特币原生地址，就是指 P2PKH 格式，一般以 <code>1</code> 开头，</p><p>相比 P2PKH，原生 SegWit 的地址格式叫 P2WPKH，中间多了个字母 W，全称是 Pay to Witness Public-Key Hash，特点是会把签名数据放在 witness 字段里，而不是每一笔 UTXO 的输出里，我们可以具体看一下，首先根据交易哈希，查询得到交易的全部数据：</p><pre><code class="bash">bitcoin-cli -datadir=./ getrawtransaction 81be2e97507d7a274029ec4d5ce9728a54fe6d885aa0f12a13ec6f54eee66c26</code></pre><p>会得到一大段编码后的数据，用这个命令来解码交易数据：</p><pre><code class="bash">bitcoin-cli -datadir=./ decoderawtransaction 020000000001018f4e8514038b93d6cc1d4f77b011f4726ba765d338bfdf1e6724d1844bc5d36e0000000000fdffffff0240420f0000000000160014400a517208b473618b98817840328c09a77d6b123eaaf629010000001600147ef4555b42b71e6ebecd687170c92ab64cce35500247304402202417ff3f6959a7d449849ae78fd5272826339cd7096ab02cdd7eccfc7779fb14022077e43ce155259a602b6172261b1d830d30e0de8b06cd6479cac02ea7c6928ff10121020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc196000000</code></pre><p>查询得到的数据结构是这样：</p><pre><code class="json">&#123;  // ...  &quot;vin&quot;: [    &#123;      // ...      &quot;txinwitness&quot;: [        &quot;304402202417ff3f6959a7d449849ae78fd5272826339cd7096ab02cdd7eccfc7779fb14022077e43ce155259a602b6172261b1d830d30e0de8b06cd6479cac02ea7c6928ff101&quot;,        &quot;020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc1&quot;      ],    &#125;  ],  &quot;vout&quot;: [    &#123;      &quot;value&quot;: 0.01000000,      &quot;scriptPubKey&quot;: &#123;        &quot;asm&quot;: &quot;0 400a517208b473618b98817840328c09a77d6b12&quot;,        &quot;desc&quot;: &quot;addr(bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc)#nry368tt&quot;,        &quot;hex&quot;: &quot;0014400a517208b473618b98817840328c09a77d6b12&quot;,        &quot;address&quot;: &quot;bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc&quot;,        &quot;type&quot;: &quot;witness_v0_keyhash&quot;      &#125;    &#125;,    &#123;      &quot;value&quot;: 49.98998590,      &quot;scriptPubKey&quot;: &#123;        // ...      &#125;    &#125;  ]&#125;</code></pre><p>首先关注 <code>txinwitness</code> 这个字段，它是一个数字，有两个部分，第一个部分是签名数据，第二个部分是公钥，这就是我们之前提到的 SegWit，对金额的签名不放在 <code>vout</code> 里，而是放在了 <code>vin</code> 里。</p><p>然后再关注 <code>scriptPubKey</code> 里的 <code>asm</code>，ASM 是 RedeemScript 的意思，表示满足什么样的条件就可以消费脚本中锁定的金额。是的我们即使是发起普通转账，实际上也是一种比特币脚本，金额锁定在了脚本中。我们查询到的脚本内容分为两段，第一段是 <code>0</code>，表示比特币脚本中的一个操作码 <code>OP_0</code>，第二段是 <code>400a517208b473618b98817840328c09a77d6b12</code>，其实就是钱包地址，经过 bech32 编码后会变成熟悉的样子 <code>bcrt1qgq99zusgk3ekrzucs9uyqv5vpxnh66cjtwl6zc</code>。</p><h3 id="5-用-btcdeb-调试"><a href="#5-用-btcdeb-调试" class="headerlink" title="5. 用 btcdeb 调试"></a>5. 用 btcdeb 调试</h3><p>刚才提到了 <code>OP_0</code> 这个操作码，它具体是什么呢？操作码是比特币脚本的关键，我们可以用 <code>btcdeb</code> 工具调试和观察一下。btcdeb 没有提供一键式的安装命令，可以按照 <a href="https://github.com/bitcoin-core/btcdeb?tab=readme-ov-file#installation">官方的教程</a> 先下载源码，然后编译安装。验证安装结果：</p><pre><code class="bash">btcdeb --version</code></pre><p><code>OP_0</code> 这个操作码本身干的事情很简单，就是把空数据压进栈结构里，尝试运行命令：</p><pre><code class="bash">btcdeb OP_0</code></pre><p>会看到这样的输出：</p><pre><code class="bash">script  |  stack --------+--------0       | #0000 0</code></pre><p>前面的 <code>script</code> 表示有一个操作 <code>0</code>, 也就是 <code>OP_0</code>，这里显示的时候自动隐去了 <code>OP_</code> 前缀。后面 <code>#0000 0</code> 则表示目前栈里内容为 0（空）。接下来的输入 <code>step</code> 命令，让 btcdeb 真正运行 <code>OP_0</code> 这个步骤，运行结果是这样，可以看到推了一个空数据到栈里，这就是 <code>OP_0</code> 干的事情：</p><pre><code class="bash">step        &lt;&gt; PUSH stack </code></pre><p>为了增加理解，我们举一个别的操作码例子来观察栈内数据的变化，尝试这个命令：</p><pre><code class="bash">btcdeb &#39;[OP_2 OP_3 OP_ADD]&#39;</code></pre><p>然后输出 <code>step</code> 命令，一直按回车直到脚本结束，输出内容的过程像是这样。默认内容是这样，此时脚本里有 3 个操作码等待执行，分别是 <code>OP_2</code>、<code>OP_3</code> 和 <code>OP_ADD</code>：</p><pre><code class="bash">script  |  stack --------+--------2       | 3       | OP_ADD  | #0000 2</code></pre><p>第一次回车执行了脚本的第一个步骤 <code>OP_2</code>，对应操作把数字 2 压入栈，执行结束后脚本里剩 2 个操作码了，同时 <code>stack</code> 中有了数字 2：</p><pre><code class="bash">step        &lt;&gt; PUSH stack 02btcdeb&gt; script  |  stack --------+--------3       |      02OP_ADD  | #0001 3</code></pre><p>第二次回车继续执行了 <code>OP_3</code> 操作码，把数字 3 压入栈，此时脚本里只剩 1 个操作码，栈中有数字 2 和数字 3:</p><pre><code class="bash">        &lt;&gt; PUSH stack 03btcdeb&gt; script  |  stack --------+--------OP_ADD  |      03        |      02#0002 OP_ADD</code></pre><p>第三次回车执行 <code>OP_ADD</code> 操作码，这个操作码会从栈里弹出两个数字，计算加法后把结果推回栈内，得到结果 5:</p><pre><code class="bash">        &lt;&gt; POP  stack        &lt;&gt; POP  stack        &lt;&gt; PUSH stack 05btcdeb&gt; script  |  stack --------+--------        |      05</code></pre><p>因为 <code>btcdeb</code> 的命令行输出并不是特别直观，所以这里尽管占用篇幅，也有必要把整个过程的输出都复制过来，还拆分了步骤，方便理解每一步在干什么。可以看到每一个操作码都会对应一些行为，这个行为是比特币程序里定义的，包括加法、减法等各种运算，也有一些行为更复杂的操作，或者对简单的操作码进行排列组合，达到实现更复杂功能的目的。我们还看到比特币脚本的执行是基于栈的，全部行为都发生在栈结构里，栈结构也就意味着完全没有动态内存分配之类的东西。</p><h3 id="6-自己编写比特币脚本-1"><a href="#6-自己编写比特币脚本-1" class="headerlink" title="6. 自己编写比特币脚本 (1)"></a>6. 自己编写比特币脚本 (1)</h3><p>刚才尝试了在 btcdeb 调试工具里运算加法，现在试着在实际的比特币交易中，写入脚本代码，并且在链上运算。这段是原始的操作码形式的脚本，要注意这个脚本是不安全的，属于自验证的脚本，任何人都可以花费这个脚本中的金额，只是在花费过程中，脚本表示的数字运算会在链上执行：</p><pre><code>[OP_2 OP_3 OP_ADD OP_5 OP_EQUAL]</code></pre><p>首先需要把操作码转变为十六进制形式，这个编码过程需要手动，或者写代码来操作。我们使用手动的方式，这个 <a href="https://wiki.bitcoinsv.io/index.php/Opcodes_used_in_Bitcoin_Script">比特币文档</a> 中列出了全部支持的操作码，以及对应的十六进制字符，到我们这个小脚本这里，对应关系就是：</p><table><thead><tr><th align="center">操作码</th><th align="center">十六进制</th></tr></thead><tbody><tr><td align="center">OP_2</td><td align="center">52</td></tr><tr><td align="center">OP_3</td><td align="center">53</td></tr><tr><td align="center">OP_ADD</td><td align="center">93</td></tr><tr><td align="center">OP_5</td><td align="center">55</td></tr><tr><td align="center">OP_EQUAL</td><td align="center">87</td></tr></tbody></table><p>因此我们按照依次拼接的顺序，得到了的十六进制脚本：</p><pre><code>5253935587</code></pre><p>接着生成 P2SH 地址。P2SH 的全称是 Pay to Script Hash，意思是支付到脚本哈希，或者说锁定金额到脚本中，相当于链上脚本的地址：</p><pre><code class="bash">bitcoin-cli -datadir=./ decodescript 5253935587</code></pre><p>命令输出中有一个 <code>p2sh-segwit</code> 字段，值是 <code>2NAzGPjCcg8DiykVTKLJRYbU2fejCEbdPbX</code>，把这个 P2SH 地址用作参数生成脚本的校验和，校验和是构造比特币交易必须要的一个参数：</p><pre><code class="bash">bitcoin-cli -datadir=./ getdescriptorinfo &quot;addr(2NAzGPjCcg8DiykVTKLJRYbU2fejCEbdPbX)&quot;</code></pre><p>得到 <code>descriptor</code> 的值为 <code>addr(2NAzGPjCcg8DiykVTKLJRYbU2fejCEbdPbX)#s260u65e</code>，后续用这个值作为脚本参数构造交易。</p><p>不过到这里还有个坑，比特币的 P2SH 脚本，只能用观察模式的钱包导入，所以需要新创建一个没有私钥的钱包：</p><pre><code class="bash">bitcoin-cli -datadir=./ createwallet &quot;arith-watch&quot; true true &quot;&quot; true</code></pre><p>用刚刚创建的新钱包，导入 P2SH 脚本。看到这个命令返回 <code>&quot;success&quot;: true</code>，才表示导入成功：</p><pre><code class="bash">bitcoin-cli -datadir=./ -rpcwallet=arith-watch importdescriptors &#39;[&#123;&quot;desc&quot;:&quot;addr(2NAzGPjCcg8DiykVTKLJRYbU2fejCEbdPbX)#s260u65e&quot;,&quot;timestamp&quot;:&quot;now&quot;,&quot;label&quot;:&quot;arith-2+3=5&quot;&#125;]&#39;</code></pre><p>现在有了 P2SH 的脚本地址，并且已经把脚本导入到钱包，接下来可以给脚本打钱了。这个命令从 learn-script 钱包转账 0.01 BTC 给脚本：</p><pre><code class="bash">bitcoin-cli -datadir=./ -rpcwallet=learn-script sendtoaddress 2NAzGPjCcg8DiykVTKLJRYbU2fejCEbdPbX 0.01</code></pre><p>挖一个区块让交易确认：</p><pre><code class="bash">bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>现在，这个脚本就上链并且有余额了。</p><h3 id="7-自己编写比特币脚本-2"><a href="#7-自己编写比特币脚本-2" class="headerlink" title="7. 自己编写比特币脚本 (2)"></a>7. 自己编写比特币脚本 (2)</h3><p>目前这个脚本地址里的钱，任何人都可以消费，消费的同时会运算一下 <code>2+3</code> 这个表达式，并且判断结果是否为 <code>5</code>。接下来构建一笔花费脚本金额的交易，真正花掉刚才存进脚本的钱。准备一个收款地址：</p><pre><code class="bash">bitcoin-cli -datadir=./ -rpcwallet=learn-script getnewaddress</code></pre><p>我新建的地址是 <code>bcrt1q0n2x7030x59j5ql9pp6mw0tps74ag0znrdp45r</code>。用这个地址构建一笔交易，注意这里 <code>inputs</code> 中的 <code>txid</code>，是刚才给 P2SH 转账的那一笔交易哈希：</p><pre><code class="bash">bitcoin-cli -datadir=./ -named createrawtransaction \  inputs=&#39;[&#123;&quot;txid&quot;:&quot;b952acd06a4f7edd7b2d5da0d509d01dfbb8e49fa15123d9cd5d3d23f944cdc2&quot;,&quot;vout&quot;:0&#125;]&#39; \  outputs=&#39;&#123;&quot;bcrt1q0n2x7030x59j5ql9pp6mw0tps74ag0znrdp45r&quot;:0.009&#125;&#39;</code></pre><p>在构建的交易中添加自动找零参数：</p><pre><code class="bash">bitcoin-cli -datadir=./ -rpcwallet=learn-script \  fundrawtransaction 0200000001c2cd44f9233d5dcdd92351a19fe4b8fb1dd009d5a05d2d7bdd7e4f6ad0ac52b90000000000fdffffff01a0bb0d00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c5300000000</code></pre><p>关键的一步，用钱包给这笔交易签名，注意这里是给找零之后的交易数据进行签名，如果不找零，节点会把找零金额当作手续费，而节点默认还有手续费的上限值，如果这一步没找零，下一步会触发手续费上限报错：</p><pre><code class="bash">bitcoin-cli -datadir=./ -rpcwallet=learn-script \  signrawtransactionwithwallet 0200000001c2cd44f9233d5dcdd92351a19fe4b8fb1dd009d5a05d2d7bdd7e4f6ad0ac52b90000000000fdffffff02a0bb0d00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c5360a0d92901000000160014a3e136e24d5a8db14f15016b99fb21ea4b0b69da00000000</code></pre><p>最后，把签名好的交易数据广播出去就行了：</p><pre><code class="bash">bitcoin-cli -datadir=./ sendrawtransaction 02000000000101c2cd44f9233d5dcdd92351a19fe4b8fb1dd009d5a05d2d7bdd7e4f6ad0ac52b90000000017160014c2d5ade24c1d0b9f27f651a71c3fe49d23d0ae13fdffffff02a0bb0d00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c5360a0d92901000000160014a3e136e24d5a8db14f15016b99fb21ea4b0b69da024730440220406a51d43ade05b240fcf2d14b58c90f31ebc705ab262189949355cac54d0431022051b592c570ef960a35e8509766e903ba836e3bcd1fb3c5cc211f0ff3442283550121021ff283ca8c9ecb45c8e19eacb7e8ae6fcb27d8addd38011d633e396487db44e300000000</code></pre><p>记得再挖一个区块让交易确认：</p><pre><code class="bash">bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw</code></pre><p>查看交易状态，验证交易已被花费，如果返回空值，说明已被花费。这里查的交易哈希是当时用钱包给脚本转账 0.01 BTC 那一笔交易的哈希：</p><pre><code class="bash">bitcoin-cli -datadir=./ gettxout b952acd06a4f7edd7b2d5da0d509d01dfbb8e49fa15123d9cd5d3d23f944cdc2 0</code></pre><h3 id="8-Troubleshooting"><a href="#8-Troubleshooting" class="headerlink" title="8. Troubleshooting"></a>8. Troubleshooting</h3><p>我本地的操作环境以及软件脚本是：</p><pre><code class="text">OS: MacOSbitcoind: v29.0.0btcdeb：5.0.24</code></pre>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;比特币脚本有点像房间里的大象，大家都知道这个东西，但是大家都看不见，或者不在乎。这个教程将从最基本的操作开始，理解比特币脚本的原理，学会自己写比特币脚本。因为比特币脚本不是图灵完备的，所以包含很多命令行操作，以及需要观察输出结果。&lt;/p&gt;
&lt;h3</summary>
        
      
    
    
    
    
    <category term="教程" scheme="https://smallyu.net/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="BTC" scheme="https://smallyu.net/tags/BTC/"/>
    
  </entry>
  
  <entry>
    <title>我从王垠的计算机科学视频班学到了什么</title>
    <link href="https://smallyu.net/2025/07/08/%E6%88%91%E4%BB%8E%E7%8E%8B%E5%9E%A0%E7%9A%84%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%E8%A7%86%E9%A2%91%E7%8F%AD%E5%AD%A6%E5%88%B0%E4%BA%86%E4%BB%80%E4%B9%88/"/>
    <id>https://smallyu.net/2025/07/08/%E6%88%91%E4%BB%8E%E7%8E%8B%E5%9E%A0%E7%9A%84%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%E8%A7%86%E9%A2%91%E7%8F%AD%E5%AD%A6%E5%88%B0%E4%BA%86%E4%BB%80%E4%B9%88/</id>
    <published>2025-07-07T19:08:42.000Z</published>
    <updated>2026-02-28T07:30:46.814Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>为了行文方便，这篇文章不使用 “王垠老师” 这样的尊称，直接称呼名字 “王垠”。</p></blockquote><p>大约 2 个月前，我开始报名学习王垠的 <a href="https://www.yinwang.org/blog-cn/2025/05/12/cs-video-course">计算机科学视频班</a>（基础班），经过 1 个月的学习后毕业，大概用了 120 个小时的学习时长。“120 个小时” 这个数字是经过认真估算的，包含了观看视频的时间和做练习题的时间。因为视频课的学习节奏由自己把控，毕业速度因人而异，所以 “1 个月” 这样的描述不够直观，用小时数更加容易理解。 120 个小时意味着，如果一天学 2 个小时的话，需要 60 天。</p><p>王垠并不认识我，但我早就认识王垠；我以前不是王垠的学生，但王垠早就是我的老师了。我关注王垠的博客多年，已经从他的博客文章中受益很多。而这次系统学习了视频课的课程，像是打开了新世界的大门。这并不夸张，我可以负责任地站在学习过课程的立场上说，王垠没有吹牛，他的课程真的有他说的那么好。你可以不赞同王垠的观点，也可以不喜欢王垠的人生态度，但是不可能怀疑王垠在计算机科学，尤其是编程语言领域的研究水平，也完全不需要担心王垠这样对自己和世界都如此较真的人，会拿一些没有含量的课程出来忽悠人。</p><p>现在毕业后经过半个月时间的沉淀，我想分享一下 “从课程中能学到什么” 这个话题。没有在毕业当天做总结，是因为怕有点浮燥总结不全，或者掺杂太多个人经历。半个月的时间其实也不够，我没有太多时间复习，课程也远远没来得及发挥出应有的成果，但是现在做总结并不妨碍以后对课程内容有进一步的感悟。王垠曾经有一篇文章《爱因斯坦谈教育》，里面提到爱因斯坦说 “被传授的知识应该被当成宝贵的礼物”，我在学完课程后无意间翻到这篇文章，看到这句话后，突然对这句话有了切实的理解。</p><p>因为课程内容是保密的，我不会透露课程具体的学习内容，只基于公开的招生说明中的内容进行解释。对于计算机初学者来说，从课程中能学到的最直接的知识，就是函数、链表、二叉树等基础的编程概念，内容涵盖了大学本科阶段的整个过程。而第 6 课关于解释器的部分，属于课程的 “最终成果”，对应一些美国顶尖大学本科高年级，至硕士低年级的阶段。为什么王垠在招生说明里的描述是 “大学博士阶段才可能学到的内容” 呢？因为中国的大学没有编程语言专业，本科课程没有教解释器的，即使到了研究生阶段，lambda 演算也属于选修课，博士早期才会接触到解释器的实现是正常现象。所以王垠的描述真的没有夸大。</p><p>解释器这种内容在中国的教育体系里本来就很稀有，比较高级和精练的教程更是少见。举例来说，当你学完了王垠的课，然后去 bilibili 上搜一些解释器相关的教程，你就会明白这些公开教程里的解释器有多么差劲，不但一眼就能看出它们缺什么要素、存在什么问题，还知道如何改进、如何用最简洁的代码写出最可靠的实现。为什么解释器这个东西重要呢？同样举个例子，以太坊的虚拟机（EVM）就是一个解释器，只不过 EVM 并不是在对编程语言做解释，而是在对以太坊的操作码（Opcodes）做解释，每个操作码都对应在栈结构上的一个动作。所以学过了解释器之后，对 EVM 的原理会有不一样的理解。</p><p>对于有计算机经验的学生来说，从课程上可以学到的，就不只是表面上的知识了。比如，课程只用到非常少的编程要素，就表达了第 1 课到第 6 课的全部内容，如果王垠不是对计算机理论有非常深刻的理解，不可能做到这种地步的深入简出。从学习者的角度，一方面可以思考一下为什么课程内容能如此精致，组织这些课程内容的思路是什么，这种高度抽象的思维背后，需要怎样的功底。另一方面由于课程内容自成一体，学习者完全有可能做到自己复刻整个课程内容，就像是手里的一个精致的玩物，随时可以拿出来复习把玩。</p><p>王垠最近在微博上评论 AI 编程的时候提到，AI 无法写出 “王垠级别” 的代码。什么是 “王垠级别” 的代码呢？上过课就知道了。我在做练习题的时候，被助教提醒最多次的问题，就是 “代码复杂”，有时候是写法上的复杂，有时候是复杂度上的复杂，但是每一次把代码写到符合课程标准之后，又不禁感叹原来代码可以如此精巧。我已经有多年的编程经验，让代码运行出练习题的结果并不难，但是把代码写的足够漂亮却不容易。假如以前给公司写的都是这种质量的代码，那公司可就太占便宜了。</p><p>还不止这些，也许有人看到招生说明会怀疑，一节选修课真能让人学会一种新的编程语言吗？我想提醒的是，不要忘了给你讲课的人是谁，是真正的编程语言专家。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;为了行文方便，这篇文章不使用 “王垠老师” 这样的尊称，直接称呼名字 “王垠”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大约 2 个月前，我开始报名学习王垠的 &lt;a</summary>
        
      
    
    
    
    
    <category term="学习" scheme="https://smallyu.net/tags/%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="王垠" scheme="https://smallyu.net/tags/%E7%8E%8B%E5%9E%A0/"/>
    
  </entry>
  
  <entry>
    <title>区块链技术面试题（2025年版）</title>
    <link href="https://smallyu.net/2025/07/06/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95%E9%A2%98%EF%BC%882025%E5%B9%B4%E7%89%88%EF%BC%89/"/>
    <id>https://smallyu.net/2025/07/06/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95%E9%A2%98%EF%BC%882025%E5%B9%B4%E7%89%88%EF%BC%89/</id>
    <published>2025-07-06T10:38:03.000Z</published>
    <updated>2025-11-09T09:22:13.316Z</updated>
    
    <content type="html"><![CDATA[<p>比起 2023 年版本相对宏观视角的《<a href="/2023/07/12/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95%E9%A2%98/">区块链技术面试题</a>》，这个版本稍微侧重工程实践一点，包含了更多技术细节。这两个版本的内容是互相补充的，不是升级性质的关系。这些题目仅仅只是基于我的个人经历，就像很多面试官在做的那样，自己会什么才问什么，问不出自己不会的东西，所以问出来的问题，无论广度和深度，都是受限于个人水平的，我也是：</p><ol><li>以太坊客户端为什么分为执行层和共识层？</li><li>以太坊的 PoS 运作流程，如何初始化一个 PoS 网络？</li><li>以太坊 PoS 的软分叉和恢复机制？Cardano 的 PoS 和以太坊一样吗？</li><li>以太坊节点有哪些类型，分别适用于什么场景？</li><li>EVM 的执行为什么是单线程的？为什么至今全世界的团队都做不出来 “并行EVM” 这种东西？</li><li>Solidity 语言有 GC 吗？是如何处理内存动态分配问题的？</li><li>Solidity 什么场景下需要内联汇编？</li><li>PBFT 共识有了解吗，大体流程是怎么样的？</li><li>PBFT 的容错能力公式是怎么来的，为什么是那个数字，而不是其他数字？</li><li>PBFT 为什么需要第二次投票？</li><li>Solana 的共识机制大体是怎样的？TowerBFT 是在对区块投票吗？</li><li>为什么 Solana 的智能合约可以并行执行，以太坊的不可以？</li><li>Cosmos 节点的升级流程是怎样的？和以太坊有什么不同？这种模式有什么风险？</li><li>Op Rollup 的大体流程？ZK Rollup 在 Op 模式的基础上，优化了哪个环节？</li><li>以太坊 L2 的资产跨链？与不同网络之间的资产跨链相比，技术上有什么异同？</li><li>以太坊最近有个大版本升级，引入的 EIP-7702 是干什么的？和 AA 钱包是什么关系？</li><li>自己平时思考过哪些区块链相关的、有意思的技术类话题？</li></ol><p>这些是我现在能想到的全部问题了。比这些问题更加有深度的工程化的内容，我也只是大概知道点方向，没亲手搞过。这两年的经历还算丰富，对比两个版本的面试题列表能看出不少变化。希望我自己可以再接再厉，不要迷路。</p><p>如果你是区块链行业的求职者，尤其是经验尚浅的工程师，千万不要被上面列出来的问题给吓到了。真实的面试过程中，几乎不会出现如此有深度的思考题。更多的问题类似于，“以太坊交易有哪些常用字段？”、“怎么取消一笔已经发送的交易？”、“Solidity 的可重入攻击是什么？”、“Op Stack 有哪几个组件？”、“以太坊合约的 create2 是什么？” 等等。放心大胆的去求职，真正懂技术的人没有那么多。</p><p>现在的区块链行业有个问题，就是没有系统化的理论知识，只有一些工业界前沿的、散碎的工程化尝试。比如对比编程语言专业，从丘奇和图灵的计算模型，到函数式编程语言、编译器、类型系统等，经过几十年学术界和工业界的发展，有高度抽象的理论支撑，有实际落地的工业应用，已经比较成熟。而区块链这种东西比较新，2008 年诞生，2013 年开始步入大众视野，短短几年的时间远没有建立起学术体系，行业内的项目方则各自为营，都在搞自己的标准、各自定义术语，账户模型、共识、合约、跨链，每条链都不一样。有人能统一区块链的理论体系吗？Vitalik 来都不行，要是 Satoshi 出山也许有希望。</p><p>因此不需要相信什么大学里的 “区块链专业”，没有出过校门的老师和教授，怎么可能有时间把区块链的技术抽象成理论、写成教材、编成课程，然后给学生讲课呢，这个周期得多长？也因此不要太相信已经出版的技术类书籍，书籍的出版需要几年时间，等书发表出来，世界已经变了。今年下半年有个比特币会议，两年前发明了铭文这个概念的项目方，可能又要发布新东西了，难道学校的课程或者书籍能跟得上这种节奏吗？行业最前沿的技术，只能来自各个项目方切实的探索和尝试，也自然就会造成不成体系的现象。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;比起 2023 年版本相对宏观视角的《&lt;a</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="面试题" scheme="https://smallyu.net/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    
  </entry>
  
  <entry>
    <title>Rust 语言容易让新手困惑的一个“过度优化”</title>
    <link href="https://smallyu.net/2025/06/30/Rust%E8%AF%AD%E8%A8%80%E5%AE%B9%E6%98%93%E8%AE%A9%E6%96%B0%E6%89%8B%E5%9B%B0%E6%83%91%E7%9A%84%E4%B8%80%E7%A7%8D%E8%BF%87%E5%BA%A6%E4%BC%98%E5%8C%96/"/>
    <id>https://smallyu.net/2025/06/30/Rust%E8%AF%AD%E8%A8%80%E5%AE%B9%E6%98%93%E8%AE%A9%E6%96%B0%E6%89%8B%E5%9B%B0%E6%83%91%E7%9A%84%E4%B8%80%E7%A7%8D%E8%BF%87%E5%BA%A6%E4%BC%98%E5%8C%96/</id>
    <published>2025-06-29T18:05:25.000Z</published>
    <updated>2025-07-01T13:18:38.203Z</updated>
    
    <content type="html"><![CDATA[<p>假如我们现在要写一些代码，随便用 <code>cargo new</code> 一个项目就行，然后写一个函数 <code>append</code>，函数的功能很好理解，就是把两个传入的字符串给拼接起来，第一个参数是字符串（的引用类型），第二个参数也是字符串，假如我们的参数是 <code>Hello</code> 和 <code>, world</code>，函数调用后会返回 <code>Hello, world</code> 给我们。函数具体这样写：</p><pre><code class="rust">fn append(s1: &amp;String, s2: &amp;String) -&gt; String &#123;    return s1.clone() + s2.clone().as_str();&#125;</code></pre><p>不需要关心 <code>return</code> 后面的语句写法，这不是我们关注的重点。在入口函数 <code>main</code> 里调用这个 <code>append</code>，运行一下，输出的内容会和我们预期一样，打印出拼接后的字符串 <code>Hello, world</code>：</p><pre><code class="rust">fn main() &#123;    let s1: String = String::from(&quot;Hello&quot;);    let s2: String = String::from(&quot;, world&quot;);    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));&#125;</code></pre><p>那么现在，保持 <code>append</code> 函数完全不变，在 <code>main</code> 函数里修改两个字符串的定义，整个 <code>main</code> 函数变成这样，猜一下输出结果会是什么？注意 Rust 是静态类型的语言，编译器对于变量类型往往具有严格的定义和判断：</p><pre><code class="rust">fn main() &#123;    let s1: Box&lt;String&gt; = Box::new(String::from(&quot;Hello&quot;));    let s2: Box&lt;String&gt; = Box::new(String::from(&quot;, world&quot;));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));&#125;</code></pre><p>我们首先的直觉是应该编译报错，因为 <code>s1</code> 的类型是 <code>Box&lt;String&gt;</code>，调用 <code>append</code> 函数的时候，传入的参数为 <code>&amp;s1</code>，对应的类型为 <code>&amp;Box&lt;String&gt;</code>，而显然 <code>append</code> 函数的定义是没有修改的，接收的参数类型仍然是 <code>&amp;String</code>。那么这种情况下，为什么编译器没有报错，而且代码还能正常运行，输出了 <code>Hello, world</code> 的结果？(先别管这里的 <code>Box</code> 是什么，反正是一种类型)</p><p>我们接着再修改一下 <code>main</code> 函数的内容，把字符串的定义改为这样：</p><pre><code class="rust">fn main() &#123;    use std::rc::Rc;    let s1: Rc&lt;String&gt; = Rc::new(String::from(&quot;Hello&quot;));    let s2: Rc&lt;String&gt; = Rc::new(String::from(&quot;, world&quot;));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));&#125;</code></pre><p>代码能通过编译吗？能正常运行吗？<code>append</code> 函数的定义仍然没有变，这里 <code>main</code> 函数中 <code>s1</code> 的类型变成了 <code>Rc&lt;String&gt;</code>，相应的传入 <code>append</code> 函数做参数的时候，类型变为了 <code>&amp;Rc&lt;String&gt;</code>。但是为什么，编译器没有报错，而且还能正常运行出结果，输出 <code>Hello, world</code>？（同样别管 <code>Rc</code> 是什么，也是一种类型）</p><p>根据刚才的代码片段，我们观察到一个现象：当函数的参数类型是 <code>&amp;String</code> 的时候，既可以接受 <code>&amp;String</code> 类型的参数，也可以接收 <code>&amp;Box&lt;String&gt;</code> 类型的参数，还可以接收 <code>&amp;Rc&lt;String&gt;</code> 类型的参数。</p><p>再疯狂一点，如果把 <code>main</code> 函数改成这样呢？</p><pre><code class="rust">fn main() &#123;    let s1: Box&lt;Box&lt;Box&lt;Box&lt;String&gt;&gt;&gt;&gt; = Box::new(Box::new(Box::new(Box::new(String::from(&quot;Hello&quot;)))));    let s2: Box&lt;Box&lt;Box&lt;Box&lt;String&gt;&gt;&gt;&gt; = Box::new(Box::new(Box::new(Box::new(String::from(&quot;, world&quot;)))));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));&#125;</code></pre><p>如果把 <code>main</code> 函数改成这样呢？</p><pre><code class="rust">fn main() &#123;    use std::rc::Rc;        let s1: Rc&lt;Rc&lt;Rc&lt;Rc&lt;String&gt;&gt;&gt;&gt; = Rc::new(Rc::new(Rc::new(Rc::new(String::from(&quot;hello&quot;)))));    let s2: Rc&lt;Rc&lt;Rc&lt;Rc&lt;String&gt;&gt;&gt;&gt; = Rc::new(Rc::new(Rc::new(Rc::new(String::from(&quot;, world&quot;)))));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));&#125;</code></pre><p>结果是 <code>main</code> 函数都可以正常运行，输出 <code>Hello, world</code> 的结果。</p><p>为了进一步观察关于类型的问题，现在新写两个 append 函数，<code>append2</code> 函数接收的类型是 <code>&amp;Box&lt;String&gt;</code>，而 <code>append3</code> 函数接收的类型是 <code>&amp;Rc&lt;String&gt;</code>：</p><pre><code class="rust">fn append2(s1: &amp;Box&lt;String&gt;, s2: &amp;Box&lt;String&gt;) -&gt; Box&lt;String&gt; &#123;    let mut result = (**s1).clone();    result.push_str(s2);    Box::new(result)&#125;use std::rc::Rc;fn append3(s1: &amp;Rc&lt;String&gt;, s2: &amp;Rc&lt;String&gt;) -&gt; Rc&lt;String&gt; &#123;    let mut result = (**s1).clone();    result.push_str(s2);    Rc::new(result)&#125;</code></pre><p>接下来分析一下，对于下面的 <code>main</code> 函数代码，编译器会在哪一行报错？</p><pre><code class="rust">fn main() &#123;    let s1: Box&lt;Box&lt;Rc&lt;Rc&lt;String&gt;&gt;&gt;&gt; = Box::new(Box::new(Rc::new(Rc::new(String::from(&quot;hello&quot;)))));    let s2: Box&lt;Box&lt;Rc&lt;Rc&lt;String&gt;&gt;&gt;&gt; = Box::new(Box::new(Rc::new(Rc::new(String::from(&quot;, world&quot;)))));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));    println!(&quot;&#123;&#125;&quot;, append2(&amp;s1, &amp;s2));    println!(&quot;&#123;&#125;&quot;, append3(&amp;s1, &amp;s2));&#125;</code></pre><p>这样呢，字符串的类型再扩展一下，编译器还会报错吗，在哪一行？</p><pre><code class="rust">fn main() &#123;    let s1: Box&lt;Box&lt;Rc&lt;Rc&lt;Box&lt;Box&lt;String&gt;&gt;&gt;&gt;&gt;&gt; = Box::new(Box::new(Rc::new(Rc::new(Box::new(Box::new(String::from(&quot;hello&quot;)))))));    let s2: Box&lt;Box&lt;Rc&lt;Rc&lt;Box&lt;Box&lt;String&gt;&gt;&gt;&gt;&gt;&gt; = Box::new(Box::new(Rc::new(Rc::new(Box::new(Box::new(String::from(&quot;, world&quot;)))))));    println!(&quot;&#123;&#125;&quot;, append(&amp;s1, &amp;s2));    println!(&quot;&#123;&#125;&quot;, append2(&amp;s1, &amp;s2));    println!(&quot;&#123;&#125;&quot;, append3(&amp;s1, &amp;s2));&#125;</code></pre><p>Rust 把这种语言特性叫做人体工学设计，为了减轻开发人员的负担。但是 Rust 在设计动不动会把变量给 move 掉、不得不使用 <code>&#39;</code> 单引号写法的时候，却放弃了人体工学，把内存安全放在了更重要的地位……倒是也没什么错，毕竟 Rust 只有内存安全是绝不能放松的。</p><p>最后再来个进阶难度的，假如在实际的业务场景中，有一个叫 <code>do_something</code> 的函数，接收泛型类型的参数，我们需要对这个函数基于原有逻辑做一些改动，原本的函数逻辑是这样：</p><pre><code class="rust">fn do_something&lt;T1, T2&gt;(t1: T1, t2: T2) &#123;    println!(&quot;&#123;&#125;&quot;, append(&amp;t1, &amp;t2));&#125;</code></pre><p>现在新增加一些处理：</p><pre><code class="rust">fn do_something&lt;T1, T2&gt;(t1: T1, t2: T2) &#123;    // 增加一个函数来处理 t1    handle_t1(&amp;t1);      println!(&quot;&#123;&#125;&quot;, append(&amp;t1, &amp;t2));&#125;</code></pre><p>那么问题来了，参数 <code>t1</code> 的类型是什么？<code>handle_t1</code> 函数的参数类型应该如何定义？在原有逻辑中，<code>t1</code> 作为参数对 <code>append</code> 函数进行了调用，是否意味着 <code>t1</code> 的类型是 <code>&amp;String</code>？如果不是 <code>&amp;String</code>，<code>t1</code> 的类型可能是什么？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;假如我们现在要写一些代码，随便用 &lt;code&gt;cargo new&lt;/code&gt; 一个项目就行，然后写一个函数</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
    <category term="Rust" scheme="https://smallyu.net/tags/Rust/"/>
    
  </entry>
  
  <entry>
    <title>Solana 智能合约开发教程 (1)</title>
    <link href="https://smallyu.net/2025/06/24/Solana%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B1/"/>
    <id>https://smallyu.net/2025/06/24/Solana%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B1/</id>
    <published>2025-06-24T13:51:06.000Z</published>
    <updated>2026-08-04T11:43:16.911Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>这是一个零基础的系列教程，可以从最基本的操作开始学会 Solana 智能合约的开发。</p><ul><li>《<a href="/2025/06/24/Solana%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B1/">第一篇</a>》：基础环境安装、HelloWorld 合约部署、链上合约调用</li><li>《<a href="/2025/06/26/Solana%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B2/">第二篇</a>》：实现 USDT 合约的最小模型，自定义数据结构与方法</li><li>《<a href="/2025/06/28/Solana%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B3/">第三篇</a>》：使用官方 SPL 库复用合约功能，完成标准化代币的发行</li></ul></blockquote><p>我们将从最基础的操作开始，学习 Solana 智能合约的开发。你只需要普通的编程基础，理解面向对象等概念就可以，不需要事先知道其他网络的智能合约概念，也不需要知道 Rust 语言的编程理念。</p><h3 id="1-安装环境"><a href="#1-安装环境" class="headerlink" title="1. 安装环境"></a>1. 安装环境</h3><p>访问 Solana 官方提供的安装教程：<a href="https://solana.com/docs/intro/installation">https://solana.com/docs/intro/installation</a></p><p>文档中提供了一键安装全部依赖的单个命令行，也有分阶段安装的详细教程。要注意其中 Solana Cli 是需要修改环境变量文件的。安装好一切后，<code>solana</code> 命令应该是可用的：</p><pre><code class="bash">solana --help</code></pre><h3 id="2-初始化项目"><a href="#2-初始化项目" class="headerlink" title="2. 初始化项目"></a>2. 初始化项目</h3><p>使用 anchor 命令来初始化一个智能合约的项目，这个命令行工具在上个步骤已经安装好了，可以先不用管生成的目录结构是什么样子：</p><pre><code class="bash">anchor init hello_solcd hello_sol</code></pre><h3 id="3-写入合约代码"><a href="#3-写入合约代码" class="headerlink" title="3. 写入合约代码"></a>3. 写入合约代码</h3><p><code>programs/hello_sol/src</code> 目录下有一个 <code>lib.rs</code> 文件，<code>.rs</code> 结尾意味着这是一个 Rust 语言的代码文件。把这些代码复制进去，注意 <code>declare_id</code> 中的内容是你的项目在初始化的时候，就会自动为你生成，不需要原封不动复制下面的内容：</p><pre><code class="rust">use anchor_lang::prelude::*;declare_id!(&quot;3Zbdw1oWu1CiMiQr3moQeT4XzMgeqmCvjH5R5wroDWQH&quot;);#[program]pub mod hello_sol &#123;    use super::*;    pub fn say_hello(ctx: Context&lt;Hello&gt;) -&gt; Result&lt;()&gt; &#123;        msg!(&quot;Hello, world!&quot;);        Ok(())    &#125;&#125;#[derive(Accounts)]pub struct Hello &#123;&#125;</code></pre><h3 id="4-编译智能合约"><a href="#4-编译智能合约" class="headerlink" title="4. 编译智能合约"></a>4. 编译智能合约</h3><p>使用 anchor 命令编译你刚才复制进去的智能合约代码，确保编译是成功的，代码没有写错。编译过程中可能会有一些警告，那些警告不要紧，因为 Rust 语言对于代码非常严格，很小的问题都会抛出大段的警告。如果一切顺利，命令行的输出不会有错误日志：</p><pre><code class="bash">anchor build</code></pre><h3 id="5-设置本地默认网络"><a href="#5-设置本地默认网络" class="headerlink" title="5. 设置本地默认网络"></a>5. 设置本地默认网络</h3><p>运行这个命令，让你本地的 solana 命令默认使用 devnet，因为 devnet 是给开发者使用的，可以用来测试自己的程序，而不需要真的花钱去买 SOL 代币：</p><pre><code class="bash">solana config set --url https://api.devnet.solana.com</code></pre><h3 id="6-创建本地账户文件"><a href="#6-创建本地账户文件" class="headerlink" title="6. 创建本地账户文件"></a>6. 创建本地账户文件</h3><p>这个命令用于在你本地的默认路径下，创建一个用来部署智能合约的 Solana 账户。因为部署智能合约需要消耗手续费，这些手续费需要一个账户来支付：</p><pre><code class="bash">solana-keygen new -o ~/.config/solana/id.json  </code></pre><p>这个命令的运行结果中，有一行 <code>pubkey: </code> 开头的输出，pubkey 后面的就是你本地的账户地址。因为上一个步骤已经设置了 devnet 为默认网络，所以可以直接使用这个命令来查看你本地账户的余额：</p><pre><code class="bash">solana balance</code></pre><p>也可以打开 devnet 的 <a href="https://explorer.solana.com/?cluster=devnet">浏览器</a>，搜索你刚才生成的地址。搜索之后的 URL 形如：<a href="https://explorer.solana.com/address/75sFifxBt7zw1YrDfCdPjDCGDyKEqLWrBarPCLg6PHwb?cluster=devnet">https://explorer.solana.com/address/75sFifxBt7zw1YrDfCdPjDCGDyKEqLWrBarPCLg6PHwb?cluster=devnet</a></p><p>当然，你会发现自己的账户余额是 <code>0 SOL</code>。</p><h3 id="7-领取-devnet-上的空投"><a href="#7-领取-devnet-上的空投" class="headerlink" title="7. 领取 devnet 上的空投"></a>7. 领取 devnet 上的空投</h3><p>运行这个命令，你的账户就可以收到 2 个 SOL。其中参数里的 2 就是请求发放 2 个 SOL 的意思。因为领水的额度限制，你只能一次性最多领 2 个。不用担心太少，足够我们接下来的步骤使用了。</p><pre><code class="bash">solana airdrop 2</code></pre><h3 id="8-部署合约到-devnet"><a href="#8-部署合约到-devnet" class="headerlink" title="8. 部署合约到 devnet"></a>8. 部署合约到 devnet</h3><p>现在我们已经有了智能合约代码，有了本地账户，并且本地账户里有 SOL 余额。现在可以部署合约到 devnet 上了。运行这个命令：</p><pre><code class="bash">anchor deploy --provider.cluster devnet </code></pre><p>如果部署成功，会看到 <code>Deploy success</code> 的字样。命令行输出中还有一行需要留意，<code>Program Id: </code> 后面的，就是部署之后的合约地址，你可以直接在 devnet 的浏览器上搜索这个地址，然后看到类似这个 URL 的页面，URL 中的 <code>3Zbdw1oWu1CiMiQr3moQeT4XzMgeqmCvjH5R5wroDWQH</code> 就是我部署的合约地址：<a href="https://explorer.solana.com/address/3Zbdw1oWu1CiMiQr3moQeT4XzMgeqmCvjH5R5wroDWQH?cluster=devnet">https://explorer.solana.com/address/3Zbdw1oWu1CiMiQr3moQeT4XzMgeqmCvjH5R5wroDWQH?cluster=devnet</a></p><h3 id="9-调用链上合约"><a href="#9-调用链上合约" class="headerlink" title="9. 调用链上合约"></a>9. 调用链上合约</h3><p>到 <code>hello_sol/app</code> 目录下，新建一个叫 <code>app.js</code> 的文件，把这些代码复制进去。简单来说，这段代码读取了你本地默认的账户文件，然后用你的 Solana 账户发起一笔对智能合约调用的交易，这个脚本每执行一次，就会在链上创建一笔交易。：</p><pre><code class="javascript">const anchor = require(&#39;@coral-xyz/anchor&#39;);const fs     = require(&#39;fs&#39;);const os     = require(&#39;os&#39;);const path   = require(&#39;path&#39;);const &#123; Keypair, Connection &#125; = anchor.web3;const RPC_URL    = process.env.RPC_URL;const connection = new Connection(RPC_URL, &#123; commitment: &#39;confirmed&#39; &#125;);const secretKey = Uint8Array.from(  JSON.parse(    fs.readFileSync(      path.join(os.homedir(), &#39;.config/solana/id.json&#39;),      &#39;utf8&#39;,    ),  ),);const wallet   = new anchor.Wallet(Keypair.fromSecretKey(secretKey));const provider = new anchor.AnchorProvider(connection, wallet, &#123;  preflightCommitment: &#39;confirmed&#39;,&#125;);anchor.setProvider(provider);const idlPath = path.resolve(__dirname, &#39;../target/idl/hello_sol.json&#39;);const idl     = JSON.parse(fs.readFileSync(idlPath, &#39;utf8&#39;));const program = new anchor.Program(idl, provider);(async () =&gt; &#123;  try &#123;    const sig = await program.methods.sayHello().rpc();    console.log(&#39;✅ tx&#39;, sig);    console.log(`🌐 https://explorer.solana.com/tx/$&#123;sig&#125;?cluster=devnet`);  &#125; catch (err) &#123;    console.error(&#39;❌&#39;, err);  &#125;&#125;)();</code></pre><p>返回 <code>hello_sol</code> 项目的顶层目录，执行这些命令来安装 nodejs 的依赖：</p><pre><code>npm init -y npm install @coral-xyz/anchor</code></pre><p>然后记得现在仍然是在顶层目录，运行这个命令，来执行刚才写的 <code>app.js</code> 脚本，脚本会到 devnet 上调用我们部署的智能合约：</p><pre><code class="bash">export RPC_URL=https://api.devnet.solana.comnode app/app.js</code></pre><p>这里有一个环境变量 <code>RPC_URL</code> 是脚本请求的 API 地址，因为 nodejs 脚本默认不走系统代理，所以对于网络受阻的同学，需要用一个比公开 RPC 更好用的 API 地址。可以使用例如 <a href="https://www.helius.dev/">Helius</a> 的服务，注册一个免费的账号就可以了。假如执行脚本的过程中遇到下面的错误，那就说明是网络问题，换一个好用的 RPC 地址就好了：</p><pre><code class="javascript">❌ Error: failed to get recent blockhash: TypeError: fetch failed    at Connection.getLatestBlockhash (/Users/smallyu/work/github/hello_sol/node_modules/@solana/web3.js/lib/index.cjs.js:7236:13)    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)    at async AnchorProvider.sendAndConfirm (/Users/smallyu/work/github/hello_sol/node_modules/@coral-xyz/anchor/dist/cjs/provider.js:89:35)    at async MethodsBuilder.rpc [as _rpcFn] (/Users/smallyu/work/github/hello_sol/node_modules/@coral-xyz/anchor/dist/cjs/program/namespace/rpc.js:15:24)    at async /Users/smallyu/work/github/hello_sol/app/app.js:40:17</code></pre><p>你也许好奇为什么不需要指定调用的合约地址，这个脚本怎么知道你刚才，部署到链上的合约在哪里？注意看脚本中有一个 <code>idlPath</code> 的变量，你可以直接打开这个路径的文件 <code>target/idl/hello_sol.json</code> 查看，里面是一些合约编译后的元信息，包括合约的地址也在里面，没错合约地址是离线生成的，不需要上链，合约就有属于自己的唯一地址了。</p><p>如果执行脚本没有输出错误，就会看到终端打印出了这一次调用合约的交易哈希，以及可以直接复制访问的浏览器 URL，例如这就是一笔调用合约的交易：<a href="https://explorer.solana.com/tx/2fnPgKkv3tGKKq72hhRxmW6WFSXuofMzXfY2UYoFZXTdJi37btdESy9NzS2gjpWzXX4CL5F7QfxugpctBVaMcBFY?cluster=devnet">https://explorer.solana.com/tx/2fnPgKkv3tGKKq72hhRxmW6WFSXuofMzXfY2UYoFZXTdJi37btdESy9NzS2gjpWzXX4CL5F7QfxugpctBVaMcBFY?cluster=devnet</a></p><p>这笔交易页面的最下方，可以看到我们写的智能合约在被交易调用后，打印出了 <code>Program logged: &quot;Hello, world!&quot;</code> 的日志，这正是我们写在合约代码中的 msg。</p><h3 id="10-Troubleshooting"><a href="#10-Troubleshooting" class="headerlink" title="10. Troubleshooting"></a>10. Troubleshooting</h3><p>如果在执行上述命令或者代码的过程中，遇到了错误，可以优先考虑是命令行工具版本的问题。由于区块链行业和技术迭代比较快，很容易出现版本不兼容的情况。我本地的环境和版本是：</p><pre><code class="text">rustup: rustup 1.28.2 (e4f3ad6f8 2025-04-28)rustc: rustc 1.90.0-nightly (706f244db 2025-06-23)solana: solana-cli 2.2.18 (src:8392f753; feat:3073396398, client:Agave)archor: anchor-cli 0.31.1node: v24.2.0@coral-xyz/anchor(nodejs): ^0.31.1</code></pre>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;这是一个零基础的系列教程，可以从最基本的操作开始学会 Solana 智能合约的开发。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《&lt;a</summary>
        
      
    
    
    
    
    <category term="教程" scheme="https://smallyu.net/tags/%E6%95%99%E7%A8%8B/"/>
    
    <category term="智能合约" scheme="https://smallyu.net/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
  </entry>
  
  <entry>
    <title>尝试开发一个最小 EVM 虚拟机</title>
    <link href="https://smallyu.net/2025/05/11/%E6%83%B3%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B0%8FEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/"/>
    <id>https://smallyu.net/2025/05/11/%E6%83%B3%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA%E6%9C%80%E5%B0%8FEVM%E8%99%9A%E6%8B%9F%E6%9C%BA/</id>
    <published>2025-05-10T17:15:07.000Z</published>
    <updated>2026-08-09T04:37:39.617Z</updated>
    
    <content type="html"><![CDATA[<p>我给这个项目命名为 EchoEVM，主要目标是从最简单的堆栈操作开始，逐步实现一个完整的以太坊字节码执行环境。</p><p>为什么选择这个方向？解析下以太坊客户端的技术模块：</p><ol><li>RPC：GRPC 套壳？重点在于协议设计而不是技术实现</li><li>P2P：有现成的 libp2p 可用，无非是节点发现、路由表之类，比如深入下 Kademlia DHT？</li><li>账户体系：ECDSA？密码学？</li><li>交易池：交易分析、密封交易、MEV保护方向</li><li>共识机制：共识机制的设计属于研究级别，至少得是个博士发论文、实验室里做研究、出各种测试数据，然后证明在哪方面做出了业界前沿的优化、最后融资雇人做工程化的实现</li><li>储存：搞数据库底层的专家应该干什么都一样，哪里都有用武之地，跟区块链没关系</li><li>数据结构：去研究 Merkle Patricia Tree 的实现吗？</li><li>状态同步：轻节点方向，比如用 Celestia 的核心技术把执行和储存分开，或者 Archive 节点数据的 offload？</li></ol><p>综合来看，我倾向于做一件侧重工程而不是学术、同时又有技术含量的事情，无论是从个人技术能力的提升，还是后续有可能带来的成果上，都要有意义。假如这个最小EVM开发出来了，是可以带来一系列成果的，后续也可以基于此延伸出很多更有价值的产品。</p><p>从 Solidity 语言到 bytecode 的转换过程，那是编译器专家干的事情，我要做的，是针对 bytecode 做执行，先从最简单的加法运算和 jump 开始，然后是 Gas 的计算、上下文环境的切换，直到能够执行全部以太坊历史交易。</p><br><h3 id="v0-0-1（2025-05-27）"><a href="#v0-0-1（2025-05-27）" class="headerlink" title="v0.0.1（2025.05.27）"></a>v0.0.1（2025.05.27）</h3><p>实现了一个非常简单的版本，现在可以用 solc 编译一个 <a href="https://github.com/smallyunet/echoevm/blob/v0.0.1/test/contracts/Add.sol">Add.sol</a> 合约，然后让 echoevm 读取生成的 <code>Add.bin</code> 部署代码，就会输出合约部署之后的运行时代码。</p><p>在实现这个版本的过程中，学习到的东西是部署代码和运行时代码的区别。我们一般会先部署一个合约到链上，然后再对这个合约产生调用，这实际上是两个不同的操作，但又都在使用相同的 EVM 执行，EVM 并不关心输入的 bytecode 是部署还是调用，只是对不同的操作码处理方式不同。一般部署代码会同时包含 <code>CODECOPY</code> 和 <code>RETURN</code> 两个操作码，可以利用这一点来区分输入的类型。</p><br><h3 id="v0-0-2（2025-06-09）"><a href="#v0-0-2（2025-06-09）" class="headerlink" title="v0.0.2（2025.06.09）"></a>v0.0.2（2025.06.09）</h3><p>这个版本增加了运行 runtime bytecode 的能力，也就是先部署合约，然后再针对部署之后的合约内容，进行调用，调用的时候可以带上一些参数，比如：</p><pre><code class="bash">go run ./cmd/echoevm -bin ./build/Add.bin -function &#39;add(uint256,uint256)&#39; -args &quot;3,5&quot;</code></pre><p>这个命令的含义是，会执行 <code>./build/Add.bin</code> 文件内的 bytecode，并且调用 <a href="https://github.com/smallyunet/echoevm/blob/v0.0.2/test/contracts/Add.sol#L7">add 函数</a>，传入参数 3 和 5，最终程序运行结束后，会返回出计算结果 8。</p><br><h3 id="v0-0-3（2025-06-24）"><a href="#v0-0-3（2025-06-24）" class="headerlink" title="v0.0.3（2025.06.24）"></a>v0.0.3（2025.06.24）</h3><p>好消息，现在 echoevm 已经可以执行以太坊主网前 10000 个区块的合约交易！因为前 10000 个区块根本没有合约交易 :P</p><p>这个版本新增了执行以太坊区块的模式，可以执行单个区块执行，也可以执行区块范围执行。当然，还需要一个获取区块数据的 url，注意对于以太坊早期的区块数据，得找 archive 模式的节点。整个命令行看起来是这样：</p><pre><code class="bash">echoevm -start-block 0 -end-block 10000 -rpc &lt;url&gt;</code></pre><p>现在 echoevm 支持的字节码有限，如果执行最新的一些区块交易，会发现报错说不支持某些字节码，这个是正常现象。</p><br><h3 id="v0-0-4（2025-07-05）"><a href="#v0-0-4（2025-07-05）" class="headerlink" title="v0.0.4（2025.07.05）"></a>v0.0.4（2025.07.05）</h3><p>这个版本新增加了从 artifact 文件读取 bytecode 数据的能力，就是 hardhat 项目在编译的时候会生成的那个 artifact 文件。之前的版本只能用读取 solc 编译生成的二进制文件，编译合约的命令是这样：</p><pre><code class="bash"># 编译合约生成字节码npx --yes solc --bin Add.sol -o ./build# 运行 echoevm 来执行字节码go run ./cmd/echoevm run -bin ./test/bins/build/Add_sol_Add.bin -function &quot;add(uint256,uint256)&quot; -args &quot;1,2&quot;</code></pre><p>现在的版本更简单一点，对于标准的 <a href="https://github.com/smallyunet/echoevm/tree/v0.0.4/test/contract">hardhat 项目</a>，每次执行这个编译命令都会生成 artifact 文件，echoevm 可以直接读取 json 文件并执行：</p><pre><code class="bash"># 编译 hardhat 项目的合约npx hardhat compile# 如果愿意，可以运行 hardhat 项目的测试npx hardhat test# 运行 echoevm 来执行字节码go run ./cmd/echoevm run -artifact ./test/contract/artifacts/contracts/Add.sol/Add.json -function &quot;add(uint256,uint256)&quot; -args &quot;1,2&quot;</code></pre><p>这个版本同样新增了一些字节码的支持，但还是不足以执行完整的以太坊区块。接下来会手动按照 Solidity 的语法特性，来逐步增加测试用例和观察字节码的欠缺情况，这也就是为什么这个版本重点优化执行方式的原因。</p><br><h3 id="v0-0-5（2025-07-27）"><a href="#v0-0-5（2025-07-27）" class="headerlink" title="v0.0.5（2025.07.27）"></a>v0.0.5（2025.07.27）</h3><p>这是一个小版本，主要是增加了比较完善的 <a href="https://github.com/smallyunet/echoevm/tree/v0.0.5/test/contract/contracts">Solidity合约</a> 作为测试用例，涵盖基本数据类型、函数、控制流、modifier、事件、接口、library、内联汇编等 Solidity 的语法特性。</p><p>而且提供了便捷的命令，只需要在项目根目录下运行这个命令，就可以看到全部测试的结果：</p><pre><code class="bash">make test-advanced</code></pre><p>当然全部测试是通过的。但是目前仍然无法执行以太坊主网的第 10000000 个区块，意味着缺少的 opcode 不属于 solidity 的基本语法特性，可能是别的什么。</p><br><h3 id="v0-0-6（2025-11-27）"><a href="#v0-0-6（2025-11-27）" class="headerlink" title="v0.0.6（2025.11.27）"></a>v0.0.6（2025.11.27）</h3><p>这个 <a href="https://github.com/smallyunet/echoevm/releases/tag/v0.0.6">小版本</a> 新增了两个命令，<code>debug</code> 命令可以用于展示字节码的执行过程：</p><pre><code class="bash">~/work/github/echoevm &gt; ./bin/echoevm run --debug 60016002PC    OP              GAS        STACK (Top)         ------------------------------------------------------------0002  PUSH1           0          0x10004  PUSH1           0          0x2Return: 0x</code></pre><p><code>repl</code> 命令 就像比特币的 <code>btcdeb</code> 一样，可以交互式地输入字节码并执行：</p><pre><code class="bash">~/work/github/echoevm &gt; ./bin/echoevm repl                EchoEVM REPLType opcodes (e.g., &#39;PUSH1 01 ADD&#39;) or hex (e.g., &#39;600101&#39;). Type &#39;exit&#39; to quit.&gt;  PUSH1 10Stack [1]:  0000: 0x10&gt; PUSH1 20Stack [2]:  0001: 0x20  0000: 0x10&gt; ADDStack [1]:  0000: 0x30&gt; exit</code></pre><br><h3 id="v0-0-44（2026-08-09）"><a href="#v0-0-44（2026-08-09）" class="headerlink" title="v0.0.44（2026.08.09）"></a>v0.0.44（2026.08.09）</h3><p>仍然在尝试和优化中；目前的方向是帮助 AI 追踪和解析 Solidity 合约执行过程中具体的 opcode 变化，最新的 <a href="https://github.com/smallyunet/echoevm/blob/main/benchmarks/trace-value-v2/README.md">benchmark 结果</a> 显示，相较于宽泛 opcode 分析，使用 EchoEVM 作为分析工具后，不但可以提升 20% 的准确率，AI 可以精准回答合约的执行过程、排查遇到的问题，而且还节省了接近 40% 的 Token 消耗。</p><p>当然现在也有很大的局限性，比如还不支持以太坊的最新共识版本 Osaka 的完整语义、KZG 操作码等，AI 方面的应用场景也仅局限于复杂问题排查、修复问题阶段而不是开发阶段。所以 EchoEVM 还有很多不足，还在继续探索中……</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;我给这个项目命名为 EchoEVM，主要目标是从最简单的堆栈操作开始，逐步实现一个完整的以太坊字节码执行环境。&lt;/p&gt;
&lt;p&gt;为什么选择这个方向？解析下以太坊客户端的技术模块：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RPC：GRPC</summary>
        
      
    
    
    
    
    <category term="计划" scheme="https://smallyu.net/tags/%E8%AE%A1%E5%88%92/"/>
    
    <category term="EVM" scheme="https://smallyu.net/tags/EVM/"/>
    
  </entry>
  
  <entry>
    <title>基于 ZK 的链上身份系统设计</title>
    <link href="https://smallyu.net/2025/04/30/%E5%9F%BA%E4%BA%8Ezk-%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E7%9A%84%E9%93%BE%E4%B8%8A%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    <id>https://smallyu.net/2025/04/30/%E5%9F%BA%E4%BA%8Ezk-%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E7%9A%84%E9%93%BE%E4%B8%8A%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</id>
    <published>2025-04-30T14:18:54.000Z</published>
    <updated>2025-11-09T09:22:13.424Z</updated>
    
    <content type="html"><![CDATA[<p>我给这个系统取名 zkgate.fun，主要想发挥零知识证明的特性，结合区块链做个小工具。关键功能是实现，用户证明自己属于某一个群组，但是不需要暴露自己真实的链上身份。</p><p>目前的设想是这样，管理员首先有一个名单列表，可以是以太坊地址的数组，然后根据这个地址列表，计算出一个 Merkle Root Hash。接着把这个 root hash 提交到智能合约上。处于这个名单中的人，可以使用 Circom 电路的 proving key，来给自己生成一个 zk proof，随后将 zk proof 提交到智能合约上。</p><p>在智能合约上，会使用 Circom 电路生成的 verifier.sol，对收到的 zk proof 进行验证，判断用于生成 zk proof 的地址，是否在 Merkle Root Hash 中，最后将判断结果返回。</p><p>这样的话，管理员不需要公开自己的群组中有哪些地址，属于群组中的地址也不需要声明自己的身份，只需要提交零知识证明生成的 zk proof，就可以证明自己真的归属于这个群组。我接下来会具体在技术上实现这个设计。</p><br><h3 id="更新-v0-1-0-版本-2025-05-09"><a href="#更新-v0-1-0-版本-2025-05-09" class="headerlink" title="更新 v0.1.0 版本 (2025.05.09)"></a>更新 v0.1.0 版本 (2025.05.09)</h3><p>首先要纠正之前设计中的一个错误的地方，管理员必须要公开自己群组的地址列表，否则无法根据地址列表来生成 Merkle Tree，用户也无法根据树结构，来找到自己地址所在的节点位置、生成路径证明。</p><p>其次是很高兴地说，现在跑通了一个非常初级的 Demo（<a href="https://github.com/smallyunet/zkgate-demo">smallyunet&#x2F;zkgate-demo</a>），这个 Demo 功能并不完善，甚至没有办法在电路中验证地址的所有权，但至少是一个工具链路层面的跑通。</p><p>具体实现是这样：</p><ol><li>有一个 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/offchain/smt.js">链下程序</a> 来根据地址列表，以及自己的地址，生成 zk 电路的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/offchain/inputs.json">inputs.json</a>，这个输入文件包含了 Merkle Root Hash 和验证节点位置所需要的路径</li><li>根据 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/merkleSmtProof.circom">电路代码</a> 来编译出一些 <a href="https://github.com/smallyunet/zkgate-demo/tree/main/circuits/build">二进制文件</a>，这些编译后的产物是用来生成 witness 文件的</li><li>基于公开的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/run.sh#L17-L28">ptau 文件</a> 生成 .zkey 文件</li><li>从 .zkey 文件中导出 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/proof.json">proof.json</a>, <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/public.json">public.json</a>, <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/verification_key.json">verification_key.json</a>，这 3 个 json 文件可以做链下离线验证，证明 prove 的有效性</li><li>从 .zkey 文件中导出 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/contracts/Groth16Verifier.sol">.sol 文件</a>，也就是智能合约代码，部署到链上</li><li>拿着 prove.json 文件和 public.json 文件的内容，作为 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/hardhat/scripts/prove.js#L41">参数</a> 调用合约的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/circuits/contracts/Groth16Verifier.sol">verifyProof</a>函数，如果 prove 有效则返回 true，否则返回 false</li></ol><p>假如一个地址不在群组列表中，有两种情况：</p><ol><li>试图用一个不在群组列表中的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/offchain/smt_non_member.js#L24">地址</a> 生成 inputs.json，然后拿着 inputs.json 去根据电路生成 prove，会直接被电路拒绝报错</li><li>试图用一些假的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.1.0/hardhat/scripts/fakeProofWithCorrectRoot.js#L26">prove 参数</a> 提交到链上做验证，最终无法通过链上验证</li></ol><p>那么目前这个最初级版本的 Demo，问题在于，构建 prove 使用的是明文地址，比如：</p><pre><code class="js">const members = [  &quot;0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&quot;,  &quot;0x70997970C51812dc3A010C7d01b50e0d17dc79C8&quot;,  &quot;0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC&quot;,];const proofKey = toField(members[0]);const &#123; siblings &#125; = await tree.find(proofKey);</code></pre><p>这个语句的含义是在让 zk 电路判断，<code>members[0]</code> 是否属于 <code>members</code> 数组构建出来的树结构，这显然是属于的。如果想要用不属于群组的地址构建 prove，只需要替换一下 proofKey 指向的地址：</p><pre><code class="js">const nonMemberAddress = &quot;0x1234567890123456789012345678901234567890&quot;;const proofKey = toField(nonMemberAddress);const &#123; siblings &#125; = await tree.find(proofKey);</code></pre><p>也就是说，members 列表必须是公开的，而现在的程序只能判断一个地址在不在 <code>members</code> 里面，但即使 <code>members[0]</code> 不是我的地址，我也能用来构建一个合法的 prove。那还要 zk 干嘛？</p><p>所以下一步要解决的问题，是让用户用私钥对某个消息进行签名，然后在 zk 电路中根据签名 recover 出地址，接着判断 recover 出来的地址是否属于 members 数组。</p><p>这个过程是不是听起来简单？可实际上用 zk 电路来 recover 出一个 ECDSA 签名算法的地址，别说复杂度非常高，难度就像用乐高搭核电站一样。难怪人们都说，搞 zk 真的很掉头发。</p><br><h3 id="更新-v0-2-0-版本（2025-05-13）"><a href="#更新-v0-2-0-版本（2025-05-13）" class="headerlink" title="更新 v0.2.0 版本（2025.05.13）"></a>更新 v0.2.0 版本（2025.05.13）</h3><p>这个版本解决了验证地址所有权的问题，基本思路是让 zk 证明和地址所有权的证明分开，链下用 zk 证明地址的路径在 Merkle Root 上，链上需要用户提交用私钥对 root 的签名，并且将签名提交到链上。然后合约 recover 出签名的地址，跟 zk 电路的 prove 中包含的地址信息对比。</p><pre><code>1. zk prove 包含地址信息 -&gt; 链上验证 zk prove -&gt; 得知 zk prove 中的地址信息2. 用私钥对 root 签名 -&gt; 链上得到签名 -&gt; recover 出签名对应的地址信息3. 判断 zk prove 中的地址 == 签名 recover 出的地址</code></pre><p>演示代码具体改动的地方有：</p><ol><li>offchain 部分的代码不需要变动，生成 inputs.json 的脚本中 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.2.0/offchain/smt.js#L37">inputs</a> 里已经有 key 的信息了 </li><li>电路代码中，需要把 inputs 中的 key 变为 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.2.0/circuits/merkleSmtProof.circom#L27">public</a></li><li>合约代码需要接受用户的 <a href="https://github.com/smallyunet/zkgate-demo/blob/v0.2.0/hardhat/contracts/ZkGateRegistry.sol#L38">签名</a> 作为参数，并且得到 recover 出的<a href="https://github.com/smallyunet/zkgate-demo/blob/v0.2.0/hardhat/contracts/ZkGateRegistry.sol#L49">地址</a>，将这个地址与 proof key 进行对比</li><li>调用合约的脚本，需要用私钥对 root 进行<a href="https://github.com/smallyunet/zkgate-demo/blob/v0.2.0/hardhat/scripts/prove.js#L44-L45">签名</a>，并且把签名数据作为参数调用合约</li></ol><p>到此为止，zkgate.fun 实现的功能是，群组管理员不必在链上公开自己的群组成员信息，只需要提交 Merkle Root Hash 到链上。对于群组内的成员，需要完整的成员列表，以及自己地址对应私钥签名后的信息，就可以生成 zk prove 去链上，证明自己确实是群组内的成员。</p><p>在这个过程中，使用 zk 唯一隐藏掉的信息，是群组成员的完整信息不必上链公开，只需要一个 Merkle Root Hash。而用户的地址目前无法隐藏，必须提交到链上用于验证。</p><br><h3 id="更新（2025-05-14）"><a href="#更新（2025-05-14）" class="headerlink" title="更新（2025.05.14）"></a>更新（2025.05.14）</h3><p>有一个现有的、以太坊基金会支持的、工具链和生态都已经比较成熟的 zk 协议，同样是用来做身份验证的项目，叫 Semaphore，官网是这个，可以直接在上面体验一下包含前端界面的 Demo：</p><ul><li><a href="https://semaphore.pse.dev/">https://semaphore.pse.dev/</a></li></ul><p>在 zkgate.fun 前面两个版本的迭代中，没有选择 Semaphore 使用 EdDSA 账户体系的方案，主要是不想脱离以太坊的账户体系，也不想放弃 ECDSA，而实际上只有 EdDSA 是 zk 友好的，可以使用 Poseidon Hash 签名，zk 电路中也能对签名进行验证，不需要 “链下签名、链上 recover” 这种丑陋的实现方式。</p><p>不得不说，从个人学习的角度，虽然没几天的时间，但是我已经大概理解了 zk（工具链）的操作过程。从行业前沿的角度，我仅凭个人力量不可能做的比 Semaphore 更好。即使 zkgate.fun 进一步开发出前端界面、可视化地演示出具体的交互过程，也顶多就是 Semaphore 的这个 <a href="https://demo.semaphore.pse.dev/">Demo</a> 的样子，而且技术上没有 Semaphore 硬核。</p><p>所以 zkgate.fun 这个项目不再继续开发，域名一年后会自动到期，不再续费。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;我给这个系统取名</summary>
        
      
    
    
    
    
    <category term="计划" scheme="https://smallyu.net/tags/%E8%AE%A1%E5%88%92/"/>
    
    <category term="zk" scheme="https://smallyu.net/tags/zk/"/>
    
  </entry>
  
  <entry>
    <title>一个 Web3 打赏系统的设计</title>
    <link href="https://smallyu.net/2025/04/29/%E5%85%B3%E4%BA%8Eweb3%E6%89%93%E8%B5%8F%E7%B3%BB%E7%BB%9Fgiveme-wtf%E7%9A%84%E8%AE%BE%E8%AE%A1/"/>
    <id>https://smallyu.net/2025/04/29/%E5%85%B3%E4%BA%8Eweb3%E6%89%93%E8%B5%8F%E7%B3%BB%E7%BB%9Fgiveme-wtf%E7%9A%84%E8%AE%BE%E8%AE%A1/</id>
    <published>2025-04-29T11:26:45.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<h3 id="产品形态"><a href="#产品形态" class="headerlink" title="产品形态"></a>产品形态</h3><p>giveme.wtf 是我刚注册的一个域名，计划做一个 web3 打赏的小工具，类似的 web2 平台有：</p><ul><li><a href="https://buymeacoffee.com/">https://buymeacoffee.com/</a></li><li><a href="https://linktr.ee/">https://linktr.ee/</a></li></ul><p>与之不同的是，giveme.wtf 的个人页面上，将显示 web3 钱包的收款地址、二维码，就像 Paypal 的个人收款链接一样，并且同时支持多种链的地址格式，包括比特币、以太坊、狗狗币等，可以自由选择。</p><p>giveme.wtf 不做任何资金的中转，仅仅只是展示打赏地址这一信息，比如，访问 giveme.wtf&#x2F;{username}，这个页面将显示出 username 设置好的收款地址信息，包括以太坊地址文本是什么，二维码是什么。就这么简单。</p><p>当然 giveme.wtf&#x2F;{username} 下，也可以设置简单的 bio，头像、域名、社交媒体等，像是一个小型的个人主页，让人知道你是谁，稍微更值得分享出去一点。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><ul><li>注册</li></ul><p>user 使用 MetaMask 钱包注册，连接钱包后可以设置 username，username 是全局唯一的，在智能合约上管理，user 需要发一笔与合约交互的交易，来将自己心仪的 username 提交到合约上。</p><ul><li>profile 信息</li></ul><p>绑定好 EVM 地址与 username 的关系后，就可以设置 profile 信息，包括头像、bio、钱包地址等。</p><p>填写信息后，前端页面将数据提交到后端，后端用 IPFS 节点保存这些数据（长期开启 Pin），同时生成 CID 信息，将 CID 返回给前端。</p><p>前端收到 CID 后，再发起一次合约交互，将 username-&gt;CID 的映射关系，写入到智能合约里。这个步骤可以和注册步骤合并，也可以拆开，因为有时候 user 只想注册，不想设置 profile。</p><ul><li>展示</li></ul><p>合约上的 username-&gt;CID 是最权威的数据，前端页面将根据 giveme.wtf&#x2F;{username} 中的 username，从合约中获取到 CID，再拿着 CID 去 IPFS 的网关查询出具体数据，根据数据渲染出页面。</p><p>profile 会是一些非常精简的 json 数据，数据量很小，同时为了加快网关的查询速度，可以用 Cloudflare 提供的 web3 gateway CDN。</p><ul><li>网络选择</li></ul><p>智能合约部署在 base 上。</p><h3 id="扩展优化"><a href="#扩展优化" class="headerlink" title="扩展优化"></a>扩展优化</h3><p>后期可以根据链上数据，统计出使用打赏系统的收款地址，以及收到打赏的金额总量，做个排行榜，按照 username 或者链分类，分析出一堆数据。</p><p>如果上了排行榜，username 下的 bio 可以增大曝光率。给你心目中的偶像上分吧，让他保持在榜首。</p><p>还可以增加一些 24小时榜单、PK 性质之类的排名。</p><p>同时也可以扩展到社交系统，如有打赏记录的地址可以形成关系图谱，甚至可以直接以某种 IM 工具的方式通讯、自动拉群等。</p><h4 id="username-找回"><a href="#username-找回" class="headerlink" title="username 找回"></a>username 找回</h4><p>MetaMask 钱包注册的问题在于，钱包丢了怎么办，是不是就失去了对 username 的控制。这里可以设计一个恢复机制，比如允许 username 设置一个恢复地址列表，只要是这个恢复列表中的地址，都可以找回 username 的控制权，进而改变 username 对应的 CID。这个机制主要是针对钱包遗失的情况。</p><p>至于钱包被黑了怎么办，黑客岂不是能直接修改恢复地址的列表。他都已经有 username 控制权了，再改也是改成他的地址，加固他对 username 的控制权。那么有没有钱包被黑还能夺回控制权的办法？web3 里没有。</p><h4 id="网络的选择"><a href="#网络的选择" class="headerlink" title="网络的选择"></a>网络的选择</h4><p>目前必须要选择一条链来部署智能合约，智能合约是数据正确性的来源。那么选择哪条链其实是个问题，因为作为 user，不一定有链上的代币作手续费。</p><p>比如选择了 base，那么 user 首先得有 ETH，其次得在 base 上有 ETH，然后才能后续的操作。光是这两步，就能劝退大多数人。</p><p>那么为了解决这个问题，后面可以考虑的方向是手续费代付，用 ERC-4337 （现在差不多凉了）的 paymaster，或者比较原始的 Meta Transaction 方式。但是又得考虑到薅羊毛的问题，代付也得付得起才行。</p><h4 id="数据可用性"><a href="#数据可用性" class="headerlink" title="数据可用性"></a>数据可用性</h4><p>MVP 里的方案是，数据用 IPFS 存，但仅仅只有一个服务器。IPFS 是比较底层的文件路由协议，可以考虑在上面包一层，像 Filecoin 一样，但是不会有 Filecoin 那么复杂，因为 giveme.wtf 的数据量比较小。PoST 难用的地方就在于需要对文件做加密解密，因为文件太大又不能全量校验，但 giveme.wtf 不一样，往简单了做就行，比如验证一下 Merkle Root Hash，也就是说，后面需要在 IPFS 的基础上，加上适当的文件校验和激励机制，让更多的节点愿意存下 giveme.wtf 完整的数据，然后用一种方式来定期检查每个节点是否真的储存了完整数据，如果存了，就给一点奖励。具体奖励给什么再说。</p><h4 id="链下数据缓存"><a href="#链下数据缓存" class="headerlink" title="链下数据缓存"></a>链下数据缓存</h4><p>每次前端页面都从合约上查 username-&gt;CID，交互太慢了，而且消耗节点的 rpc 资源。需要考虑链下来缓存这部分数据，比如有一个中心化的后台程序，监听合约的事件，实时拿到 username-&gt;CID 的内容，然后写入到 Cloudflare Workers KV 服务里。前端页面首先请求 Cloudflare Workers KV，如果没有内容再 fallback 到合约上查。</p><p>那么这里又涉及到一个问题，如果中心化的服务作恶，或者被黑了怎么办，username-&gt;CID 的映射关系一改，钱直接打到黑客的地址上了。</p><p>这个链下数据完整性校验的问题，其实是 Optimistic Rollup 在解决的问题，也有相对成熟的方案。然后结合 Zetachain 的跨链逻辑，可以这样设想。</p><p>首先用来缓存的链下程序，将每一个 username-&gt;CID 的数据作为子节点，构建一个 Merkle Tree，最终会得到一个 Merkle Root Hash，这个 root hash 将是校验数据完整性的凭证，把这个 root hash 定时提交到合约上，前端页面去合约上查一下这个 root hash，就可以知道从缓存里拿到的 CID 有没有被篡改。</p><p>其次链下的索引程序可以有多个，通过 TSS 协商出一个私钥，只有这个私钥，才可以向合约提交 Metkle Hash Root，并且这多个索引程序，只有 root hash 相同，才会协商成功。相当于做了多签。</p><p>最后是冷静期+挑战期，Merkle Root 提交之后，在冷静期内不生效，同时任何人都可以发起挑战，如果挑战成功，则新提交的 Root 作废，继续用旧的 Root。当然这个步骤中的挑战是很麻烦的，得考虑到怎么发起挑战，尤其是怎么挑战才算是成功这个机制。但是好在不用着急做那么复杂，这个属于后期可以优化的方向。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;产品形态&quot;&gt;&lt;a href=&quot;#产品形态&quot; class=&quot;headerlink&quot; title=&quot;产品形态&quot;&gt;&lt;/a&gt;产品形态&lt;/h3&gt;&lt;p&gt;giveme.wtf 是我刚注册的一个域名，计划做一个 web3 打赏的小工具，类似的 web2</summary>
        
      
    
    
    
    
    <category term="计划" scheme="https://smallyu.net/tags/%E8%AE%A1%E5%88%92/"/>
    
    <category term="web3" scheme="https://smallyu.net/tags/web3/"/>
    
  </entry>
  
  <entry>
    <title>鼓吹 Cursor 的人技术能力都差</title>
    <link href="https://smallyu.net/2025/04/12/%E9%BC%93%E5%90%B9Cursor%E7%9A%84%E4%BA%BA%E6%8A%80%E6%9C%AF%E8%83%BD%E5%8A%9B%E9%83%BD%E5%B7%AE/"/>
    <id>https://smallyu.net/2025/04/12/%E9%BC%93%E5%90%B9Cursor%E7%9A%84%E4%BA%BA%E6%8A%80%E6%9C%AF%E8%83%BD%E5%8A%9B%E9%83%BD%E5%B7%AE/</id>
    <published>2025-04-12T12:33:38.000Z</published>
    <updated>2025-12-04T10:51:53.972Z</updated>
    
    <content type="html"><![CDATA[<p>有点标题党，不要太介意。我想表达的是不应该过于关注使用的工具。很简单的逻辑，只有自己工程能力不如 Cursor，才会觉得 Cursor 厉害，干什么都行。当然这句话属于比较空的废话。</p><p>事实上，Cursor 的代码补全能力没有比 GitHub Copilot 好多少，底层使用的模型是一样的，不是 Claude 就是GPT。而在工程化调教方面，Cursor 和 Copilot 都是在把一些文件作为 context，继续后面的对话。</p><p>Cursor 比 Copilot 好一点的地方在于，会自己去当前工程目录下，搜索和参考其他文件的写法，这一点确实有用，你不需要告诉他具体引用那哪些文件，他自己会不断的尝试，Copilot 这种插件是不具备这种能力的，这确实是好的一面，如果项目下存在大量可复用的代码，Cursor 可以比较好的发挥出它的能力。</p><p>但有时候又会觉得 Cursor 过度智能，它甚至会自己在你的工程下面创建新文件，而不需要经过你同意，这就导致在用 Cursor 的时候需要时刻关注，他是否改变了你预期之外的代码文件。相比之下，（GoLand 下的）Copilot 只是插件的形式，只会给出代码片段，用不用是你自己的事情。VS Code 下的 Copilot 现在和 Cursor 倒是有类似的体验了，会给你一个接受或不接受更改的选项。</p><p>所以相比来说， Cursor 和 Copilot 使用了一样的大语言模型（不会有人觉得 Cursor 自己训练了个模型出来吧），然后 Cursor 拥有更大的、项目级别的控制力，而 Copilot 像他的名字一样，只是辅助级别的能力，这是它们最大的区别。</p><p>回到模型本身，o1 是迄今为止最厉害的模型，在日常工作中深有体会，我经常用 o1 来精准定位编程中遇到的 bug，而对于相同的问题，其他模型往往给出错误或者不准确的解释，包括 o3-mini-high 和 4o。</p><p>比如，在调用智能合约的时候，原本的命令是</p><pre><code>cast call requestPrice(string) &quot;BTC&quot; —-rpc-url=http://eth:8545</code></pre><p>现在需要调用另一个函数，比原本的函数多了一个入参，我就直接复制代码写成</p><pre><code>cast call requestPrice(string) &quot;BTC&quot; &quot;USD&quot; —-rpc-url=http://eth:8545</code></pre><p>不知道你有没有第一时间看出问题？其实 4o 倒是给出了正确的分析，只是没有特别精准，给出了 3 种有可能的错误原因，而 o1 直接就说对了。</p><p>当然大模型之间的差异不会体现在这么微小的问题上，只是正好有印象就随便提一下，我想表达的是，从日常体验来看，o1 是最好用的模型。</p><p>我严重怀疑鼓吹 Cursor 的人，都是之前没有使用过 AI、体会过 AI 强大能力的人，在用到 Cursor 之后，才明白原来现在的生成式 AI 已经这么强大了，欣喜若狂。而当需要使用生成式 AI 的时候，他们的第一反应不是打开 ChatGPT 的聊天框或者 Gotk3 的界面，而是打开了 Cursor 的代码框，开始去聊天。</p><p>所以鼓吹 Cursor 的人，实际上是把生成式 AI 的能力，误以为是 Cursor 的能力，才因此觉得 Cursor 异常强大。</p><p>我在工作中就不止一次被强烈推荐使用 Cursor ，让我别再用 GoLand，还说出 “GoLand 就是垃圾” 这种话，Cursor 最好用什么的。</p><p>首先我一直觉得喜欢用什么编辑器是个人的选择，管这个干啥。其次如果觉得使用什么编辑器会给使用者带来鄙视链和优越感，未免有点小儿科。最后就是我喜欢用 GoLand 的理由，只有两点：1.箭头非常直观的表达了接口的实现关系。2.前进后退快捷键很好用。基于这两点，我才可以比较快速的读懂和理解代码。</p><p>也因此带来一个差异是，使用 GoLand 的人，往往更加关注代码逻辑，比如在寻找代码位置的时候，喜欢通过代码之间的跳转，例如接口的实现关系等。而使用 Cursor（VS Code）的人，更加关注项目的目录结构、文件名、文件的位置，因为 Cursor 没有提供很好的代码跳转功能，所以不得不更加依赖通过项目结构来梳理代码功能。</p><p>代码的自动补全方面，GoLand+Copilot 插件能应付日常场景，需要补全的往往是打日志之类的内容，简单用用就可以，我不太敢用 AI 写侧重逻辑的代码。</p><p>给我推荐使用 Cursor 可能还有一种心理就是，觉得我不知道怎么使用 AI，或者觉得我不知道怎么使用 Cursor (?)，也挺奇怪的。我在 2023 年（ChatGPT 开始大火的那段时间）就试用了 ChatGPT，还写了《<a href="/2023/03/30/%E4%B8%8D%E8%A6%81%E5%B0%8F%E7%9E%A7-ChatGPT/">不要小瞧 ChatGPT</a>》，而现在不论是日常工作还是生活，都在高频率使用 ChatGPT。</p><p>总的来说，我的意思是，你可以喜欢 Cursor，也可以使用 Cursor，但最好不要鼓吹 Cursor。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;有点标题党，不要太介意。我想表达的是不应该过于关注使用的工具。很简单的逻辑，只有自己工程能力不如 Cursor，才会觉得 Cursor 厉害，干什么都行。当然这句话属于比较空的废话。&lt;/p&gt;
&lt;p&gt;事实上，Cursor 的代码补全能力没有比 GitHub Copilot</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>关于 Code Review 的礼节</title>
    <link href="https://smallyu.net/2025/03/25/%E5%85%B3%E4%BA%8E-Code-Review-%E7%9A%84%E7%A4%BC%E8%8A%82/"/>
    <id>https://smallyu.net/2025/03/25/%E5%85%B3%E4%BA%8E-Code-Review-%E7%9A%84%E7%A4%BC%E8%8A%82/</id>
    <published>2025-03-25T13:45:43.000Z</published>
    <updated>2026-07-25T07:05:50.695Z</updated>
    
    <content type="html"><![CDATA[<p>前情提要，推荐王垠的两篇博客文章：</p><ul><li>《<a href="https://www.yinwang.org/blog-cn/2015/03/03/how-to-respect-a-programmer">怎样尊重一个程序员</a>》</li><li>《<a href="https://www.yinwang.org/blog-cn/2015/03/11/git-etiquette">关于Git的礼节</a>》</li></ul><p>由于过往不规范的工作经历，我之前是缺少对 Code Review 的理解的。最近因为同事对这个问题情绪化的表达，我开始关注到关于 Code Review 的问题。</p><h3 id="基本礼仪：不要用-FYI"><a href="#基本礼仪：不要用-FYI" class="headerlink" title="基本礼仪：不要用 FYI"></a>基本礼仪：不要用 FYI</h3><p>谷歌公开出来的 Code Review 规范 《<a href="https://google.github.io/eng-practices/review/reviewer/standard.html">The Standard of Code Review</a>》已经非常具有指导意义，内容很全面，包括我现在实际遇到的流程问题，也完全可以依照这个规范来消化解决。当然前提是所有团队成员事先对这个规范的内容已经达成一致，而不是假设公司的员工已经知道并且开始遵循这个规范。</p><p>关于规范（TSCR）中已经提到的流程问题、礼貌问题，这里是不需要赘述的。我关注到的是其中一个小章节《<a href="https://google.github.io/eng-practices/review/reviewer/comments.html#label-comment-severity">Label comment severity</a>》，也就是对 Code Review 之后的 comment 进行重要程度的区分，并且加上前缀，让 author 可以明确知道哪些留言是必须要改的，哪些是无关紧要的。</p><p>除去必须要改的 comment 不加前缀，谷歌的规范中提到有三类前缀，这些都是指站在 Reviewer 的立场，如何去写 comment，本质上这三类前缀都不影响代码的 approve 和合并：</p><ul><li><code>Nit</code> (Nitpick): 你应该改，但是不改我也能接受。</li><li><code>Optional</code>：只是建议，你自由选择改还是不改。</li><li><code>FYI</code> (For Your Information)：这个 PR 中完全不需要因此有改动，但我觉得这是一个有意思的点，后续你可以关注下。</li></ul><p>其中第三个前缀 FYI，每当看到 For Your Information 这个短语的时候，我总是下意识的会把这个短语翻译为中国传统社会普遍流行的一句古话： “为了你好”。</p><p>相信在中国本土长大的华人，即使没有遭受过严苛古怪的家庭教育，也都能深刻理解到 “为了你好” 的威力。</p><p>为了你好，你要好好学习<br>为了你好，你不能打游戏<br>为了你好，你要考公务员<br>为了你好，你要早点结婚<br>为了你好，你必须生孩子<br>……</p><p>一些情况下，父母的 “为了你好” 只是他们满足自己变态控制欲望的借口，另一些情况下，有些父母发自真心的 “为了你好”，然后由于自身有限的眼光给出了不正确的建议。</p><p>总的来说，中文语境下的 “为了你好” 绝不是什么好词，如果把这种话语带到工作中，就更匪夷所思了。虽然 For Your Information 并不能直译为 “为了你好”，但是为了避免歧义，还是建议大家不要使用这样的话术。</p><p>所以，Code Review 的基本礼仪就是，不要用 FYI。</p><h3 id="为什么-“为了你好”-往往是错的"><a href="#为什么-“为了你好”-往往是错的" class="headerlink" title="为什么 “为了你好” 往往是错的"></a>为什么 “为了你好” 往往是错的</h3><h4 id="先说结论"><a href="#先说结论" class="headerlink" title="先说结论"></a>先说结论</h4><p>因为没有人可以在实际上了解另一个人。</p><p>父母真的了解自己的孩子吗？结婚多年的夫妻，真的知道对方的心思吗？审查犯人的警察，足够了解自己在调查的罪犯了吗？心理学的专家，能猜到自己女朋友早上为什么生气吗？对于每一个人，你真的了解自己吗？</p><p>所以其实可以得到这样一个和技术无关的结论，出于礼貌，我们应该尽量避免对别人说 “为了你好”。</p><h4 id="具体事例"><a href="#具体事例" class="headerlink" title="具体事例"></a>具体事例</h4><p>最近的工作中，我提交的代码包含一个简单的事件总线（Event bus）的实现，其中事件的 Publish 和 Subcribe 都用了 RW 锁来保证 map 读写的线程安全：</p><pre><code class="Go">func (eb *EventBus) Publish(event Event) &#123;    eb.mu.RLock()    defer eb.mu.RUnlock()    if ch, exists := eb.channels[event]; exists &#123;        ch &lt;- event    &#125;&#125;</code></pre><p>而我得到的 CR 建议是用 Sync map 改写为：</p><pre><code class="Go">func (eb *EventBus) Publish(event Event) &#123;    if ch, ok := eb.channels.Load(event); ok &#123;        // 注：这里需要类型断言        ch.(chan Event) &lt;- event    &#125;&#125;</code></pre><p>这实际上最多是一个 Nit 级别的 comment，我是不太在意的，这个事情的处理起来也非常简单。</p><p>而事实上问题在于，我听到了类似于 “为了你好” 的话，大致意思是，为了你好，你要了解清楚 RW 锁、互斥锁、Sync map 的区别，然后选择在 Event bus 场景下最正确的实现方式，并且能够条理清晰地去说服别人。</p><p>如果说我从自身技术发展的角度，是不太在乎这种问题的。这个问题本质上是我们平时面试时候说的八股文，随着最近几年面试风向的转变，也越来越多的人开始逐渐达成一致、反感八股文一类的东西了。</p><p>当然追求这一类底层问题到极致的人，肯定是有技术追求的人，这并不是什么坏事，我们应该尊重任何努力以及对技术较真的人。只是技术也分很多种方面。</p><h4 id="我的兴趣"><a href="#我的兴趣" class="headerlink" title="我的兴趣"></a>我的兴趣</h4><p>我算不算有技术追求的人呢？也许有时候算吧。无论是技术还是别的东西，其实我们所有人都愿意追求有趣的东西。</p><p>从过往经历来看，我关心的技术话题并且乐于出自兴趣去学习、思考的，比如：</p><ul><li><p>PoW 和 PoS 的本质区别是什么，PoW 好还是 PoS 好</p><ul><li>《<a href="/2024/04/14/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4PoW%E6%AF%94PoS%E6%9B%B4%E5%8A%A0%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96/">为什么说 PoW 比 PoS 更加去中心化</a>》</li></ul></li><li><p>以太坊的 PoS 和 Cardano 的 PoS 有什么区别</p><ul><li>为什么说 Cardano 的 PoS 是比较纯粹的 PoS，又为什么以太坊的 PoS 更加去中心化</li></ul></li><li><p>PBFT 的优缺点是什么，PBFT 有哪些优化的空间</p><ul><li>《<a href="/2025/01/05/%E6%89%80%E6%9C%89-BFT-%E5%85%B1%E8%AF%86%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E9%83%BD%E6%98%AF%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84/">所有 BFT 共识的区块链都是中心化的</a>》</li><li>《<a href="/2024/06/09/Ethereum-Casper-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-EIP-7251/">Ethereum Casper 为什么需要 EIP-7251</a>》</li><li>《<a href="/2024/06/03/%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%AD%E7%9A%84PBFT%E4%B8%8D%E9%9C%80%E8%A6%81%E7%AC%AC%E4%BA%8C%E6%AC%A1%E6%8A%95%E7%A5%A8/">区块链中的 PBFT 不需要第二次投票</a>》</li></ul></li><li><p>不同类型的区块链是如何处理分叉的</p><ul><li>《<a href="/2023/07/01/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6%E7%9A%84%E7%90%86%E8%A7%A3/">对区块链共识机制的理解</a>》</li><li>《<a href="/2024/08/22/PoS%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%88%86%E5%8F%89/">PoS 类型的区块链如何处理分叉</a>》</li></ul></li><li><p>区块链可能有哪些有趣的应用场景</p><ul><li>《<a href="/2023/05/18/Pebbling-Game-%E9%B9%85%E5%8D%B5%E7%9F%B3%E6%B8%B8%E6%88%8F/">Pebbling Game 鹅卵石游戏</a>》</li><li>《<a href="/2023/02/22/%E4%B8%80%E7%A7%8D%E5%9C%A8%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%8A%E7%94%9F%E6%88%90%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%9A%84%E6%9C%BA%E5%88%B6/">一种在区块链上生成随机数的机制</a>》</li><li>《<a href="/2022/12/27/%E2%80%9C%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842-3%E2%80%9DdApp-%E6%B8%B8%E6%88%8F%E8%AE%BE%E8%AE%A1/">“猜均值的2&#x2F;3” dApp 游戏设计</a>》</li></ul></li></ul><p>知道这些东西有用吗？没什么用，面试的时候几乎不会有人问，工作中更是用不到，就仅仅只是出于兴趣爱好去探索这些技术问题。这些问题的答案，从来也都不是现成的，网络上是搜索不到的，ChatGPT 也是没办法精准回答的，只有经过一段时间的学习，加上查阅论文资料，结合自己的亲身经历和理解，才可以形成技术观点，无论观点本身是对还是错。</p><p>因此可能出于某种思维上的惯性，我很少关心太过基础的编程类问题。这种事情因人而异，不能强求，自然也不能强迫别人因为 FYI 就去关心某些问题。</p><h4 id="FYI：成为好的-writer"><a href="#FYI：成为好的-writer" class="headerlink" title="FYI：成为好的 writer"></a>FYI：成为好的 writer</h4><p>（刚说完不要 FYI，我这里就在 FYI）</p><p>《<a href="https://www.joecotellese.com/posts/rework-book-summary/">Rework</a>》 这本书里有一个章节印象挺深，标题是 “Hire great writers”，书中的观点是，不是因为工作中需要发表什么文章、写什么报告，而是好的 writer 往往具备逻辑清晰表达问题的能力，可以帮助到工作。</p><p>回到技术问题上，现在各种形式的技术文章也都非常普遍，比如对锁的使用场景比较有心得的话，完全可以落实到文字上，输出成果形成一篇文章，分门别类的介绍锁的种类、最佳的使用场景，然后发表到各种平台上，获得成千上万人的关注，再然后如果对其他技术细节也有心得，内容逐渐丰富，直到写出了一本书，甚至有出版社看上，或者公开到网络上作为电子书开源……FYI……何必跟我较真呢。</p><h3 id="补充（2025-12-13）"><a href="#补充（2025-12-13）" class="headerlink" title="补充（2025.12.13）"></a>补充（2025.12.13）</h3><p>为什么只敢，并且很乐于在 “窝里” 横呢？这是很奇怪的现象。我根本不在乎你的 “窝”，我随时可以离开。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;前情提要，推荐王垠的两篇博客文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;《&lt;a</summary>
        
      
    
    
    
    
    <category term="程序员" scheme="https://smallyu.net/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    
  </entry>
  
  <entry>
    <title>发行加密货币的最好方式</title>
    <link href="https://smallyu.net/2025/01/10/%E5%8F%91%E8%A1%8C%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E7%9A%84%E6%9C%80%E5%A5%BD%E6%96%B9%E5%BC%8F/"/>
    <id>https://smallyu.net/2025/01/10/%E5%8F%91%E8%A1%8C%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E7%9A%84%E6%9C%80%E5%A5%BD%E6%96%B9%E5%BC%8F/</id>
    <published>2025-01-09T16:39:12.000Z</published>
    <updated>2026-08-04T11:43:17.271Z</updated>
    
    <content type="html"><![CDATA[<p>前几天某个互联网无关行业的知名人物，在 pump.fun 上发行了一个 memecoin。然后这两天看到有人在公开频道里讨论，关于把 memecoin 迁移为公链的方案，比如保留原始地址和余额启动新链之类。</p><p>这个 memecoin 的动机很简单，就是发行者需要募集资金，因为他们从事的活动需要资金来源，也因为他们在从事的活动，这个 memecoin 已经被很多钱包屏蔽掉了。</p><p>当然我们这里不会讨论发行加密货币的动机，只会讨论发行加密货币的技术手段，并且对技术手段进行对比。</p><h3 id="最好的方式"><a href="#最好的方式" class="headerlink" title="最好的方式"></a>最好的方式</h3><p>先说结论，发行加密货币，最可靠的技术方案是，直接使用以太坊的客户端，运行一条和以太坊 Chain ID 不同的链。</p><h3 id="为什么不是-pump-fun"><a href="#为什么不是-pump-fun" class="headerlink" title="为什么不是 pump.fun"></a>为什么不是 pump.fun</h3><p>pump.fun 是一个很好的平台，提供了一键发币的能力，能保证合约的安全性，不 rug pull，没有预售，没有安全漏洞。这一点相比于自己写合约发币，要方便和安全很多，</p><p>pump.fun 最厉害的地方在于，发币即可交易，使用 bonding curve 机制来定义价格，让一个币种即使交易者很少，也可以正常交易，这是其他发币方式做不到的。</p><p>不过 pump.fun 也有问题，首先是 SOL 生态，不知道为什么 pump.fun 最初选择了 Solana 而不是 Ethereum，买家在购买新发的币种之前，要先理解和拥有 SOL 才可以继续后续的步骤。虽然 SOL 也非常知名了，但买家为什么要先知道 SOL？</p><p>其次是当币种市值在 100k 以下，这个币的交易行为就和 pump.fun 平台绑定了，不访问 pump.fun 这个网站，你就找不到可以交易的地方。如果网站没了，或者域名没了，就真的没有入口了，这对于币种的长期发展并不友好。市值到 100k 以上就会进入 Raydium，那么有多少买家理解 Raydium是什么？岂不是又依赖于一个Dex平台？</p><p>所以享受 pump.fun 提供便利的同时，就要承担 pump.fun 这个平台本身的风险，还要承受 Solana 这条链有可能出现的风险，比如，Solana 会长久存在吗？多久算久？</p><p>pump,fun 对自己的定位还挺准确的，memecoin 发行平台。</p><h3 id="为什么不是-PoW"><a href="#为什么不是-PoW" class="headerlink" title="为什么不是 PoW"></a>为什么不是 PoW</h3><p>PoW 是最去中心化的技术形式，但是发行 PoW 链的成本太高了，不但没有现成的技术框架可以复用，需要硬核的技术，而且维护成本也很高，没有人挖矿就得自己挖，算力还不能太低。</p><h3 id="为什么不是-Cosmos"><a href="#为什么不是-Cosmos" class="headerlink" title="为什么不是 Cosmos"></a>为什么不是 Cosmos</h3><p>Cosmos 生态的项目往往伴随着两个负面的关键词，BFT，联盟链，所以可以排除了。</p><p>ATOM 是一个市值仅排名 50 左右的币种，生态上有自己的垂直领域，可以说，如果不知道为什么要用，就不要用。</p><p>另外 Cosmos 的生态建设其实不好，到目前都没有一个像 Metamask 一样能连接任意 RPC 的钱包，要真用上 Cosmos 会遇到不少问题。</p><h3 id="为什么不是-Polkadot-x2F-Avalanche"><a href="#为什么不是-Polkadot-x2F-Avalanche" class="headerlink" title="为什么不是 Polkadot &#x2F; Avalanche"></a>为什么不是 Polkadot &#x2F; Avalanche</h3><p>区块链行业专业的从业者，估计都没整明白、用不来这两条链，不在于它们无法理解，而是理解成本高。</p><p>而且像 Polkadot 比如 Existential Deposit 这种特性简直离谱，不明白为什么会存在。类似的未知问题还有很多，是不能轻易选择使用的。</p><h3 id="为什么不是-Ethereum-Layer-2"><a href="#为什么不是-Ethereum-Layer-2" class="headerlink" title="为什么不是 Ethereum Layer 2"></a>为什么不是 Ethereum Layer 2</h3><p>以太坊的 Layer 2 本身也不是发币用的，是给项目方挣协议费的，L2 是以太坊的扩展，原生代币仍然是 ETH。而且 L2 虽然技术开源也好用，但是需要中心化的运营，以及不间断提交 fault proofs 到 Layer 1，手续费得用真实的 ETH，挺贵的，如果用户少，手续费都挣不回来。</p><h3 id="为什么不在-Ethereum-上发行-ERC-20"><a href="#为什么不在-Ethereum-上发行-ERC-20" class="headerlink" title="为什么不在 Ethereum 上发行 ERC-20"></a>为什么不在 Ethereum 上发行 ERC-20</h3><p>智能合约一般人写不明白，主要是安全漏洞风险高，即使发行方觉得合约没问题，买家也很难判断合约安不安全，识别难度很高，所以不推荐这种方式。即使合约没问题，以太坊的手续费也很贵，很不友好。</p><p>Layer 2上发行 ERC-20 呢？问题是选择哪个 L2 网络？L2 网络的手续费倒是低，但是不同网络数据又不互通，从这个角度，L2 在杀掉以太坊，至少让以太坊变得分裂，而不是在帮助以太坊。</p><h3 id="为什么是-Ethereum"><a href="#为什么是-Ethereum" class="headerlink" title="为什么是 Ethereum"></a>为什么是 Ethereum</h3><p>不可否认的几点事实是：</p><ol><li>以太坊的 EVM 已经成为区块链行业最广泛认可和使用的智能合约标准</li><li>以太坊的 PoS 是除了 PoW 之外最去中心化的共识机制</li><li>以太坊的地位无法撼动，ETH Killer 也许会在某些指标上超越以太坊，但 EVM 标准这一点不会</li><li>相比于其他公链，以太坊的社区生态更加活跃，基础设施更加完善</li><li>新兴公链都在试图兼容 EVM，而不是推翻 EVM</li></ol><p>基于这些事实，发行区块链网络最好的方式只有一种：</p><ul><li>使用以太坊客户端，修改配置文件和启动参数之后，启动一个 Layer 1 网络</li></ul><p>从网络运行的角度，即使只使用目前版本的以太坊客户端，哪怕后续客户端不再跟随以太坊的步骤进行升级，也可以让网络长久稳定运行下去。</p><p>至于手续费，币价低手续费就便宜，所以几乎不太可能贵到手续费无法接受。</p><h3 id="如何解决初期交易和空投问题"><a href="#如何解决初期交易和空投问题" class="headerlink" title="如何解决初期交易和空投问题"></a>如何解决初期交易和空投问题</h3><p>除了 pump.fun 平台和 PoW 链，其余的发币方式都无法解决初始阶段币种交易的问题，初期上不了 CEX 也上不了 DEX，买家怎么买，拿什么买？</p><p>唯一能想到的就是以 ICO 的方式预售，在 TGE 的时候正式启动网络。尽管这个过程中往往会产生一大堆不和谐的事件和争议，但是如果用以太坊的节点，只要把 Genesis 文件中的每一个地址都说明，网络本身就可以是大家认可并且没有问题的。</p><p>网络创世没问题之后，剩下的就交给网络自身的通胀，也不会有明显的问题，网络就能平稳运行下去了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;前几天某个互联网无关行业的知名人物，在 pump.fun 上发行了一个 memecoin。然后这两天看到有人在公开频道里讨论，关于把 memecoin 迁移为公链的方案，比如保留原始地址和余额启动新链之类。&lt;/p&gt;
&lt;p&gt;这个 memecoin</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>所有 BFT 共识的区块链都是中心化的</title>
    <link href="https://smallyu.net/2025/01/05/%E6%89%80%E6%9C%89-BFT-%E5%85%B1%E8%AF%86%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E9%83%BD%E6%98%AF%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84/"/>
    <id>https://smallyu.net/2025/01/05/%E6%89%80%E6%9C%89-BFT-%E5%85%B1%E8%AF%86%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E9%83%BD%E6%98%AF%E4%B8%AD%E5%BF%83%E5%8C%96%E7%9A%84/</id>
    <published>2025-01-05T03:57:59.000Z</published>
    <updated>2026-08-04T11:43:16.732Z</updated>
    
    <content type="html"><![CDATA[<p>首先给出一个共识机制在去中心化程度上的排名，这个排名几乎是毋庸置疑的：</p><p>PoW &gt; PoS &gt; DPoS &gt; BFT</p><p>然后从处理分叉的角度，对比一下 PoS 和 BFT 的差异。</p><p>因为 BFT 算法本身决定了，所有使用 BFT 共识的链，都不会存在分叉，无论是软分叉还是硬分叉。没有分叉的链，意味着整个网络同一时刻只会有一个版本，而这个版本取决于项目发行方，哪怕项目发行方不是官方，这一版本也只能来自于某个中心化的组织。所以，使用 BFT 共识的区块链都是中心化的。</p><p>假如网络发行方对网络进行了让人无法接受的更改，会发生什么？</p><p>在 PoS 共识下，验证者可以选择旧的规则，也可以选择新的规则，这两种规则可以同时存在，直到大多数验证者达成一致，网络恢复一致。如果验证者始终无法达成一致，就会一直分叉下去。</p><p>在 BFT 共识下，验证者可以选择旧的规则，也可以选择新的规则，但是如果一方数量达到半数，网络将会停止。直到验证者线下达成一致，网络才会重新启动。</p><p>也就是说，当面临本应该分叉的情形时，BFT 会直接停机，这也是为什么 Solana 和 SUI 都出现过网络停止的原因。</p><p>到这里你就明白，这里说的中心化，是指在 BFT 网络中不会同时存在两个网络，当然使用其他共识的网络也几乎不会出现这种情况，但是容许这种情况发生。</p><p>更进一步的说明，这里说的中心化，是指 BFT 网络中如果一定比例的验证者想要让网络停止，网络就可以停止，只能通过新启动另外一个网络（其实也属于硬分叉的一种）来让网络恢复正常。</p><p>这种差异会产生什么影响？以太坊网络中，即使大多数节点已经挂掉，只要还有少数存在，网络就能够正常运行。而 BFT 网络对验证者的容错能力不到一半，如果半数验证者停掉，网络会直接瘫痪，你的所有链上资产无法继续转移。</p><p>从投资的角度，如果你打算长期持有某种代币，你觉得哪种网络更安全，更能让你的资产安全受到保障？</p><p>不过还要注意的是一点，网络的可靠性不一定来自于去中心化程度，Coinbase 的 Base 网络可靠性来自于美国政府的监管和半合规化，很多交易所和政府机构都会把钱放到 Coinbase Prime 的信托服务里，所以 Base 网络也是比较可靠的。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;首先给出一个共识机制在去中心化程度上的排名，这个排名几乎是毋庸置疑的：&lt;/p&gt;
&lt;p&gt;PoW &amp;gt; PoS &amp;gt; DPoS &amp;gt; BFT&lt;/p&gt;
&lt;p&gt;然后从处理分叉的角度，对比一下 PoS 和 BFT 的差异。&lt;/p&gt;
&lt;p&gt;因为 BFT</summary>
        
      
    
    
    
    
    <category term="BFT" scheme="https://smallyu.net/tags/BFT/"/>
    
  </entry>
  
  <entry>
    <title>对 2025 年区块链行业的预测</title>
    <link href="https://smallyu.net/2024/12/16/%E5%AF%B92025%E5%B9%B4%E5%8C%BA%E5%9D%97%E9%93%BE%E8%A1%8C%E4%B8%9A%E7%9A%84%E9%A2%84%E6%B5%8B/"/>
    <id>https://smallyu.net/2024/12/16/%E5%AF%B92025%E5%B9%B4%E5%8C%BA%E5%9D%97%E9%93%BE%E8%A1%8C%E4%B8%9A%E7%9A%84%E9%A2%84%E6%B5%8B/</id>
    <published>2024-12-16T15:04:43.000Z</published>
    <updated>2026-08-04T11:43:17.357Z</updated>
    
    <content type="html"><![CDATA[<p>2024 年发生的事情：</p><ol><li><p>比特币减半、比特币通过现货 ETF、比特币新高</p></li><li><p>特朗普喊单 BTC，马斯克喊单 DOGE</p></li><li><p>比特币叙事熄火，铭文、符文无人问津，Layer 2 技术没有一个出圈（Lighting Network、Taproot、RGB、RGB++），Layer 2 项目没有一个靠谱（Nervos、Merlin、Nubit、Fractal Bitcoin)</p></li><li><p>以太坊 Cancun 升级引入 Blob，让 Layer 2 成本大幅降低，但是现在 Layer 2 充值到交易所仍然需要25分钟等待期，这一点体验很不好，既然有可能重组，交易就是不安全的</p></li><li><p>NFT 市场消失，Coinbase 和 Binance 关闭 NFT 交易市场</p></li><li><p>DEXX 交易所被盗，上千名用户上千万资金，交易所是华人背景，用户也是华人背景，小白多</p></li></ol><hr><p>2025 年的趋势预测：</p><ol><li><p>比特币价格新高到 14 万美元</p></li><li><p>比特币不需要 OP_CAT</p></li><li><p>以太坊的地位无法撼动，所有 ETH Killer 都没有潜力，包括 Solana、Ton、Tron、Polkadot、Cardano、Avalanche、Cosmos、SUI</p></li><li><p>现有的公链格局不会改变，也不会有新的高市值 Layer 1 出现</p></li><li><p>会有新的技术整合类型的链出现，把低成本的东西作为原生功能，比如预言机、随机数、链上治理、Subnet、Web Assembly、DID 等老技术大杂烩，为了提高吸引力，还会蹭 ZK 和 AI 热度，但会发现实际上 ZK 起不到作用，和 AI 也没有关系</p></li><li><p>跨链一直是刚需，但一直没有去中心化的方案落地，以后也不会有</p></li><li><p>新的链一定会兼容 EVM，新的项目也会优先支持 EVM</p></li><li><p>生态类的项目还会不断出现，这些项目会追随某一条公链的技术，干自己的事情，Ethereum &#x2F; Polkadot &#x2F; Cosmos &#x2F; Internet Computer &#x2F; Avalanche 都提供了这样的生态环境</p></li></ol><hr><p>作为一个普通人，想参与到区块链中，能做的只有 3 件事:</p><ol><li><p>定投比特币</p></li><li><p>定投比特币</p></li><li><p>定投比特币</p></li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;2024 年发生的事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;比特币减半、比特币通过现货 ETF、比特币新高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;特朗普喊单 BTC，马斯克喊单 DOGE&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;比特币叙事熄火，铭文、符文无人问津，Layer 2</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>Restaking 项目的经济难题</title>
    <link href="https://smallyu.net/2024/11/18/Restaking%E9%A1%B9%E7%9B%AE%E7%9A%84%E7%BB%8F%E6%B5%8E%E9%9A%BE%E9%A2%98/"/>
    <id>https://smallyu.net/2024/11/18/Restaking%E9%A1%B9%E7%9B%AE%E7%9A%84%E7%BB%8F%E6%B5%8E%E9%9A%BE%E9%A2%98/</id>
    <published>2024-11-18T15:13:15.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<p>Restaking 是一个相对早期的赛道，其中比较有名的项目是 Eigen Layer，有一段时间很火，因为 Eigen Layer 高薪聘请了以太坊基金会的 Researcher，舆论认为以太坊基金会的成员拿了好处，以至于让以太坊往中心化的方向发展了。</p><p>Eigen Layer 搞得声势浩大，不可否认的是 Restaking 的商业模式行得通，因为 Eigen Layer 已经把路走出来了，在运作模式上不用做过多怀疑。</p><p>但是回到 Restaking 的定义上，仍然有问题值得思考。比如，为什么是 Restaking，而不是 Staking？</p><p>因为 Restaking 项目的本质不是技术问题，而是经济问题。换句话说，Restaking 项目没有技术壁垒，有的是商业模式壁垒，关键看在商业合作上能不能运转起来。</p><p>为什么这么说？</p><p>为什么要 Restaking？因为收益有限。作为一个质押用户，我先把 ETH 质押成 stETH，可以稳定拿 3% 的质押收益，在此基础上，我把 stETH 质押到 Eigen Layer 上，有收益最好，没有也无所谓，反正是白给的，哪怕没有收益，我还有 3% 的保底收益。</p><p>如果是 Staking 呢？就有问题了。我手里拿着 ETH，是去拿 stETH 的稳定 3%，还是去拿 Eigne Layer 上不靠谱的项目收益？</p><p>这里就涉及到一个问题，Eigen Layer 的收益来自哪里？</p><p>PoS 链的质押奖励是原生的，非常稳定。Eigen Layer 呢？奖励只能是来自于使用 AVS 提供服务的用户。那么，AVS 提供了什么服务？</p><p>有一个理论是这样：</p><p>对于 PoS 链的逻辑，用户质押了 token，来给 validator 提供质押权重，然后，validator 提供对块数据的验证。</p><p>Restaking 项目中，用户质押了 token，来给 operator 提供质押权重，然后，operator 可以对任意计算任务提供验证。</p><p>听起来是不是很诱人？PoS 链竟然是 Restaking 项目的子集，这么说起来 Restaking 大有可为。</p><p>这种理论有一定道理，不过稍微较真一点，和 Restaking 比较的应该是 DPoS 共识，而不是 PoS 共识。在去中心化程度上，DPoS 是要比 PoS 差的。</p><p>那么就拿 DPoS 和 Restaking 项目比，Restaking 项目会具有更大的优势吗？</p><p>这里得再考虑一个问题，为什么 DPoS 优先被用于对块数据的验证了？而没有拿着用户的质押份额去验证别的东西？</p><p>答案很简单，因为没有比用户资产更重要的数据了。比如，是银行存款的余额数据重要，还是明天天气预报数据的准确度重要？所以 DPoS 在此前有且仅有一个场景，那就是验证区块交易数据，而且，想再找到一个比资产数据更重要的场景，是非常困难的。</p><p>在明确了这个概念后，回到 Restaking 收益的问题，假如 DPoS 能提供 3% 的质押收益，Restaking 能提供多少收益呢？按道理是少于 3% 的，因为 Restaking 在验证的数据，不会比用户的资产数据更重要。</p><p>这就是为什么 Restaking 项目一定要 Restaking 而不是直接 Staking，因为收益率比不过 Staking。在 Staking 的基础上做 Restaking，性质就不一样了。</p><p>回到收益来源的问题，其实 DPoS 和 Restaking 的质押收益，都来自于使用服务的用户。</p><p>DPoS 中，有一批人质押了 token 成为 validator，另一批人依赖于 validator 提供的资产安全能力。所以用户允许 DPoS 链增发 3% 的 token 作为块奖励。供应总量增加，不就相当于洗劫了所有人嘛。</p><p>Restaking 中，有一批人质押了 token 成为 operator，另一批人依赖于 operator 提供的数据验证能力，基本逻辑和 DPoS 是相似的。这里的 “另一批人”，就指使用 AVS 服务的用户，而这些用户，也将会为 Restaking 的收益付费。</p><p>Restaking 的收益不可能凭空产生，直接来源就是用户，而付费方式也可以很简单，就是字面意思的付费，我调用一次 AVS 上的某个数据，就付费 0.1 美元，类似这种方式，直接计费就好了，都不需要涉及什么供应量。</p><p>用户支付的费用，有多少才够呢，假如有 1M 的 stETH 质押量，为了给质押者提供 1% 的年化，假如有 1 万个用户每天使用一次的话……</p><p>到这里问题就又来了，有哪个冤种用户会愿意为了这种服务花钱？要知道以太坊 L1 的链上交易，一次 1 美元可就贵死了，更是有很多很多人，舍不得开 9 块钱的爱奇艺会员看视频，无数程序员，舍不得为自己日夜使用的 IDEA 买个正版……</p><p>当然，只要泡沫不破裂，大家的财富就都在膨胀，牛市来临之后，没有人是在亏钱的，只要泡沫不破裂，只要能够在泡沫破裂前离场，一切都会很美好。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Restaking 是一个相对早期的赛道，其中比较有名的项目是 Eigen Layer，有一段时间很火，因为 Eigen Layer 高薪聘请了以太坊基金会的</summary>
        
      
    
    
    
    
    <category term="Restaking" scheme="https://smallyu.net/tags/Restaking/"/>
    
  </entry>
  
  <entry>
    <title>如何看懂任意区块链项目的技术架构</title>
    <link href="https://smallyu.net/2024/10/15/%E7%9C%8B%E6%87%82%E4%BB%BB%E6%84%8F%E5%8C%BA%E5%9D%97%E9%93%BE%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84/"/>
    <id>https://smallyu.net/2024/10/15/%E7%9C%8B%E6%87%82%E4%BB%BB%E6%84%8F%E5%8C%BA%E5%9D%97%E9%93%BE%E9%A1%B9%E7%9B%AE%E7%9A%84%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84/</id>
    <published>2024-10-15T15:43:36.000Z</published>
    <updated>2025-11-09T09:22:13.493Z</updated>
    
    <content type="html"><![CDATA[<h3 id="基本结构"><a href="#基本结构" class="headerlink" title="基本结构"></a>基本结构</h3><p>最基本的区块链，就是一条区块链本身，包含有通过共识出块的能力，可能有很多节点，也可能只有一个节点，每个节点都提供接收交易的 RPC 接口：</p><img src="1.png" width="60%"><p>因为以太坊的诞生，区块链的交易不仅仅是交易，还具有了智能合约的能力：</p><img src="2.png" width="60%"><p>总会有一个外部的程序来和智能合约交互，也就是发起交易：</p><img src="3.png" width="60%"><p>从这里开始需要明确两种行为：</p><ol><li>凡是和区块链有交互的，可能是提交交易或者查询交易，都算是链上交互</li><li>和区块链没有任何交互的，属于链下行为</li></ol><p>向智能合约发起交互一定属于链上交互，同样的，无论发起交易的外部程序，是用什么语言写的，可能是 Javascript 或者 Golang，都叫做智能合约的 SDK：</p><img src="4.png" width="60%"><p>除了以太坊的 EVM 合约，还有可能是其他虚拟机（SVM、WASM）的合约，或者 Cosmos 直接操作状态数据库的方式（Native合约），可以统一理解为链上智能合约：</p><img src="5.png" width="60%"><p>只要是链上合约，都会需要链下程序来发起交易，才能够实现某些功能。</p><p>而当智能合约有了具体的逻辑，很可能会触发一些事件，这些事件往往由链下节点来监听和处理：</p><img src="6.png" width="60%"><p>这个链下节点起什么名字都可以，用什么语言写都可以，总之会获取到合约中的事件，一般监听事件的方式有两种：</p><ol><li>主动查，不断请求节点的接口，看有没有新的事件</li><li>被动接受，比如 websocket 建立的长链接</li></ol><p>当合约里触发某个事件后，链下节点监听到事件，会根据事件进入不同代码分支，后面进行多么复杂的操作都可以。</p><p>链下节点可能有多个，也可能有多种角色，但是不重要，重要的是，他们都是在和链上的事件进行交互：</p><img src="7.png" width="60%"><p>一般链下节点之间不会擅自进行通信，而是紧跟区块链的块内容，因为链下节点也需要保持状态的一致，区块链场景下，链本身已经是非常好的能提供状态一致的手段了：</p><img src="8.png" width="60%"><p>所以，这个时候你就知道，其实对于区块链项目来说，链下节点和链上节点之间，只有两种动作：</p><ol><li>通过交易向区块链发送数据</li><li>通过监听事件来从区块链查询事件</li></ol><p>Cosmos 有一个能支持 EVM 合约的项目，还提供了 evm_hook 的接口，当 EVM 合约触发事件的时候会主动调用接口函数。这种 hook 函数本质上也是合约事件的被动监听，无非是从合约事件到链下节点调用的方式，从 RPC 请求变成了函数的直接调用。唯一增加的复杂度是 EVM 合约触发了 Cosmos 模块的函数，把两种合约的实现方式串起来了：</p><img src="9.png" width="60%"><p>复杂架构也不会改变的是，链下节点和链上节点之前的交互，只有两种动作。</p><p>如果再复杂一点的话，会往什么方向复杂呢，链下节点开始向链上提交交易了：</p><img src="10.png" width="60%"><p>也许在监听到事件后提交了一笔交易，交易触发了另一个事件，监听到另一个事件后进行了更多的操作。但是总之，链下节点和链上节点之前的交互，仍然只有两种动作。</p><h3 id="填充业务逻辑"><a href="#填充业务逻辑" class="headerlink" title="填充业务逻辑"></a>填充业务逻辑</h3><p>有了基本的技术手段，再往上填充业务逻辑，就容易理解了。</p><p>面对很长的业务逻辑，总是能分清楚哪些部分是链上交互、哪些部分是链下行为，核心区别在于数据状态存在哪里，是区块链上，还是链下节点：</p><ol><li>如果是通过交易发送到链上，业务的复杂程度就取决于合约的逻辑。</li><li>如果是链下节点监听到事件后的行为，那就取决于链下代码的复杂程度。</li></ol><p>如果复杂度超出了上述两种情况，就只能是链下节点之间脱离区块链进行了擅自的通信，并且还产生了不一致的状态，这样的链下节点可以认为已经在区块链项目之外了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;基本结构&quot;&gt;&lt;a href=&quot;#基本结构&quot; class=&quot;headerlink&quot; title=&quot;基本结构&quot;&gt;&lt;/a&gt;基本结构&lt;/h3&gt;&lt;p&gt;最基本的区块链，就是一条区块链本身，包含有通过共识出块的能力，可能有很多节点，也可能只有一个节点，每个节点都提供接收交易的</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>为什么不要做以太坊的二次开发</title>
    <link href="https://smallyu.net/2024/09/12/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%A6%81%E5%81%9A%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91/"/>
    <id>https://smallyu.net/2024/09/12/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%A6%81%E5%81%9A%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91/</id>
    <published>2024-09-12T07:29:05.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<p>道理很简单，以太坊是有核心开发团队的，以太坊基金会为中心，各个以太坊客户端的开发团队，都拿了以太坊基金会的钱给以太坊做开发。Vitalik 来中国融资，拿完钱到某个国家组建开发团队，密集开发一年后有了以太坊。当年以太坊基金会以不到 $1 的价格参与以太坊的 ICO，现在 ETH 的价格已经涨了几千倍，以太坊基金会的钱就是这么来的。</p><p>以太坊生态主要有三个部分，Layer 1、Layer 2 以及生态类项目。（Staking 类不需要你开发，Restaking 和技术有关系？）</p><p>Layer 1 部分，有几大客户端团队，执行层的 Geth、Nethermind、Besu、Erigon、Reth，共识层的 Prysm、Lighthouse、Teku、Nimbus、Lodestar，先不管这些团队和以太坊的利益关系如何，如果你自己或者你所在的公司，说是想要基于以太坊的客户端做二次开发，那么请问，要开发什么？</p><p>如果是对以太坊网络本身有益的改进，能够提高性能、优化数据结构之类，你大可以直接给以太坊提交 Issue 以及 PR，甚至建立合作关系，直接让以太坊客户端的官方版本来支持你的优化。何况这些客户端的创始团队仍然处于活跃的开发状态，你觉得自己有理由可以比他们 “自己人” 做的更出色吗？无论是客户端功能方面还是性能方面，无论你是个人还是公司的力量。</p><p>例如并行 EVM，试图提高 EVM 交易的执行速度，这是 Geth 团队都没能解决的难题，随便一个小团队能做得成？</p><p>如果是对以太坊无益而对自己链有益的改进，你预期以太坊的开发团队不会接受你的提议和改进，那这个问题就更加奇特，你的链是有多么特殊的需求，需要做这些必须 “自有” 的开发？以太坊的完成度已经比较高了，如果遇到这样的场景，似乎需要回到一开始的需求来评估整件事情。</p><p>Layer 2 的部分，比较大的团队有 Polygon、Optimism、Zksync 等。以太坊的 Layer 2 为什么能做大做强能火？Layer 2 是 Vitalik 认可的方向。ENS 项目为什么能广泛普及？ENS 是 Vitalik 认可的项目。AA 钱包为什么热闹了一段时间？Vitalik 喊灵魂绑定带起来的。你可以从 Vitalik 的博客中看到，凡是目前比较大的生态类项目，都和 Vitalik 本人有直接的联系，这些项目的创始人都是可以和 Vitalik 说上话的。</p><p>Vitalik 是整个以太坊生态背后的大 Boss，那么假如你说你想搞一个以太坊生态的项目，是优先考虑技术能力方面的问题吗？一个项目能不能成，先看什么？先看以太坊的 Roadmap 有没有这个方向，再看以太坊生态里这个方向的头部项目是哪个，然后呢？你会发现跟你没什么关系。以太坊基金会又没有给你钱，你凑什么热闹？</p><p>站在开发者的角度，假如你想参与以太坊的生态建设，那么请问，要以太坊的核心团队干什么？如果你想改进 Op Stack 的 Fault Proofs，那么请问，要 Optimism 的核心团队干什么？你作为一个局外人，花费时间和资源去给别人抬轿子？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;道理很简单，以太坊是有核心开发团队的，以太坊基金会为中心，各个以太坊客户端的开发团队，都拿了以太坊基金会的钱给以太坊做开发。Vitalik 来中国融资，拿完钱到某个国家组建开发团队，密集开发一年后有了以太坊。当年以太坊基金会以不到 $1 的价格参与以太坊的 ICO，现在</summary>
        
      
    
    
    
    
    <category term="程序员" scheme="https://smallyu.net/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    
  </entry>
  
  <entry>
    <title>为什么不要做智能合约开发和 DeFi 开发</title>
    <link href="https://smallyu.net/2024/09/11/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%A6%81%E5%81%9A%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%92%8CDeFi%E5%BC%80%E5%8F%91/"/>
    <id>https://smallyu.net/2024/09/11/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%A6%81%E5%81%9A%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%BC%80%E5%8F%91%E5%92%8CDeFi%E5%BC%80%E5%8F%91/</id>
    <published>2024-09-11T12:51:35.000Z</published>
    <updated>2026-08-04T11:43:16.383Z</updated>
    
    <content type="html"><![CDATA[<p>目前市场上的招聘，“合约开发”一般指“以太坊上的EVM合约开发”。而事实上，世界上不只有以太坊一条链，可能有上百条链，也不止以太坊有智能合约，Solana 有 SVM，Polkadot 有 Wasm，Cosmos 在用平行链的方式达到智能合约的效果，等等，还有很多各种各样的实现形式。</p><p>所以看出问题了吗？如果一个程序员号称自己是 “合约开发”，说明他把自己局限在了一个狭窄的方向上。Solidity 是以太坊团队创造出来的脚本语言，而“合约开发”把自己的职业生涯交付在了这一种不成熟脚本语言上。至少，咱们应该是 “程序员”，而不是 “Solidity 程序员” 吧。</p><p>程序员可以在需要的时候，做一些合约开发的工作，而合约开发者，就只能做开发合约的工作。单就 Solidity 这种语言来说，语法的学习成本是多高呢？一般半个星期左右就可以开始上手写。</p><p>再是关于 DeFi 开发，因为做合约开发的大多都在学习 DeFi 开发。这里的问题在于，无论是 Centralized Finance，还是 Decentralized Finance，本质都是 Finance，核心是 “金融”，“去中心化” 只是金融的修饰词。</p><p>“金融”是什么？完全和“程序员”是两个行业，全世界的精英都在华尔街搞金融，一个半路出道的程序员，你能搞金融？现在的 DeFi 项目是不是都和杠杆、质押、借贷有关？Luna 暴雷是不是杠杆加太高了？Luna 背后有几家公司的资本力量参与？一般人能整明白 Luna 暴雷的原因吗？整不明白的，专业的金融人士分析半天也许能有点结果。</p><p>也就是说，专业的金融人士未必是程序员，而程序员几乎不可能成为专业的金融人士。金融领域的水很深，不是会写代码学一学 Solidoty 就可以的，更不是一个程序员励志要做 DeFi，就能学得会的。</p><p>更进一步，假如程序员把 DeFi 学懂了，能做些什么事情？能做的就是给资本大佬打工，让你实现什么业务逻辑就实现什么业务逻辑，有自由发挥的余地吗？难道你要自己设计一种金融逻辑？搞笑呢？有权力控制大量资产的一定不是程序员，而程序员一定没有权力控制大量资产。</p><p>我的意思是，如果一个程序员想搞懂金融然后表达对整个金融行业独到的见解，是…几乎不可能的，难度非常非常大，有那样能力的人不会是程序员。而如果你只是想要搞懂某个技术领域的情况然后发表一些观点，还是存在可能性的，至少不需要你拥有（或者替别人管理）大量资产吧。</p><p>如果懂金融的程序员自己开一家金融公司呢？你确定？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;目前市场上的招聘，“合约开发”一般指“以太坊上的EVM合约开发”。而事实上，世界上不只有以太坊一条链，可能有上百条链，也不止以太坊有智能合约，Solana 有 SVM，Polkadot 有 Wasm，Cosmos</summary>
        
      
    
    
    
    
    <category term="程序员" scheme="https://smallyu.net/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    
    <category term="智能合约" scheme="https://smallyu.net/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
  </entry>
  
  <entry>
    <title>我的加密货币定投策略（一）</title>
    <link href="https://smallyu.net/2024/08/28/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%AE%9A%E6%8A%95%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%80%EF%BC%89/"/>
    <id>https://smallyu.net/2024/08/28/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E5%AE%9A%E6%8A%95%E7%AD%96%E7%95%A5%EF%BC%88%E4%B8%80%EF%BC%89/</id>
    <published>2024-08-27T18:15:14.000Z</published>
    <updated>2025-11-17T14:37:00.733Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>随着时间的推移以及对市场的更深入了解，定投策略尤其是投资组合会产生非常大的变化，所以在标题中用（一）标识，如果定投策略有变动，可能还会有（二）、（三）……当然也会同时说明变动的原因。</p></blockquote><p>今年以来开始更加关注投资的话题，也因为乱操作损失过一些钱。</p><p>最近中了李笑来的毒，定投策略的理论让人感到很兴奋，可以从以下资料更加深入了解：</p><ul><li>《<a href="https://ri.firesbox.com/#/">定投改变命运</a>》—— 李笑来，所有关于定投的内容都在这本书里</li><li><a href="https://youtu.be/tmRQImBk6NA?si=Cl7Fwiq2WcfM28P2">定投改变命运直播公开课</a> —— Youtube 视频，李笑来本人的解读</li></ul><p>除了定投，还有几个其他投资方面的参考资料：</p><ul><li>《<a href="https://github.com/xiaolai/the-self-cultivation-of-leeks">韭菜的自我修养</a>》—— 李笑来</li><li><a href="https://www.youtube.com/@ChandlerGuoChannel">郭宏才（宝二爷）的 Youtube 频道</a></li></ul><p>我想实践一下定投的投资方式，其中涉及到的几个问题：</p><ol><li>是否要加入李笑来的投资实践群？没有必要，尤其是对于有自制力的人</li><li>是否要买 <a href="https://b.watch/">BOX</a>？江湖传言，李老师的书必须看，课可以听，币千万别买</li><li>定投的方式？Binance 自带的定投功能就可以，0 手续费（要记得时不时提到钱包）</li><li>定投的频率是？每天，因为价格变化太快了，如果频率太低，周期会变得太久</li><li>定投的周期是？目前规划了用来定投 1 年的钱</li><li>定投的标的是？</li></ol><p>我没有盲目跟从李笑来 BOX 中标的的选择和比例。李笑来的 BOX 一开始给了 EOS 很高比例，事实证明 EOS 失败了，所以现在 BOX 的成分里已经不包含 EOS。最新的情况是，BOX 中 BTC 占了 92% 的比例。另外 BOX 的成分中一直含有 XIN，说明人都会受到立场的左右，因为我们大多数人可能都不知道 XIN 是什么，而李笑来多年坚定的选择这个币。</p><p>我选择的标的和分配的比例是：</p><table><thead><tr><th align="center">序号</th><th align="center">标的</th><th align="center">比例</th><th align="center">标签</th><th align="center">风险</th><th align="center">供应量上限</th></tr></thead><tbody><tr><td align="center">1</td><td align="center">BTC</td><td align="center">50%</td><td align="center">PoW, UTXO</td><td align="center">低</td><td align="center">✅</td></tr><tr><td align="center">2</td><td align="center">ETH</td><td align="center">10%</td><td align="center">智能合约平台</td><td align="center">中</td><td align="center">❌</td></tr><tr><td align="center">3</td><td align="center">LTC</td><td align="center">5%</td><td align="center">PoW, UTXO</td><td align="center">高</td><td align="center">✅</td></tr><tr><td align="center">4</td><td align="center">DOGE</td><td align="center">5%</td><td align="center">PoW, UTXO</td><td align="center">高</td><td align="center">❌</td></tr><tr><td align="center">5</td><td align="center">BCH</td><td align="center">5%</td><td align="center">PoW, UTXO</td><td align="center">高</td><td align="center">✅</td></tr><tr><td align="center">6</td><td align="center">ADA</td><td align="center">5%</td><td align="center">PoS, UTXO</td><td align="center">高</td><td align="center">✅</td></tr><tr><td align="center">7</td><td align="center">SOL</td><td align="center">5%</td><td align="center">智能合约平台</td><td align="center">高</td><td align="center">❌</td></tr><tr><td align="center">8</td><td align="center">FIL</td><td align="center">5%</td><td align="center">PoSt</td><td align="center">非常高</td><td align="center">❌</td></tr><tr><td align="center">9</td><td align="center">TON</td><td align="center">5%</td><td align="center">智能合约平台</td><td align="center">非常高</td><td align="center">❌</td></tr><tr><td align="center"><del>10</del></td><td align="center"><del>XEC</del></td><td align="center"><del>3%</del></td><td align="center"><del>PoW, UTXO</del></td><td align="center"><del>非常高</del></td><td align="center"><del>✅</del></td></tr><tr><td align="center"><del>11</del></td><td align="center"><del>DASH</del></td><td align="center"><del>2%</del></td><td align="center"><del>PoW, UTXO</del></td><td align="center"><del>非常高</del></td><td align="center"><del>✅</del></td></tr><tr><td align="center">10</td><td align="center">XMR</td><td align="center">5%</td><td align="center">PoW, UTXO</td><td align="center">非常高</td><td align="center">❌</td></tr></tbody></table><p>（2024.09.26 更新）</p><p>可以从这个公开的 <a href="https://coinmarketcap.com/watchlist/66d339a5c316be09d04b7b16/">Watchlist</a> 看到这些币种的集合，链接会跳转到 CoinMarketCap 网站上。能注意到的是，几乎所有币种的市值排名都在 100 之内，这条原则来自《<a href="/2024/05/04/%E7%82%92%E5%B8%81%E6%8A%95%E8%B5%84%E7%9A%84%E5%B0%8F-tips/">炒币投资的小 tips</a>》第 4 条。</p><p>我的定投组合中，BTC 一定是首位，占 50% 的比例。另外 5 个 PoW 的币总计占了 20% 的比例，也就是说，PoW 币总计占比 70%。PoW 的币大都有供应量上限。</p><p>由于 “比特金，莱特银” 的说法，LTC一直比较强势，DOGE 和 LTC 是难兄难弟，挖矿算法一样，<a href="https://www.litecoinpool.org/">矿池</a>都是同时挖这两个币，所以 LTC 和 DOGE 的价格不能简单像 BTC 一样估算 “矿机关机价”，而 DOGE 又属于 memecoin 的老大，历史久，马斯克喜欢，也放不下的，所以 LTC 和 DOGE 要一起出现。</p><p>BCH 是 BTC 在手续费超高年代（2017年）的 workaround 方案，Blocksize War 真的已经结束了吗？BCH 是大区块的代表，所以要把 BCH 选进来。BTC 已经脱离 “电子现金” 的目的，变成了 “储存价值”，相信这个问题上的争议还没有彻底结束。那后来 BCH 又分叉出了 BSV，为什么没有把 BSV 涵盖进来？BSV 的生态小，而且生态里有一些活跃但无知的项目（Note 之类），BSV 的创始人还在被法院通缉，等等原因。</p><p>DASH 和 XEC 是最没道理的两个，冷门到很多人没听过，也是风险最高的两个。其中 XEC 是因为 BCH 的 <a href="https://github.com/bitcoin-cash-node/bitcoin-cash-node">节点代码</a> fork 自 XEC 的代码，相信 XEC 有不错的技术能力。DASH 是 LTC 的一个 fork，更多是凑数性质，因为很多老牌钱包比如 Unstoppable 支持的币种列表大都是 PoW 系列的币，DASH 就在其中，而且币安的矿池服务支持的币种不多，其中也有 DASH，所以就把 DASH 作为 “电子现金” 的高风险备选了。</p><p>然后是 ADA，学术能力强，纯粹的 PoS 共识，用了 UTXO 模型，属于 BTC 在 PoS 共识上的平替，而且 ADA 技术上在不断更新，如果有一天 PoS 赢了，ADA 绝对是绕不过去的一个，所以预计 ADA 能有更好的未来。</p><p>FIL 的话，属于分布式存储领域。计算机科学有两大方向，分布式计算和分布式存储，ETH 说自己是 “世界计算机”，担起了分布式计算的职责，而 FIL 是存储领域的老大。印象里 libp2p 是 Filcoin 开发的，而很多区块链像 ETH 用的 p2p 模块，就直接用的是 libp2p。以及目前非常广泛使用的 IPFS，也是 Filecoin 实验室开发的。所以 FIL 在研发方面实力很强，未来也许会有得到突破的一天。</p><p>最后是智能合约平台类型。其实我不太喜欢智能合约，很多人喜欢把智能合约等同于区块链，或者认为只有支持智能合约的区块链才叫区块链。在囤币方面，币只是智能合约平台的 <a href="/micro-blog/#2024-31">燃料</a>，又没有供应量上限，囤它干嘛。但是智能合约平台火，玩的人多，价格居高不下。ETH、SOL、TON 的特点都是支持智能合约、现在关注量比较大，所以适当分配一些比例上去。</p><p>另外这是一些热门币没有被涵盖进来的原因：</p><table><thead><tr><th align="center">币种</th><th align="center">未选择原因</th></tr></thead><tbody><tr><td align="center">BNB</td><td align="center">平台币，不是链</td></tr><tr><td align="center">DOT</td><td align="center">没有供应量上限、APR 很高</td></tr><tr><td align="center">ATOM</td><td align="center">没有供应量上限、APR 很高</td></tr><tr><td align="center">AVAX</td><td align="center">没有供应量上限、没看到明显优点</td></tr><tr><td align="center">APT, SUI</td><td align="center">BFT 类共识</td></tr><tr><td align="center">所有 ERC-20 代币</td><td align="center">不是链</td></tr><tr><td align="center">所有 Memecoin</td><td align="center">没有长期价值</td></tr><tr><td align="center">铭文、符文</td><td align="center">BTC 手续费太贵</td></tr></tbody></table><p>祝自己好运！</p><h3 id="更新（2024-09-26）"><a href="#更新（2024-09-26）" class="headerlink" title="更新（2024.09.26）"></a>更新（2024.09.26）</h3><p>去掉了原先投资组合中的 XEC 和 DASH。因为在原先的选择中，XEC 和 DASH 本就是理由不充分的两个标的，而他们的总市值排名在 100 左右甚至之外，社区热度也比较差。</p><table><thead><tr><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th></tr></thead><tbody><tr><td align="center"><del>10</del></td><td align="center"><del>XEC</del></td><td align="center"><del>3%</del></td><td align="center"><del>PoW, UTXO</del></td><td align="center"><del>非常高</del></td><td align="center"><del>✅</del></td></tr><tr><td align="center"><del>11</del></td><td align="center"><del>DASH</del></td><td align="center"><del>2%</del></td><td align="center"><del>PoW, UTXO</del></td><td align="center"><del>非常高</del></td><td align="center"><del>✅</del></td></tr></tbody></table><p>更重要的是，现在要用 XMR 来代替它们原先占有的共 5% 的比例。XMR 是暗网使用的 <a href="https://x.com/DarkDotFail/status/1765104459913330820">主要货币</a> 之一，暗网仅接收 BTC 和 XMR。XMR 在隐私保护方面非常强。而在暗网的应用领域，XMR、XEC、DASH 其实是经常被 <a href="https://x.com/Altcoinbuzznews/status/1746989440373784958">并列讨论</a> 的三个币种。要在这三个同类型代币中选择一个的话，就首选 XMR。</p><table><thead><tr><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th><th align="center"></th></tr></thead><tbody><tr><td align="center">10</td><td align="center">XMR</td><td align="center">5%</td><td align="center">PoW, UTXO</td><td align="center">非常高</td><td align="center">❌</td></tr></tbody></table>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;随着时间的推移以及对市场的更深入了解，定投策略尤其是投资组合会产生非常大的变化，所以在标题中用（一）标识，如果定投策略有变动，可能还会有（二）、（三）……当然也会同时说明变动的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;今年以来开始更加关注投</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>PoS 类型的区块链如何处理分叉</title>
    <link href="https://smallyu.net/2024/08/22/PoS%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%88%86%E5%8F%89/"/>
    <id>https://smallyu.net/2024/08/22/PoS%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%88%86%E5%8F%89/</id>
    <published>2024-08-22T09:41:01.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<p>主流公链从共识机制的角度基本上可以分为 3 类，分别是 PoW、PoS、PBFT。选择了不同的共识，也就很大程度上决定了网络的 TPS、去中心化程度、节点规模。</p><p>除了 PoW，另外两种共识 PoS 和 PBFT 都面临一个基本的问题，就是当网络发生了软分叉，该如何恢复？由于 PoS 和 PBFT 产生块不需要算力成本，也就不能用和 PoW 一样的最长链原则。</p><h3 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h3><h4 id="共识概览"><a href="#共识概览" class="headerlink" title="共识概览"></a>共识概览</h4><p>PoW 系列的链有 BTC、BCH、BSV、LTC、DOGE、ZEC 等。PoW 都使用最长链原则，节点在面对多个发生了分叉的链时，直接选择块高度最高的一条就行了。由于每产生一个块都需要庞大的算力，攻击成本比较高。</p><p>PoS 系列的链有 ETH、BNB、TRON、DOT、TON、ADA、AVAX、NEO 等。几乎目前所有智能合约平台类型的链，都属于 PoS 共识。</p><p>PBFT 系列的链有 ATOM、SOL、TON、ONT、APT、SUI 等。其中 Cosmos 最为知名，Solana 有超越以太坊的势头，The Open Network 今年也发展的很好。</p><p>这里可能会觉得有点奇怪，怎么把 SOL 归类到 PBFT 上了？SOL 不是 PoH 共识吗？SOL 不也有质押的功能，比如在 Solflare 钱包上还可以质押获得收益的吗？TON 也有质押和收益啊？</p><p>这里是两个问题。</p><p>首先 Solana 的确开发和使用了 BFT 类的共识，叫 <a href="https://solana.com/news/8-innovations-that-make-solana-the-first-web-scale-blockchain">Tower BFT</a>，PoH 是用来解决 Solana 链上的时钟问题的，而不是一种完整的共识机制。 </p><p>其次是 PoS+BFT 类共识，算 PoS 还是 BFT？上面提到的分类，PoS 主要指 PoS、dPoS、PoS Casper 这些，凡是用到 BFT 的都归类为 PBFT 作为区分。最明显的就是 Cosmos 也有质押和收益功能，但很少有人会说 Cosmos 用的是 PoS。</p><h4 id="关于分叉"><a href="#关于分叉" class="headerlink" title="关于分叉"></a>关于分叉</h4><p>PoS 共识和 PBFT 共识面对分叉问题的时候，有两个方面。</p><p>一方面是质押者列表（以太坊叫 <code>Validators</code>，Cardano 叫 <code>Stakeholders</code>）是否一致，因为 PoS 和 PBFT 大都是使用 VRF 从一组候选列表中选择出一个节点作为出块节点，那么 PoS 和 PBFT 类共识在处理这个问题的时候有哪些异同？</p><p>另一方面是当网络分叉后，在选择链的规则（以太坊叫 <code>Forkchoice</code>，Cardano 叫 <code>Chain selection rule</code>）方面有什么异同？</p><h3 id="质押者列表不一致"><a href="#质押者列表不一致" class="headerlink" title="质押者列表不一致"></a>质押者列表不一致</h3><h4 id="联盟链"><a href="#联盟链" class="headerlink" title="联盟链"></a>联盟链</h4><p>从最简单的联盟链开始分析。联盟链的特点是没有 coin，也就完全没有质押方面的内容，只是单纯的 PBFT。</p><p>联盟链顾名思义，有非常高的准入门槛，需要经过审核或者某种授权也能够成为联盟成员。具体到技术层面，就是想要加入网络，需要在其他节点都知道的情况下，比如所有节点的配置文件里，都包含一个网络成员的列表，列表里定义了网络的节点公钥以及对应的 index，想要增加节点就需要其他所有节点都改一下配置文件。</p><p>节点在出块的时候，就会从这个列表中使用 VRF 随机选择一个作为出块节点。一般 VRF 返回的是一个简单的数字，对应公钥列表的 index，出块节点用这个公钥来对块签名。</p><p>这样的做法比较笨拙，但也是联盟链的特点。在这种模式下，节点的质押着列表不太可能不一致，如果不一致就是配置文件写错了。而且配置错误的情况下，它将永远是错误的，排查起来很简单。</p><h4 id="Cosmos"><a href="#Cosmos" class="headerlink" title="Cosmos"></a>Cosmos</h4><p>Cosmos Hub 用的共识叫 <a href="https://docs.cometbft.com/v0.37/introduction/">CometBFT</a>，基本流程是花费不少于 180 个 ATOM 注册成为 Validaotr，然后就有可能会选为出块节点。</p><p>由于 BFT 类共识在出块之前就需要投票，所以假如网络中真的出现了质押者列表不一致的情况，在同一个块高度会有两个节点产生出两个块，此时网络中的其他节点会对这两个块进行投票。</p><p>这个时候也分两个场景，就是网络正常和网络异常的情况。</p><p>在网络正常的情况下，现在有两个块，一定只能有一个块收到大于 2&#x2F;3 的投票，不可能两个块都收到大于 2&#x2F;3 的投票。所以在出块之前，就已经把质押者列表和其他节点不一致的节点排除在外了，不会影响后续流程。</p><p>在网络异常的情况下，节点感知不到其他节点的存在，即使当前节点的质押者列表正好是当前子网络中的有限几个节点，其他几个节点也不会把票投过来。除非整个子网络都断网了、质押者列表还发生了一样的错误，那这个自网络就自己在局域网玩吧。网络异常本身就是一种异常情况了，与外界隔绝。</p><h4 id="Cardano"><a href="#Cardano" class="headerlink" title="Cardano"></a>Cardano</h4><p>Cardano 的 PoS 是最纯粹的 PoS，没有投票机制。Cardano 的共识经历了<a href="https://iohk.io/en/blog/posts/2022/06/03/from-classic-to-chronos-the-implementations-of-ouroboros-explained/">很多次演进</a>（内容很多很复杂，我没看完）。</p><p>Cardano 网络的规则是，任何人都可以质押任意金额到 Stake pools 中成为 <a href="https://docs.cardano.org/about-cardano/learn/delegation/">Delegator</a>，这些 Delegator 按照质押金额的比例共享矿池的收益，但是不会有出块的资格。</p><p>在 Cardano 网络中真正有出块权限的是 Stake pools，也就是说有可能被选为出块节点的节点，都在 <a href="https://preprod.cexplorer.io/pool">矿池列表上</a> 了，数量不多，目前大概 300 个左右，每个 solt 将从中随机选择一个来产生块。</p><p>那么 Stake pools 节点注册之后，如果节点之间出现 Stake pools 列表不一致的情况怎么办？Cardano 的文档中有 <a href="https://developers.cardano.org/docs/operate-a-stake-pool/introduction-to-cardano#how-it-works">描述</a>，当遇到同一 slot 产生了两个块的时候，就开始启用链选择的规则（Chain Selection Rule）了。也就是说，实际上当第二个块被产生出来，链就已经分叉了，然后所有节点都启用链选择的规则，来进行恢复。</p><h4 id="Ethereum"><a href="#Ethereum" class="headerlink" title="Ethereum"></a>Ethereum</h4><p>以太坊要成为 Validator 需要花费 32 个 ETH 把节点信息注册到 <a href="https://etherscan.io/address/0x00000000219ab540356cbb839cbe05303d7705fa">质押合约</a> 上，然后其他所有的 Validator 都会从质押合约获取质押者列表的信息。</p><p>那怎么确定其他 Validators 都已经把质押者信息从合约同步到本地了？你可以在 Beacon Chain 浏览器的任意一个 <a href="https://beaconcha.in/block/20584195">块信息</a> 上，找到一个叫 <a href="https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#eth1-data">Eth Data</a> 的字段，这个字段对于质押者列表非常重要，当一个 validator 被选为出块节点时，它会把当前节点同步到的质押者列表信息，一起打包进块里，包括质押者的总数以及 Deposit root 信息。</p><p>以太坊网络大概每 17 个小时进行一次 <a href="https://github.com/ethereum/consensus-specs/blob/v1.3.0/specs/phase0/validator.md#process-deposit">质押者列表的更新</a>。在这个周期中，只有超过半数区块的 Eth Data 包含了新增的 validator，新的 validator 才会真正加入到网络中。</p><p>所以以太坊要加入 validators 的过程是漫长而且严格的，首先要确认其他 proposer 已经同步了相关信息，才会真正更新质押者列表。在这样的规则模式下，质押者列表很难不一致。</p><h3 id="分叉链选择"><a href="#分叉链选择" class="headerlink" title="分叉链选择"></a>分叉链选择</h3><h4 id="Ethereum-1"><a href="#Ethereum-1" class="headerlink" title="Ethereum"></a>Ethereum</h4><p>以太坊中如果出现了多条分叉的链，选择起来时相对容易的，因为以太坊有投票机制，每一个块上都包含了有多少个 validators 对块进行了投票。可以猜想到，在发生分叉时，只要不断选择投票数多的块就可以了。</p><p>而实际上以太坊的分叉选择基于 <a href="https://ethos.dev/beacon-chain">checkpoint 机制</a>，每个块是一个 slot，每 32 个 slot 是一个 epoch，每个 epoch 都是一个检查点。一个检查点收到大于 2&#x2F;3 的投票，就进入了 justified 的状态，当一个检查点的下下个检查点也进入 justified 状态，当前检查点就认为是 finalized 状态了。所以在以太坊中，一笔交易最终被标记为 finalized 需要 15 分钟。</p><p>这里提到的检查点，也就是 <a href="https://arxiv.org/abs/1710.09437">FFG</a> 进行 forkchoice 的依据，每条链会选择 checkpoint 多的链。所以以太坊的共识不是选择 “有最多块的链” 原则，而是选择 “有最多检查点的链” 原则，检查点最多的链就是主链。</p><h4 id="Cardano-1"><a href="#Cardano-1" class="headerlink" title="Cardano"></a>Cardano</h4><p>Cardano 最新在使用 <a href="https://developers.cardano.org/docs/operate-a-stake-pool/introduction-to-cardano/#what-if-for-some-reason-there-is-a-fork">Chain selection rule</a>，是由 <a href="https://dl.acm.org/doi/10.1145/3243734.3243848">Ouroboros Genesis</a> 版本提供的。</p><p>Ouroboros Genesis 的上一个版本是 <a href="https://link.springer.com/chapter/10.1007/978-3-319-78375-8_3">Ouroboros Praos</a>，Praos 版本中提出了一套叫 <code>maxvalid</code> 的规则，Genesis 版本基于 <code>maxvalid</code> 做了一点改进，把 <code>moving checkpoint</code> 的特性结合了进来，形成了新版本叫做 <code>maxvalid-mc</code> 的规则。</p><p>移动检查点简单理解就是，本地链在面对多条分叉链时，如果没有超过 k 个块，就选最长的链，如果超过了 k 个块，就直接不选它。也就是说本地链只会在 k 个块的范围内，选择链最长的一个。而 k 个块的范围就是所谓的移动检查点（moving checkpoint）。加了这样限制的好处就是可以避免最长链攻击。当然 Cardano 制定这样的规则经过了一系列学术上的推演以及实际场景的检验。</p><h4 id="Cosmos-1"><a href="#Cosmos-1" class="headerlink" title="Cosmos"></a>Cosmos</h4><p>PBFT 链在网络正常的情况下，只要保证质押者列表一致，就不会分叉。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>总的来说，每种共识的具体实现，都包含了详细的处理分叉的规则，而且这些老牌公链都经过了实际运行的检验。具体如何实现与链的设计理念有关。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;主流公链从共识机制的角度基本上可以分为 3 类，分别是 PoW、PoS、PBFT。选择了不同的共识，也就很大程度上决定了网络的 TPS、去中心化程度、节点规模。&lt;/p&gt;
&lt;p&gt;除了 PoW，另外两种共识 PoS 和 PBFT</summary>
        
      
    
    
    
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>Ethereum Casper 为什么需要 EIP-7251</title>
    <link href="https://smallyu.net/2024/06/09/Ethereum-Casper-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-EIP-7251/"/>
    <id>https://smallyu.net/2024/06/09/Ethereum-Casper-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-EIP-7251/</id>
    <published>2024-06-09T15:57:17.000Z</published>
    <updated>2025-11-09T09:22:13.307Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://arxiv.org/abs/1710.09437">Casper the Friendly Finality Gadget</a> 是以太坊现在使用的共识机制，属于 PoS 的一种实现。这种关系类似于同样是 PoW 挖矿，Bitcoin 使用 sha256 而 Dogecoin 使用 scrypt。其他的 PoS 实现还有比如 Cardano 的 Ouroboros。</p><p><a href="https://eips.ethereum.org/EIPS/eip-7251">EIP-7251</a> 的主张是增加单个验证者的质押额度上限，原先是 32 ETH，希望改为 2048 ETH，这样可以有效减少验证者的数量，同时有效 P2P 网络的通信量。</p><p>这项改动有点迫在眉睫，因为以太坊在测试环境中模拟了大量质押者的情况，<a href="https://notes.ethereum.org/@parithosh/bigboi-beaconchain-test-2">测试结果</a> 显示，当质押者数量达到 2.1 M，网络的投票率会不到 50%，已经不能正常进入 Final 状态，意味着检查点机制失效，整个网络处于非常不安全的状态。而以太坊现在的验证者数量已经达到了 <a href="https://beaconscan.com/stat/validator">1.4M</a>。如果不及时做出改变，以太坊网络将在不久的将来奔溃。</p><p>那为什么以太坊会面临这样的困境？PoS 不是公链专属的共识机制，能够适用于大规模网络的吗？</p><p>究其原因，Ehtereum Casper 其实是对 BFT 的改进，而不是对 PoS 的改进。</p><p>先来看看 Vitalik 是怎么描述 Ethereum Casper 的，他把 Ehtereum Casper 相对于 BFT 的改进视为重中之重：</p><img src="1.png" /><p>再来看一下 Ehtereum Casper 的具体流程：节点质押资产成为验证者，然后通过 VRF 来随机选择一个节点出块，出块后所有验证者都对块的有效性进行一次投票。这些投票会先投递给委员会的成员，委员会成员聚合投票结果之后，再在委员会成员之间同步。委员会成员是每隔一段时间随机选举出来的。</p><p>对于了解 BFT 但是不了解 Ethereum Casper 的人，在接触到以太坊网络后，当知道只有收到 2&#x2F;3 投票的块才有资格被标记为 Final 状态时，会不会对 2&#x2F;3 这个数字有点敏感？因为 2&#x2F;3 是 BFT 一直在强调的投票比例，以保证 3f+1 的容错能力。</p><p>BFT 的投票机制保障了网络绝对不存在分叉，以太坊引入了 BFT 的这个优点，使得 Ethereum Casper 处理分叉场景相对容易，只需要判断哪个区块的得票率最高，就可以认定主流块了。如果验证者同时对两个块投票，验证者会为此受到惩罚，这也是以太坊在众多 PoS 链中唯一一个有 Slash 机制的原因。同时结合 checkpoint 机制，以太坊就可以面对非常复杂的分叉情况，整个网络分叉成树都能从中找出主链。</p><p>问题在于，Ethereum Casper 在引入 BFT 优点的同时也引入了 BFT 的缺点，那就是通信量过大。BFT 的通信量是 O(n<sup>2</sup>) 级别的，一般只能承受 100 个以下的节点规模，例如 <a href="https://ar5iv.labs.arxiv.org/html/2303.11045">这篇报告</a> 就给出了具体的数值。</p><p>可以大致计算对比一下 BFT 和 Ethereum Casper 的消息量。</p><p>BFT 在 100 个节点的时候大概是 50 tps 的能力，消息膨胀量 O(n<sup>2</sup>)，那么消息数量是：</p><pre><code>n = (100^2) * 50)  = 500000  = 0.5 M/s</code></pre><p>Ethereum Casper 在 2M 验证者的时候大概 50% 的投票率，以太坊的块时间是 12 秒，一共 64 个委员会，消息膨胀量 O(n)，那么消息数量为：</p><pre><code>n = 2M * 0.5 / 12 * 64  = 1000000 / 12 * 64  = 5 M/s</code></pre><p>这样计算比较草率和粗略，结果数字上差了一个数量级，但是考虑到两种共识机制具体实现上有很大差异，包括测试的硬件环境差异，有出入很正常，总体上差不太多。</p><p>所以由于以太坊集成了 BFT 的投票机制，导致以太坊网络需要大量的通信量。或者说，Ehtereum Casper 改进了 BFT 并且把 Stake 机制加入其中，使得 BFT 更进一步能够支撑起十万规模的节点数量。</p><p>同时，有没有注意到，Ethereum Casper 的消息膨胀量仅仅只是 O(n)，为什么呢，因为 Ethereum Caspe 不需要进行第二次投票，一次就够了。</p><p>另外，委员会机制有点像联盟链的分层共识。有些国内公司需要在没有 token 概念的前提下，对区块链技术进行改进，但是 BFT 算法最多只能撑起几十个节点的规模，于是有了基于 BFT 的分层共识，基本思路是，从所有节点中选出一部分节点作为提案节点，然后提案节点来进行出块和投票，其他节点只接收数据，并且每隔一段时间换一次共识组（提案节点）。</p><p>对于联盟链，VRF + BFT + 分层共识已经是比较完善的技术组合了。</p><p>与之相比，以太坊多出来的是 Stake 机制，联盟链中每一个节点都是验证者，都有机会出块，而以太坊想成为验证者，需要事先质押一定量的 token 才行。后面的委员会机制相比分层共识，也有一些改进，委员会机制保留了每一个验证者的投票权，只是选出一些代表来归集投票结果。而分层共识直接剥夺了多数节点的出块权，只有少数节点负责出块。</p><p>所以以太坊的共识能简单理解为 Stake + VRF + BFT + 委员会机制。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/1710.09437&quot;&gt;Casper the Friendly Finality Gadget&lt;/a&gt; 是以太坊现在使用的共识机制，属于 PoS 的一种实现。这种关系类似于同样是 PoW 挖矿，Bitcoin</summary>
        
      
    
    
    
    
    <category term="以太坊" scheme="https://smallyu.net/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A/"/>
    
    <category term="BFT" scheme="https://smallyu.net/tags/BFT/"/>
    
  </entry>
  
  <entry>
    <title>区块链中的 PBFT 不需要第二次投票</title>
    <link href="https://smallyu.net/2024/06/03/%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%AD%E7%9A%84PBFT%E4%B8%8D%E9%9C%80%E8%A6%81%E7%AC%AC%E4%BA%8C%E6%AC%A1%E6%8A%95%E7%A5%A8/"/>
    <id>https://smallyu.net/2024/06/03/%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%AD%E7%9A%84PBFT%E4%B8%8D%E9%9C%80%E8%A6%81%E7%AC%AC%E4%BA%8C%E6%AC%A1%E6%8A%95%E7%A5%A8/</id>
    <published>2024-06-03T03:09:21.000Z</published>
    <updated>2025-11-09T09:22:13.311Z</updated>
    
    <content type="html"><![CDATA[<p>PBFT 为什么需要进行两次投票，第二次投票的作用是什么？这个问题困扰我很久。</p><h3 id="逆向推导"><a href="#逆向推导" class="headerlink" title="逆向推导"></a>逆向推导</h3><p>从这个角度想，第二次投票在什么情况下是发挥作用的？在第二次投票的结果和第一次不一致的情况下，才是发挥作用的。如果第二次投票的结果和第一次严格一致，那当然没有必要进行第二次投票。</p><p>那在什么情况下，第二次投票的结果会和第一次不一样？只有当恶意节点存在并且刻意在第二次投票阶段投出不同的票，两次投票的结果才会不一样。</p><p>这是传统 PBFT 的常规操作流程图，其中节点 3 是错误节点或者恶意节点，从始至终没有响应：</p><img src="1.png" width="70%"><p>这是去掉 prepare 阶段，只保留一次投票过程的流程图，其中节点 3 仍然是错误节点，没有响应：</p><img src="2.png" width="60%"><p>关键在于，在这个场景中，节点 0、1、2 都是诚实节点，绝不可能恶意投票或者不投票，那么 commit 阶段的结果一定是和 prepare 的结果一致的，所以即使去掉 prepare 阶段，系统最终也会达成一致。</p><p>节点 3 一直都是恶意节点，如果在 commit 阶段，0、1、2 中的某个节点投出了和 prepare 不一致的票，整个系统就存在超过 1 个恶意节点，超出了容错能力。</p><h3 id="正向理解"><a href="#正向理解" class="headerlink" title="正向理解"></a>正向理解</h3><p>要证明第二次投票是必要的，等同于说明如果没有第二次投票，系统将会无法正常运转。</p><p>逻辑上，即使说第二次投票有各种各样的好处，通过冗余来增加系统的容错能力、能够及时发现错误并且快速调整到一致的状态等，也不能说明第二次投票是非要不可的。比如这个 <a href="https://cs.stackexchange.com/questions/54152/why-is-the-commit-phase-in-pbft-necessary">Why is the commit phase in PBFT necessary?</a> 中的高赞回答，说了很多但只是正向解释了 commit 阶段的设计和作用。</p><p>我目前看到比较靠谱的一个解释在这里：<a href="https://stackoverflow.com/questions/51125238/pbft-why-cant-the-replicas-perform-the-request-after-2-3-have-prepared-why-do">PBFT: Why cant the replicas perform the request after 2&#x2F;3 have prepared? why do we need commit phase?</a> </p><p>其中提到如果没有 commit 阶段，当 view change 的时候，节点将无法保证请求执行的顺序。</p><p>我觉得 StackOverFlow 中的描述和高赞回答提到的论文含义还是有出入的。高赞回答的意思是，节点的 execute 因为缺少 commit 阶段而不一致，有的快有的慢。但即使有两轮投票，节点也可能在 commit 阶段之后 execute 之前发生故障，导致执行上的差异，所以这种故障还不是关键场景。</p><p>更加合理的场景是论文 <a href="https://pmg.csail.mit.edu/papers/bft-tocs.pdf">Practical Byzantine Fault Tolerance and Proactive Recovery </a> 中提到的，view change 发生的时候，不同的请求使用了相同的序列号，被打包进不同的 view 中。（这句话很凌乱）</p><blockquote><p>Replicas may collect prepared certificates in different views with the same sequence number and different requests. The commit phase solves this problem as follows.</p></blockquote><h3 id="单次投票流程"><a href="#单次投票流程" class="headerlink" title="单次投票流程"></a>单次投票流程</h3><p>这个场景基于只投票一次的流程，也就是没有 prepare 阶段的流程。</p><h4 id="场景设置"><a href="#场景设置" class="headerlink" title="场景设置"></a>场景设置</h4><p><strong>视图 V1</strong></p><ol><li>R1 提出提议 P，并广播给 R2, R3, R4。</li><li>提议 P 在 R2, R3, R4 被执行，但 R1 未执行</li></ol><pre><code>R1: --R2: P --&gt; 执行 PR3: P --&gt; 执行 PR4: P --&gt; 执行 P</code></pre><p><strong>视图切换到 V2</strong></p><ol><li>假设 R1 发生故障，视图切换到 V2</li><li>R2 提出新的提议 P’</li><li>R2 提出新的提议 P’ 并广播给 R1, R3, R4</li><li>新的提议 P’ 被所有副本执行</li></ol><pre><code>R1: --      P&#39; --&gt; 执行 P&#39;R2: P --&gt; 执行 P      P&#39; --&gt; 执行 P&#39;R3: P --&gt; 执行 P      P&#39; --&gt; 执行 P&#39;R4: P --&gt; 执行 P      P&#39; --&gt; 执行 P&#39;</code></pre><h4 id="具体示例"><a href="#具体示例" class="headerlink" title="具体示例"></a>具体示例</h4><p>假设提议 P 和 P’ 是对相同账户余额的操作：</p><ul><li>提议 P：增加账户 A 的余额 10 单位。</li><li>提议 P’：减少账户 A 的余额 5 单位。</li></ul><p>在视图 V1 和 V2 中的操作顺序和结果如下：</p><p><strong>视图 V1</strong></p><pre><code>R1: 账户 A 余额 = 100 (未执行 P)R2: 账户 A 余额 = 110 (执行 P)R3: 账户 A 余额 = 110 (执行 P)R4: 账户 A 余额 = 110 (执行 P)</code></pre><p><strong>视图 V2</strong></p><pre><code>R1: 账户 A 余额 = 100 (未执行 P) --&gt; 执行 P&#39; --&gt; 账户 A 余额 = 95R2: 账户 A 余额 = 110 (执行 P) --&gt; 执行 P&#39; --&gt; 账户 A 余额 = 105R3: 账户 A 余额 = 110 (执行 P) --&gt; 执行 P&#39; --&gt; 账户 A 余额 = 105R4: 账户 A 余额 = 110 (执行 P) --&gt; 执行 P&#39; --&gt; 账户 A 余额 = 105</code></pre><h4 id="场景分析"><a href="#场景分析" class="headerlink" title="场景分析"></a>场景分析</h4><p>再来重复一下这句话，不同的请求（R2）使用了相同的序列号（R1 认为是 P），被打包进不同的 view （P’）中。相同的序列号应该是指执行的时序，就是当前时间点轮到哪个请求执行了。</p><p>在上面这个场景中，确实由于 A 节点故障导致最终状态出现了不一致。</p><h3 id="两次投票"><a href="#两次投票" class="headerlink" title="两次投票"></a>两次投票</h3><p>两次投票的流程又是如何解决上述场景中的问题？</p><ol><li>如果 A 节点故障发生在收到 prepare 结果之后、开始 commit 之前，所有节点都不会进入 execute 阶段。</li><li>如果 A 节点故障发生在收到 commit 结果之后、开始 execute 之前，A 节点会根据 commit 结果再次尝试执行 P，然后再执行 P’</li></ol><h4 id="场景分析-1"><a href="#场景分析-1" class="headerlink" title="场景分析"></a>场景分析</h4><p>是不是注意到，第 2 条存在一点不公平？</p><p>两次投票的场景下，A 节点可以根据 commit 结果再次尝试执行 P。</p><p>单词投票的场景下，A 节点并没有根据 commit 的结果再次尝试执行 P，而是直接执行了 P’。</p><p>那么其实两次投票并没有完全避免在 execute 之前节点故障导致的状态不一致，仅仅只是通过增加一次通讯的形式，来反复确认其他节点的状态和自己预期是一致的，减少状态不一致的风险。</p><p>两次投票把发现故障的时间提前了，如果节点 A 没有在 commit 阶段发出投票，其他节点就知道 A 节点故障了，而不是等到自己已经 execute 了，才发现 A 没有 execute。多一次确认多一份保障，减少系统 execute 后回滚的成本，尽可能在 execute 之前就商量好。两次投票最大的作用应该也就这样了。</p><p>总的来说，第二次投票始终都没有体现出必须存在的意义，而只是带来了一些好处，加强了系统的安全性。这个问题可能类似于，TCP 为什么需要 3 次握手才能建立连接？2 次不行吗？估计 1 次也行，只是会引起一些麻烦，3 次确认足够保险。</p><h3 id="无状态与有状态"><a href="#无状态与有状态" class="headerlink" title="无状态与有状态"></a>无状态与有状态</h3><p>为什么 PBFT 需要反复确认，尽量避免 execute 之后的状态不一致呢？也许任何系统的回滚都是一件非常慎重的事情，所以不惜增加 execute 之前的沟通成本。</p><h4 id="无状态"><a href="#无状态" class="headerlink" title="无状态"></a>无状态</h4><p>回到上面单次投票的场景，出故障的 A 节点在什么情况下就不会执行 P’ 了？</p><ol><li>A 节点知道自己执行 P 失败了</li><li>执行 P’ 之前一定要执行 P</li></ol><p>满足这两个条件，即使是单次投票，也可以实现和两次投票一样的效果。</p><p>对于无状态的系统，如果节点只记录了一个最终的数字，那还挺难办的，节点知道自己没有执行 P，然后收到了一个 P‘，节点 A 将无法分辨 P’ 的位置，是在 P 后面还是和 P 同等位置。</p><p>正常顺序是：</p><pre><code>O -&gt; P -&gt; P&#39;</code></pre><p>对于 A 节点来说，知道自己没有执行 P，但是收到了一个 P’：</p><pre><code>O -&gt; (P&#39;)?</code></pre><p>要不要执行呢？A 节点就执行了，状态就错乱了。</p><p>基于这一点原因，无状态的系统的 execute 是非常慎重的。</p><h4 id="有状态"><a href="#有状态" class="headerlink" title="有状态"></a>有状态</h4><p>区块链属于有状态的系统，天然记录了自己的执行记录（区块），以及会对请求进行强制的排序（区块哈希、父哈希）。</p><p>一个节点收到了区块，它一定能够判断出这个区块的位置，是否应该本轮执行，以及自己是否缺少区块，及时从其他节点把区块同步过来。</p><p>所以在区块链的使用场景下，如果只是为了达到多数节点最终状态一致的效果，完全没有必要进行第二次投票。</p><h3 id="疑问"><a href="#疑问" class="headerlink" title="疑问"></a>疑问</h3><p>PBFT 为什么需要进行两次投票？这个问题在 GPT-4o 的知识边界，详细追问它，它就会开始胡说八道了，这符合 GPT-4o 不了解就开始编造的特点。</p><p>以我有限的互联网信息搜索能力，我一直没有找到一个足以让我信服的理由，证明 PBFT 中的第二次投票是必要的。</p><p>经过我自己反复的推演，我能得到的结论只有二次投票并不是必须的，仅有一次投票，也可以达到多数节点一致的结果。</p><p>可为什么长久以来，PBFT 包括各种变体 Tendermint、HotStuff，都保留了两次投票的流程？为什么从来没有人质疑过第二次投票其实不需要？</p><p>我到底错在哪里？也许是对 PBFT 了解不够深入，还没有触及到第二次投票真正发挥作用的场景吧？可如果真的存在这样的场景，为什么没有找到资料把这种场景直接了当地描述出来？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;PBFT 为什么需要进行两次投票，第二次投票的作用是什么？这个问题困扰我很久。&lt;/p&gt;
&lt;h3 id=&quot;逆向推导&quot;&gt;&lt;a href=&quot;#逆向推导&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="BFT" scheme="https://smallyu.net/tags/BFT/"/>
    
  </entry>
  
  <entry>
    <title>开发者的思维方式</title>
    <link href="https://smallyu.net/2024/05/30/%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E6%80%9D%E7%BB%B4%E6%96%B9%E5%BC%8F/"/>
    <id>https://smallyu.net/2024/05/30/%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E6%80%9D%E7%BB%B4%E6%96%B9%E5%BC%8F/</id>
    <published>2024-05-30T05:46:26.000Z</published>
    <updated>2025-11-09T09:22:13.448Z</updated>
    
    <content type="html"><![CDATA[<p>前情提要：《<a href="https://www.yinwang.org/blog-cn/2015/02/01/creative-thinking">创造者的思维方式</a>》——王垠</p><hr><p>发现一个现象，当你说 A 好的时候，别人会说 A 没有 B 好，所以 A 不好。</p><p>第一个案例是当你说 SOL 的技术比 BNB 好的时候，就会引来争议了。有人拿 SOL 宕机过几次来证明 SOL 技术不好，还有人说炒币根本不是技术导向，而是资本导向，技术好不好不重要。这些观点反映了各自不同的立场。</p><p>从开发者的角度，没有人会把 BNB 看作一条区块链，因为它是平台币，节点中心化运维，运营模式也不是链的方式。技术上 fork Geth 的代码做了一些魔改，比 Geth 早一步用上了 PoS，以增加对整条链的控制。ETH 完成坎昆升级后，BNB 也模仿着搞上了自己的 BIP-4844，显然和 ETH 属于相同的技术体系。</p><p>另一个观察角度是，ETH 的 ETF 接近通过后，大家开始讨论下一个能上 ETF 的是 DOGE 还是 SOL，甚至是 DOT，但没有人会提到 BNB，充分说明了 BNB 和其他公链不一样，都不是一个赛道。判断两个产品是否属于同一个赛道的方法，可以看其是否存在竞争关系，你死我活那种。如果 BNB 用了 ETH 的技术还希望 ETH 死掉，在逻辑上是不通顺的。正常逻辑应该是希望 ETH 更好，BNB 也会跟着受益。</p><p>而从使用者（炒币）的角度，管你用的什么技术，管你技术好不好，一个表情包都能市值前十，一个戴帽子的狗都能市值上亿，投资人在哪儿我就去哪儿，资本炒什么我就买什么，FOMO 就完事了。</p><p>第二个案例是当你说 SOL 技术好的时候，别人会说 ETH 更好，所以 SOL 不好。</p><p>同样是立场问题，站在使用者的角度，有无数理由来对比两个项目的好坏。当你说 A 好的时候，总是有人可以找到一个比 A 好的 B，或者只有某个方面好于 A 的 C。使用这样的技巧，他们可以打败任何人、任何项目。而且这里面也存在非常大的争议空间、鄙视链，就好像到底 PHP 是不是世界上最好的语言？</p><p>但是站在开发者的角度，如果我要开发一条链，我的链要和 SOL 比技术、抢市场，我能不能做的比 SOL 更好？肯定不能。那么在这个立场下，我说一句 SOL 技术挺好，不过分吧？</p><p>这里的 SOL 只是举例，其实只要是 CMC 前 100 的老牌公链，都有一些闪光点，尤其是开创和尝试了不一样共识算法的，它们也许不如 ETH 和 SOL 备受关注，但是也做出了不错的成绩。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;前情提要：《&lt;a href=&quot;https://www.yinwang.org/blog-cn/2015/02/01/creative-thinking&quot;&gt;创造者的思维方式&lt;/a&gt;》——王垠&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;发现一个现象，当你说 A 好的时候，别人会说 A 没有 B</summary>
        
      
    
    
    
    
    <category term="思维方式" scheme="https://smallyu.net/tags/%E6%80%9D%E7%BB%B4%E6%96%B9%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>发币的核心要点</title>
    <link href="https://smallyu.net/2024/05/26/%E5%8F%91%E5%B8%81%E7%9A%84%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9/"/>
    <id>https://smallyu.net/2024/05/26/%E5%8F%91%E5%B8%81%E7%9A%84%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9/</id>
    <published>2024-05-26T14:31:26.000Z</published>
    <updated>2026-06-30T07:15:33.012Z</updated>
    
    <content type="html"><![CDATA[<p>首先关于《炒币投资的小 tips》再补充两点：</p><ol><li><p>比特币本位还是法币本位或者其他币本位，主要还看手里有什么、什么币来的容易，没有银弹的逻辑适用于任何场合.</p></li><li><p>随着 ETH 的 ETF 通过了一半，意味着 SEC 也许会接受 PoS，只是要求 ETF 的资金不能用于质押（我还没理解为什么）。之前提到要在 CMC 前 100 里挑，可能需要再加一个条件，就是没有曾经被 SEC 称作证券的。也许在换人后，或者新法案通过后，加密货币将不归 SEC 管，但至少说明在同等的判断标准下，很多币明显存在问题，ETH 一直属于模棱两可的状态，所以目前在 ETF 的状况比较复杂。<br>  其实比较安全可靠的币屈指可数。例如 DOT 的出镜率不高，SEC 没有对 DOT 有过定义，但是 DOT 的质押率已经达到流通量的 57%，而且还保持着 17% 的年利率，我还无法理解这是一种什么样的状况，质押率高的后果是什么，为什么 DOT 收益率可以这么高，假如所有持有者都平等获得年利率，不就相当于通胀吗，币价得跌。对于网络来说，质押率越高倒是越安全。但是流通中的币越少，说明这个币没啥用，甚至不能用于合约的手续费来消耗。这种理解不了的币也是不能买的。</p></li></ol><p>然后是关于发币。最近无意间听到李笑来以前在网上流传的录音，还是有不少启发。</p><p>所有币的核心要点就一个，就是把币通过有价值的形式分发出去。持有者认为它值什么价格，它就是什么价格，只要持有者不卖，价格就不会跌。包括 BTC，是通过挖矿的形式分发，10 分钟发一次，无论多少人参与，10 分钟、固定数量，总会有一个胜出者，而且胜出者有随机性，增加了活跃参与的程度。</p><p>ETH 也是通过挖矿，每 12 秒钟一次，把块奖励发给矿工，奖励分发上和 BTC 差不太多，ETH 最大的两个黑点是：</p><ol><li>开发团队有预挖，挖了多少不清楚，ICO 低价给了多少人是黑箱</li><li>无限增发的代币模型，总供应量没有上限，是否会带来通胀，是否意味着开发团队可以不断印钱</li></ol><p>李笑来发过一个叫 Caddy 的币，当时给他一个什么社区合作，几百万真实用户，以十分之一的价格给那个群体，让他们在网上点赞转发炒新闻。李笑来自己说，要策划好一个币的前提是自己是网红，有流量、有影响力，是非常重要的条件。因为李笑来自己就算个网红，经常出书、到处演讲、是新东方有名的老师，他的背景是非常深厚的。</p><p>他说的这个关于网红的要点，其实最终作用也就是招来大量粉丝，让这些粉丝认可这个币，币会通过某个价格到达这些粉丝的手里。所以无论是挖矿，还是早期的空投、交易所的 LaunchPool、激励形式的发放，目的都是以有价值的方式发给持有者，持有者必须要付出一点代价，币才会有价值。广撒网白给，币肯定不值钱，但如果和美元一对一锚定，那又是稳定币。所以这个价值还得控制好，不能太具体，也不能没有。</p><p>另外十倍百倍的币是没有逻辑的，一般认为 ETH 的火爆有两个主要原因，一是当时国家出了个政策，交易所里面的 BTC 不能提出去。二是当时有个传销盘，用的 ETH，日利率 1%，大量用户在交易所里花钱买完 ETH 就提出去，放到传销盘里了，币价就涨了上来。好笑的是期间 ETH 的价格涨了上千倍，传销盘甚至可以坚持三年不崩盘……</p><p>FIL 曾经也在国内大量挖矿，价格一度涨到 200，为什么价格那么高，也是传销盘……后来国内不让搞，现在 FIL 就一点活力都没有了，技术是真的好啊。另外一个技术不好的，NEO 也涨了百倍起来的，大家都疯了不知道为什么这么值钱，只有十来个中心化的节点，技术也不行用 C# 写的，号称国产版本的以太坊，主要还是资金盘拉起来的，创始人手里也没多少币，直到现在也能看出来，币价隔一段时间就会拉起一波，只有资金盘能操纵起这样的场面。</p><p>所以技术也不是绝对优先地位，技术属于基础，用来给大家做横向比较的，比如同时有几个项目叫 ETA、ETB、ETC、……、ETH，即使是传销盘也会从中选一个技术相对可靠、有东西可炒作的币来用。然后就是天意了。</p><p>还有是八卦相关的，很多新闻报道李笑来曾经是币圈首富，用四分之一的身价低价买了六位数的比特币，拿了好几年然后经历过好几波牛熊，最后高价卖掉了。但是事情没有那么简单，网上可以查到很多李笑来关于比特币的演讲视频，他绝不是单纯买完币啥也不干，躺着等币价涨，再卖掉。他从复杂的知识层面对比特币有理解、做演讲，肯定是逻辑上相信比特币，才会进入币圈的。这件事情告诉我们，别瞎买，看准了再买，别在交易所上看见个喜欢的代号就买了，或者看见个推广文章就买了，真正能赚钱的人背后都有深厚的逻辑支撑，做了大量功课的。</p><p>另一个版本的八卦是，李笑来曾经组织过一个比特币基金，私募，几万个比特币的规模，用来投资挖矿、交易所、买股票，结果亏的很惨，很多人跟着亏钱还拉了维权群。后来投 EOS 什么的又赚回来了，打折把欠的钱还上。李笑来吹牛自己有六位数的比特币，结果没有啊，警察去搜都搜不着，到现在李笑来都被边控着出不了国。谁知道他到底有没有比特币，他要真有呢，不能放他出去。这件事情告诉我们的道理是，没有人靠炒币发财的，李笑来本来就有钱，有钱了都还在进行各种操作，做投资、搞基金，赚钱总是得做事情，别想着炒币挣钱，买完币放在那儿就等着涨。即使当年属于币圈早期，参与了李笑来比特币基金的人，对比特币的未来也是持怀疑态度，拿不准比特币以后的价值，过程中也洗掉了一大批人。何况现在呢。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;首先关于《炒币投资的小 tips》再补充两点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;比特币本位还是法币本位或者其他币本位，主要还看手里有什么、什么币来的容易，没有银弹的逻辑适用于任何场合.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;随着 ETH 的 ETF 通过了一半，意味着 SEC</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>炒币投资的小 tips</title>
    <link href="https://smallyu.net/2024/05/04/%E7%82%92%E5%B8%81%E6%8A%95%E8%B5%84%E7%9A%84%E5%B0%8F-tips/"/>
    <id>https://smallyu.net/2024/05/04/%E7%82%92%E5%B8%81%E6%8A%95%E8%B5%84%E7%9A%84%E5%B0%8F-tips/</id>
    <published>2024-05-04T12:44:13.000Z</published>
    <updated>2025-11-17T14:37:09.866Z</updated>
    
    <content type="html"><![CDATA[<h3 id="1-BTC-是行业底线"><a href="#1-BTC-是行业底线" class="headerlink" title="1. BTC 是行业底线"></a>1. BTC 是行业底线</h3><p>有 Bitcoin，才有加密货币的整个行业。如果哪一天 Bitcoin 被否认了，说 BTC 不安全了、不去中心化了、没有价值了，那么整个行业就没了，BTC 从来没有被超越。而且事实上，除了比特币，其他的全是山寨币，尤其是在老矿工眼里，经历过只有比特币的时代。山寨币都是从作为比特币的模仿者开始，试图做出某些改变，包括以太坊。Vitalik 也搞过染色币、从 PoW 开始发链、来中国募资。一切都从比特币开始。</p><h3 id="2-PoW-抗跌"><a href="#2-PoW-抗跌" class="headerlink" title="2. PoW 抗跌"></a>2. PoW 抗跌</h3><p>2024年4月12日前后，由于多方面因素，普遍认为是因为两个国家擦枪走火、有可能挑起战争的缘故，所有的币平均下跌 20% 左右，币安的 Gainner 排行榜上也全是红色，可以简单将其称为 412 事件。</p><p>观察一下这一波价格变动的情况，可以明显看出，PoW 系列的币，跌幅普遍比 PoS 系列的币小。以 BTC 为首，Doge、LTC、BCH、ZEC、ETC、ETHW。毕竟 PoW 的链是矿工实实在在投入算力在上面，硬件成本在那儿，他们不愿意低价格卖掉。PoS 不一样，把一大堆钱质押进服务器，钱生钱，来的相对容易。ETh、DOT、ADA、COSMOS 等，很多很多，CMC 前 100 估计有 50 个 PoS 的、有质押功能的。</p><h3 id="3-BTC-本位"><a href="#3-BTC-本位" class="headerlink" title="3. BTC 本位"></a>3. BTC 本位</h3><p>炒币的基本逻辑是 BTC 本位的，也就是炒山寨币对比特币的汇率。假如本来有 1 个 BTC，觉得 Doge 最近的上涨幅度会超过 BTC，就把 BTC 换成 Doge，等 Doge 涨完涨了 20%，再把所有的 Doge 换成 BTC，这个时候，BTC 就从 1 个变成了 1.2 个，这就是 BTC 本位的含义。</p><p>从此以后，你将无视市场行情的波动，无惧牛熊，你眼里只有比特币的数量，是 1 个，还是 1.2 个，还是 0.8 个。至于 BTC 对于法币的价格，随便怎么变化，总之每隔 4 年翻一倍，跌了会涨，涨了还会涨，All Time High 反反复复。</p><p>要是用法币本位去炒币，十有九伤，大多会死得很惨。特指现货，合约千万别碰。</p><h3 id="4-挑-CMC-前-100-的买"><a href="#4-挑-CMC-前-100-的买" class="headerlink" title="4. 挑 CMC 前 100 的买"></a>4. 挑 CMC 前 100 的买</h3><p>尽可能把眼光聚焦在 CMC（Coin Market Cap）排行前 100 名的币上，挑这些里面的买。一方面是因为暴跌的概率小，100 名开外的没底。另一方面是生态支持差，没准什么时候钱包不支持了，转不了账带来很多麻烦。或者某个 X-20 币的合约地址变了，就很离谱，前段时间有一个 AI 板块下的，合约地址说变就变，而且原合约的 Dex pool 还在以 20% 的价格运行，合约地址切换后，价格变为了原来的 80% 左右。</p><h3 id="5-创始人的离开是去中心化的开始"><a href="#5-创始人的离开是去中心化的开始" class="headerlink" title="5. 创始人的离开是去中心化的开始"></a>5. 创始人的离开是去中心化的开始</h3><p>BTC 比较特殊，创始人从一开始就隐瞒身份，这是非常聪明的做法。也正因为如此，BTC 才可以被大肆炒作。为什么 BTC 上的资产类型很多，染色币、RGB++、Taproot Assets、铭文、符文，L2 资产还在打架。因为没有人能站出来给 Bitcoin 画一个 Roadmap，谁也不知道接下来应该怎么发展。</p><p>与之相比，ETH 的创始人还在全世界开会、演讲，给 Ethereum 画出了路线图，让 L2 有了整齐划一的技术方向。当 ETH 遭遇黑客攻击，创始人能够带领整个社区把攻击历史分叉掉，还能引领社区把共识机制从 PoW 转换为 PoS。如此大的影响力，说明 ETH 是中心化的。ETH 没有理由能通过 Howey 测试，在 SEC 的标准下，ETH 八九不离十被认定为证券。</p><p>如果一个币是你发的，你在那儿喊单，是不是很不合适？可如果一个币是去中心化的，你随便喊，就喜欢 Doge，就喜欢，怎么喊都行。</p><p>BSV 的 CW 也在今年打输了官司。社区里有人用这一点来安慰自己，说 CW 的离开是好事，BSV 会变成真正社区驱动的币。不管 BSV 前景如何，至少创始人的离开是一个重要的指标。</p><p>还有一个例子是 ETC 和 ETHW。这两个币都是宝二爷分叉出来的，很多人对他有成见，但是这两个币的市值、价格、TVL 摆在那儿，412 这一波，这两个币的抗跌能力有体现。虽然确实不敢买，从 ETH 分叉出来以后，技术上就没有进一步的更新了，GitHub 上活跃度很低，感觉很难长久下去，但价格上又很是让人感到疑惑。ETC 和 ETHW 其实是真正没有创始人而且是 PoW 的币。</p><p>另外一个现象是，自从以太坊将共识机制从 PoW 转为 PoS，ETH 对 BTC 的汇率就一直在下跌，从接近 0.1 到现在 0.05 左右。</p><h3 id="6-其他"><a href="#6-其他" class="headerlink" title="6. 其他"></a>6. 其他</h3><p>拿出 CMC 前 10 来简单对比一下：</p><ol><li>BTC：行业底线，市值占比 50%</li><li>ETH：开创了 EVM 和加密货币版 Defi 市场，有庞大的生态，市值占比 16%</li><li>USDT：使用范围最广的稳定币，美国国债储备</li><li>BNB：世界上最大的交易所平台币</li><li>SOL：PoH+DPoS 共识机制，中心化，依赖于少数排序器对交易排序后，才会交给验证者打包</li><li>USDC：最安全的稳定币，黄金储备</li><li>XRP：RPCA 共识机制</li><li>DOGE：PoW 共识，最大的 Meme 币，马斯克喜欢</li><li>TON：Telegram 发行的链</li><li>ADA：创始人有名气，使用 Haskell 开发，UTXO + PoS</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;1-BTC-是行业底线&quot;&gt;&lt;a href=&quot;#1-BTC-是行业底线&quot; class=&quot;headerlink&quot; title=&quot;1. BTC 是行业底线&quot;&gt;&lt;/a&gt;1. BTC 是行业底线&lt;/h3&gt;&lt;p&gt;有 Bitcoin，才有加密货币的整个行业。如果哪一天</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>为什么说 PoW 比 PoS 更加去中心化</title>
    <link href="https://smallyu.net/2024/04/14/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4PoW%E6%AF%94PoS%E6%9B%B4%E5%8A%A0%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96/"/>
    <id>https://smallyu.net/2024/04/14/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%AF%B4PoW%E6%AF%94PoS%E6%9B%B4%E5%8A%A0%E5%8E%BB%E4%B8%AD%E5%BF%83%E5%8C%96/</id>
    <published>2024-04-13T16:50:00.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<p>想从准入门槛的角度对比一下 PoW 和 PoS。</p><p>我们经常描述一条链是 permissionless 还是 permissioned 的。其含义是成为出块节点，需要被授权还是不需要被授权。</p><p>PoW 是公认不需要被授权就可以挖矿的共识机制，只要你有一台计算机，就可以加入网络开始挖矿，只要你能计算出符合要求的哈希值，哪怕手算心算都可以，只要是正确的，你就拥有出块的权利，这个块中包含哪些交易由你来决定。当然能不能算的出来是另一回事。</p><p>BFT 是典型的需要被授权的共识机制。很多使用 BFT 类共识的项目，直接需要 CA 中心来签发证书，只有拥有证书的节点才可以在网络冲承担起挖矿的职责。而 CA 中心为什么要给你签发证书呢，完全是线下行为。成为出块节点的权限牢牢控制在 CA 中心手里。</p><p>PoS 存在一些争议。</p><p>SEC 曾经把 66 种加密货币定义为证券，包括 SOL、ADA 等市值比较高的币种。但其中没有包含 ETH，这一点给了很多人期望，也让 ETH 的 PoS 存在模棱两可的空间。</p><p>一个显著的事实是，PoS 是属于 permissioned 的共识机制。</p><p>在 PoS 中，节点往往会需要质押一定数量的 token 才能成为出块节点，而质押的那么多 token，就是 PoS 的门槛。</p><p>一个节点想成为出块节点，需要获得很多 token，而这些 token 从哪里来？要么来自项目的发行方，在创世阶段就分配出来到钱包里，要么后续从矿工手里买，因为矿工会增加 token 的供应量。</p><p>问题就在这里，设想一种极端的情况，如果所有持有 token 的人不愿意出售持有的 token，整个网络的控制权不就在他们手里了吗？不管他们是十个人还是十万个人，总之是一个圈子。而且在 PoS 的机制中，拥有越多 toekn 权力就越大，更是加剧 PoS 的中心化程度。</p><p>这个时候可能有疑问，以太坊如此成功，有上千万的持有者，成千的机构组织管理着上万个出块节点，难道还不够去中心化吗？</p><p>所以要区分两个概念，共识机制是去中心化的，和一条链是去中心化的，是两码事。</p><p>只要愿意，即使 BFT 类共识只能支持几十个节点的规模，如果这几十个节点分散在世界各地且有利益冲突的大财团手里，这条链仍然可以认为是去中心化的，因为几十个个利益相关方不可能有超过 2&#x2F;3 比例的数量合作。</p><p>同样的道理，以太坊只是做的去中心化了，它成为了一条去中心化的链，不代表 PoS 是去中心化的技术。</p><p>为什么很多项目方愿意选择 PoS 而不是 PoW 来发币？因为 PoW 太不可控了，被攻击的风险非常高。可以观察一下，目前存在的 PoW 链，几乎都是从比特币分叉而来，而且很少有新增。新出现的山寨币，多数源自以太坊的技术栈。</p><p>PoS 为什么可控？只要你不把创始阶段的钱分散出去，你就拥有整条链的控制权，PoS 让人更有安全感。分散出去也没事，只要整体比例可控，链就仍然在你的控制下。</p><p>那按照这么说，比特币的矿机资源不也被控制在少数生产商手里吗？只要他们不愿意出售矿机，比特币就始终被控制在一定范围内。也不全是，即使没有先进的矿机，也是能够参与挖矿的。落后的矿机堆积的多了，算力也可以很高。至少没有人有权力，剥夺你成为出块节点的资格。</p><p>与之相比，PoS 是不一样的，只要没有足够的钱，就没有成为出块节点的资格。如果 token 持有者不愿意出售，这种资格是你无论花费多少努力都无法获得的。PoS 本质上还是关起门来大家协商的共识机制，在有资格的节点中随机选一个出块，而且还得投票。</p><p>对了，如果存在一种 PoS，没有准入门槛呢？所有节点都是出块节点，每一轮随机选择一个节点负责出块。这样的机制免不了会受到女巫攻击，最终变为比拼节点数量的游戏。其实比拼节点数量也不是不行，节点数量最终也是硬件资源的体现。拥有更多硬件资源的节点有更大几率被选中。我倒是觉得这样的共识机制有进一步思考的空间，不过面临的现实问题也非常多，比如怎么通过 VRF 来选择下一个出块节点，以及如此庞大的出块节点数量，需要的网络带宽也许是不是现有技术能够承载的。另外，其实没有准入门槛就不叫 PoS 了。</p><p>15 年过去了，比特币从未被超越……</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;想从准入门槛的角度对比一下 PoW 和 PoS。&lt;/p&gt;
&lt;p&gt;我们经常描述一条链是 permissionless 还是 permissioned 的。其含义是成为出块节点，需要被授权还是不需要被授权。&lt;/p&gt;
&lt;p&gt;PoW</summary>
        
      
    
    
    
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>牛市对普通人来说意味着什么</title>
    <link href="https://smallyu.net/2024/03/13/%E7%89%9B%E5%B8%82%E5%AF%B9%E6%99%AE%E9%80%9A%E4%BA%BA%E6%9D%A5%E8%AF%B4%E6%84%8F%E5%91%B3%E7%9D%80%E4%BB%80%E4%B9%88/"/>
    <id>https://smallyu.net/2024/03/13/%E7%89%9B%E5%B8%82%E5%AF%B9%E6%99%AE%E9%80%9A%E4%BA%BA%E6%9D%A5%E8%AF%B4%E6%84%8F%E5%91%B3%E7%9D%80%E4%BB%80%E4%B9%88/</id>
    <published>2024-03-13T15:58:55.000Z</published>
    <updated>2025-06-19T03:30:33.149Z</updated>
    
    <content type="html"><![CDATA[<p>如果你曾经在市场低迷的时候，持有一些加密货币，那么牛市最大的意义，就是让你有限的资产获得了价值上的增长。</p><p>如果你是一位比特币信仰者，牛市最大的意义还在于，能够证明你的信仰是对的、几年前的眼光具有前瞻性，但也仅此而已，如果当时没有投入很多钱的话。</p><p>很多人看到现在 7W 的比特币眼红，后悔当年 3.8W 的时候没有多买点。但是放到 1 年前，3.8W 的比特币你敢买吗？是真不敢，谁知道当时还能跌到什么程度。如果我告诉你，一年后比特币会涨到 20W，你现在敢买吗？也不敢，是真的不敢。很简单的道理，没有人可以预测未来，只有在回顾历史的时候，人人都是股神。</p><p>最近比特币已经上了两次微博热搜，价格一涨人们就开始关注了。我也是眼睁睁看着 ETH 从 3400 掉到 1800，现在又涨到 4000。可是话说回来，这样的波动，能带来多大的改变呢，1W 块钱变 2W 块钱？好像也没太大意思。</p><p>牛市往往伴随着很多暴富故事。按照我的理解，有这么几种赚钱的方式：</p><ol><li><p>炒币，拿现货，价值投资。这样相对稳健，风险可控，但是收益也有限。敢投入多少资金，取决于你自己有多少可用于投资的资金。别看鲸鱼们动不动挣百万千万，他们真实拥有的资产，一定至少在用于投资的 5 倍以上。同样的，收益虽然绝对值多，但其实相对于他们的资产总量，也就那么回事。</p><p> 按照类似的比例，对普通人而言，收益也是有限的。敢把房子卖掉梭哈比特币的，那叫赌狗，那种做法，一般来说，挣多少钱就会赔多少钱，怎么挣来的就会怎么赔进去。除非能赚一波及时收手，可以能收住的也不是一般人，自制力已经超越人性。</p></li><li><p>开合约，加杠杆。这种类型的交易，拿个 10 块 20 块玩一玩就行了。如果不是玩，想赚钱，就不要碰，多少钱都得进去出不来，一般人玩不了这个。庄家做庄可以，如果你有能力控盘，但那个不是一般人。</p></li><li><p>发行 Meme 币。蹭当下的热度，什么火热蹭什么，AI 出新技术了就发个 AI 币，马斯克发推特有火箭的照片，就发个 ROCKET 币。总会有不长眼的玩家，为了热度瞎买，干这种事情，指不定一不小心就能赚很多很多钱。运气成分占很大一部分。另外是要有一定的运营和技术基础，得及时开交易的池子，懂得在社交媒体上推广，还得有一些美术能力，画个像样的 LOGO。</p><p> 哪怕不是 Meme 币，发个土狗币，只要有盘子，有人进来玩，有人愿意接盘，就能赚钱。但是技术、美术、运营各方面条件加起来，也需要点小小的成本。很多暴富的例子其实都是从土狗币来的。我亲眼见过有人干这个事情挣了钱的，但是我不太眼红这个，这种钱咱挣不了。</p></li><li><p>钓鱼网站。这个属于明确的违法行为了，需要黑客基础，以及相关经验，这个是不能考虑的。</p></li><li><p>撸空投。现在的空投猎人已经是一个小产业，批量参与早期项目，广撒网。这个也是随机性很强，需要运气，前段时间疯狂打铭文的，也有很多很多人花了手续费进去，最后啥也没捞到的。另外这里也有一个收益比例的问题，别看有些空投猎人一个项目上拿百万，他能够控制的用于投入的资产一定也非常多，这种机会给你，你也拿不出那么多钱啊，一般人也是没啥机会。我平时接触的空投性质的项目不多，也许是我消息闭塞，不懂这些人是怎么玩的。</p></li><li><p>DeFi。就是赚个收益，把钱压进质押平台，赚几十个点的利息。这种需要承担的是项目方 Rug 的风险，得看准项目方靠不靠谱。</p></li><li><p>MEV。每年几亿的市场，已经被很多有经验的厂商盯着了，想从里面挣钱可能得拼网速。</p></li><li><p>实打实地开发一个有技术含量的项目，作为核心开发成员，拿到初期分配的 token，然后等着 token 翻上几百倍。这种方式显然更安全，但是机会也非常少，尤其是现在的市场环境，几乎没有敢从头搞开发的，大多数项目是拼凑型的，类似于技术方案整合商。有能力在某一个技术方向有突出贡献的项目不但少，而且技术门槛也非常高，这个也不是普通人搞得了的。</p></li><li><p>正经工作，安心打工。像交易所的技术栈和互联网公司其实差别不是很大，办公地点不传统以及工资发 token 属于比较显著的特色。</p></li><li><p>挖矿。现在的挖矿应该不挣钱了。</p></li></ol><p>牛市有两种，一种是政策利好带来的价格增长，另一种是新技术引爆市场，吸引更多玩家进入圈子。目前的牛市属于前者，除了价格很疯狂，似乎没有太显著的技术出现，也没有现象级别的新玩意出现。除了价格在变，似乎什么都没变。或者说，可以认为目前还处于牛市早期，敢加仓的赶紧追高 :P</p><p>还有一点是，牛市会吸引更多资本的眼光，开始投入更多新的项目、新的公司，打工人的工作机会应该会变多一点。最近发现偶尔会出现一些名字小众的小公司，可能用人市场稍微好转了一点点？但是不出意外的话，猜测这些小公司，做的项目，也无非就是想象中的那几样……在哪儿打工不是打呢，身处裁员名单的人可能会觉得轻松点吧。</p><p>对于普通人而言，目前的牛市似乎真的没有意味着什么。今晚以太坊完成了坎昆升级，不知道能不能引起又一波 Layer 2 的风潮。我自己不太是以太坊信仰者，Layer 2 给人一种残缺的感觉，而现在的以太坊致力于推进 Layer 2 的发展。同样的，近期有一些公司在做比特币的 Layer 2 项目，同样不太看好，Bitcoin core 本身没兴趣搞扩容，甚至有能力直接让 BRC-20 一类的项目直接消失。</p><p>那么该信仰比特币吗，比特币好像跟普通人也没啥关系。期待有能代表信仰的项目出现。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;如果你曾经在市场低迷的时候，持有一些加密货币，那么牛市最大的意义，就是让你有限的资产获得了价值上的增长。&lt;/p&gt;
&lt;p&gt;如果你是一位比特币信仰者，牛市最大的意义还在于，能够证明你的信仰是对的、几年前的眼光具有前瞻性，但也仅此而已，如果当时没有投入很多钱的话。&lt;/p&gt;
&lt;p&gt;</summary>
        
      
    
    
    
    
    <category term="币圈" scheme="https://smallyu.net/tags/%E5%B8%81%E5%9C%88/"/>
    
  </entry>
  
  <entry>
    <title>如何健康地远程工作</title>
    <link href="https://smallyu.net/2023/10/10/%E5%A6%82%E4%BD%95%E5%81%A5%E5%BA%B7%E5%9C%B0%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C/"/>
    <id>https://smallyu.net/2023/10/10/%E5%A6%82%E4%BD%95%E5%81%A5%E5%BA%B7%E5%9C%B0%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C/</id>
    <published>2023-10-09T16:39:09.000Z</published>
    <updated>2025-11-09T09:22:13.424Z</updated>
    
    <content type="html"><![CDATA[<p>最近两个月在以远程工作的形式上班，当然远程工作并不是像很多人憧憬的那样，穿着泳衣在沙滩上晒太阳，谈笑之间上百万的交易就成交了。远程工作仅仅只是办公场地的不同，至于工作制度和工作流程，属于整个公司和团队的氛围和文化选择，暂时不讨论这方面的问题。</p><p>远程工作和现场办公相比，有好有坏。好的地方是能比较方便打造适合自己的工作环境，比如电脑桌的高度不太对，可以换一张让自己舒服的桌子，在公司的办公桌就不那么方便换掉了。</p><p>也有一些不好的地方，比如生活和工组场所相同导致生活和工作没有明确的边界感，像我之前有过比较离谱的情况，找 bug 没有头绪，心烦意乱，晚上已经躺下准备睡觉了，睡不着，半夜 1 点多起床 debug 了一小时代码，稍微有点进展后继续睡觉，第二天 8 点钟起床继续 debug，然后继续上一天班。一天下来状态会很差，这是非常非常不健康的行为，一定要想办法杜绝。</p><p>为了针对和解决不好的习惯，需要一些明确可行的规则来规范个人行为，主要是作为参与者，应该如何在远程工作的状态下，保持自己的身心健康。</p><h3 id="工作地点"><a href="#工作地点" class="headerlink" title="工作地点"></a>工作地点</h3><p>因为需要合适的工作环境，所以并不能太自由地选择工作地点，比如背上电脑到商场，在咖啡店呆一天，或者找个图书馆、自习室，都是不合适的，不但要花钱买咖啡，而且那种消费类型的座位都不适合久坐，工作中还有需要视频会议的时候，在公共场合的话也不方便。尤其是对于程序员来说，工作需要 27 寸的外接显示器，需要人体工学椅，也需要能让人专注的环境，还涉及到工作时长的问题，如果下班晚，咖啡店不会营业到那个时候，就有麻烦了。</p><p>WeWork 之类的共享办公室本身选择比较少，而且价格很高，一天要 100～300 块钱，长期用的话非常贵了，很多共享办公室也是不支持保存个人物品的，产业整体并不成熟，不是好的选择。</p><p>综合来看，工作地点只能是在自己的房间里。如果有属于自己的房子，有一间闲置的书房，或者空闲的房间，那么多余的房间就可以作为工作地点。</p><p>如果是租房子住，情况会稍微恶劣一点，一般租房子只会租一间自己的卧室，卧室的书桌就变成了工作的地方。如果专门为了远程工作租另外一间屋子，成本是比较高的。而且提供办公场所原本是公司的责任，在远程工作的形式下这个步骤就省略了，如果让这个成本转嫁到员工身上，也挺违反直觉的，所以自己另外租一间房子实在是没有道理。</p><p>还有一种方案是移居到低消费的城市，由于远程工作不受地点约束，降低消费的同时还能提高生活质量是可行的，但是这种方案仅适用于在当前城市没有任何朋友的情况，否则即使只为了某一个人，也没办法换城市。</p><h3 id="生活和工作要要有明确界限"><a href="#生活和工作要要有明确界限" class="headerlink" title="生活和工作要要有明确界限"></a>生活和工作要要有明确界限</h3><p>在现场工作的时候，生活和工作会有明显的区分，至少身处的环境是完全不一样的两个地方，一个是家，一个是公司，比较容易做出生活和工作的区分。可能会有一种感觉，即使工作再累，回到家也是该休息的时候。也许电脑放在公司就不会带回家，也许带回家不打开，也许工作环境在公司内网，你想在家工作都不行。总之种种原因，会给你一个明确的、生活时间和工作时间的区分。</p><p>但是如果生活和工作的场所相同，都在同一间屋子里，而且还是远程工作，就出问题了。</p><p>想象一下，公司在你的办公桌旁边放了一张床，还提供了所有生活所需的物品，然后你今后要住在公司的办公桌旁边，起床就能看见办公桌，是不是一件很可怕的事情。</p><p>再想象一下，你一整天的生活，起床之后一个小时就开始工作，工作结束之后两个小时就准备睡觉，在工作之外的时间你都仍然呆在房间里，是不是一种会让人发疯的场景。</p><p>远程工作最重要的一点，就是要保证生活和工作有明确的界限，为了达到这样的效果，简单列出几点行为：</p><ul><li>按时起床，预留出“通勤”时间</li><li>在原本应该通勤的时间里，出门散步，假装在通勤，绝对不可以睡懒觉</li><li>上班时间要穿好衣服和鞋子，不能穿居家的衣服或睡衣，必须是随时能够出门的状态</li><li>摆正外接摄像头的位置，表示进入上班时间</li><li>打开桌面上的时钟摆件，表示进入上班时间</li><li>打开窗户，表示进入上班时间</li><li>下班时间后，外出散步相当于通勤，结束工作</li><li>下班时间后，回到房间，拖一次地，让地板是湿的</li><li>下班时间后，不在电脑上进行工作相关的操作</li><li>……</li></ul><p>这些行为并不是具体的，也不一定是好的，只是举一些例子，重点是通过一系列物理上的行动，来给自己明确的信号，经过四五种操作以后，就进入上班时间了，经过另外四五种操作以后，就进入下班时间了。单纯光靠时间概念，到点了就上下班，很难让自己有明确的感受，也许本就非常自律的人可以做到，如果还做不到，就需要先依赖一些繁琐的行为，来给自己制造分界线。</p><h3 id="户外活动"><a href="#户外活动" class="headerlink" title="户外活动"></a>户外活动</h3><p>上面有提到，在本应该通勤的时间，要出门散步，假装在通勤。不只是为了保持和现场工作一样的生活节奏，更重要的是为了保持身体健康，确保有足够的时间在户外进行活动，哪怕是简单的散步，也一定好过整天待在家里。</p><p>有人认为远程工作的好处之一是可以省去通勤的时间，用来睡懒觉。这是非常错误的想法，多出来的时间不但不可以用来休息，还要刻意用来锻炼身体才对，天天在家里呆着估计也没人受得了。</p><h3 id="参加社交活动"><a href="#参加社交活动" class="headerlink" title="参加社交活动"></a>参加社交活动</h3><p>远程工作以后，我甚至开始怀疑和人类说话是一种生理需求。也许是认真工作的缘故，也许是工作日从早到晚都在同一个场所中，而且见不到活人、不能和人说话，感觉周末异常想出去玩，然后连续玩两天。</p><p>不在乎游戏好不好玩，只是想换个地方呆着，换换脑筋放松一下，见到不同的人，玩不同的游戏，干点和工作无关的事。</p><h3 id="番茄工作法"><a href="#番茄工作法" class="headerlink" title="番茄工作法"></a>番茄工作法</h3><p>现场办公的时候，可能时不时有人和你说句话、从你工位旁边走过、制造出什么声响，或者自己站起来去饮水机接一杯水、下楼买瓶饮料、去一趟洗手间等，各种各样的小事件，都会分散你的注意力，打断你的思考。这些事情也许会让你觉得麻烦，但是这些事情也能让你不要长时间专注于某件事情。</p><p>过于长时间的专注不是一件好事，会让头脑变得麻木迟钝，对于身体也是一种压力，会让人感觉腰酸背痛。人需要隔一段时间就站起来活动活动、舒展筋骨。</p><p>番茄工作法的大致含义是，每工作 25 分钟，休息 5 分钟，算作一个番茄时间。番茄工作法能有比较高的知名度，还是有一定道理的，这样的时间分配是一种不错的实践。</p><p>不要用 Stretchly 之类会在屏幕上弹窗的软件，使用体验并不好，它会直接打断你正在进行的工作，这很让人生气，你大概率会手动直接关掉它的弹窗。即使有些软件可以提前 30 秒通知你要弹窗了，体验也不会变好，手头上的事情如果在 30 秒内无法放下呢，本次 break time 就直接失效了。而且弹窗直接覆盖掉整个屏幕的方式，会让人有轻微的不安，担心是否有人给自己发消息，担心错过屏幕上的什么动态。</p><p>所以最好用会响起闹铃的倒计时。开始工作后计时，25 分钟后响起闹铃，如果不手动关闭闹铃，闹铃就会一直响下去。这种闹铃其实是一个很好的特性，如果你不放下手上的工作，闹铃就在不断提示你，是时候站起来放松一下了。当然自己也要自律，直到自己真的站起身，再动手把闹铃关掉。</p><p>但有时候开始工作会忘记启动倒计时，而且频繁手动开启倒计时也很麻烦，可以直接设定多个闹钟，在固定的时间响起，比如每个小时的 25 分和 55 分都响起一次。你不一样严格按照闹钟的时间进行工作，但至少闹钟会按时提醒你，该休息了。</p><p>实践证明，适度的休息放松不但不会耽误工作，而且会让工作效率变得更高。有些难题会一时想不到解决办法，往往在休息放松的时候，就有灵感了。</p><h3 id="仪式感"><a href="#仪式感" class="headerlink" title="仪式感"></a>仪式感</h3><p>作为内容的补充，偶然发现上面描述的种种行为，其实是在创造一种仪式感，上班需要仪式感，下班需要仪式感，很多时候仪式感是周围环境创造出来的，有时也可以自己主动去创造。生活需要仪式感。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近两个月在以远程工作的形式上班，当然远程工作并不是像很多人憧憬的那样，穿着泳衣在沙滩上晒太阳，谈笑之间上百万的交易就成交了。远程工作仅仅只是办公场地的不同，至于工作制度和工作流程，属于整个公司和团队的氛围和文化选择，暂时不讨论这方面的问题。&lt;/p&gt;
&lt;p&gt;远程工作和现场办</summary>
        
      
    
    
    
    
    <category term="工作" scheme="https://smallyu.net/tags/%E5%B7%A5%E4%BD%9C/"/>
    
    <category term="远程工作" scheme="https://smallyu.net/tags/%E8%BF%9C%E7%A8%8B%E5%B7%A5%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>为什么比特币不用概率加密函数</title>
    <link href="https://smallyu.net/2023/10/04/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%AF%94%E7%89%B9%E5%B8%81%E4%B8%8D%E7%94%A8%E6%A6%82%E7%8E%87%E5%8A%A0%E5%AF%86%E5%87%BD%E6%95%B0/"/>
    <id>https://smallyu.net/2023/10/04/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%AF%94%E7%89%B9%E5%B8%81%E4%B8%8D%E7%94%A8%E6%A6%82%E7%8E%87%E5%8A%A0%E5%AF%86%E5%87%BD%E6%95%B0/</id>
    <published>2023-10-04T14:36:18.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<h3 id="概率加密函数"><a href="#概率加密函数" class="headerlink" title="概率加密函数"></a>概率加密函数</h3><p>在常用的对称加密中，一个明文对应一个密文。有一种叫概率加密的方式，会在加密的过程中加入随机数，达到一个明文对应多个密文的效果，</p><ol><li>概率加密函数为什么很少用</li></ol><p>需要区分三个概念：加密、签名、哈希。概率加密函数属于对称加密的一种，而实际上区块链系统中很少用到加密，无论是对称加密还是非对称加密。比特币的白皮书中只提到过签名和哈希，签名是在发起交易的时候用来确认资产所有权的，哈希是在挖矿的时候确认出块权的，并没有提到关于加密的使用。</p><p>加密的应用场景比较少是主要原因，存储类项目可能会用到对文件的加密，但也属于用户行为，区块链系统本身不参与数据的加密保护。</p><p>概率加密函数想要替代签名和哈希更是不可能的事情，它本身就不具备那样的能力。</p><ol start="2"><li>概率加密函数的概率分布</li></ol><p>有概率就会有概率分布，比如正态分布。对于概率加密函数来说，均匀的概率分布是比较理想的，否则在概率分布已知的情况下，根据加密出的密文内容，攻击者有可能还原出明文内容，是很危险的。只有随机强度一致，攻击者才无从下手。</p><p>所以概率加密函数几乎没有选择，尽可能分布均匀是常见的做法。</p><h3 id="新式落地窗"><a href="#新式落地窗" class="headerlink" title="新式落地窗"></a>新式落地窗</h3><p>在一个优美和睦的动物森林里，不知怎的流行起了落地窗风格的房子，不仅透光良好，而且设计又美观又高级。小狐狸是有名的建筑师，能够建造出这种流行的带有很大落地窗的房子，森林里的小动物们纷纷找小狐狸盖房子。</p><p>小鹦鹉是森林里的百事通，消息灵通能说会道，同时也是河水霸主河马身边的军师，常常给河马出谋划策。河马听说了最近流行落地窗的房子，也想要盖一间。但是河马碍于比较高的地位和身份，不想直接套用大家都在用的设计，于是让小鹦鹉想想办法。</p><p>小鹦鹉到处打听，无意间听说了“玻璃纤维”这个词，据说是一种新出现的合成材料，不易碎、韧性特别好。小鹦鹉高兴极了，赶紧告诉河马，自己发现了一种新的建筑材料，这是史无前例的伟大创新，能够颠覆传统的建筑方式，用更高级的玻璃纤维来代替玻璃，简直能引领下一代房屋建筑的新潮流！</p><p>河马听了挺满意，出钱出力让小鹦鹉找人把房子盖出来，而且建造方案要保密！可不能让人知道了这么重要的创意。</p><p>小狐狸听说了这件事情，也挺好奇，自己盖房子这么多年，并不知道有什么新材料，小鹦鹉竟然能做出如此重大的创新。正好小鹦鹉的施工队招人，小狐狸欣然报名，参与到河马房子的施工大队中。</p><p>但是小狐狸一进去傻眼了，玻璃纤维不是玻璃，是不透光的呀！怎么能代替玻璃呢？如果坚持用玻璃纤维，这房子就没法住了。如果不用玻璃纤维，就只能用玻璃，那就没有创新了！</p><p>后来小狐狸离开了小鹦鹉的施工队……</p><h3 id="达克效应"><a href="#达克效应" class="headerlink" title="达克效应"></a>达克效应</h3><p>达克效应是认知偏差的一种，尤其用于描述低估或高估自己能力的现象。对于在某一个领域的专家，试图对领域之外的事情进行革命性创新的行为，可以归因于达克效应上，但是也存在细节上的差异，更贴切一点的描述是“超越领域的自信”，或者用更专业的心理学概念描述为“专业人士错觉”。</p><p>从心理学角度解释这种现象是有一定道理的，不过实际上造成这种现象的，还有一个重要的因素，就是存在一种无形的力量……</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;概率加密函数&quot;&gt;&lt;a href=&quot;#概率加密函数&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="心理" scheme="https://smallyu.net/tags/%E5%BF%83%E7%90%86/"/>
    
    <category term="加密" scheme="https://smallyu.net/tags/%E5%8A%A0%E5%AF%86/"/>
    
  </entry>
  
  <entry>
    <title>程序员的 “服从权威” 心理</title>
    <link href="https://smallyu.net/2023/07/30/%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E2%80%9C%E6%9C%8D%E4%BB%8E%E6%9D%83%E5%A8%81%E2%80%9D%E5%BF%83%E7%90%86/"/>
    <id>https://smallyu.net/2023/07/30/%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9A%84%E2%80%9C%E6%9C%8D%E4%BB%8E%E6%9D%83%E5%A8%81%E2%80%9D%E5%BF%83%E7%90%86/</id>
    <published>2023-07-30T14:35:34.000Z</published>
    <updated>2025-11-20T02:26:34.914Z</updated>
    
    <content type="html"><![CDATA[<p>最近经历了一件事情，就是入职一家初创公司，工作两周后主动离职。虽然只有短短两周但感觉过了很长时间，从中也学到或者意识到很多东西。离开的原因有很多，也涉及到比较具体的问题，我会继续反思和总结。今天想到一个有意思的话题，可以作为简单的切入点。当然有一些不方便说的内容我是不会说的 :P</p><p>我发现程序员群体普遍存在一种 “兽性”：谁技术好谁就厉害，谁技术好我就听谁的、我就服谁，我甚至会叫他大神、大佬，崇拜他，相信他，反之，要是技术不好的人当我的小组长、技术负责人、CTO，支配我的工作，我就不服他、不愿意听他的话。对于管理者来说，如果自己的能力不服众，事情推进起来也会格外艰难。</p><p>这个问题罗永浩在某个节目里也提到过，但是我不记得是在他的创业课播客还是什么访谈节目中了，特意找了一下没有找到，他的语录内容实在太多了。同时我也意识到，当有过加入初创公司的经历，观察到一些现象后，再回头去看他的 <a href="https://www.youtube.com/@zhuangyuanli/videos">创业课播客</a>，觉得他说的那些东西对创业公司还真挺有价值。</p><p>这种现象不止存在于程序员群体，在很多影视剧中都有类似的情节，比如在森林中，战斗力最强的狮子就是王，猴群中最能打的当猴王，或者在骁勇善战的原始人部落，以武力争高低，谁赢了听谁的。再比如西部牛仔比试枪法，胜利者获得某种地位和象征之类，等等。</p><p>这样的现象叫 “服从权威” 心理。分析一下权威这个词。权威其实不是一个负面的词语，虽然我们会看到一些言论说要打倒权威、不要盲目崇信权威，但其实那个语境中的权威是指非法获得的、德不配位的、迷惑群众的权威。“服从权威” 这个词中的权威，指群体中真正的强者。相信大多数人都不会反感真正的强者。</p><p>回到职场关系的问题，按照我粗浅的理解，可以简单把职场关系分为 “合作关系” 和 “从属关系”。</p><p>典型的合作关系比如公司与公司合作、公司的合伙人之间合作、两个部门负责人之间合作、两个小组长之间，等等，地位是平等的，在这种合作的情况下，能力可以是互补的，一方拥有 A 能力，另一方拥有 B 能力，合作起来才能发挥更大作用。很多公司都有 CEO、CTO、COO 的角色，角色背后是极其专业的能力。</p><p>从属关系也好理解，老板和员工之间，领导和下属之间，只要存在上下级关系都属于从属关系。一般来说，从属关系的能力是垂直的，比如 CTO 负责制定技术路线和战略方向，一线的技术人员根据规划完成具体的工作。这个属于角色分工的不同，但 CTO 和下属都是技术背景的人员。如果让公司的 COO 来领导技术团队，事情不就乱套了吗。</p><p>程序员的 “服从权威” 心理就存在于从属关系中，需要有纵向的比较，才能够产生所谓的 “权威”。合作关系中的能力都不同在一个标准下，就很难进行比较。</p><p>既然这种心理现象是普遍存在而且是客观存在的，那对于我们来说有什么启发呢？</p><p>对于非权威一方：明白自己要追求的权威是什么。比如工作中感受到了来自上级的压力，要清楚是有意义的压力还是无意义的压力，如果对方是自己领域的权威，就争取提高自己的能力，打败他，自己成为新的权威。如果对方不是自己领域的权威，只是凭借着某些原因在支配你，你应该奋起反抗，或者及时脱离苦海。</p><p>对于权威一方：明白自己权威的来源是什么。比如技术能力也是分层次的，假如自己有着高瞻远瞩的技术视野，而有些非权威的人拿着一分钟能打多少个字的数据来挑战你，你完全可以置之不理，不需要担心什么。</p><p>如果不具备权威的能力，又身处权威的位置，该怎么办呢？将从属关系变为合作关系，就不用担心有人来挑战权威、自己不能服众、有人不服气的问题了。因为 “服从权威” 的心理只存在于从属关系中而很少出现在合作关系中。简单来说，就是外行的领导要懂得放权。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近经历了一件事情，就是入职一家初创公司，工作两周后主动离职。虽然只有短短两周但感觉过了很长时间，从中也学到或者意识到很多东西。离开的原因有很多，也涉及到比较具体的问题，我会继续反思和总结。今天想到一个有意思的话题，可以作为简单的切入点。当然有一些不方便说的内容我是不会说的</summary>
        
      
    
    
    
    
    <category term="程序员" scheme="https://smallyu.net/tags/%E7%A8%8B%E5%BA%8F%E5%91%98/"/>
    
    <category term="心理" scheme="https://smallyu.net/tags/%E5%BF%83%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>区块链技术面试题</title>
    <link href="https://smallyu.net/2023/07/12/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    <id>https://smallyu.net/2023/07/12/%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF%E9%9D%A2%E8%AF%95%E9%A2%98/</id>
    <published>2023-07-11T17:01:09.000Z</published>
    <updated>2025-11-09T09:22:13.316Z</updated>
    
    <content type="html"><![CDATA[<p>最近找工作也经历过很多次面试，这几次面试比较大的感受到一个共同的现象，就是和面试官聊不到一块儿，因为一般面试官也只是问他自己会的、关心的技术，很多东西我平时不怎么关心，而我简历上写的东西，对方也不怎么问。举例来说，我简历上有提到 State channels 相关的工作内容，我想但凡面试官知道 State channels 是什么，看到这部分简历，一定会多少问一句 HTLC 的原理是什么这种问题。然而我经历过的面试中，只有一家公司的面试官问到了这个，他们也确实是专业做区块链项目的团队。然后人家没要我。</p><p>虽然我面试了很多次，也不通过面试很多次，但是大多数情况是问我一些 web2 风格很重的技术细节，很少有人在区块链方面把我问住，尤其是我简历上提到的工作相关的内容，所以并没有太多挫败的感觉，更多时候是在抱怨，为什么那些人都在关心那些，我觉得不重要的东西，我能把某种需要两三个月才能搞明白的区块链技术理解清楚，半个小时就能看懂的某个关于编程语言的问题还能学不会吗？</p><p>所以就想到，如果我是面试官，面对在应聘区块链开发职位的求职者，我可能会提问哪些问题，然后根据这些问题来判断候选人的水平。这些问题是基本上通用的，其中一些问题能深入和扩展：</p><ol><li>比特币的共识是什么，UTXO 是怎么回事</li><li>以太坊的共识是什么，如何选定出块节点的</li><li>你还知道哪些链，这些链有什么技术特点</li><li>PBFT 的流程是什么，国内有哪些联盟链</li><li>PBFT 有哪些变体，哪些链在用</li><li>Layer 2 有哪几种技术类型</li><li>State channels 的原理是什么，Rollups 的原理是什么</li><li>账户钱包是怎么生成的，keystore 文件是什么</li><li>存储类区块链有哪些，其中的难点是什么</li><li>Solidity 语言有哪些限制，和 Go 语言的不同</li><li>面向对象了解吗，写过 Java 吗</li><li>区块链里有随机数吗，怎么使用</li><li>合约交易是怎么经过虚拟机处理的，怎么增加对另一种语言的支持</li><li>日蚀攻击和女巫攻击分别是什么</li><li>了解过资产跨链吗，大概是什么流程</li></ol><p>这些问题没有明确的先后顺序，也比较浅，抛砖引玉吧。突然想起来我面试还被问到过，区块链里面的区块是怎么连接起来的？答案是下一个区块会存上一个区块的块哈希，这种问题就属于没入门系列，还是问点门里面的问题吧。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近找工作也经历过很多次面试，这几次面试比较大的感受到一个共同的现象，就是和面试官聊不到一块儿，因为一般面试官也只是问他自己会的、关心的技术，很多东西我平时不怎么关心，而我简历上写的东西，对方也不怎么问。举例来说，我简历上有提到 State channels</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="面试题" scheme="https://smallyu.net/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
    
  </entry>
  
  <entry>
    <title>如何区分公有链和联盟链</title>
    <link href="https://smallyu.net/2023/07/12/%E5%A6%82%E4%BD%95%E5%8C%BA%E5%88%86%E5%85%AC%E6%9C%89%E9%93%BE%E5%92%8C%E8%81%94%E7%9B%9F%E9%93%BE/"/>
    <id>https://smallyu.net/2023/07/12/%E5%A6%82%E4%BD%95%E5%8C%BA%E5%88%86%E5%85%AC%E6%9C%89%E9%93%BE%E5%92%8C%E8%81%94%E7%9B%9F%E9%93%BE/</id>
    <published>2023-07-11T17:00:58.000Z</published>
    <updated>2025-11-09T09:22:13.424Z</updated>
    
    <content type="html"><![CDATA[<h3 id="简短版"><a href="#简短版" class="headerlink" title="简短版"></a>简短版</h3><p>发币的就是公链，不发币的就是联盟链。</p><h3 id="详细版"><a href="#详细版" class="headerlink" title="详细版"></a>详细版</h3><p>因为我经常否定联盟链，就自然而然产生一个问题：到底什么是联盟链？公链和联盟链的根本区别是什么，分界线在哪儿？本以为这是一个简单的问题，但是仔细想想又没那么简单，不是特别容易去下一个明确的定义，</p><p>简单来说，公开给所有人访问使用的区块链就是公链，在一个小范围内部使用的区块链就是联盟链。这种区别就像是互联网和局域网的区别。但是如果把一条联盟链开放出来，给所有人使用了，它就变成公链了吗？如果这条链使用原始版本的 PBFT 作为共识机制，那它仍然还是联盟链，始终都是联盟链。公链和联盟链的区分，不能仅仅根据使用人群范围的大小。</p><p>再比如存在一个主观的问题，什么是所有人？多大范围算是所有人？如果一共有 100 个人，100 个人就是所有人，那么 99 个人算不算？在现实生活中，似乎也算，99 个人在使用的链，不可能说它是一条联盟链。那 80 个人算不算？好像也算，已经是大多数人了，比起 20 个人的小部分，80 个人不至于认为是一个联盟。60 个人呢？已经超过半数了，能代表所有人吗？</p><p>所以如何判断一条链是公链还是联盟链？经过一段时间的思考，从技术特点的角度去考量，我认为要同时满足这三个条件，才算是公链：</p><ol><li>节点之间网络互联互通</li><li>每个节点平等地拥有成为出块节点的机会</li><li>成为出块节点的门槛是合理的</li></ol><p>你可能注意到，在简短版中，把有没有加密货币作为了区分公链和联盟链的唯一指标，而在详细版里却没有提加密货币。主要是判断的维度不一样，有没有发币是整个系统体现出来的使用方式，上面列举的三个条件要更通用一点，是技术方面的特性。满足三个条件而没有发币仍然能认为是公有链，只不过加密货币对于矿工是很重要的激励手段，是整个系统的一部分，一般是不可或缺的，项目方通常会把加密货币设计进去。</p><h4 id="节点之间网络互联互通"><a href="#节点之间网络互联互通" class="headerlink" title="节点之间网络互联互通"></a>节点之间网络互联互通</h4><p>这一点要求节点不能从网络层面设置访问权限，任何人都能通过互联网访问节点。如果节点没有部署在公网上，而是运行在局域网中，这个节点就不属于公链的节点。如果节点部署在公网上，但是仅限指定的 IP 访问和使用，那这个节点就属于设置了访问权限，是不够开放的行为，如果所有节点都存在类似的设置，整条链就不算是公链。</p><h4 id="每个节点平等地拥有成为出块节点的机会"><a href="#每个节点平等地拥有成为出块节点的机会" class="headerlink" title="每个节点平等地拥有成为出块节点的机会"></a>每个节点平等地拥有成为出块节点的机会</h4><p>比如 PoW，只要算力足够，就会被认可成为出块节点。比如 PoS，只要质押 32 ETH，就有机会成为出块节点。这种属于典型的公链的例子。像是原始版本的 PBFT，出块节点是固定的，还不能更换，就属于联盟链。有一些链在 PBFT 的基础上加入了随机更换共识组的功能，每隔一段时间就会随机选择一些节点作为共识节点负责出块，包含这种设计的区块链是属于公链的，不过项目方需要考量这样的做法是否安全，容错能力怎么样，是否面临女巫攻击的风险。</p><h4 id="成为出块节点的门槛是合理的"><a href="#成为出块节点的门槛是合理的" class="headerlink" title="成为出块节点的门槛是合理的"></a>成为出块节点的门槛是合理的</h4><p>合理是一种主观的感受，因为不好量化，需要项目在设计上去权衡。比如 PoW，获得足够算力去出块是有很高成本的，这个成本就是门槛，如果门槛太低，所有节点都轻易出块，网络会乱，如果门槛太高，没有人能达到，出不了块，都是不合适的。这个门槛需要一个折中的、合适的位置。再比如有些 PBFT 联盟链中，成为出块节点需要 CA 发行的证书，而这个 CA 是由项目方控制的，需要线下通过一系列认证，才能够加入联盟获得证书，这种属于典型的联盟链。</p><h3 id="为什么喜欢区块链"><a href="#为什么喜欢区块链" class="headerlink" title="为什么喜欢区块链"></a>为什么喜欢区块链</h3><p>技术属性上，区块链有一种反叛的精神在里面，区块链主张去中心化的技术理念，自成一套体系，既然你中心化的机构靠不住，那我们就各自为营，我们首先相信自己，其次才相信别人，在这样的模式下，历史记录的准确性非常高。目前能在全世界范围达成数据一致的系统，也就只有区块链才能做到。</p><p>资产属性上，加密货币抵抗地缘政治变化的能力非常强，你可以轻易地持有和世界货币锚定的货币资产，不会因为世界局部的货币价值波动而受到影响。</p><h3 id="为什么不喜欢联盟链"><a href="#为什么不喜欢联盟链" class="headerlink" title="为什么不喜欢联盟链"></a>为什么不喜欢联盟链</h3><p>国内的联盟链项目，很多是政府机构搞信息化建设，最后都是政府出钱。假如雄安新区一个信息平台的项目 2000 W，其中会包含各种技术领域像人工智能、大数据、物联网，区块链是其中一项，具体到这一个区块链类目上，可能 300 W，这 300 W 也不是一般小公司能拿下的，至少得有点强硬的关系，比如外包大厂。然后这个区块链类目又包含很多小的部分，比如某个部门的数据管理平台，要接入区块链进行数据上链，如果这家外包大厂不具备区块链的研发能力，就会从 300 W 中分出 30 W，找一家专门开发区块链的公司，把这部分功能完成了。最终开发联盟链的公司，挣个辛苦钱，同时指望一年能多做几个项目，多挣点钱。</p><p>这种项目大都会走招投标的流程，可以想象一下，光是能不能把项目揽下来就会牵扯到多方的利益纠缠，投标的时候人家会给你公司的整体实力打分，包括申请过多少专利、企业过往的营收能力之类。而且这种项目和区块链的理念没有半点关系，换一个名头比如 5G 技术，还是一模一样的项目流程，招标投标做项目交付，只是其中用到的具体技术不一样而已。所以我说联盟链不是真正的区块链，联盟链是在用金锄头锄地。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;简短版&quot;&gt;&lt;a href=&quot;#简短版&quot; class=&quot;headerlink&quot; title=&quot;简短版&quot;&gt;&lt;/a&gt;简短版&lt;/h3&gt;&lt;p&gt;发币的就是公链，不发币的就是联盟链。&lt;/p&gt;
&lt;h3 id=&quot;详细版&quot;&gt;&lt;a href=&quot;#详细版&quot;</summary>
        
      
    
    
    
    
    <category term="公有链" scheme="https://smallyu.net/tags/%E5%85%AC%E6%9C%89%E9%93%BE/"/>
    
    <category term="联盟链" scheme="https://smallyu.net/tags/%E8%81%94%E7%9B%9F%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>对 Layer 2 项目创业想法的回复</title>
    <link href="https://smallyu.net/2023/07/08/%E5%AF%B9Layer-2%E9%A1%B9%E7%9B%AE%E5%88%9B%E4%B8%9A%E6%83%B3%E6%B3%95%E7%9A%84%E5%9B%9E%E5%A4%8D/"/>
    <id>https://smallyu.net/2023/07/08/%E5%AF%B9Layer-2%E9%A1%B9%E7%9B%AE%E5%88%9B%E4%B8%9A%E6%83%B3%E6%B3%95%E7%9A%84%E5%9B%9E%E5%A4%8D/</id>
    <published>2023-07-08T05:25:31.000Z</published>
    <updated>2025-11-09T09:22:13.439Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>说明：没有任何不尊重来信的意思，邮件中对于 L2 的观点带给我很多启发，因为其中有一些内容和我认知不完全一致，我认真理解和思考了来信的含义，所以想借此表达一下我对 L2 的看法。博客中 L2 相关的内容比较少，就放到博客上来了，这里只是单纯作为技术问题讨论一下</p></blockquote><h3 id="来信"><a href="#来信" class="headerlink" title="来信"></a>来信</h3><pre><code>你好，还在找工作吗？Jul 6, 2023, 5:39 PM (2 days ago)to mewangyu 你好！无意间看到了你的博客，也不知道这个邮箱能不能收到。今年开始 L2 开始火起来了。各种大公司，只要有点钱的，都开始怼 L2从 L2beat 来看，还有不少 L2 即将上线 https://l2beat.com/scaling/tvl#upcoming也不清楚是不是天下 L2 一大抄。但总感觉目前的 L2 技术都差不多来源于几家头部公司的开源代码。如果真的是这样，是不是就可以意味着阿猫阿狗都可以自己 Fork 出一个版本来了呢？如果是的话，那么我们就是那个&quot;阿猫阿狗&quot; :)我们创业团队挺有兴趣蹭蹭 L2 公链&quot;热潮&quot;的，感觉您在这块应该是一把好手。如果你也有兴趣，我们可以进一步聊聊合作方式？我大概的想法是远程、由你自由主导的链演化路线PS: 我的 telegram 是 @****** 欢迎在线联系我。</code></pre><h3 id="回复"><a href="#回复" class="headerlink" title="回复"></a>回复</h3><p>您好！</p><p>非常高兴收到您的邮件，也很抱歉没有太及时回复。</p><p>我尝试认真理解您来信的内容，对于其中提到一些关于 L2 的观点，我想描述一下我的理解。</p><h4 id="1-“技术类型”-和-“项目”-的区别"><a href="#1-“技术类型”-和-“项目”-的区别" class="headerlink" title="1. “技术类型” 和 “项目” 的区别"></a>1. “技术类型” 和 “项目” 的区别</h4><p>Layer 2 有 4 种技术类型，State channels、Sidechains、Optimistic rollups、ZK rollups，所有的项目都属于这 4 种类型内，在即将上线的项目中，大多数是 rollups 类型，但是没有信息他们的代码是不是 fork 已有项目的。</p><img src="a.png" width="50%" /><p>具体项目的话是有这 4 个：</p><img src="b.png" width="30%" /><p>但是从已上线的项目列表看，即使是 fork 相同的项目，他们的应用场景也是有很大差异、高度定制化的:</p><img src="c.png" width="50%" /><h4 id="2-现有的项目不是简单的-fork"><a href="#2-现有的项目不是简单的-fork" class="headerlink" title="2. 现有的项目不是简单的 fork"></a>2. 现有的项目不是简单的 fork</h4><p>以 zkSync 为例，在列表上能看到 3 个项目有这个标识：</p><img src="d.png" width="30%" /><p>分别是 zkSync Era、zkSync Lite 和 ZKSpace，其中 zkSync Era 和 zkSync Lite 是同一家。</p><p>再具体看 ZKSpace，他确实是用了 zkSync 的合约，但并不是单纯的 fork 整个项目，而是使用了 zkSync 的合约代码，在其基础上做一些修改适配自己的业务。同时，ZKSpace 也使用了其他项目的代码。</p><img src="e.png" width="50%" /><p>从 ZKSpace（以前是 ZKSwap）的白皮书能看到，ZKSpace 想做的事情是 Layer 2 上的 Uniswap，包含了 AMM 的功能，也就是交易所市商的那些东西。</p><img src="f.png" width="60%" /><p>zkSync 是一个通用的 Layer 2 项目，本身不提供太具体的功能，ZKSpace 使用了一些 zkSync 的合约代码，在那个基础上做一些事情，而且是业务强相关、有具体目标的事情。他并不和 zkSync 本身抢市场。</p><p>再看比如有 OP 标的项目，也就是基于 OP 项目 fork 的。</p><ul><li>Arbitrum 一开始是 fork OP 的，然后改了一些经济模型方面的东西，后来做大了又重构项目。而且 arb 的运营能力很强，举办过几次奥德赛。</li><li>Boba 的特点是 withdraw 余额不需要等 7 天，马上提取，另外通过他自己叫做混合计算的技术，把 web2 的网页能力带到区块链上。</li><li>Zora 是一个专做 NFT 领域的 layer2.</li><li>Mantle 提供了去中心化的序列器之类。</li></ul><p>总的来说，这些项目都是有具体的目的，想要实现某一种功能，然后把现成的代码拿过来，复用一下，重点可能比较在于这些项目的目标，也就是想干什么样的事情。他们都是结合了自己的业务场景，而不是简单的 fork 下来就能运行。</p><h4 id="3-L2-不是链"><a href="#3-L2-不是链" class="headerlink" title="3. L2 不是链"></a>3. L2 不是链</h4><p>目前的 rollups 项目都是中心化的，本身不是一条链，没有共识之类的东西，主要是项目方在运营。</p><h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>所以关于您提到的想做 L2 方向的项目，我的意思是，从您的描述来看，可能没有您想的那么简单，其中会很涉及到一些具体问题。也许需要应用场景上的创新，和 defi 或者 gamefi 场景结合之类，或者技术特点上的创新，能够改善某种痛点。</p><p>我也很想蹭热点，也希望能有合适的创业机会，我现在也是在找工作，不过从我的角度看还是挺迷茫的，我本身没有太直接的想法，一个项目该往什么样的方向做之类，我觉得那些都涉及到挺市场化的东西，需要很大的资本力量。</p><p>不知道您具体是什么样的想法。由于内容比较多，就以邮件的方式回复了。有问题您随时联系我。</p><h3 id="补充"><a href="#补充" class="headerlink" title="补充"></a>补充</h3><p>有一些在回复中没有提到的话题，顺便展开聊一聊。</p><p>首先是 Layer 2 为什么火？因为 Vitalik 在最近的博客中频繁提到 zkEVM，比如在文章 <a href="https://vitalik.ca/general/2021/12/06/endgame.html">Endgame</a> 中，把支持 ZK-rollup 认为是以太坊未来的重要工作之一。有些问题没办法在 Layer 1 直接解决，就希望通过 Layer 2 解决。有 Vitalik 这样的备受关注的开发者引领生态上的技术方向，Layer 2 就成为了以太坊的发展热点。</p><p>Vitalik 比较看好 ZK-SNARKs 类的 zkEVM，从技术角度，ZK-rollups 将是所谓的 Endgame。但是 ZK 的开发成本非常高，目前还处于研究开发，小体量的资本玩不起，太贵了。</p><p>Dapp-learning 社区在 Youtube 上有一个 Scroll 的技术分享<a href="https://www.youtube.com/watch?v=SEp5SFaYQHY">视频</a>，内容挺好的。Scroll 的整个技术团队都在国内，从视频的讲解中感觉似乎 Scroll 的技术方案是有一些暴力成分在里面的，就是既然写 circuits 不容易，那就把某一类型的 circuits 结果作为一张表，用的时候查表，然后去不断扩充出各种表以支持整个 EVM。这是 Scroll 对比了各 ZK 项目实现 EVM 完备的方式：</p><img src="g.png" width="80%" /><p>Vitalik 的博客文章 <a href="https://vitalik.ca/general/2022/08/04/zkevm.html">The different types of ZK-EVMs</a> 也提到了对 zkEVM 的分类。不管怎么说，Layer 2 的火热应该是指基于 ZK-rollup 的项目打得火热。</p><p>Optimistic rollups 更像是 L2 的一种过渡方案，相信 ZK 出来后 OP 类的项目都会受到打击，因为 OP 并不依赖于加密技术的可靠性，从命名上就能看出来他是乐观主义，先认为一笔交易没有问题，然后给 7 天的挑战期，如果交易有问题，需要用户自主发起挑战，或者由第三方验证者来完整挑战，有很大程度的人为干预在里面。</p><p>再一个是 L2 项目中心化的问题。除了 Sidechains 的项目，L2 一定是中心化的，因为共识交给 L1 去做，L2 本身不管这个，从动机上，L2 起到的作用是快速地把交易收集起来打包发到 L1 上，一笔交易最终的信任是落到 L1 上的，用户相信的不是 L2 而是 L1 上的交易记录，所以 L2 不需要去中心化，用户不需要，项目方也没有必要。L2 一般是有项目方在发行和运营，用户信任项目的技术方案，才愿意到项目上玩，把资产质押进去。</p><p>还有 L2 使用体验的问题，目前很少有用户会把 OP 或者 ARB 作为低成本资产转移的方案，宁愿用 BNB 或者 TRX 来实现匿名场景的支付，使用比较多的还是治理 token 的投资或者 DApp 项目形式的交互。OP 赎回资产需要 7 天的等待期这一点就挺难用的，L2 链之间的资产转移也是个问题，比如在 OP 上的 USDT 能不能转移到 ARB 上，不然我用 OP，另一个人用 ARB，我们就没法交易了。在这个方面还有很大的提升空间。</p><p>L2 是很有前景的技术方向，期待它能带给我们更好的使用体验，至于其中有没有低成本的获利空间，就不太清楚了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;说明：没有任何不尊重来信的意思，邮件中对于 L2 的观点带给我很多启发，因为其中有一些内容和我认知不完全一致，我认真理解和思考了来信的含义，所以想借此表达一下我对 L2 的看法。博客中 L2</summary>
        
      
    
    
    
    
    <category term="Layer2" scheme="https://smallyu.net/tags/Layer2/"/>
    
    <category term="创业" scheme="https://smallyu.net/tags/%E5%88%9B%E4%B8%9A/"/>
    
  </entry>
  
  <entry>
    <title>对区块链共识机制的理解</title>
    <link href="https://smallyu.net/2023/07/01/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6%E7%9A%84%E7%90%86%E8%A7%A3/"/>
    <id>https://smallyu.net/2023/07/01/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6%E7%9A%84%E7%90%86%E8%A7%A3/</id>
    <published>2023-07-01T09:19:29.000Z</published>
    <updated>2025-11-09T09:22:13.447Z</updated>
    
    <content type="html"><![CDATA[<p>共识机制的作用，是让大多数节点的数据能够保持一致。共识机制有两种大的类型，一种是 PoW 风格的，一种是 PBFT 风格的。几乎所有的共识都在这两种风格之下。</p><p>可以从头思考一下，如果是一个人类的群体，怎么样才能让大多数人保持一致呢？一种方法是选出一个领袖，大家都听领袖的意见，再一种方法是大家一起商量进行决策，达成统一的意见。</p><p>对于第一种方法，问题在于如何选出领袖，依据什么样的标准选出，或者怎么样的人才能够成为领袖。对于第二种方法，问题在于哪些人可以参与商量，如何做出决策。</p><p>先看第一种方法，流程上主要有 3 个步骤：</p><ol><li>通过某种方式选出领袖</li><li>所有人在时间单位内都听领袖的</li><li>所有人在时间单位内将会达成一致</li></ol><p>PoW 的过程就是这样：</p><ol><li>解出哈希难题的矿工成为出块节点</li><li>所有节点接收并验证矿工的块</li><li>所有节点的块数据达成一致</li></ol><p>在这种 PoW 风格的共识过程中，最大的变数是第 1 步，也就是如何选出出块的节点，也因此有了很多 PoW 共识的变种。</p><ul><li><p>Proof of Stake (PoS)：以太坊在使用的共识机制，币种名称为 ETH。质押一定量资产的节点会随机成为出块节点，随机的过程由在信标链上运行 VRF 函数完成，并且质押的资产越多，成为出块节点的可能性越大。</p></li><li><p>Proof of Authority (PoA)：以太坊测试网支持的共识机制。在网络的初始化阶段就已经确定哪些节点可以出块，之后按照顺序依次出块。确认验证者的过程是线下完成的，网络本身不具备对验证节点的纠正能力，比较中心化的一种共识。</p></li><li><p>Proof of Importance (PoI)：Nem 在使用的共识机制，币种名称为 XEM。制定了一些给节点打分的机制，在多少天内交易多少次、交易额度是多少之类，以根据分数对节点进行排名，分数高于指定标准的节点，随机成为出块节点。</p></li><li><p>Proof os Elapsed Time (PoET)：Hyperledger Sawtooth 项目使用的共识机制，是英特尔开发的一个项目。每个节点随机生成一个等待时间，等待时间最短的节点成为出块节点。出块节点需要提供一个最短时间的证明，这个证明和硬件设备相结合，达到无法伪造的效果。</p></li><li><p>Proof of Burn (PoB)：Slimcoin 在使用的共识机制，币种名称为 SLM。节点通过销毁资产的交易获得销毁哈希，销毁哈希的计算包含了销毁的金额以及节点在一段时间内销毁的频率等信息，然后系统对每个节点提交的哈希值进行比较，哈希值最小的节点将进行本轮的出块。</p></li><li><p>Proof of Capacity (PoC)：Burst 在使用的共识机制，币种名称为 Burst。节点使用 hard-to-pebble graph 的数据结构，在磁盘上进行文件的写入，这个步骤需要足够多的磁盘空间，写入完成后节点随机打开文件的某个位置，计算对应的哈希值，直到找到满足要求的哈希值，节点就可以进行出块。</p></li><li><p>Proof of History (PoH)：Solana 在使用的共识机制，币种名称为 SOL。这种共识机制的创新之处在于，每一笔交易或者其他链上的操作，都会对应一个哈希值，然后 PoH 生成器生成块，这个块由一系列连续的哈希值，也就是一系列链上操作组成，从而保证链上数据的一致。这里的 PoH 生成器就是出块节点，PoH 生成器的选择标准则是质押金额最多的节点。</p></li></ul><p>这些是 PoW 风格共识的例子，可以看出它们在想方设法改变选择出块节点的方式，但不管具体使用怎么样的机制，这些区块链都符合 3 个步骤的流程。</p><p>再看第二种方法，流程上也是 3 个步骤：</p><ol><li>通过某种方式选出参与决策的人</li><li>参与决策者进行交流、达成一致</li><li>其余所有人都依照决策结果，达成一致</li></ol><p>PBFT 的过程类似这样：</p><ol><li>从所有节点中选出共识节点，然后共识节点依次作为提案节点</li><li>参与共识的节点经过两轮投票，对提案内容进行决策</li><li>共识节点达成一致，其余节点跟风达成一致</li></ol><p>可以看出和第一种方法相比，由单一的领袖变为了多个决策者。在这种 PBFT 风格的共识过程中，最大的变数也是第 1 步，如何从所有节点中选出共识节点，以及共识节点成为提案节点的顺序。这是一些变种：</p><ul><li><p>Delegated Byzantine Fault Talerance (DBFT)：Neo 在使用的共识机制，币种名称为 NEO。每个持有资产的节点都可以进行共识节点的选举，将自己的资产委托给共识节点。拥有委托资产的共识节点进行排名，前几名将依次作为提案节点。</p></li><li><p>Federated Byzantine Agreement (FBA)：Stellar 在使用的共识机制，币种名称为 Stellar。网络中的所有节点都是共识节点，都可以参与两轮投票。为了减少网络的拥堵，节点也可以委托自己的投票权给另一个节点，使用切片或者子网络的方式提高共识效率。</p></li><li><p>HoneyBadgerBFT：一种支持完全异步网络的 BFT 类共识，不依赖于同步的时间顺序，这是 PBFT 不具备的能力。不过异步网络的共识效率相对低一点。</p></li><li><p>HotStuff：一个允许部分网络异步的 BFT 类共识。它的特点是，网络中可以同时存在多个提案，提案节点将选择最好的一个提案，进行后续的流程。这样的方式相当于产生提案的过程是并行进行的，提高了整个共识的效率。</p></li><li><p>VBFT：Ontology 在使用的共识，币种是 ONT。使用 VRF 随机函数进行提案节点的选择，每一轮的提案节点都是随机、不可预测的。</p></li></ul><p>总体而言，PoW 风格和 PBFT 风格都是通过某种方式，筛选出最终能达成一致的内容，但是在选择内容的方式上存在根本区别。PoW 中其余节点将无条件接受来源的块，只要来源的块是符合某种条件的。而 PBFT 中，其余节点先收到块的内容，然后对这个块的去留进行决策，在接受块之前有一个投票的过程。</p><p>现在也有一些创新类型的共识出现，或者是结合多种共识的混合共识等形式，不管是对节点进行信誉评估、根据历史交易质量之类打分，还是根据手机、物联网等硬件设备进行数据的验证，还是对 PBFT 做某个阶段上并行、容错能力和网络效率之类的优化，共识机制终究还是在 PoW 风格和 PBFT 风格的体系之下。</p><p>因此你可以大胆的说，共识机制也就这么回事，只是在具体的设计和实现上有差异而已。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;共识机制的作用，是让大多数节点的数据能够保持一致。共识机制有两种大的类型，一种是 PoW 风格的，一种是 PBFT</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>Pebbling Game 鹅卵石游戏</title>
    <link href="https://smallyu.net/2023/05/18/Pebbling-Game-%E9%B9%85%E5%8D%B5%E7%9F%B3%E6%B8%B8%E6%88%8F/"/>
    <id>https://smallyu.net/2023/05/18/Pebbling-Game-%E9%B9%85%E5%8D%B5%E7%9F%B3%E6%B8%B8%E6%88%8F/</id>
    <published>2023-05-18T08:36:33.000Z</published>
    <updated>2025-11-09T09:22:13.308Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个在线的网页游戏：<a href="https://smallyunet.github.io/pebbling-game/" target="_blank">Pebbling Game</a>。可以看这个嵌入进来的网页：</p><div><iframe src="https://smallyunet.github.io/pebbling-game/" width="900px" height="580px" frameborder="0" scrolling="yes" style="border: 5px double #e4e4e4;"> </iframe></div><p>为了直观展示 Pebbling Game 的游戏规则，经过几十次调整，GPT-4 完成了这个在线的游戏页面。</p><p>游戏的规则是：</p><ol><li>点击节点的圆圈，可以在节点中放入鹅卵石</li><li>只有指向当前节点的所有节点，都已经放置了鹅卵石，当前节点才能够放置鹅卵石</li><li>游戏目的是在节点 0 放置鹅卵石</li><li>任何时间都能够从任意节点取走鹅卵石</li></ol><p>如果直接点击节点 0，可以看到两个红色闪烁圆圈的提醒，意思是节点 1 和 2 都还没有放入鹅卵石，所以节点 0 不能放入鹅卵石。</p><p>节点 7 没有来源节点，所以可以直接放入鹅卵石。点击节点 7，能看到节点内出现了黑色的实心圆。此时如果想把鹅卵石放入节点 3，会提示因为节点 6 还空着，放入失败。节点 3 的来源节点是 6 和 7.</p><p>那么在这样的游戏规则下，问：最少需要多少颗鹅卵石？</p><p>如果鹅卵石足够多，这个图中一共有 10 个节点，手里有 10 个鹅卵石，就不需要取走鹅卵石的操作，直接按照顺序把节点填满就行。</p><p>如果鹅卵石有限，寻求鹅卵石数量最少的解法，这个图应该至少需要 5 个鹅卵石。</p><p>鹅卵石游戏的特点就是，总会存在一个最小值，如果鹅卵石的数量少于这个值，游戏将不能完成，因为最终的节点依赖于下层节点，而下层节点依次依赖于更下层的节点。如果中间节点的鹅卵石被取走，还需要从最下层开始重新放置。</p><p>鹅卵石游戏对于 Hard-to-pebble graphs 的数据结构具有启发意义，理解了游戏的规则，就理解了区块链如何证明磁盘空间的大小。</p><h3 id="空间证明"><a href="#空间证明" class="headerlink" title="空间证明"></a>空间证明</h3><p>Hard-to-pebble graphs 是一种结合了 Merkle 树的 DAG，特点就是需要一定数量的储存空间才能够完成最顶点的计算。就像是鹅卵石不够就无法完成游戏，储存空间不够就无法完成挑战。</p><p>由于图的多种多样，需要鹅卵石的数量没有通用的最优解，只能是针对某一种类型的图，去计算空间复杂度。</p><p>区块链场景的需求是既要占用空间大，又要验证速度快。Stack expender graph 的图结构在 Proof of Space 中使用比较广泛。在验证阶段，只需要按照 Merkle 树的特点，验证图中的某些节点，就可以确认图的完整性了，同时也能根据图的深度，推算出占用了多大的磁盘空间。</p><p>如果既要验证空间大小，又要验证空间占用的持续性，就在空间证明的基础上加上对时间的证明，比如 Chia 就用了 Delay Verifiable Function 的方式，先验一遍空间证明，等一段时间后，用 VDF 验证确实经过了足够多的时间，然后再验一遍空间证明，就达到了 Proof of Space-Time 的效果。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;这是一个在线的网页游戏：&lt;a href=&quot;https://smallyunet.github.io/pebbling-game/&quot; target=&quot;_blank&quot;&gt;Pebbling Game&lt;/a&gt;。可以看这个嵌入进来的网页：&lt;/p&gt;
&lt;div&gt;
&lt;iframe</summary>
        
      
    
    
    
    
    <category term="文件证明" scheme="https://smallyu.net/tags/%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E/"/>
    
  </entry>
  
  <entry>
    <title>PDP 文件证明的局限性</title>
    <link href="https://smallyu.net/2023/05/17/PDP%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7/"/>
    <id>https://smallyu.net/2023/05/17/PDP%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E5%B1%80%E9%99%90%E6%80%A7/</id>
    <published>2023-05-17T07:53:06.000Z</published>
    <updated>2025-11-09T09:22:13.308Z</updated>
    
    <content type="html"><![CDATA[<h3 id="存储证明"><a href="#存储证明" class="headerlink" title="存储证明"></a>存储证明</h3><p>对于一个文件来说，PDP 最基础的用法，就是根据文件内容生成对应的 TAG，然后把文件发送到另一个环境里。接着用保存下来的 TAG 验证对方是否真的储存了文件。如果对方没有储存文件，是不可能通过第一次验证的。在接下来的验证中。因为挑战是随机生成的，会选择文件不同位置的片段，能在概率上提高验证的可靠性。</p><p>所以这样的用法，至少能够证明文件储存在对方环境中过。但是这样的证明方式存在疏漏，就是文件不需要完整，或者说缺少对文件完整性的证明。如果文件非常大，而挑战的数量一般是固定的，文件越大，挑战越不全面。虽然挑战的文件片段是随机的，仍然存在可能性，对方不需要完整的文件，而只需要文件的一部分，2&#x2F;3、1&#x2F;2、1&#x2F;3，就足以完成挑战了。</p><h3 id="文件完整性证明"><a href="#文件完整性证明" class="headerlink" title="文件完整性证明"></a>文件完整性证明</h3><p>要让 PDP 能够证明文件的完整性，可以把 PDP 证明和 Merkle 树的数据结构结合起来。PDP 证明一般需要把文件拆分成数据片段，根据数据片段生成 TAG，并不强调数据片段的序列化方式。</p><p>和 Merkle 树结合的地方，就是在把文件拆分为数据片段之后，把数据片段序列化到 Merkle 树中。由于 Merkle 树的特性，父节点的值需要依赖子节点的值才能够推算出来，如果验证了父节点是正确的，就足以说明子节点是全部存在的，也就间接证明了文件的完整性。</p><p>在实际的使用中，往往抽取 Merkle 树的一些节点，对 Merkle 节点的路径进行验证。也是验证 Merkle 树完整性的一般方法。在使用了 Merkle 树作为文件的数据结构，并且在验证 PDP 证明的同时验证 Merkle 树的完整性，就能确保文件是完整存在了。</p><p>Merkle 树的验证覆盖率也不可能 100%，但通常认为，伪造 Merkle 树节点的成本比真正保留了完整数据的成本还要高。</p><h3 id="文件大小证明"><a href="#文件大小证明" class="headerlink" title="文件大小证明"></a>文件大小证明</h3><p>PDP 证明本身不包含文件的元信息，它只能证明文件是存在的、文件是完整的，至于是什么样的文件，文件名是什么、文件类型是什么、文件有多大，一概不知。PDP 的证明信息里不包含文件的元信息，而元信息中比较有用的是文件的大小信息。PDP 不是区块链时代的技术，也就没怎么关心这个问题。</p><p>有的区块链用磁盘空间的大小来描述节点的算力，或者根据文件大小来让使用者付费，或者根据储存的文件大小来给予存储节点奖励，这些都意味着文件大小的信息至关重要。</p><p>那么，对于文件的元信息缺失的问题，一种做法是在 PDP 证明系统之外，将元信息与文件数据关联起来。另一种做法是，对文件进行修改，将元信息附加到文件数据里，只要证明文件数据的完整，就能够确认文件元信息的准确。</p><p>然而，无论哪种方式，都不能保证文件元信息的可靠。在区块链的场景下，信任模型发生了一些变化，和 PDP 要解决的问题存在差异。以前的 PDP 是让用户相信存储节点，而现在，是让区块链来相信存储节点。</p><p>第一种方式需要额外的机制保证元信息和文件数据之间的映射关系，这会带来很大的开销。第二种方式在实现上是简便的，问题在于将元信息附加到文件数据的步骤，由谁来完成？是可信的吗？如果恶意附加了错误的信息呢？另外，怎么读取这些元信息？要把文件数据或者一部分文件数据下载下来？下载的信息还不能保证是正确的？</p><p>总之，在加密学的范畴里，PDP 并不能解决对文件大小的证明。</p><h3 id="文件持续性证明"><a href="#文件持续性证明" class="headerlink" title="文件持续性证明"></a>文件持续性证明</h3><p>PDP 证明需要不断地发起挑战请求，对方能够完成挑战则说明文件安好。比如现在发起一次挑战，一个小时之后又发起一次挑战，至少能够说明对方在完成挑战的时刻，文件是完整存在的。那在没有进行挑战的时间呢？没有限制。</p><p>如果在第一次完成挑战后，就把文件转移到了另外一个地方，等第二次挑战的时候，再把文件拿过来完成挑战，这完全是可以的。PDP 并不也没有能力阻止这样的情况。PDP 无法对文件的持续性提供证明。</p><p>这样的能力限制会对区块链造成什么样的影响呢，首先能想到的就是算力换空间。因为在有些区块链的规则里，文件越大，矿工的收益越高。矿工完全可以利用 PDP 的证明间隔，把空间腾给其他文件用，用磁盘的 IO 换取磁盘空间大小。</p><p>再就是证明成本的提高。挑战越频繁，文件越安全，这对寻求证明的一方是一种压力。</p><h3 id="文件多副本证明"><a href="#文件多副本证明" class="headerlink" title="文件多副本证明"></a>文件多副本证明</h3><p>PDP 自然也不具备证明文件有多个副本的能力。如果需要对方存 2 个副本，而对方实际上只存了 1 份，对于 PDP 是没有任何感知的，这很容易理解。</p><p>如果想分散文件损失的风险，把文件存到了不同地方，在使用 PDP 作为验证手段的情况下，是无法达到目的的。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;存储证明&quot;&gt;&lt;a href=&quot;#存储证明&quot; class=&quot;headerlink&quot; title=&quot;存储证明&quot;&gt;&lt;/a&gt;存储证明&lt;/h3&gt;&lt;p&gt;对于一个文件来说，PDP 最基础的用法，就是根据文件内容生成对应的 TAG，然后把文件发送到另一个环境里。接着用保存下来的</summary>
        
      
    
    
    
    
    <category term="文件证明" scheme="https://smallyu.net/tags/%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E/"/>
    
  </entry>
  
  <entry>
    <title>不要小瞧 ChatGPT</title>
    <link href="https://smallyu.net/2023/03/30/%E4%B8%8D%E8%A6%81%E5%B0%8F%E7%9E%A7-ChatGPT/"/>
    <id>https://smallyu.net/2023/03/30/%E4%B8%8D%E8%A6%81%E5%B0%8F%E7%9E%A7-ChatGPT/</id>
    <published>2023-03-30T09:09:36.000Z</published>
    <updated>2026-08-04T11:43:17.182Z</updated>
    
    <content type="html"><![CDATA[<p>之前我不了解 Filecoin 是如何管理磁盘空间的，既然它使用了 Proof of Space，那么 Sector 在创建的时候就一定会占用掉磁盘空间，当有用户的文件存入 Sector，Filecoin 是如何及时把占用的空间释放出来的？每写入一次文件，就释放一次占用的空间，这种做法太生硬了。</p><p>我看了 Filecoin 的 <a href="https://docs.filecoin.io/basics/what-is-filecoin/storage-model/">文档</a> 和 <a href="https://spec.filecoin.io/systems/filecoin_mining/sector/">Spec</a>，都没能很直接地找到答案。运行 Filecoin 节点的成本又比较高，不但对硬件需求高，而且还得在实际的操作过程中去观察磁盘容量的变化。</p><p>后来无意间问了 ChatGPT 一句，没想到它给出了很详细的解释，让我对 ChatGPT 刮目相看。Filecoin 不会实时释放磁盘空间，只在 Sector 存满文件之后有一个 Sealing 的过程，这个过程会把 Sector 封存起来，然后用真实的数据替换到之前随机生成的用来占用空间的文件，用真实数据进行挖矿并获得收益。</p><p>以前在一个视频上听到过 Filecoin 的磁盘利用率只有 50% 的说法，但没有很理解为什么那么说。现在才算是明白，Sealing 之前 Sector 会占用两倍磁盘空间的含义。</p><p>在这件事情上，最令人惊讶的是 ChatGPT 的能力，其实回头想想，如果有一个经验丰富的 Filecoin 研究者，他很轻易就能够解答我的疑惑。但我经常面临的问题是，找不到那样资深的研究者，我向他咨询问题，他还乐意给我解答。现在我找到了。</p><p>2020年12月21日，我在<a href="/micro-blog/#2020-32">微博</a>上写下这样一句话：</p><blockquote><p>单纯的记忆是没有意义的，把书本和网络上的内容背下来，只能“显得”厉害，欺骗愚昧的人。如果把计算机和互联网理解成另一种形式的生物，它将比任何人类都博学。</p></blockquote><p>没想到，事到如今，这样的 “生物” 真的出现了，ChatGPT 集成了各个领域的专业知识，并且能够以人类的语言进行对话。我以前就知道，单纯提高自己 “死知识” 的储备没有用，总有你无法超越的人，甚至对方不是人。</p><p>最近有一个 ChatGPT 超越人类案例：<a href="https://www.zhihu.com/question/592511373">如何看待一男子宠物狗患病兽医难断病因，询问 GPT-4 后获救？</a>内容大意是，由于兽医经验有限，对于狗狗的病因，兽医只冲着 A 方向去诊断。后来主人把狗狗的各项身体指标让 ChatGPT 分析，ChatGPT 给出了 A、B 两种可能，去医院一查，果然是 B。由于 ChatGPT 不像兽医一样受到过往阅历的限制，ChatGPT 显得比兽医更加博学。</p><p>随着年龄的增长，我们必须学会抛弃极端的思维方式。我们不能相信 AI 会替代人类，因为现在的 AI 没有智能。我们也不能否认 AI 在某一些方面强过人类，可以作为人类很好的助手。</p><p>现在最先进的大语言模型是 GPT-4，需要在 ChatGPT 的网站上花 20 美元一个月开订阅才能用到。免费使用的预览版本是 GPT-3.5，不过 GPT-3.5 也很厉害。当人们以为 AI 聊天只停留在微软小冰和苹果 Siri 那样水平的时候，GPT-3.5 横空出世，广为流传。我以前用 GPT-3.5 也做过一些事情，帮朋友生成播客的好评评论、帮朋友完成专科学校的编程习题作业，等等。</p><p>ChatGPT 的翻译能力也是很强的，个别词汇的翻译比谷歌翻译要厉害。比如 “兜底机制” 这个词，谷歌翻译会翻译成 “Pocket Mechanism”，而 ChatGPT 会翻译成 “fallback mechanism”，显然谷歌翻译是直译，ChatGPT 是在理解词汇含义之后再翻译的。再比如 “布偶猫”，谷歌翻译为 “cat plush”，ChatGPT 翻译为 “Ragdoll cat”，高下立判。</p><p>不过 GPT-3.5 自诞生就存在一个广为诟病的问题，就是 “一本正经地胡说八道”。作为一个语言模型，程序员应该能比较容易理解，为什么会出现这样的现象，像是套模板，从一堆现成的语言模板里挑出比较好的回答。GPT-3.5 似乎就是这样。也就是说，想靠 GPT-3.5 来获取知识，是有很大风险的，它能告诉你正确的内容，也能把编造的内容当作知识讲出来。</p><p>谷歌的大语言模型 <a href="https://bard.google.com/">Bard</a> 不但综合能力比 ChatGPT 差，对代码的处理能力很弱，而且同样存在 “一本正经地胡说八道” 的情况，还经常 “答非所问”。中国的产品就更不用比了。</p><p>与 GPT-3.5 相比，GPT-4 在各方面应该都有所改善，不是很清楚在信息的准确性上具体有多大程度的改善，但对语言的理解能力确实超过 GPT-3.5，比如这里的<a href="https://v2ex.com/t/927744">讨论</a>。GPT-4 用了和 GPT-3.5 不一样的训练模型架构，微软发布过关于 GPT-4 的<a href="https://arxiv.org/abs/2303.12712">技术论文</a>。从论文看， GPT-4 是支持多模态的，也就是支持根据输入数据生成统计图之类，可能现在还没有开放出来给公众使用。</p><p>GPT-4 的能力过分强大也引来很多人的担忧，甚至马斯克都签署了<a href="https://futureoflife.org/open-letter/pause-giant-ai-experiments/">暂停训练比 GPT-4 更强大 AI 的公开信</a>。没有人知道 GPT-4 的出现，会给人类社会带来有益的还是有害的影响。</p><p>目前已经有很多产品接入了 GPT 的能力，微软的 Office 办公套件是首先支持的，毕竟是自家产品。不知道实际使用效果怎么样，也许会带来翻天覆地的变化，很多重复性的工作，GPT 完全具备更出色的处理能力。你不需要知道 Excel 里的某个函数怎么写，那些函数也许专业的程序员都用不来，何况几乎不懂计算机的文员，有了 GPT，用自然语言描述就可以了。以后的计算机教育也许会增加一个门类：如何与 ChatGPTG 高效沟通。</p><p>微软的 Bing 搜索引擎也引入了 GPT 的能力，没有 GPT 的 Bing 在搜索结果上是无法和 Google 相比的，有了 GPT，Bing 就有了使用自然语言搜索的能力。New Bing 的方式是，根据用户要搜索的内容，GPT 去搜索一遍，然后归纳总结，再用自然语言描述出来。这样做的好处是，GPT 说的都是正确的内容，缺点是处理过程比较慢。尝鲜可以，对于程序员来说，快速解决问题还得是 Google。</p><p>更加值得期待的是 GPT-4 的插件，让 GPT 融入到更多的场景中，比如取代 GitHub Copilot，在生成代码的能力上绰绰有余，只是使用形式上的问题。Copilot 这个东西，一旦用过就回不去了，本以为能够这样自动补全代码已经很先进，没想到 Copilot 发布没多久，GPT 就出来了。Copilot 是面临危险的，也推出了 Copilot X 方案，不过我更看好 GPT 的未来。我已经取消了 Copilot 每月 10 美元的订阅，买了每个月 20 美元的 ChatGPT Plus，相信以后 GPT 能大放异彩。</p><p>现在 ChatGPT 的使用对于美国之外的用户，还有一些门槛。首先需要能够正常访问 ChatGPT 的网站，然后注册账号需要美国的手机号码，最后订阅 Plus 需要美国的银行卡。经过一系列的折腾，我的建议是直接买现成的账号最划算，150 人民币就可以买到一个独立使用的、开通了 Plus 的账号。否则即使付出很高的成本，也未必能把事办成。</p><p>外国的手机号码可以花 300 块钱买 Ultra 实体卡，不需要 KYC，号码是 +1 开头，长期保号，每月 3 美元月租，开了 Wi-Fi Calling 可以免费使用 100 条短信。美国手机号还是比较有用的，可以开美区的 PayPal 账户，美区的 Apple ID 可以用美区的 PayPal 支付。美区的 PayPal 可以绑国内开的 Visa 信用卡。很多 IM 软件也会需要国外的手机号码作为验证。</p><p>国外的银行卡可以试试 Depay 虚拟卡，最近因为 ChatGPT 会员的需要发展速度很快，充值加密货币，然后以美元消费。但是 ChatGPT 的订单有一系列的风控规则，IP 地址、订单地址不一致都有可能触发风控，导致订阅失败。只是开通 ChatGPT Plus 的话，苹果手机访问 ChatGPT 能看到 Apple Pay 的选项，有美区 PayPal 也可以试试。总的来说，直接买账号是最快最省力的办法。</p><p>另外，最近 AI 领域的文字生成图片也比较火。我试用了比较热门的几个产品。</p><ul><li><a href="https://labs.openai.com/">DALL-E</a> 和 GPT 是同一家，都是 OpenAI 的产品，但是 DALL-E 绘制出来的人物经常是畸形的</li><li><a href="https://replicate.com/explore">Replicate</a> 提供了多种模型的试用，比如 <a href="https://replicate.com/stability-ai/stable-diffusion">stable-diffusion</a>，同样的问题，生成的人物总是怪怪的</li><li><a href="https://www.midjourney.com/">Midjourney</a> 是在 Discord 里面交互的一个产品，生成的图片质量不差，但是图片里的场景都比较小</li></ul><p>从我的感受上，目前文字生成图片的效果并不好，而且生成图片的效果很大程度取决于，输入的描述语是不是精确符合模型的规则，希望以后借助 GPT 的自然语言能力，文字生成图片会更加好用。</p><p>拥有 ChatGPT 就相当于有了一个空前强大的知识库，它拥有丰富的文字能力，一心一意为你服务，你难道不应该拥有一个吗？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;之前我不了解 Filecoin 是如何管理磁盘空间的，既然它使用了 Proof of Space，那么 Sector 在创建的时候就一定会占用掉磁盘空间，当有用户的文件存入 Sector，Filecoin</summary>
        
      
    
    
    
    
    <category term="AI" scheme="https://smallyu.net/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>为什么炒币不是一个好主意</title>
    <link href="https://smallyu.net/2023/03/15/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%82%92%E5%B8%81%E4%B8%8D%E6%98%AF%E4%B8%80%E4%B8%AA%E5%A5%BD%E4%B8%BB%E6%84%8F/"/>
    <id>https://smallyu.net/2023/03/15/%E4%B8%BA%E4%BB%80%E4%B9%88%E7%82%92%E5%B8%81%E4%B8%8D%E6%98%AF%E4%B8%80%E4%B8%AA%E5%A5%BD%E4%B8%BB%E6%84%8F/</id>
    <published>2023-03-15T10:38:01.000Z</published>
    <updated>2026-06-30T07:16:22.262Z</updated>
    
    <content type="html"><![CDATA[<p>最近很长时间我都被贫穷的痛苦困扰，但我不认为炒币是一个获得财富的好办法。尤其是短线操作，以及各种加杠杆的合约交易。</p><p>币圈就像一个充满金钱诱惑的赌场，各大交易所、各种 DeFi Dapp 都是赌桌上的游戏。赌徒也许会赚钱也许会赔钱，但最终赚钱的一定是赌场。很少有赌徒可以从赌桌上全身而退。</p><p>比较聪明的人都只是把加密货币作为理财手段，按照投资的性质分配一部分可支配财产进去。长线操作才算是投资，短线就是赌。</p><p>造成这样的现象，最根本的原因是，没有人可以预测市场的走向。</p><p>很多人喜欢在行情发生变化后，有理有据地分析行情为什么会发生，消息面有哪些利好、技术面有哪些趋势、周期上有什么规律。这些分析最大的共同点，就是全都在事后分析，事情发生前，没有人知道会发生什么事情。看看近一年的几次黑天鹅事件，有哪一次是被经济学家、区块链科学家预测到的吗？</p><p>究其原因，没有人可以预测未来。</p><p>再就是最简单的道理，如果那些号称量化大师、预测行情的专业人士，真的掌握着财富密码，为什么要说出来呢？为什么唯恐大家不知道呢？说明他们都只是瞎猜而已，扩大知名度能够带来的收益，比财富密码的收益要高。</p><p>从投资的角度，炒币能带来的收益是比较有限的，却需要承担非常高的风险。当然和传统的基金和股票比起来，变化幅度要大很多，但是加密货币的行情波动还远达不到，让人暴富的程度。10 年前也许可以，回到 10 年前，你真的敢买吗？</p><p>站在历史的高点，看任何事情都轻而易举，可做为历史的亲历者，想超前于时代是很困难的事情，需要过人的眼光和魄力。</p><p>另外，加密货币野蛮发展到现在，已经有一点脱离价值投资的范畴，大盘的涨跌、牛熊市的转换，是因为人们对加密货币本身的认知变化吗？至少最近几年不是，大都因为美联储的货币政策。也就是说，现在加密货币的市场就这么大，除非扩大加密市场的容量，不然新出的链只是在和已有的链争市场份额。</p><p>所以扩大加密市场的机会，在 Web 3.0 和元宇宙上。要让更多的非加密行业内的人，参与到加密市场中去。</p><p>炒币能够稳定获利的方式是老鼠仓，前提要有那种层面的信息源。KOL 在一定程度上是能够影响市场的，如果粉丝数量非常庞大，在 KOL 的言论引导下，粉丝跟风集火到某一个点上，也算是操纵市场的一种方式。这种跟风活动最终获利的肯定也不是后来参与进去的人，自然不靠谱。</p><p>至于说为什么需要关心炒币的问题，因为工作本身并不能让你变得富有。打工可以是一个积累财富的过程，但不会出现让你财富突然增长的情况。思考和寻找能带来更多收益的机会是很有必要的事情。</p><p>关于企业运行的逻辑，一般开始的时候花投资人的钱，有盈利后再把收益还给股东。企业往往会有多个产品线，只要整体上营收大于成本，企业就能够生存下去。</p><p>其实企业在裁员的时候也是充满纠结的，假如因为盈利问题，需要裁掉一个产品线，那么纠结之处在于，如果扛一扛过去这段困难时间，也许未来这个产品能带来更多收益，也许因为没有及时裁掉而带来更多亏损。如果直接裁掉，那么以后再想打造这么一个产品线，又要付出的更多的成本。</p><p>还有一个裁员中让人纠结的问题是，一个人在 A 领域打 60 分，另一个人在 B 领域打 80 分，现在因为 B 领域所在的产品线整个裁掉了，B 领域的所有人都要裁掉，也就是说企业裁掉了更加优秀的人。所以裁员这事不好说，和企业的运行情况和战略方向关系比较大。</p><p>总的来说，炒币不可取，盲目依赖于上班所在的企业也不可取。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近很长时间我都被贫穷的痛苦困扰，但我不认为炒币是一个获得财富的好办法。尤其是短线操作，以及各种加杠杆的合约交易。&lt;/p&gt;
&lt;p&gt;币圈就像一个充满金钱诱惑的赌场，各大交易所、各种 DeFi Dapp</summary>
        
      
    
    
    
    
    <category term="投资" scheme="https://smallyu.net/tags/%E6%8A%95%E8%B5%84/"/>
    
  </entry>
  
  <entry>
    <title>一种在区块链上生成随机数的机制</title>
    <link href="https://smallyu.net/2023/02/22/%E4%B8%80%E7%A7%8D%E5%9C%A8%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%8A%E7%94%9F%E6%88%90%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%9A%84%E6%9C%BA%E5%88%B6/"/>
    <id>https://smallyu.net/2023/02/22/%E4%B8%80%E7%A7%8D%E5%9C%A8%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%8A%E7%94%9F%E6%88%90%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%9A%84%E6%9C%BA%E5%88%B6/</id>
    <published>2023-02-22T10:19:59.000Z</published>
    <updated>2025-06-19T03:30:33.055Z</updated>
    
    <content type="html"><![CDATA[<p>区块链本身不允许存在随机数，因为全部节点都需要同步计算结果，如果每次运行的结果不一样，整个网络就会混乱。也就是说，在区块链上，如果有一个函数的作用是生成随机数，这个函数需要符合两点要求：1. 返回值是不可预测的；2. 返回值是确定的。</p><p>这两点要求似乎相悖，但目前已经有解决方案了，Chainlink 的预言机提供了使用 VRF 来生成随机数的方法。由于 VRF 的特性，正好适合在区块链的场景中生成随机数。</p><p>不过现在要讨论的是另一个问题：矿工作恶。可以参考一下这篇文章的内容：<a href="https://hackingdistributed.com/2017/12/24/how-not-to-run-a-blockchain-lottery/">How Not To Run A Blockchain Lottery</a></p><p>假如在区块链上运行一个彩票游戏，而矿工也参与了彩票下注，由于彩票中奖后会获得巨额奖励，只要巨额奖励超过矿工的挖矿所得，矿工就有足够的动机作恶。作恶的方式是，因为一笔交易的执行结果是确定的，而矿工会先知道结果，如果交易结果对自己不利，矿工可以拒绝出块。对于矿工来说，在经济上不一定划算，但是这种情况的存在，不但给彩票游戏增加了不公平的性质，还给了矿工作恶的权利。矿工也有被贿赂的可能性，如果矿工集体作恶，网络就乱套了。</p><p>面对这样的情况，我们需要一种方式来生成随机数，要求矿工不能知道这个数字是什么。等区块确认上链后，随机的数字才被公开。</p><p>有什么是真正的随机、难以预测呢？未来，未来无法预测。</p><p>我们可以尝试使用这样的方式：随机数用本次交易块高度 +2 个块的块哈希作为随机数种子。</p><p>比如发起一笔交易，要生成一个随机数，现在的块高度是 1，这笔交易提交后，得到的随机数是 null，真正的随机数将会在块高度达到 3 时才真正显示。因为这个随机数使用 3 的块哈希作为随机数的种子，在块高度达到 3 之前是不可能有人知道，这个数字将会是什么。</p><p>在彩票的场景中，抽奖结果在块高度为 1 是已经确定了，用户在块高度为 1 的交易中已经参与了抽奖，只是块高度为 3 时才公布抽奖结果。这样几乎能避免矿工作恶的问题，因为在块高度为 1 时，矿工也不知道结果是什么，在块高度为 3 时，参与抽奖的顺序和结果已经确定了。</p><p>那矿工在块高度 3 的时候，不还是可以拒绝出块吗？直接拒绝 1 块或者拒绝 3 块没有差别啊？</p><p>这里就需要区分两个情况：</p><ul><li>在块高度 1 发起的请求，在块高度 3、4、5 去查，得到的都是以块高度 3 的块哈希为种子，计算出的随机数</li><li>在块高度 1 发起的请求，以块高度 3 的块哈希作为种子；在块高度 2 发起的请求，以块高度 4 的块哈希作为种子</li></ul><p>要想达到完全随机的效果，应该使用第二种方式。</p><p>那问题又来了，当前块高度是不断变化的，这个随机值不就成变量了吗？用户在块高度是 3 的时候生成随机数，然后想知道这个随机数具体是多少，结果永远没办法得到这个值，因为等块高度是 5 的时候才真正产生这个数，而块高度是 5 的时候去查询，要等到块高度 7……</p><p>这里还需要区分两个概念：生成随机数和查询随机数。上面两种情况都是按照查询随机数的机制来描述的。</p><p>按照生成随机数的机制来设定，使用第二种方式是必须的，不然随机数就成常量了。那查询呢？不能否认的事实是，只要用户能查到的结果，矿工一定能查到，而且会提前知道。问题回到了一开始的困局，似乎无解了。</p><p>不对不对。</p><p>为什么要设定为使用块高度 +2，而不是 +1 块高度的块哈希呢？是为了避免矿工提前知道结果，如果是 +1，矿工挖出 1 个块就能知道结果，如果是 +2，矿工一般很难领先网络 2 个块。</p><p>所以更好的做法，是在块高度 1 发出生成随机数的请求，然后将块高度 2 和块高度 3 的块哈希，作为随机数种子。在这种情况下，矿工手里的第 3 个块会对随机数产生一定影响，但又不是决定性的影响，有可能第 2 个块就已经确定矿工与中奖无缘了，矿工将没有必要在第 3 个块上进行违规操作。</p><p>那如果第 3 个块也能很大程度决定随机数的内容呢？矿工仍然可以拒绝出块。</p><p>从这个角度来看，延后的块数越多，矿工的影响力越小。假如随机数由 +10 个块的块哈希决定，前 9 个块已经让所有矿工都出局了，矿工就不作恶了。</p><p>还有其他更好的方案吗？好像没有。假如将 +2 个块的块哈希作为对称加密的私钥，在 +2 块产生前随机值已经产生但是无法被解码，问题在于，合约也无法提前把 +2 块作为密钥对随机值进行加密。</p><p>只能降低矿工的影响力了。</p><p>未来无法预测，但当未来来临的时候，总有人能先知先觉。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;区块链本身不允许存在随机数，因为全部节点都需要同步计算结果，如果每次运行的结果不一样，整个网络就会混乱。也就是说，在区块链上，如果有一个函数的作用是生成随机数，这个函数需要符合两点要求：1. 返回值是不可预测的；2.</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="MEV" scheme="https://smallyu.net/tags/MEV/"/>
    
  </entry>
  
  <entry>
    <title>为什么以太坊的私钥计算不可逆</title>
    <link href="https://smallyu.net/2023/02/20/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E7%A7%81%E9%92%A5%E8%AE%A1%E7%AE%97%E4%B8%8D%E5%8F%AF%E9%80%86/"/>
    <id>https://smallyu.net/2023/02/20/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E7%A7%81%E9%92%A5%E8%AE%A1%E7%AE%97%E4%B8%8D%E5%8F%AF%E9%80%86/</id>
    <published>2023-02-20T15:42:24.000Z</published>
    <updated>2026-08-04T11:43:16.560Z</updated>
    
    <content type="html"><![CDATA[<p>你有没有过好奇，为什么以太坊的私钥无法从账户地址逆推算出来？当你拥有一个私钥，想要得到这个私钥对应的账户地址，你可以在 MetaMask 里导入这个账户，或者使用 ether.js 这样的 SDK 在代码层面导入一个账户到钱包，然后打印出账户的地址。这个导入账户的过程有没有黑箱操作？</p><p>我前几天无意中在 Medium 上看到一篇文章，作者用很简洁的代码写出了从私钥到地址的计算过程，作者的代码在这里 <a href="https://gist.github.com/RareSkills/eb51623908f348663cd6a241d9dbf115">RareSkills&#x2F;generate-ethereum-address-lower-level.py</a>。</p><p>我把代码复制过来：</p><pre><code class="python">from ecpy.curves import Curvefrom sha3 import keccak_256private_key = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80cv     = Curve.get_curve(&#39;secp256k1&#39;)pu_key = private_key * cv.generator # just multiplying the private key by generator point (EC multiplication)concat_x_y = pu_key.x.to_bytes(32, byteorder=&#39;big&#39;) + pu_key.y.to_bytes(32, byteorder=&#39;big&#39;)eth_addr = &#39;0x&#39; + keccak_256(concat_x_y).digest()[-20:].hex()print(&#39;private key: &#39;, hex(private_key))print(&#39;eth_address: &#39;, eth_addr)</code></pre><p>这段代码只有四五行，计算的关键有两个地方，<code>private_key * cv.generator</code> 和 <code>keccak_256(concat_x_y)</code>。除了这两个地方，其他的部分都是常量计算，乍一看挺复杂，仔细看看拆解一下，都是很简单的字符串拼接。</p><p>其中，<code>private_key * cv.generator</code> 是椭圆曲线的计算，上看定义的私钥 <code>private_key</code> 是一个 16 进制的数字，注意是 <code>int</code> 类型，然后用椭圆曲线的生成器去计算出一个值。这个计算过程是不可逆的，就是在椭圆曲线上绕圈那个过程，可以类比理解为，一个数字取余之后，你无法还原出取余之前的数字。椭圆曲线只是用更复杂的方式，提供了比 RSA 更加安全的计算结果。</p><p>第二个不可逆的计算是 <code>keccak_256(concat_x_y)</code>，这是一个计算哈希值的过程，keccak 是 sha3 的一种。摘要算法不可逆，这也是毫无疑问的事情。</p><p>也就是说，从私钥到地址的计算过程，有两处是不可逆的，所以整体上，无法从以太坊的账户地址，逆推计算出私钥。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;你有没有过好奇，为什么以太坊的私钥无法从账户地址逆推算出来？当你拥有一个私钥，想要得到这个私钥对应的账户地址，你可以在 MetaMask 里导入这个账户，或者使用 ether.js 这样的 SDK</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>关于以太坊的私钥碰撞</title>
    <link href="https://smallyu.net/2023/01/18/%E5%85%B3%E4%BA%8E%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E7%A7%81%E9%92%A5%E7%A2%B0%E6%92%9E/"/>
    <id>https://smallyu.net/2023/01/18/%E5%85%B3%E4%BA%8E%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%9A%84%E7%A7%81%E9%92%A5%E7%A2%B0%E6%92%9E/</id>
    <published>2023-01-18T07:27:25.000Z</published>
    <updated>2026-08-04T11:44:00.456Z</updated>
    
    <content type="html"><![CDATA[<p>如果你有 1&#x2F;100000000 的机会暴富，你愿意尝试一下吗？</p><h3 id="以太坊私钥"><a href="#以太坊私钥" class="headerlink" title="以太坊私钥"></a>以太坊私钥</h3><p>以太坊的账户完全由私钥控制，根据私钥可以推算出账户的地址。私钥是一个 64 位长度的 16 进制字符串，比如：</p><pre><code>0xd110227375ab838e8743192d278c105e30f253c966987c50b754412c9b986fe3</code></pre><p>你可以在任何支持以太坊账户的钱包，用这个私钥导入对应的账户。这个私钥对应的账户地址是：</p><pre><code>0x00000006A3D4DA3A559829B1730603CAeE97cC3D</code></pre><p>你也可以根据这个账户地址，在以太坊浏览器上查询到账户相关的<a href="https://etherscan.io/address/0x00000006A3D4DA3A559829B1730603CAeE97cC3D">交易记录</a>等信息。</p><h3 id="私钥碰撞"><a href="#私钥碰撞" class="headerlink" title="私钥碰撞"></a>私钥碰撞</h3><p>既然私钥只是一个字符串，那么有没有可能，随机生成一个字符串，这个私钥对应的账户地址，正好有钱？因为有了私钥就能够控制账户上的所有资产。以太坊上有钱的账户那么多，有的余额还特别高，万一正好生成了一个私钥，那岂不是直接暴富。</p><p>当然随机生成的字符串，必须是 64 位长度 16 进制。为了避免以太坊的 SDK 存在某些防碰撞的规则，我们可以直接粗暴一点生成，比如这样：</p><pre><code class="javascript">let s = &quot;0123456789abcdef&quot;;let hex = &quot;0x&quot;;for (let i = 0; i &lt; 64; i++) &#123;  hex += s[Math.floor(Math.random() * 16)];&#125;</code></pre><p>这样做足够直观，在字符串层面随机拼接出 64 位长度的。LeetCode 的简单题就是天天搞这种。虽然实际上以太坊并没有防碰撞的机制。</p><h3 id="碰撞概率"><a href="#碰撞概率" class="headerlink" title="碰撞概率"></a>碰撞概率</h3><p>我们来计算一下，随机生成私钥，碰撞到有效账户的概率。假如存在一个地址，我们想要正好随机生成这个地址的私钥，可能性是多大？</p><p>16 进制就代表每一位字符，和目标私钥一致的可能性是 1&#x2F;16，一共 64 位长度，概率就是 (1&#x2F;16)<sup>64</sup>。</p><pre><code>p = (1/16)^64  = 1 / 16^64  = 1 / 115792089237316195423570985008687907853269984665640564039457584007913129639936</code></pre><p>把零头抹掉，只计算数量级的话：</p><pre><code>p = 1 / 115792089237316195423570985008687907853269984665640564039457584007913129639936  = 1 / 100000000000000000000000000000000000000000000000000000000000000000000000000000  = 1 / 10^77</code></pre><p>这是一个非常低的概率，中国中奖率最低的大乐透彩票，中奖概率是千万分之一，也就是 1&#x2F;10^8。以太坊的私钥碰撞，想正好是某一个账户的可能性，相当于连续中 10 次彩票。这是几乎不可能发生的事情。</p><p>不过彩票的中奖是有速度限制的，比如需要 1 天才开奖。加密货币领域的私钥可不是，如果提高随机生成私钥的速度，能不能增加碰撞成功的概率呢？</p><p>目前比特币全网的<a href="https://www.blockchain.com/explorer/charts/hash-rate">哈希率</a>是 270M TH&#x2F;s。比特币挖矿的过程本身就是不断地做哈希计算，直到找到拥有 n 和前导 0 哈希值的字符串，所以比特币的哈希率也可以用来描述私钥的生成速率。做一下<a href="https://en.bitcoinwiki.org/wiki/Hashrate">单位换算</a>：</p><pre><code>r = 270M TH/s  = 270000000 TH/s  = 270000000000 GH/s  = 270000000000000 MH/s  = 270000000000000000 kH/s  = 270000000000000000000 /s  = 100000000000000000000 /s  = 10^20</code></pre><p>这个结果只保留了数量级。假如你拥有全球比特币矿机的算力，去做以太坊的私钥碰撞，每秒钟的概率是：</p><pre><code>ps = p * r   = 1 / 10^57   = 1 / 1000000000000000000000000000000000000000000000000000000000</code></pre><p>这仍然是一个低到遥不可及的可能性。如果碰撞持续尝试 1 年呢？</p><pre><code>py = ps * 60 * 60 * 24 * 365   = ps * 31536000   = ps * 10000000   = ps * 10^7   = 1 / 10^50   = 1 / 100000000000000000000000000000000000000000000000000</code></pre><p>持续运行 1 亿年呢？</p><pre><code>pb = py * 100000000   = py * 10^8   = 1 / 10^42   = 1 / 1000000000000000000000000000000000000000000</code></pre><p>可以看到，时间对于概率的增加幅度，是微乎其微的。计算能力也许有可能成倍增长，比如苹果 M 系列芯片，比 Intel 芯片高几倍速度的提升。但是在 70 个 0 的数量级面前，这样的提升仍然微不足道。</p><p>即使拥有 1 亿个地球的比特币算力，运行 1 亿年，也只是能再减少几个 1 后面的 0，远远达不到碰撞成功的目的。</p><h3 id="尝试一下"><a href="#尝试一下" class="headerlink" title="尝试一下"></a>尝试一下</h3><p>虽然可能性非常小，但我还是想尝试一下。</p><p>我写了一个脚本 <a href="https://github.com/eth-collision/eth-collision-random">eth-collision&#x2F;eth-collision-random</a>，随机生成私钥后，根据私钥的账户地址，自动去 <a href="https://etherscan.io/">etherscan.io</a> 上查询账户余额，如果余额大于 0 会把私钥输出到日志文件里。由于 Etherscan 对 API 的调用次数有限制，脚本会以每秒 20 个地址的速度去查询，也就是一天能尝试 172 万个地址。</p><p>这个速度不够快，有更好的办法吗？凡是第三方服务，大多数会有 API 速率的限制，想不受限制进行查询，只能自己运行一个以太坊节点了。以太坊节点最低的硬件要求，是 4 核 16G 内存 1T 硬盘。这样配置的服务器，在 <a href="https://www.vultr.com/">Vultr</a> 上的价格至少要 300 美元一个月。有点贵。而且运行起节点后，还需要自己写一写程序，根据每个块的交易信息统计出有余额的地址。整个周期会很长。</p><p>我注意到 <a href="https://etherscan.io/accounts/1?ps=100">这个页面</a> 提供了以太坊上余额最多的地址列表，1 页 100 个，总共 100 页。我写了一个爬虫 <a href="https://github.com/eth-collision/eth-address-top-list.git">eth-collision&#x2F;eth-address-top-list</a> 到这个页面上抓取数据，这样就能得到以太坊余额最多的 10000 个账户地址的列表。</p><p>根据抓到的 10000 个地址，程序就可以比较快地运行了，而不需要受到 API 速率的限制，也不需要自己跑全节点。由于不看好 JavaScript 的运行速度，我用 Golang 写了一个程序 <a href="https://github.com/eth-collision/eth-collision-match-address.git">eth-collision&#x2F;eth-collision-match-address</a>，启动 100 个协程来加快生成私钥的速度。程序会把 10000 个地址读到一个 map 里，然后判断随机生成的账户是否在目标列表内。JavaScript 的一个 for 循环也足够把 CPU 跑满，但是直觉上总感觉 Golang 会更快一点。</p><p>对了，Golang 中的 map 记得加互斥锁。因为 map 不是并发安全的。不过加锁会降低速率。为了解决需要加锁的问题，因为只有 10000 个地址，占不了多少内存，所以可以实例化多个 map，有多少个协程就有多少个 map，每个协程在自己的 map 上做验证。这样就能充分保证私钥的生成和验证效率。</p><p>10000 个地址是不是太少了？以太坊一共有那么多个地址。一共有多少个呢？从 Etherscan <a href="https://etherscan.io/chart/address">统计</a>目前有 230M 左右，大概 2 亿多个。</p><p>后来我发现了一个仓库 <a href="https://github.com/eth-collision/Wallet-private-key-collision-brute-force-tool">eth-collision&#x2F;Wallet-private-key-collision-brute-force-tool</a>，这个仓库里提供了一个 OneDrive 文件的下载链接，是一个包含 1.8 亿条账户地址的文件，这些账户都在链上有过交易记录。文件的压缩包有 4.4G 大，解压后 16G 左右，是 pkl 格式的文件，然后我用 Python 解析成了 txt 文件。</p><p>在得到这么多账户地址的列表后，现在的问题就变成了，该怎么利用这些数据。</p><p>首先是像原先一样，把所有数据读到 map 里。这么多的数据读到内存里，占用的内存可能至少 16G，也就是需要 24G 内存的服务器。Vultr 上最便宜的符合要求的服务器，接近 150 美元一个月。况且那么大数据量，Golang 的查询效率也是个疑问，还得加锁，限制比较多。</p><p>有没有好的方案？用数据库的话，MySQL 是支撑不住亿级别的数据查询的，记得之前参与的一个项目，在上亿数据里做最简单的查询要 10 秒以上，还是单次查询。现在的需求是每秒钟查询上千次。Redis 呢？Redis 的查询效率应该是有保证的，可 Redis 还是会把所有数据读到内存里。在 Redis 的<a href="https://redis.io/docs/getting-started/faq/">介绍</a>里，1M 个 key 需要 85M 的内存，换算一下：</p><pre><code>1M   -&gt; 85M180M -&gt; 180 * 85M     = 15300M     = 15G</code></pre><p>Vultr 上，16G 的 Redis 价格很高，比服务器贵多了，需要 480 美元一个月。至于更沉重一点的数据库，像 Elasticsearch 或者更专业一点的 HBase 之类，从数据库搭建到导入数据，然后处理数据，一整套下来确实麻烦了一点。服务器开销也不见得更低。</p><p>再后来，看到有个东西叫布隆过滤器，正好符合这样的使用场景。我试了一下，容量 2 亿条数据的布隆过滤器，容错率 10 亿分之一，在加载完 1.8 亿条数据后，导出的<a href="https://github.com/eth-collision/eth-address-all">二进制文件</a>，只占用 1G 的磁盘空间。加载到内存中，也只需要占用 1G。</p><p>这样的方案其实挺好了。我在服务器上运行碰撞程序，每个月需要 30 美元的 2核 4G 服务器，每个小时可以尝试 60M 个地址。每个月需要 600 人民币的 8核 16G 服务器，每个小时可以尝试 160M 个地址。</p><p>当然，用再好的服务器都没有用，用整个地球上的算力都没用。</p><h3 id="靓号地址"><a href="#靓号地址" class="headerlink" title="靓号地址"></a>靓号地址</h3><p>以太坊的账号地址也有靓号一说，比如地址 <code>0x00000006A3D4DA3A559829B1730603CAeE97cC3D</code> 包含有 6 个前导 0。关于靓号地址可以参考<a href="https://www.theblockbeats.info/news/33808?from=telegram">这篇文章</a>。</p><p>我写了这个程序 <a href="https://github.com/eth-collision/eth-collision-find-address">eth-collision&#x2F;eth-collision-find-address</a> 来寻找靓号地址，一般来说，找到 6 个前导 0 的地址是非常容易的事情，在 Macbook Air M1 上几分钟就可以。包含 7 个前导 0，可能需要半个小时。包含 8 个前导 0，就需要一两天。</p><p>包含有 10 个前导 0 的地址，已经算是非常非常稀有了。不过当然靓号地址没什么用，你一旦知道私钥，这个账号就是你的了，给别人别人也不要。</p><h3 id="在线手动碰撞"><a href="#在线手动碰撞" class="headerlink" title="在线手动碰撞"></a>在线手动碰撞</h3><p>这是一个在线手动进行私钥碰撞的网页，可以根据私钥计算出公钥，也可以生成随机的公私钥对，并且点击链接能够直接跳转到 Etherscan 检查地址对应的余额。</p><p>地址：<a href="http://eag.smallyu.net/">http://eag.smallyu.net</a></p><p>源码：<a href="https://github.com/smallyunet/eth-address-generator">smallyunet&#x2F;eth-address-generator</a></p><p>页面是纯前端实现的，可以离线使用，不会有任何安全问题。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;如果你有 1&amp;#x2F;100000000 的机会暴富，你愿意尝试一下吗？&lt;/p&gt;
&lt;h3 id=&quot;以太坊私钥&quot;&gt;&lt;a href=&quot;#以太坊私钥&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>“猜均值的2/3” dApp 游戏设计</title>
    <link href="https://smallyu.net/2022/12/27/%E2%80%9C%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842-3%E2%80%9DdApp-%E6%B8%B8%E6%88%8F%E8%AE%BE%E8%AE%A1/"/>
    <id>https://smallyu.net/2022/12/27/%E2%80%9C%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842-3%E2%80%9DdApp-%E6%B8%B8%E6%88%8F%E8%AE%BE%E8%AE%A1/</id>
    <published>2022-12-27T07:48:46.000Z</published>
    <updated>2025-06-23T14:02:31.685Z</updated>
    
    <content type="html"><![CDATA[<p><a href="https://zh.m.wikipedia.org/zh-cn/%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842/3">猜均值的2&#x2F;3</a> 是一个经典的博弈游戏，现在把它改造成运行在区块链上的 dApp 游戏，这是游戏的基本规则：</p><ol><li>玩家支付 <code>任意金额</code> 参与游戏</li><li>计算所有用户支付的 <code>金额总和</code></li><li>计算 <code>金额总和</code> 平均值的 2&#x2F;3 作为 <code>胜利数字</code></li><li>玩家支付的 <code>任意金额</code> 最接近 <code>胜利数字</code> 则获胜</li></ol><p>游戏的奖励规则是：</p><ol start="5"><li>获胜者获得本轮游戏所有用户支付的 <code>金额总和</code></li><li>合约在每轮游戏抽取 <code>金额总和</code> 的 1% 作为服务费</li></ol><p>一个重要的问题是，每轮游戏多少玩家参与？或者说每轮游戏什么时候结束？关于游戏结束的规则是这样：</p><ol start="7"><li>每一轮游戏，当玩家参与的时候，可以知道玩家是第 <code>n</code> 个参与的</li><li>生成一个随机数 <code>x</code>（10&lt;&#x3D;x&lt;&#x3D;100）</li><li>根据 <code>n</code> 和 <code>x</code>，生成一个随机数的边界 <code>l, r = n-x, n+x</code></li><li>在 <code>[l, r]</code> 的区间内生成一个随机数 <code>y</code></li><li>如果 <code>y == n</code>，本轮游戏结束</li></ol><p>还有一点补充规则：</p><ol start="12"><li>如果一个用户多次参与游戏，以第一次参与结果为准</li><li>如果多个用户支付的 <code>任意金额</code> 相同，第一个参与的用户获胜</li></ol><p>上面是这个游戏的全部规则。</p><p>结束游戏的规则有点复杂，举一个具体的数值，比如有一个玩家，是第 <code>n=50</code> 个参与进游戏的，然后生成了一个随机数 <code>x=10</code>，那么生成的随机数边界就是 <code>l, r = 45, 55</code>。在范围 [45, 55] 中再生成一个随机数，如果正好是 50，本轮游戏结束。</p><p>为什么要这样做？因为区块链上的数据是公开的。在原版的“猜均值的2&#x2F;3”游戏中，玩家选择的数字，相互是保密的。但在区块链上很难做到这一点。所以这里提出的 dApp 版本游戏规则，至少有两点和原版规则不同：</p><ul><li>玩家参与游戏支付的金额是无限制的，可以是 0，也可以无限大，而原版规则有上限</li><li>玩家数量是不确定的，原版规则是知道有几名玩家参与</li></ul><p>这样差异的设计都是因为，区块链上的数据是公开的，所以要引入一些随机的机制。如果没有随机结束的机制，在玩家数量确定的情况下，越是后面参与的玩家，越有优势，因为可以根据前面参与玩家的记录，推算出自己用哪个数字参与更有可能获胜。</p><p>当然，最后参与的玩家并不一定有能力扭转局势，尤其是在参与数值有上限的情况下。但如果玩家数量是随机的，就不存在“后面参与的玩家”一说，因为谁都不知道游戏什么时候结束，谁是最后一个玩家。所以即使玩家能够支付出扭转局势的金额，也会被后面的玩家拉下来。</p><p>至于原版游戏要求参与数值为整数，dApp 版本的参与数值可以没有限制，可以是小数。是因为原版游戏是人为给出参与数值，而 dApp 版本可以在计算机上自由输入，并且结果是智能合约计算的，不限制精度也没有影响。</p><p>然后解释一下游戏结束的规则。这个规则的效果是，能在大多数情况下，让游戏在参与人数为 [10, 100] 的区间内结束。这个区间对应 <code>规则 9</code> 中 <code>x</code> 的范围。</p><p>这个规则可以这样理解，每个用户都拥有能力结束游戏，但是能力是随机的，最高 1&#x2F;10，最低 1&#x2F;100。如果拥有 1&#x2F;10 的能力，就是有 1&#x2F;10 的可能性让游戏结束。</p><p>假设所有用户都拥有最高的能力 1&#x2F;10，那么游戏大概率会在 10 个人参与后结束。每个人触发“游戏结束”这个事件的概率是独立的，不管多少人参与都是 1&#x2F;10。参与的人数越多，结束的可能性越大，因为不结束的可能性是 9&#x2F;10 的人数次方。</p><p>假设所有用户都拥有最低的能力 1&#x2F;100，那么游戏大概率会在 100 个人参与后结束。</p><p>所以 <code>规则 9</code> 中 <code>x</code> 的范围大概就限定了参与人数的范围。当然，这里的人数范围 [10, 100] 不是严格的概率计算。</p><p>按照上面定义的游戏结束规则，这里模拟了 10 万次运行的结果，记录下游戏会在“有多少个玩家参与”的时候结束，代码在这里：<a href="https://github.com/guessavg/emulate_tool">guessavg&#x2F;emulate_tool</a></p><img src="chart1.png" width="60%"><p>这个图的横坐标是参与游戏的玩家数量，纵坐标是在对应的数量上，游戏结束的次数。比如在接近 1 的位置上，有 2500 次，说明在 10 万次游戏中，有 2500 次 1 个玩家参与就结束了。</p><img src="chart2.png" width="60%"><p>如果次数不够直观的话，可以看这个比例的图，每个数字上结束的比例不超过 2.5%。越靠前的位置，游戏结束的可能性越大，因为每次都是从 0 开始，前面的位置结束游戏的机会更多一点。</p><img src="chart3.png" width="60%"><p>这个比例统计图更能够说明结果，游戏有接近 20% 的概率在参与人数小于 10 的情况下结束，有接近 70% 的概率在参与人数为 [10, 100] 的范围内结束。参与人数大于 300 才结束游戏的概率，只有 0.07%。</p><p>虽然这个规则形成的结果，概率上不是完整的正态分布，但是基本上能够满足一开始的需求，也就是让游戏在随机的时间点结束，并且结束的时间不会很离谱，不会导致参与人数太少或者太多，而且还提供了一定的小概率，允许参与人数达到 500 左右。</p><p>相信这会是一个合理的设计。</p><p><br/><br/></p><h3 id="更新（2025-04-22）"><a href="#更新（2025-04-22）" class="headerlink" title="更新（2025.04.22）"></a>更新（2025.04.22）</h3><p>一年多前，我写过一个 《<a href="/2022/12/27/%E2%80%9C%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842-3%E2%80%9DdApp-%E6%B8%B8%E6%88%8F%E8%AE%BE%E8%AE%A1/">“猜均值的2&#x2F;3” dApp 游戏设计</a>》的设想。</p><p>偶然想起来，现在有了功能强大的 ChatGPT 的帮助，让这个游戏在实现上变得简单，合约和前端页面都可以轻松完成。今天只用了 2 个小时，就完成了这个网页的 Demo。</p><p>可以通过这个页面访问游戏：<a href="https://guessavg.oiia.network/">https://guessavg.oiia.network/</a></p><p>有几点需要说明：</p><ol><li>目前这个 dApp 小游戏运行在 Oiia Network 上，合约地址 <a href="https://explorer.oiia.network/account/0x6eb079C9D3005Bd596E8a0E5065fA33C80aBA8F1"><code>0x6eb07...BA8F1</code></a></li><li>参加游戏实际上就是调用合约的函数，同时给合约转账一笔钱，例如交易 <a href="https://explorer.oiia.network/tx/0x1bfb286c9ed796e16870cc36488bd3c11db6eef43e34c425e58ac76715010936"><code>0x1bfb2...10936</code></a></li><li>合约代码在仓库 <a href="https://github.com/guessavg/contract">guessavg&#x2F;contract</a>，理论上可以部署到任意网络，因为 OIIA 不要钱，我就拿来用了。</li><li>前端代码仓库在 <a href="https://github.com/guessavg/game">guessavg&#x2F;game</a>，和合约代码配合使用。</li><li>OIIA 可以在 faucet 领到一些 <a href="https://faucet.oiia.network/">https://faucet.oiia.network/</a></li><li>这个私钥有 10 个 OIIA，也能拿来玩一下。但是从游戏机制上，是不允许相同地址重复参与的，需要先把 OIIA 转到陌生地址才行。 <code>fdf0aec857f3ac4fe146e0d00fb3a7a729646a081719df3f4e168a541a21893b</code></li><li>前端网页在添加 Oiia 网络到 Metamask 的时候，会报错，但实际上成功了，我没有深入排查原因。一个月前相同的代码还没报错来着。</li><li>Oiia Network 随时会消失。</li><li>只是玩玩……</li></ol><br><h3 id="更新（2025-05-06）"><a href="#更新（2025-05-06）" class="headerlink" title="更新（2025.05.06）"></a>更新（2025.05.06）</h3><p>Oiia Network 今天起就完全停止运行了，因为我不想为没有人用的网络付服务器费用了。现在这个猜均值的游戏 Demo 已经转移部署到 Base 网络上。</p><ol><li>游戏 Demo 地址：<a href="https://guessavg.github.io/game/">https://guessavg.github.io/game/   </a></li><li>Base 上的合约地址：<a href="https://basescan.org/address/0x4BbeE9F876ff56832E724DC9a7bD06538C8868D2"><code>0x4BbeE...868D2</code></a></li><li>需要 Base 网络上的 ETH 参与游戏。</li><li>游戏降低了难度，只需要两三个玩家就会结束一轮游戏。</li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;&lt;a href=&quot;https://zh.m.wikipedia.org/zh-cn/%E7%8C%9C%E5%9D%87%E5%80%BC%E7%9A%842/3&quot;&gt;猜均值的2&amp;#x2F;3&lt;/a&gt; 是一个经典的博弈游戏，现在把它改造成运行在区块链上的 dApp</summary>
        
      
    
    
    
    
    <category term="dApp" scheme="https://smallyu.net/tags/dApp/"/>
    
  </entry>
  
  <entry>
    <title>Proof of Storage/Space/Replication 的区别</title>
    <link href="https://smallyu.net/2022/12/20/Proof-of-Storage-Space-Replication-%E7%9A%84%E5%8C%BA%E5%88%AB/"/>
    <id>https://smallyu.net/2022/12/20/Proof-of-Storage-Space-Replication-%E7%9A%84%E5%8C%BA%E5%88%AB/</id>
    <published>2022-12-20T09:58:48.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<h3 id="时间线"><a href="#时间线" class="headerlink" title="时间线"></a>时间线</h3><style>table {    display: inline;}</style><center><table><thead><tr><th>简称</th><th>全称</th><th>年份</th></tr></thead><tbody><tr><td>PDP</td><td>Provable Data Possession</td><td>2007</td></tr><tr><td>PORs</td><td>Proofs of Retrievability</td><td>2007</td></tr><tr><td>PoS</td><td>Proofs of Storage</td><td>2009</td></tr><tr><td>PoS</td><td>Proofs of Space</td><td>2013</td></tr><tr><td>PoST</td><td>Proofs of Space-Time</td><td>2016</td></tr><tr><td>PoRep</td><td>Proof of Replication</td><td>2017</td></tr><tr><td>PoC</td><td>Proof of Capacity</td><td>2017</td></tr></tbody></table></center><h3 id="Proofs-of-Storage"><a href="#Proofs-of-Storage" class="headerlink" title="Proofs of Storage"></a>Proofs of Storage</h3><p>PDP 和 PORs 在 2007 年各自独立地发表，在文件证明的方式上各有优劣，是对同一类问题不同分支的解决方案。</p><p>PoS（Proofs of Storage）的概念至少在 2009 年就已经出现，是一种统称，指客户端对服务端上文件进行验证的交互式协议，同时包含了 PDP 和 PORs 的范畴。比如论文《Proofs of Storage from HomomorphicIdentification Protocols》，第一作者是 Giuseppe Ateniese。</p><p>由于 2009 年区块链还没什么发展，所以 PoS（Proofs of Storage）和共识机制没有关系，和 PoS（Proof of Stack）也仅仅只是简称撞了，没什么联系。</p><p>PDP 和 PORs 属于 PoS 的前身，PoS 把它们用一个名字统一起来了。</p><h3 id="Proofs-of-Space"><a href="#Proofs-of-Space" class="headerlink" title="Proofs of Space"></a>Proofs of Space</h3><p>PoS（Proofs of Space）开始于 2013 年的论文《Proofs of Space》，第一作者是 Stefan Dziembowski。</p><p>也是巧合，PoS（Proofs of Space）和 PoS（Proofs of Storage）的简称一样，有时候可能会引起混淆。这个小节的 PoS 指 Proofs of Space。</p><p>PoS 的理念是，对标 PoW（Proof of Work）。所以从 PoS 开始，就是区块链中的概念了，它是一种共识机制。</p><p>PoW 是用 CPU 的算力进行挖矿，PoS 的想法是用磁盘的容量进行挖矿，想办法证明服务器上有某个数值的磁盘空间。</p><p>最基础的办法，是客户端生成一个文件，比如 1G，然后发送到服务器上，接着只要验证服务器上保存了那个文件，就能证明服务器确实有 1G 的磁盘空间了。不过这个办法太笨了，不但消耗客户端的磁盘空间，还会给网络传输带来非常大的压力。</p><p>PoS 提供的办法是，使用一种 hard to pebble graphs 的数据结构，比如 Merkle hash tree。这种数据结构的特点是，生成上层的数据必须要依赖于下一层的数据。</p><p>比如在使用了 Merkle hash tree 的系统里，客户端可以要求服务端，返回某一个上层节点的整条链路，然后自行验算路径是否正确。可以交叉验证多条链路，基本上就能保证服务端的可信了。</p><h3 id="Proofs-of-Space-Time"><a href="#Proofs-of-Space-Time" class="headerlink" title="Proofs of Space-Time"></a>Proofs of Space-Time</h3><p>PoST（Proofs of Space-Time）出现于 2016 年，论文标题《Simple Proofs of Space-Time and Rational Proofs of Storage》，第一作者是 Tal Moran。</p><p>PoST 是基于 PoS（Proofs of Space）的方案，因为 PoS 可以证明服务器端拥有一定量的磁盘空间，但是不能证明，服务器端的空间容量一直保持在期望的水平。比如，在进行验证的时候，服务器的磁盘空间是 1G，一旦验证结束，服务器就把空间用到别的地方了。再验证的时候，就再生成一遍 1G 的文件，用于验证。</p><p>所以 PoS 提议每 1 分钟都进行一次验证，以保证服务器的诚实。这显然不是很聪明的做法。</p><p>PoST 期望解决这个问题。PoST 提供的方法是，加大初始化阶段的难度，也就是 PoS 生成文件的阶段，想办法让服务器，必须要足够多的时间，才能够生成文件。</p><p>怎么保证需要足够多的时间呢，PoW（Proof of Work）就能够做到，比如计算 2^30 次哈希值，就意味着花费了那么多的时间。</p><p>PoST 就把 PoW 和 PoS 结合了起来，在初始化的阶段，让服务器必须消耗足够多的时间，才能够生成文件，然后在证明的阶段，去验证初始化阶段生成的文件。</p><h3 id="Proof-of-Replication"><a href="#Proof-of-Replication" class="headerlink" title="Proof of Replication"></a>Proof of Replication</h3><p>PoRep（Proof of Replication）源自 2017 年的论文《Proof of Replication》，第一作者是 Juan Bene。</p><p>PoRep 属于 Proofs of Storage 的一种，是 Protocol Labs 的研究成果，Protocol Labs 还开发了 IPFS 和 Filecoin。PoRep 也是 Filecoin 在使用的共识机制。</p><p>PoRep 基于 Proofs of Space 和 Proofs of Retrievability，在其基础上增加了一种能力，就是可以区分出服务器端的副本数量。做法也相对简单，就是在生成 Tags 的阶段，给每个副本都带一个唯一标识，让每一份副本都变得独一无二。</p><p>因为是去中心化的网络，Filecoin 需要保证整个网络中存在多个副本，如果节点联合起来作恶，之前的证明方式是无法应对的，所以 Filecoin 使用了 PoRep 的共识机制。</p><p>2017 版的 PoRep 相对简单，在 2018 版的 PoRep 里，才开始提到使用 Depth Robust Graphs 的数据结构。论文标题是《PoReps:  Proofs of Space on Useful Data》，第一作者是 Ben Fisch。</p><h3 id="Proof-of-Capacity"><a href="#Proof-of-Capacity" class="headerlink" title="Proof of Capacity"></a>Proof of Capacity</h3><p>PoC（Proof of Capacity）是 2017 年 Burstcoin 区块链使用的一种共识机制，</p><p>PoC 提供了一种挖矿的方式，产生新的区块需要一个 nonce 值：</p><pre><code>1 nonce = 8192 hash value = 4095 scoops</code></pre><p>其中 hash value 是使用 Shabal 作为哈希函数计算出的哈希值，每两个哈希值为一个 scoops。</p><p>在 0 到 4095 个 scoop nubmer 中随机选出一个，然后和相应的 nonce 结合，去计算出一个 <code>deadline</code> 值。和其他所有节点相比，谁的 deadline 值最小，谁就可以产生新的区块。</p><p>PoC 更倾向于一种纯粹的共识机制。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;时间线&quot;&gt;&lt;a href=&quot;#时间线&quot; class=&quot;headerlink&quot; title=&quot;时间线&quot;&gt;&lt;/a&gt;时间线&lt;/h3&gt;&lt;style&gt;
table {
    display:</summary>
        
      
    
    
    
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
    <category term="文件证明" scheme="https://smallyu.net/tags/%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E/"/>
    
  </entry>
  
  <entry>
    <title>Proofs of Retrievability 文件证明的含义</title>
    <link href="https://smallyu.net/2022/12/16/Proofs-of-Retrievability-%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E5%90%AB%E4%B9%89/"/>
    <id>https://smallyu.net/2022/12/16/Proofs-of-Retrievability-%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E5%90%AB%E4%B9%89/</id>
    <published>2022-12-16T09:22:05.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<p>PORs（Proofs of Retrievability）的论文和 PDP 在同一年 2007 年发表，论文题目是《PORs: Proofs of Retrievability for Large Files》，第一作者是 Ari Juels。</p><p>PORs 不但可以证明文件确实被保存在服务端，而且可以知道文件有没有被修改、删除，这是 PDP 不具备的能力。不过 PORs 的局限性在于，只能用于加密文件，或者说必须对文件进行加密。PORs 实现的关键在于复杂的 setup 阶段。</p><p>PORs 的思路是，客户端生成一些随机的编码记录在数组中，这些编码被称为哨兵。比如 <code>[6, 4, 0]</code> 是三个哨兵。</p><p>先对客户端的文件进行对称加密，而且必须使用 block cipher 的方式。在加密过程中，把哨兵加入到文件随机的位置。两个哨兵之间，可能隔着 1 个 block 或者无数个 block，哨兵的位置是随机的。</p><p>然后把加密后的文件发送到服务端保存。</p><p>客户端发起挑战的内容，就是要服务端返回，文件在随机 n 个位置上的编码。挑战中包含哨兵所在的位置。</p><p>由于文件是加密的，服务端并不能判断，挑战的位置是原始文件的数据，还是哨兵位置的数据，服务端是难以推测出哨兵位置的。</p><p>对于客户端来说，只需要验证哨兵所在位置对应的编码，就可以判断文件是否存在以及完好无损。如果文件被修改或者中间的某些数据丢失，哨兵所在位置的编码一定是大相径庭的，挑战无法完成。</p><p>Retrievability 是可检索的意思，在 PORs 中的含义是，对于客户端来说，我可以通过指定哨兵位置的方式，对文件的某个部分验证，比如想验证第 1 个哨兵到第 3 个哨兵之间的文件是否完好，或者去验证第 5 个哨兵到第 9 个哨兵之间的文件，指哪打哪。</p><p>至于说为什么一定要对文件进行加密，应该一方面 PORs 依赖于对称加密的 block cipher，另一方面，对于不加密的文件，服务端比较容易推测出哨兵的位置。所以 PORs 只能用于加密文件。</p><p>论文中还通过引入纠错码的方式，来增加文件的容错能力。在 setup 阶段，先对 block 加入纠错码，再进行对称加密。引入纠错码的好处，就是在两个哨兵之间，在一定的错误范围内，如果文件有损坏，可以去修复文件。</p><p>为什么在 PORs 的机制中，纠错码开始显得重要？因为要和 PORs 的特点 Retrievability 结合。如果文件被储存在多个服务器环境，遭到了不同程度的损坏，因为有哨兵的存在，比较容易从其他服务器中恢复文件的一部分。在这样的背景下，引入纠错码就是对文件容错能力的进一步加强了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;PORs（Proofs of Retrievability）的论文和 PDP 在同一年 2007 年发表，论文题目是《PORs: Proofs of Retrievability for Large Files》，第一作者是 Ari Juels。&lt;/p&gt;
&lt;p&gt;PORs</summary>
        
      
    
    
    
    
    <category term="文件证明" scheme="https://smallyu.net/tags/%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E/"/>
    
  </entry>
  
  <entry>
    <title>对 S-PDP 文件证明的示例和解释</title>
    <link href="https://smallyu.net/2022/12/14/%E5%AF%B9S-PDP%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E7%A4%BA%E4%BE%8B%E5%92%8C%E8%A7%A3%E9%87%8A/"/>
    <id>https://smallyu.net/2022/12/14/%E5%AF%B9S-PDP%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E%E7%9A%84%E7%A4%BA%E4%BE%8B%E5%92%8C%E8%A7%A3%E9%87%8A/</id>
    <published>2022-12-14T13:39:59.000Z</published>
    <updated>2025-11-09T09:22:13.447Z</updated>
    
    <content type="html"><![CDATA[<p>我们现在说的 PDP，一般指发表于 2007 年论文《Provable Data Possession at Untrusted Stores》里的 PDP，第一作者是 Giuseppe Ateniese。这篇论文之前，已经有一些文件证明的概念，比如 B-PDP，但是都没有做到，能够保证服务器端保存了文件。</p><p>S-PDP 是论文中首次提出的一种 scheme，可以用于客户端确认，某个文件确实被保存到了服务器环境上，这个服务器环境是不受信任的。</p><p>PDP 在解决的问题，就是我把一个文件保存到服务器上，不是说服务器告诉我它保存了，我就相信它真的保存了，我需要一种机制，确认文件真的在服务器上了。</p><p>PDP 也有很多种类型，有公开的、私有的、静态的、动态的。S-PDP 是比较基础的一种公开验证的 PDP。</p><p>同态加密是实现 S-PDP 的关键。</p><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><p>我想举一个简单的例子来说明 S-PDP 的过程。因为论文中不着边的东西比较多，概括性的定义比较多，而且语焉不详，没有提供太具体的实现方式。我是按照自己的理解来解释。</p><p>现在客户端有一个原始的文件，内容是：</p><pre><code>F = 12345</code></pre><p>把这个文件分割成小的 block，比如分成 5 份：</p><pre><code>F = [1, 2, 3, 4, 5]</code></pre><p>在客户端这边生成一个随机数组成的数组 W，数组的长度和文件的 block 数量一致。W 的内容一定要是不可预测的：</p><pre><code>W = [8, 1, 7, 3, 6]</code></pre><p>生成同态加密标签分 2 步，首先，我们对 F 和 W 使用加法同态加密，接着，使用客户端公钥对同态加密后的数组，进行非对称加密：</p><pre><code>T = r[ h(9),  h(3),  h(10),  h(7),  h(11)]  =  [rh(9)) rh(3), rh(10), rh(7), rh(11)]</code></pre><p>客户端将会把原始文件 F、同态加密标签 T 一起发送到服务端进行保存，客户端只保留本地生成的随机数组 W，W 是唯一私密不能泄漏的内容。客户端发送完毕后，就可以把本地的原始文件 F 和同态加密标签 T 都删掉了。</p><p>当客户端想要验证服务端的文件，由客户端生成一个挑战，比如随机验证第 1 个和第 3 个 block：</p><pre><code>chal = [1, 3]</code></pre><p>服务端在收到挑战后，生成证明也分 2 步，首先，使用客户端的公钥对原始文件 F 的第 1 个和第 3 个 block 进行非对称加密，接着，使用同态加密标签 T 去做减法同态加密：</p><pre><code>V = [rh(9), rh(10)] - r[h(1),  h(3)]  = [rh(9), rh(10)] - [rh(1), rh(3)]  = [rh(8), rh(7)]</code></pre><p>客户端拿到证明 V 后，使用私钥对证明进行非对称解密：</p><pre><code>sW = r&#39;[rh(8), rh(7)]   =   [ h(8),  h(7)]</code></pre><p>可以验证，证明经过解密后的 sW 正对应随机数组 W 第 1 个和第 3 个索引的值。由于同态加密的使用，整个过程中，W 的内容都没有泄漏。</p><p>用户只要手里有一开始生成的随机数组 W，在没有原始文件的情况下，就可以验证服务端的文件确实存在。这个随机数组 W 的数据占用是非常少的。而且过程中由于非对称加密的使用，服务端必须同时拥有同态加密标签 T 和原始文件 F，才能够完成挑战。因为 T 是公钥加密的，如果服务端作恶，客户端会解密不出来，服务端挑战失败。</p><h3 id="局限性"><a href="#局限性" class="headerlink" title="局限性"></a>局限性</h3><p>在 PDP 的过程中，存在个问题：既然 V &#x3D; T - F，如果服务端事先把整个 V 保存下来，即使删掉 T 和 F，也是可以通过挑战的，而且客户端并不会发现？</p><p>这是 PDP 的局限性，PDP 只能保证服务器至少保存了这个文件 1 次（如果一次都不保存，是不能生成 V 的），但是无法保证文件持续保存在服务器上，也无法反复验证证明的有效性。</p><p>假如第一次挑战是 [1, 3]，服务器通过了挑战并且保存了挑战为 [1, 3] 的证明，那么之后只要是 [1, 3] 的挑战，服务器都可以直接返回已经通过挑战的证明，而不需要对文件进行计算。客户端无法知晓，证明是立即生成的，还是早已生成的。这是所有 PDP 证明的局限性。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;我们现在说的 PDP，一般指发表于 2007 年论文《Provable Data Possession at Untrusted Stores》里的 PDP，第一作者是 Giuseppe Ateniese。这篇论文之前，已经有一些文件证明的概念，比如</summary>
        
      
    
    
    
    
    <category term="文件证明" scheme="https://smallyu.net/tags/%E6%96%87%E4%BB%B6%E8%AF%81%E6%98%8E/"/>
    
  </entry>
  
  <entry>
    <title>我的加密货币交易机器人</title>
    <link href="https://smallyu.net/2022/12/03/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E4%BA%A4%E6%98%93%E6%9C%BA%E5%99%A8%E4%BA%BA/"/>
    <id>https://smallyu.net/2022/12/03/%E6%88%91%E7%9A%84%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81%E4%BA%A4%E6%98%93%E6%9C%BA%E5%99%A8%E4%BA%BA/</id>
    <published>2022-12-03T07:29:27.000Z</published>
    <updated>2025-06-19T03:30:33.051Z</updated>
    
    <content type="html"><![CDATA[<p>当然不是什么机器人，只是一个简单的脚本。不过值得一说的是，它确实能带来收益。</p><p>目前运行了 10 天左右，收益约 5%，在我看来已经是很好的结果，比自己操作要靠谱很多，反正我手动炒币只有亏钱的份。</p><p>如果本金是 $100，那么按照 5% 的收益率，一个月可以赚 $15，至少能把开服务器的钱挣回来。能平衡开支也是一件值得开心的事情。</p><p>换个角度想，10 天 5%，一个月 15%，一年 150% ……这收益率简直逆天。这还只是用 spot 交易，如果加上杠杆，收益会高很多。</p><p>经过最近几天的运行调试，解决了一些常见的错误情况，程序已经基本上稳定了。</p><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p><a href="https://www.bi123.co/">https://www.bi123.co/</a> 这个网站提供了各种加密货币的信号，会在整点的时候，通过邮件发送过去，某一种加密货币是看涨还是看跌。经过很长时间的人为观察，这个信号挺准的，至少在大方向上没有错。</p><p>那么脚本在收到邮件后，就根据信号，自动在 Binance 上进行交易。Binance 提供了完善的 API。</p><p>脚本会实时推动消息到 Telegram 的机器人，方便查看和关注交易结果。</p><h3 id="收益分析"><a href="#收益分析" class="headerlink" title="收益分析"></a>收益分析</h3><p>在一个价格波动的周期中，如果能做到低点买、高点卖，那么每一个上升的波段都是收益。无所谓价格最终有没有变高，只要有波动，就能有收益。</p><p>bi123 的信号是整点发的，也许会感觉不及时，会错过收益的机会或者带来更大的损失，不过以至少一个小时为周期，也可以防止太小幅度的波动，带来不必要的买卖。</p><p>由于各种加密货币的价格涨跌不一致，脚本支持配置多种不同的加密货币，随个人喜好选择。</p><h3 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h3><p>也没什么技术含量，代码在这里：<a href="https://github.com/smallyunet/txbot">smallyunet&#x2F;txbot</a></p><h3 id="2023-01-12"><a href="#2023-01-12" class="headerlink" title="2023.01.12"></a>2023.01.12</h3><p>现在已经过去了一个多月的时间，来总结一下我实际的使用情况和结论。</p><p>首先是由于犯了很多错误，导致我最近一个月的余额是亏损的，大概亏损 3% 左右。一开始我没有留意，信号是分级别的，有 1 小时级别、4 小时级别、1 天的级别。不同的级别对市场的响应速度不一样。bi123.co 的 “信号趋势” 这个种类的信号，原理是对比收盘价，比如现在的价格是 1500 美元， 4 个小时之前是 1300 美元，那么就发出看涨的信号，就这么简单，就这么暴力。所以不同级别强度的信号，差异很大。一般来说，不推荐使用 1 小时的信号，我也确实在使用 1 小时信号的情况下，亏损率比较高。后来换成 4小时级别的信号，效果好多了。我推荐用 4 小时的信号。</p><p>再一个是信号种类。bi123.co 提供了 “信号趋势”、“RSI 背离”、“MD5 短线”、“多空头排列” 等多种信号，我之前只用了 “信号趋势” 一种，比较容易导致对市场的反应不及时。我推荐所有种类的信号一起用。</p><p>现在交易机器人脚本是支持配置使用，不同级别的信号强度以及信号种类的，默认配置是我感觉比较好的一种配置。</p><p>再看机器人的效果，目前机器人是无法逆市场而为的，也就是说很难在熊市的情况下获利，因为目前机器人只支持 spot 交易，没有添加做空或者倍率的交易方式，后期可以考虑加入。而且机器人会在市场短期震荡的情况下，发生亏损，也就是横盘的情况，机器人不但无法获利，而且会有少量亏损。从这个角度看，机器人似乎很肋助。</p><p>但机器人也不是毫无用处，机器人可以帮助你顺应市场，在跌的时候及时卖出，涨的时候及时买入，而不至于发生，跌的时候继续持有、涨的时候没有买入等情况。而这些操作都不需要手动完成，你甚至不需要关心这笔资产的情况，一切都会自动发生，它可以帮助你，不错过任何时候市场的机会。市场下跌及时止损，市场上涨及时跟进，就是这样。</p><p>机器人的另一个用处是，支持多币种的配置，可以让你不错过某个币种疯狂上涨的获利机会。市场上经常出现，某一个币种逆势上涨的情况，其他币都没怎么动，某个币拉疯了。如果是手动操作资产，很难不错过这样的机会，但如果是机器人自动批量操作，似乎很容易，也很自然而然可以抓住这样的机会。</p><h3 id="2023-01-15"><a href="#2023-01-15" class="headerlink" title="2023.01.15"></a>2023.01.15</h3><p>短短两天，市场疯涨，机器人的表现也符合预期，市场拉升，机器人会自动买入，及时跟上收益。这是最近一整个月，在机器人的控制下，我的资产变动情况（纵坐标的单位不重要）：</p><img src="balance.png" width="100%"><p>在一段时间的实践之后，我仍然推荐使用这样的机器人。</p><h3 id="2023-03-16"><a href="#2023-03-16" class="headerlink" title="2023.03.16"></a>2023.03.16</h3><p>经过最近一段时间对机器人的试用，我决定放弃并停止使用这样的机器人。我一共标的了 56 种加密货币，接受它们的信号，并且根据信号自动进行交易，这是最近一个月的资产变动情况：</p><img src="balance2.png" width="100%"><p>也许是标的货币的种类太多，导致单种货币投入的金额太小，又或者是其他什么原因，总之由于机器人实际效果的不理想，以及现在我对炒币这件事情的不认可，不再推荐使用这样的机器人。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;当然不是什么机器人，只是一个简单的脚本。不过值得一说的是，它确实能带来收益。&lt;/p&gt;
&lt;p&gt;目前运行了 10 天左右，收益约 5%，在我看来已经是很好的结果，比自己操作要靠谱很多，反正我手动炒币只有亏钱的份。&lt;/p&gt;
&lt;p&gt;如果本金是 $100，那么按照 5%</summary>
        
      
    
    
    
    
    <category term="加密货币" scheme="https://smallyu.net/tags/%E5%8A%A0%E5%AF%86%E8%B4%A7%E5%B8%81/"/>
    
  </entry>
  
  <entry>
    <title>对区块链行业的见闻</title>
    <link href="https://smallyu.net/2022/11/15/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E8%A1%8C%E4%B8%9A%E7%9A%84%E8%A7%81%E9%97%BB/"/>
    <id>https://smallyu.net/2022/11/15/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E8%A1%8C%E4%B8%9A%E7%9A%84%E8%A7%81%E9%97%BB/</id>
    <published>2022-11-15T09:05:50.000Z</published>
    <updated>2025-11-09T09:22:13.448Z</updated>
    
    <content type="html"><![CDATA[<p>最近有点迷茫，随便写写，可能比较乱。之前只写过工作方面的回顾，没有概念方面的内容。这一年过的很快，发生了很多事情，又没什么大的变化。</p><p>最近一年都是熊市，尤其是五月份之后，区块链行业进入严重的寒冬，没有像之前各种概念层出不穷，似乎没有大事发生，大家都还在想，下一轮加密货币行业的叙事会是什么？</p><h3 id="ETH"><a href="#ETH" class="headerlink" title="ETH"></a>ETH</h3><p>以太坊是加密行业的半壁江山，很多项目都是基于以太坊构建的，很多 token 都在以太坊的合约里，很多概念也都是以太坊提出来的，比如 DAO，这样的体量是不会轻易倒下的，如果倒下那些项目要跟着死。比特币的价值在于开创了整个行业，但未来区块链拓展应用场景和生态，还得是以太坊，以太坊能玩得起来。</p><p>再一个是公链的开发周期长，当年以太坊也是苦苦拉投资，“2017 年 Vitalik 来中国学中文”，后来回西方国家开发了两年，EVM 才成型，并且现在几乎已经成为智能合约的标准，各种链都开始试图兼容 EVM，因为智能合约的虚拟机和生态，没有完成度比 EVM 高的。以太坊一开始发布的白皮书，也是以智能合约为卖点，PoS 是后来版本的白皮书才有的。</p><p>2017 年到 2019 年是公链爆发的时间段，在共识机制上创新，或者应用场景上创新，现在的很多链都是那个时候留下的，投资人也愿意往底层链上投资。现在发链的比较少，因为竞争力不大，回报周期也比较长。今年的 Aptos 和 Sui 主打 Move 编程语言，共识机制上没有太大创新，我觉得 Move 编程语言也不会很流行。</p><p>号称历史上最大升级的以太坊 merge，其实啥事都没有，对于普通用户来说，发生了和没发生一样，一点感觉都没有。合并之后，币价从 1700 跌到了 1400。说好的发行量减少、会通缩、价格会涨，都没有。</p><p>不过以太坊合并之后价格没崩，说明以太坊的价值不来源于挖矿。有的人认为挖矿是币价的保障，因为有一个成本在里面。以太坊的事情说明，不用算力挖矿、没有硬件设备做抵押，也能行。</p><h3 id="CEX"><a href="#CEX" class="headerlink" title="CEX"></a>CEX</h3><p>加密货币的价格和美股关联性很强，涨跌全看美联储的加息幅度和 CPI 之类的指标。不过也有不那么相关的时候，如果用户对美股失去信心，资金就会进入加密货币，如果对加密货币失去信心，资金就会进入美股。</p><p>加密货币行业的盘子比较小，好像全部只有几千亿美元，和美股几十万亿的盘子比起来，影响不会很大。有的阴谋论会说，加密货币是美国货币的蓄水池，为了缓解通胀。</p><p>这两天 FTX 暴雷，挪用用户的加密货币资产，导致用户对加密货币尤其是 CEX 失去信心，加密货币进入新一轮熊市，以太坊价格好不容易回到 1700 左右，一下打回 1300。各大交易所开始公布资产证明。</p><p>交易所里面的交易真的是赌博，我玩不来。我曾经也有一个暴富的梦，拿着 100 美元，开了 125 倍的杠杆，做空 Mask，几秒钟被拉爆仓。后来又拿着 100 美元，做多 Sol，只剩下一半左右。不敢玩了，币圈都是赌狗，涨跌全靠消息，doge coin 全靠 Musk 喊单。FTX 拿的 SHIB 都比 ETH 多。</p><p>顺便说一句，凉兮素质很低，马一龙没啥文化，KOL 都是跳梁小丑。孙宇晨其实是看不起这些人的，只是借助他们的流量和噱头。</p><h3 id="Finance"><a href="#Finance" class="headerlink" title="Finance"></a>Finance</h3><p>NFT 的出圈挺突然的，但是现在热度也下来了。看到一篇文章里说，区块链的场景有两种，一种是 web 2 转移到 web 3，那不叫创新，叫链改，另一种是 web 3 原生。本来想着用 NFT 作为艺术品的链上存证，没起来，结果蓝筹 NFT 在链上搞原创，链下搞周边，火了。现在的 NFT 还是投资的属性多一点，靠品牌大和稀有度。</p><p>各种 Fi 其实最终都落在 Finance 上，SoicalFi、GameFi、DeFi，都在金融的圈子里。目前还没有靠谱的场景能行得通，最大的 web 3.0 和元宇宙都是概念阶段。国外都没玩通，国内就更不行了，天然抵制 Fi。在这样的背景下，你说绝望吧。</p><p>有聊天室和邮箱基于 ENS 的，问题是谁用啊。有小游戏结合链上资产的，玩家挣不到钱，项目方也挣不到钱，那怎么办。</p><p>在一些一线机场买 VPN 的时候，发现支持 TRC-20，还挺意外的，原来 Tron 还活着，还有商家在用，倒是给了我下载 Trx 钱包的理由，它真的能用来买东西，不用担心国内的支付方式有问题了。不过遗憾的是，依然是 Finance 场景。</p><h3 id="DSN"><a href="#DSN" class="headerlink" title="DSN"></a>DSN</h3><p>分布式储存网络有两个头部，Filecoin 和 Arweave。</p><p>Filecoin 是链下储存的典型，或者说基于合约的 DSN，链下用 IPFS 存数据，链上做存证、结算、挑战。Filecoin 的学术能力很强，先是做出了 IPFS，在 PoC 方面也基本上是老大，在文件证明的方向有很多创新，超越了基础的 PDP、Proof of Retrivailiabily。在链式的文件证明上都是发顶会的论文。</p><p>链下储存的优点是文件可以很大，不会影响到链的效率，但是 Filecoin 广为人知的问题是，数据有丢失的风险，数据没了就真的没了。而且 Filecoin 的节点和 SDK 真的不怎么好用，至少不亲民，据说因为要根据磁盘空间计算算力，故意控制文件的编码速度到一个频率，导致用户的体验非常差，可能要一两个小时才能知道，文件有没有成功储存。现在也少有项目是用 Filecoin 构建项目。</p><p>Arweave 是链上储存的典型，或者说基于链的 DSN。Arweave 的创新能力比起 Filecoin 差很多，块结构和 Filecoin 类似，会在块中记录一个 calldata，随机验证上一个块 calldata 前 10% 的内容。Filecoin 的 PoC 共识比较纯粹一点，Arweave 还是要依赖 PoW 挖矿，先做文件的校验，然后 randomX 挖矿。</p><p>Arweave 利用经济模型，文件越稀有，挖矿的几率越高，用这样的方式达到文件永久储存的效果，以区别于 Filecoin 的临时储存。Arweave 和 Meta 合作也让 Ar 的价格涨了很多。</p><p>链上储存的好处就是，文件永远不会丢。问题是数据膨胀量也比较大，对于大文件的储存可能不是很友好，比较适合存 NFT 之类的数据。以太坊也早就有结合 IPFS 做链下储存 NFT 的方案。</p><p>我理解链上储存和链下储存，或者基于链的储存和基于合约的储存，简单的区分方式是，看生成新的块要不要依赖上一个块的文件数据。</p><p>有人认为目前的区块链，基本方向还在于分布式计算，至于分布式储存，可能路径不是特别明确。Filecoin 也是在搞一种 actor 的节点，支持 EVM 的执行环境。其实早在多年前《硅谷》的电视剧里，就已经畅想过分布式储存的前景了。</p><p>参考之前的思维方式，分布式储存是要做链改，替换掉 web 2 的云服务器市场呢，还是在 web 3 上搞创新呢。</p><h3 id="Layer-2"><a href="#Layer-2" class="headerlink" title="Layer 2"></a>Layer 2</h3><p>首先 layer 2 还在 Finance 的范畴里。以太坊计划把 layer 2 发扬光大，提高主网的性能，分散主网的计算压力。</p><p>目前 layer 2 还没有真正的头部，各个 layer 2 项目是割裂的，相互之间不能交互，对用户的体验是很大的问题。用户需要选一个 layer 2 站队，那么只有一些小项目会放到 layer 2 上。layer 1 的大合约不太可能会放到 layer 2 上。</p><p>layer 2 的技术设施也不完善，zk 就不用想了，op 和 arb 相对能用，但是比较中心化，和 layer 1 的交互也比较慢。arb 今年在奥德赛的时候，由于用户的突然增加，gas 费接近主网的 3 倍，属于世界名画了，问题也一时难以修复。</p><p>op 的挑战周期比较长，据说是一到两周，对于普通用户有点不能接受，而且问题在于，谁来验证主网上的交易。用户自己肯定不干，交给第三方的话，又要把信任放到第三方上。</p><p>state channels 和侧链的局限性都比 rollups 要大，更加不那么安全，op 似乎是从 plamsa 演进来的。</p><p>以太坊对 layer 和 sharding 都有很多蓝图，但是就像不知道谁说的，“以太坊啥都好，就是啥都慢”，我们对以太坊的期待要等很久。</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>DID 几年前就有了，没有项目真正做起来。现在阶段，还是认为小狐狸或者钱包是 web 3 的入口，如果要搞 DID，可能还需要一大波市场的清洗。</p><p>Solana 的价格在 FTX 的事件中跌的厉害，Solana 的特点是 PoH 共识，因为经历过之前的几次宕机，大家不是特别看好。</p><p>Polkadot 主要是它不好用啊。</p><p>DeFinity 看起来挺好用的，理念上也结合了各种链的优点，号称无限扩容，全部数据上链。问题是他没有太大创新，更多的是把各种东西整合到一起。</p><p>Conflux 是国内的链，从大学实验室里开始的，在国内根本做不起来，啥场景都没有。</p><p>比特币由于耗费大量算力的问题，长期看会被淘汰，也会被历史记住。它最稳，也最不稳。</p><p>其他的我也不知道了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近有点迷茫，随便写写，可能比较乱。之前只写过工作方面的回顾，没有概念方面的内容。这一年过的很快，发生了很多事情，又没什么大的变化。&lt;/p&gt;
&lt;p&gt;最近一年都是熊市，尤其是五月份之后，区块链行业进入严重的寒冬，没有像之前各种概念层出不穷，似乎没有大事发生，大家都还在想，下一</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>随机确认块的共识机制</title>
    <link href="https://smallyu.net/2022/09/25/%E9%9A%8F%E6%9C%BA%E7%A1%AE%E8%AE%A4%E5%9D%97%E7%9A%84%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    <id>https://smallyu.net/2022/09/25/%E9%9A%8F%E6%9C%BA%E7%A1%AE%E8%AE%A4%E5%9D%97%E7%9A%84%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/</id>
    <published>2022-09-25T09:43:28.000Z</published>
    <updated>2025-11-09T09:22:13.556Z</updated>
    
    <content type="html"><![CDATA[<h3 id="步骤"><a href="#步骤" class="headerlink" title="步骤"></a>步骤</h3><ol><li><p>在一个时间窗口内，一个节点会收到多个通过验证的块，在这些块中随机选取一个作为自己的下一个块。</p></li><li><p>在下一个时间窗口，节点如果没有收到块，同时发现网络整体的块高度已经高于自己，将丢弃掉上一个确认的块。</p></li><li><p>如果块高度相同，少数服从多数。</p></li></ol><h3 id="为什么要这样"><a href="#为什么要这样" class="headerlink" title="为什么要这样"></a>为什么要这样</h3><ul><li>收到多个通过验证的块</li></ul><p>Proof of work 的思路是，在一个时间窗口内，只有一个正确答案，这样就保证了整个网络的一致。换个思路的话，为什么不能在同一个时间窗口内，产生多个正确答案呢？</p><ul><li>随机选取一个块</li></ul><p>由于网络中同时存在多个正确的块，那么就需要一种机制来从中选取一个，公平起见，就用完全随机的方式。</p><ul><li>丢弃上一个块</li></ul><p>丢弃块的机制是为了给系统提供自我纠错的能力。Ethereum 的 DAO 攻击事件，需要开发者带领社区对区块链进行硬分叉，说明系统本身是没有纠错能力的，需要人为干预，似乎目前所有区块链系统都是这样。</p><ul><li>少数服从多数</li></ul><p>在没有交易发生的情况下，有可能所有节点都处于等待的状态，此时因为块高度相同，没有节点愿意丢弃自己的块，整个网络是割裂的。这是一种博弈的状态，所以要按照少数服从多数的原则打破平衡。</p><h3 id="一些问题"><a href="#一些问题" class="headerlink" title="一些问题"></a>一些问题</h3><ul><li>为什么要随机？</li></ul><p>为了避免富有的人越富有，有算力的节点更富有，有财产的节点更富有，公平地给予每个参与到网络中的节点，获取块奖励的机会。</p><ul><li>谁来随机？</li></ul><p>每个节点各自随机，选取一个块。而不是使用类似 Varifiable random function 的方式，让每个节点都使用确认的结果。</p><ul><li>完全随机还是有限制的随机？</li></ul><p>完全随机。</p><p>完全随机是指，节点收到 100 个块，选中某个块的概率就是 1%。有限制的随机是指，例如，上一个块已经是某个节点提供的，下一个块就不再使用它的块。相应的，块奖励也不会给它。</p><p>如果是有限制的随机，已经产生过块的节点，和没有产生过块的节点，就有了不一样的权重，无论权重侧重于哪一边，都不是合理的方式。如果产生过块的节点更有权重，依然会存在富有的人越富有的问题。如果没有产生过块的节点更有权重，网络中的参与者就会疯狂创建新的账户，使用没有获得过块奖励的节点产出块。</p><ul><li>随机会带来分叉？</li></ul><p>一定会，但分叉是小范围的。如果网络效率很高，那么只有最新的一两个块会处于不确定的状态，等大多数节点确认下来，就成为主流的一条链了。</p><ul><li>分叉会一直存在吗？</li></ul><p>不会，因为分叉会被淘汰。新加入网络的节点，肯定要选择某一个节点同步数据。对于其他节点，随机选择是指从通过验证的块中选择，通过验证的含义是，历史数据和自己一样。那么当某一条分叉链没有交易产生，由于纠错机制的存在，分叉链会不断丢弃掉自己的块，直到和主网络一致。</p><ul><li>如果新节点不同步数据呢？</li></ul><p>如果新加入的节点不同步已有数据，而是另起炉灶，那么它需要拉拢超过整个网络大多数节点规模的追随者。比如有 1000 个节点，由于完全随机的机制，它需要另外 1000 个节点，自己的链才会变为网络中的主流。这种做法可以视为攻击网络的一种方式。难度似乎比 51% 攻击还要高。</p><ul><li>存在 51% 攻击？</li></ul><p>由于块高度优先的方式加上少数服从多数的原则，可能会认为，攻击者甚至不需要 51%，只需要掌控块高度最高节点数的 51%，就可以形成攻击。</p><p>这里也许存在概念上的误区，攻击是指颠覆原有的数据而不是产生新的数据。51% 是不存在的。</p><ul><li>回滚会带来不确定性？</li></ul><p>对于用户来说，确实是不好的体验，可能上一分钟交易已经成功了，下一分钟节点丢弃了块，交易撤销了。</p><p>不过好在这种不确定是短暂的，可以认为确认一个块需要两个时间窗口或者更多个。因为即时网络中存在小范围的分叉，最终也会趋于大多数一致。</p><p>所以问题变成了客户端确认一笔交易的时机。</p><ul><li>未确认的块会造成资源浪费？</li></ul><p>一个节点收到 10 个块，确认 1 个，其他的块就浪费掉了。包括反复的纠错，也属于资源的浪费。</p><p>PoW 浪费的是算力资源，这种随机确认块的方式，浪费的是网络传输的资源，因为如果一个节点想要自己的块被确认，它首先要让其他节点接收到它的块。那么假如网络中有 1000 个节点，就需要每一轮都把块广播给 1000 个节点。</p><p>好在网络传输资源的浪费是有上限的，区块链网络本身就需要把每一笔交易广播到网络中，与其相比，现在只是需要多广播一个块的内容，算不上太大的负担。而且由于是完全随机地选择块，节点试图通过更高的网络配置来多次广播块也是没有意义的。</p><ul><li>如果网络异常，节点会丢弃所有块？</li></ul><p>如果节点获取到网络中块高度非常低，按照规则 2，是不是就会逐渐丢弃掉所有块？</p><p>不会。要区分清楚，从网络中获取到异常信息，和连接不到网络、无法从网络总获取信息，是两回事。假如一个节点的路由表中有 1000 个节点的连接记录，由于网络状况异常，只能连接到 10 个，此时就属于异常情况，节点不应该按照正常的共识流程丢弃块。</p><ul><li>有哪些已有的提案？</li></ul><p>在这篇论文《<a href="https://arxiv.org/abs/2001.07091">Blockchain Consensus Algorithms: A Survey</a>》的第 16-17 页，提到了随机选择出块节点的共识方式，以及基于 coin-age 的选择方式。这确实是一个随机的概念，但论文描述的是随机是指随机选择节点，而不是针对块，并且是在 PoS 的语境下讨论的，出块节点有被选定、被内定的意味。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;步骤&quot;&gt;&lt;a href=&quot;#步骤&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>VRF + BFT 共识引起交易失败的问题</title>
    <link href="https://smallyu.net/2022/09/03/VRF-BFT-%E5%85%B1%E8%AF%86%E5%BC%95%E8%B5%B7%E4%BA%A4%E6%98%93%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/"/>
    <id>https://smallyu.net/2022/09/03/VRF-BFT-%E5%85%B1%E8%AF%86%E5%BC%95%E8%B5%B7%E4%BA%A4%E6%98%93%E5%A4%B1%E8%B4%A5%E7%9A%84%E9%97%AE%E9%A2%98/</id>
    <published>2022-09-02T22:39:51.000Z</published>
    <updated>2025-11-09T09:22:13.309Z</updated>
    
    <content type="html"><![CDATA[<p>昨天遇到一个问题，是 Ontology 的节点不出块了。节点使用的是 VBFT 共识，网络互通没有隔离，也就没有分叉，报的错是提案（proposal）过来的块，和预期的块哈希（MerkleRoot）不一样。由于种种原因，昨天的问题没有深入去查，用比较暴力的方法先让网络恢复正常。不过提到 VBFT，我想到了以前公司的一些事情。</p><p>我是在之前的公司开始接触区块链的，当时的项目号称自研区块链，也是用 VBFT（VRF + BFT）的共识，不过在共识方面不那么说，叫成 UBFT 还是什么。</p><p>我猜测 VRF + BFT 的主意是 Ontology 提出来的，我之前的公司把这种创意抄了过来，模仿着实现了一下。说来讽刺。</p><p>主要想说的是之前公司由于对 VRF + BFT 不靠谱的实现，引起的一个隐藏的 bug。那件事情距今快 2 年了，不记得当时为什么没有写博客记录一下，可能是在忙着做 PPT？昨天遇到共识相关的问题，我想起有那么一回事，正好现在有时间有心情写一下。由于过去时间太长了，细节上可能有出入。</p><h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>先介绍一下那个项目的情况，主打的特点有几个。</p><p>一个是异构多链，含义是可以在同一个节点上包含多条异构的链。异构是指一条链可以使用不同的共识机制、基于不同数据库运行起来。多链是指多条链可以在同一个节点上运行，因为觉得一条链不够用，一条链就相当于传统业务里的一张数据表，多条链可以方便地进行数据拆分，在联盟链的场景下更好地支持业务。异构多链，可以理解为把以太坊不同 chain id 的链，用同一个二进制包启动了。现在的某条开源联盟链，还在拿从异构多链演变来的灵活装配作为一大特点呢，猜猜为什么。</p><p>再一个是多数据库的支持，同时支持很多关系型数据库和非关系型数据库，做法是针对各种数据库，写数据操作的中间层做适配。</p><p>还有就是共识机制方面，基于开源的 Tendermint 项目。Tendermint core 是一个对 BFT 类共识的实现，在那个基础上，做的改动是把轮询选择提案节点，用 VRF 函数，替换为随机选择提案节点。另外还增加了对分层共识的支持，也就是共识组的概念，每隔多少个块换一次共识组，换共识组的方式借鉴 BFT 的流程，保证换共识组过程的安全性。分层共识这个理念也不知道起源于哪儿，可能同时期的项目流行这个？</p><p>项目的这些特点都是在我接触之前就已经开发完成的，我也只是有所了解。</p><p>我当时遇到的问题是，如果向区块链发送一笔失败交易，节点会立即返回交易失败的结果，然后如果再次发送一笔失败交易，第二笔交易的结果将迟迟不返回，节点不出块了。接下来如果仍然是失败交易，第三笔、第四笔，都会是同样的现象。这个时候，如果发送一笔正确的交易，节点会立即返回结果，之后一切恢复正常。而且这种现象是概率性出现的，并不是每一次失败交易都会引起问题。</p><h3 id="前提"><a href="#前提" class="headerlink" title="前提"></a>前提</h3><p>首先是失败交易指合约返回执行结果为失败的交易。区块链系统的交易失败有两种，一种是交易不能被执行，另一种是交易能被执行，但是合约中返回了合约层面的失败。那个项目并没有严格区分这两种失败的类型，合约有权限返回交易层面的失败，这其实是有问题的设计。</p><p>不正确的交易，将会在提案的时候，被忽略掉，因为不正确交易没有必要记录在区块链上。加上项目对失败交易错误的处理，造成的现象就是，合约执行失败的交易，会被忽略掉。这是前提。</p><p>BFT 共识的基础，是投两轮票，最终确定一个块。不管是什么 BFT，在前面加什么字母，不管通过多么复杂的流程决定出哪个节点提案、如何提案，不管对共识的效率做什么优化，是并行提案还是流水线共识什么的，只要是 BFT 类的共识，都是投票两轮。对两个阶段的命名可能不一样，不管是用 proposal 还是 prepare 来描述，都是那样一个过程。</p><p>BFT 的流程，是先有一个节点生成一个块，然后把这个块发送给其他节点，如果超过 2&#x2F;3 节点同意，会进行下一轮投票。第二轮投票如果超过 2&#x2F;3 节点同意，这个块就算确认下来了。两轮投票是理解 BFT 共识的关键。至于为什么投票两轮就可以达到 3f + 1 的容错效果、为什么至少要两轮，我也不知道。</p><p>忽略掉失败交易的操作，是在检查交易的过程中完成的。项目里有两次检查，共识前检查和共识后检查。有一些交易是没办法在共识前进行检查的，比如在合约里进行的写数据库操作，如果共识前就写库了，然后共识失败了，数据不就乱套了吗。所以只能共识后进行检查。这是第二个前提。</p><h3 id="第一笔失败交易"><a href="#第一笔失败交易" class="headerlink" title="第一笔失败交易"></a>第一笔失败交易</h3><p>我们根据 bug 的现象分析一下，第一笔失败交易的流程是正常的。一笔失败交易进来，在共识之前是不会检查出失败的，所以预提案的节点正常提出了一个块，分发给其他节点，进行第一轮投票。之后正常进行第二轮投票，在确认块的阶段，写入块之前，会进行共识之后的检查，检查过程中发现交易失败，并且这个块只包含这一笔交易，这个块就作废了，没出块。同时，其他节点也都会返回消息告诉提案节点，这个块没出来，这笔交易失败了。所以第一笔失败交易是正常返回结果的。</p><p>第二笔失败交易进来，按照同样的处理流程，一切都应该是正常的才对。因为即使是失败交易，即使是在确认块的阶段，如果检查失败了，也会广播处理结果给其他节点。整个协议中投票失败或者落块失败，都是用空消息表示。其他节点不会因为交易失败，就收不到消息苦苦等待超时。那么既然 BFT 协议的流程没有问题，为什么还是出现 bug？</p><p>这个时候要提到项目在 VRF 方面的改造。</p><p>在 BFT 的协议中，是需要一个节点去生成一个块，分发给其他节点开始进行第一轮投票的。那么由哪个节点来进行这个生成块的操作呢？总不能是同一个节点吧，那就太中心化了。Tendermint 的做法是依次进行，比如有 4 个节点，第一次节点 A，第二次节点 B，这样轮询。</p><p>VRF（Verfiable Random Function）做的事情，是改变依次选择节点的方式。因为如果按照顺序来，那很容易预测到下一轮要由哪个节点去生成块，顺序可以预测之后，就存在节点被贿赂、节点被攻击等安全隐患。VRF 的功能是参数相同结果一定相同，参数不同则结果随机。把块高度、投票的轮数作为参数，就可以很好地实现，每一个块都能由随机的节点来生成，无法预测。这个改动也是作为项目的一个亮点的。</p><p>不过 VRF 存在一个问题，既然是随机的，那就有一定可能，第一次随机到节点 A ，第二次也随机到节点 A，这样的概率还是不小的。如果节点 A 是恶意节点，然后由节点 A 连续两次生成块，会给网络带来一些负担，虽然不至于破坏网络，但也是一点小小的麻烦。所以项目为了解决这个问题，在 VRF 的基础上加了黑名单的机制。</p><p>如果上一轮是节点 A 生成块，就把节点 A 放到黑名单里。如果 VRF 的结果在黑名单里，就再 VRF 一次，避免重复选择相同的节点。</p><h3 id="第二笔失败交易"><a href="#第二笔失败交易" class="headerlink" title="第二笔失败交易"></a>第二笔失败交易</h3><h4 id="不返回结果"><a href="#不返回结果" class="headerlink" title="不返回结果"></a>不返回结果</h4><p>结合 VRF 和黑名单，再来看看第一笔失败交易发生了什么。节点 A 收到交易，会先把这笔交易广播给其他节点，然后打包成块进行投票的流程。此时节点 A 在黑名单里。投票失败后，节点 A 返回失败，并且这笔交易已经不在节点 A 的交易池里了，因为已经处理过了。</p><p>那么节点 B 呢？块里面的交易验证失败了，但是交易池里收到的交易还在，因为这笔交易还没有处理啊，处理的只是广播过来的块里面的交易。这个时候是不是应该把交易池里面的交易删掉？对，但是没删。所以造成一个问题，节点 B 被选作生成块的节点，把这笔交易打包了一下，广播了出去。这个块当然也是提案失败的。此时节点 A、节点 B 都在黑名单里。</p><p>以此类推，就这一笔交易，一轮下来，4 个节点全在 VRF 的黑名单里。但是对这笔交易结果的返回是没有影响的，因为交易结果在节点 A 的时候就已经返回了。</p><p>第二笔失败交易过来了，所有节点全在黑名单里，会发生什么？当然不能选不出节点，节点全在黑名单里，黑名单就失效了。VRF 的结果是哪个节点，就是哪个节点。</p><p>分析一下第二笔失败交易。同样是节点 A 收到交易，假如这一次是节点 B 负责生成块，然后这个块验证失败了，节点 B 就会删掉这笔交易，对吧，这个没问题。</p><p>注意，删掉交易的同时，通知客户端，交易失败了，返回交易结果。节点 B 被选中，节点 B 生成块，节点 B 返回通知。但提交这笔交易的客户端，连的是节点 A 啊！</p><p>第一笔失败交易为什么会收到响应？因为黑名单还没有失效，所有节点都处理了一遍交易，所有节点都返回了一遍交易结果。现在黑名单失效了，只有节点 B 会返回结果，所以节点 A 的客户端收不到交易结果。</p><p>那为什么黑名单失效，就不能像第一笔交易一样，所有节点都处理一遍？</p><h4 id="阻塞后续交易"><a href="#阻塞后续交易" class="headerlink" title="阻塞后续交易"></a>阻塞后续交易</h4><p>接着分析一下第二笔失败交易，节点 C 还有交易啊，节点 C 上面的这笔失败交易还没处理呢，节点 C 就开始用 VRF 选节点了。</p><p>刚才提到，此时黑名单失效，VRF 选出哪个就是哪个。刚才选出了节点 B，这一轮有没有可能再选一次节点 B？黑名单失效，就变得可能了。这个时候如果又是节点 B 负责生成块，会发生什么？</p><p>节点 B 生成不了块，因为节点 B 已经没有交易了，它已经把唯一的失败交易，在上一轮就删掉了。也就是说，在新一轮的共识过程中，4 个节点全部在等节点 B 生成块，节点 B 自己也知道该自己了，但是节点 B 拿不出块，节点 B 直接放弃这一轮共识，进入下一轮，并且节点 B 没有发出任何消息。</p><p>在分布式系统中，没有消息是一件可怕的事情。其他节点都在等节点 B 呢，节点 B 自己玩了。这个时候，节点 B 的轮数要比其他节点快一轮。</p><p>在 BFT 共识中，有两个索引值，一个是块高度，一个是共识的轮数。同一个块高度，有可能因为块没确认，就经过很多轮共识。由于节点 B 自己没生成块，轮数增加了，其他节点还不知道。</p><p>现在，所有节点都在 VRF 的黑名单里，节点 B 的共识轮数高于其他节点，</p><p>如果节点 A 再收到失败交易，有两种情况。一种情况是 VRF 又选中节点 B 了，节点 B 提出的块会被拒绝，因为其他节点还在等节点 B 上上轮的块，它拿出了高轮数的块，是对不上的。另一种情况是，VRF 选中了其他节点，那其他节点首先要等节点 B 上上轮的出块超时。超时之后，其他节点把轮数最高的数值同步一下，共识就算恢复正常了，然后 VRF 再选。</p><p>但是注意，这可是一笔失败交易，此时黑名单仍然失效，即使共识恢复正常，也还是有概率重蹈整个覆辙，节点 A 仍然收不到交易结果。至于具体的概率是多少，就懒得算了。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>可以看到，这个 bug 是由很多系统性的不合理设计共同造成的，直接原因在于 VRF 黑名单的失效，因为所有节点都在黑名单里了。或者说，黑名单没有及时清空，原先的错误之处在于，只有在块高度变化的时候才清空黑名单，可能是认为每个块的产生都应该由不同的节点来处理。这种想法的失误在于忽略了相同块高度的时候，共识的轮数也会发生变化，每一轮都会产生一个新的块。所以只要在共识轮数发生变化的时候，也清一下黑名单就好了。实际的代码改动只有两行。</p><p>上面写的东西，可能我自己也不想仔细去看，不好理解、抽象，而且文字的表达能力也弱，看起来费劲。这种类似状态机状态转换的文字描述，看起来是很痛苦的事情。尤其是内容和当时项目的耦合很深。总的来说，对于这个问题的分析和解决，我认为在逻辑上是自治的，能很好的解释成因和现象，以及用最简单的方式在表面上修复它。</p><p>时隔近 2 年的时间，我竟然还能记起来这些，感觉也是很奇怪。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;昨天遇到一个问题，是 Ontology 的节点不出块了。节点使用的是 VBFT</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="BFT" scheme="https://smallyu.net/tags/BFT/"/>
    
  </entry>
  
  <entry>
    <title>为什么要重视编程思想</title>
    <link href="https://smallyu.net/2022/07/24/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E9%87%8D%E8%A7%86%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3/"/>
    <id>https://smallyu.net/2022/07/24/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E9%87%8D%E8%A7%86%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3/</id>
    <published>2022-07-24T14:53:55.000Z</published>
    <updated>2025-08-22T08:54:30.259Z</updated>
    
    <content type="html"><![CDATA[<h3 id="（1）"><a href="#（1）" class="headerlink" title="（1）"></a>（1）</h3><p>前两天遇到一个小问题，Solidity 写的智能合约超过 24 KB，不能部署到以太坊主网上，因为 EVM 对合约的代码小小有限制。于是考虑怎么减小合约的大小，当时对合约大小的概念都是模糊不清的。</p><p>其中注意到一个地方，合约是可以引入其他合约、调用其他合约方法的，只需要把部署后的合约地址作为参数传到合约里：</p><pre><code class="solidity">contract Demo &#123;&#125;contract Main &#123;    Demo demo;    constructor(Demo _demo) public &#123;        demo = _demo;    &#125;&#125;</code></pre><p>合约大小包括引入的合约吗？EVM 在执行合约的时候，会不会先把其他合约的代码也加载进来，然后一起运行？代码大小的计算要包括所有合约？那可就麻烦了。</p><p>后来注意到，可以使用接口替代合约：</p><pre><code class="solidity">contract IDemo &#123;&#125;contract Demo is IDemo &#123;&#125;contract Main &#123;    IDemo demo;    contract(IDemo _demo) public &#123;        demo = _demo;    &#125;&#125;</code></pre><p>接口的代码量一定是少于具体实现的，因为接口不包含方法体，把引入的合约全部替换成接口，合约不就小多了？</p><p>当然，在这里纠结的不是 Solidity 合约怎么写或者合约代码大小怎么计算的问题，后来搞清楚了。比较在意的是，那个时候突然有点恍惚，用接口和直接用合约，有什么区别？</p><p>之前给合约定义接口是为了提供一个对外方法的描述，这里才意识到接口可以替代合约本身，直接用来定义变量，并且使用接口定义的变量，去调用合约里面的方法。但为什么可以呢，它不就只是一个接口吗？</p><h3 id="（2）"><a href="#（2）" class="headerlink" title="（2）"></a>（2）</h3><p>如果你刚学习过 Java，或者使用 Java 作为工作语言，一定会有哑然失笑的感觉，这个问题太幼稚了，这不就是多态吗？</p><p>上第一节 Java 的课程，老师就告诉我们，面向对象有三大特性，封装、继承、多态，这句话时至今日我都能想起来，这是多么基础的概念，结果在工作多年后的今天，我竟然在实际工作上因为如此简单的问题犯了难，一时没反应过来，用接口作为类型的写法是什么意思。这太荒唐了。可能也是因为很久没写 Java，现在一直在用 Golang。</p><p>不得不说 Java 是面向对象编程语言的标杆，Solidity 虽然是一种看似新的用于智能合约的脚本语言，揉杂了多种语言的特性，但基本的编程思想还是基于面向对象的。合约就是类，部署一个合约就是实例化了一个对象，合约地址就是对象的内存地址，合约调用就是对象的方法调用……</p><p>只要是支持面向对象的编程语言，就包含有面向对象的特性，就可以使用面向对象的写法，就离不开最基本的像多态一样的特性。从面向对象的角度去理解，Solidity 有什么难的呢？无非不就是换了一些表面上的形式，编程思路甚至可以一模一样，此外再添上一些区块链特有的概念，像转账、块高度之类，就没了。</p><p>从编程语言的角度看，Solidity 和 Java 那样成熟的语言自然没法比，面向对象的特性是残缺的，modifier、require 之类的写法看似好用却增加了很多理解成本，而且代码结构也变得不是太统一。EVM 怎么能和 JVM 相提并论呢？但作为一种轻量级的脚本语言，Solidity 又要使用静态类型那样冗余的写法。</p><p>当然要注意，编程思想是先于编程语言的，我仍然会认为形式上的编程语言<a href="/2022/02/16/%E6%88%91%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%AD%A6%E4%B9%A0%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/">不值得学习</a>，但是不否认从学习编程语言的角度入手去学习编程思想。比如<a href="https://en.wikipedia.org/wiki/Polymorphism_(computer_science)">多态</a>这个概念，含义是使用统一的符号去代表不同的类型，包括三种类型的解释，一是支持多种类型的参数，对应 Java 里方法的重载，二也是支持多种类型的参数，对应 Java 里的范型，三是子类型，也就是把接口作为类型，对应上面提到的场景中的多态的含义。</p><p>面向对象是一种编程思想，包含很多计算机科学的概念，而 Java 是一种完全的面向对象的编程语言，不但涵盖众多有用的特性，而且实现的完整漂亮，如果你学习了 Java，自然也就知道面向对象是怎么回事了，受用无尽。从这个角度看，和 Java 相比，Golang 有什么值得学习的地方吗？是 struct 的写法还是 * 号的用法？可能 Golang 更像是一种快餐式的语言吧，可以很方便地 go func()。不过要是为了学习，就不是太推荐了。</p><p>花了几分钟看 Java 文档的目录，倒是能很快想起来那些内容，毕竟实在是太基础了。也是要告诫自己，别忘了代码怎么写。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;（1）&quot;&gt;&lt;a href=&quot;#（1）&quot; class=&quot;headerlink&quot; title=&quot;（1）&quot;&gt;&lt;/a&gt;（1）&lt;/h3&gt;&lt;p&gt;前两天遇到一个小问题，Solidity 写的智能合约超过 24 KB，不能部署到以太坊主网上，因为 EVM</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>对 Web 3.0 的理解</title>
    <link href="https://smallyu.net/2022/06/19/%E5%AF%B9Web3.0%E7%9A%84%E7%90%86%E8%A7%A3/"/>
    <id>https://smallyu.net/2022/06/19/%E5%AF%B9Web3.0%E7%9A%84%E7%90%86%E8%A7%A3/</id>
    <published>2022-06-19T11:47:30.000Z</published>
    <updated>2025-11-09T09:22:13.447Z</updated>
    
    <content type="html"><![CDATA[<h3 id="Web-3-0-开发"><a href="#Web-3-0-开发" class="headerlink" title="Web 3.0 开发"></a>Web 3.0 开发</h3><p>Web 3.0 是一个几年前就存在的概念，可能随着区块链的推广越来越有名了。当人们还不知道 3.0 版本的 Web 会是什么样子的时候，区块链出现了，尤其是以太坊的 dApp 提供了一种很大的可能性，于是 Web 3.0 就和区块链、去中心化、自我主权这些概念绑定在一起。</p><p>前几天<a href="https://smallyu.net/micro-blog/#2022-18">提到</a>说。「Web 3.0 开发」是可以作为一种职业定位去描述的，而且这个词可以涵盖区块链开发的范畴，立意比「区块链开发」这个描述高一点。</p><p>Web 2.0 时代，我们说的 Web 开发指普通的前后端开发，前端用所谓的三大框架 React.js、Vue.js、Angular.js 结合组件库，后端用 Spring 全家桶，加上各种中间件 Zookeeper、Kafka、Elasticsearch 之类，还有常用的数据库 MySQL、Oracle，就是 Web 开发的常用技术栈。</p><p>Web 3.0 开发的技术栈，可能会演进为 Remix、Hardhat、Ruffle 这些智能合约的开发工具和框架，人们像关注 Java 的语言特性一样去关注 Solidity，以及各种区块链节点的搭建运行调用、二次开发，甚至及区块链节点本身的开发等等。当然，很难简单地把这些技术栈去和 Web 2.0 一一对应。一个简单的例子是，当你从事区块链开发的工作，你已经很难用前端开发或者后端开发来形容自己的工作内容了，就只能是区块链开发，或者智能合约开发，或者其他的描述方式。</p><p>现在已经有一些岗位在用「web3 开发」的形容了，不过我们要区分清楚 Web 3.0 和 web3 不是一回事，目前很多岗位说的 web3 指以太坊的那个 web3 框架。我们需要一个新的描述，同时我们需要有一个更好的、更有前景的职业定位，那就是 Web 3.0 开发。</p><p>我这里想说的是，要相信我们走在正确的道路上。</p><h3 id="一级市场和二级市场"><a href="#一级市场和二级市场" class="headerlink" title="一级市场和二级市场"></a>一级市场和二级市场</h3><p>区块链的一级市场，指比特币、以太坊这种原生的链。二级市场更多是基于这些链，衍生出的一些项目，基于以太坊的项目尤其多，Layer 2、预言机、NFT、ENS，都属于二级市场。</p><p>有一些团队是做一级市场也就是区块链开发的，Solana、Filecoin、Neo 都是在以太坊之后出现的，Dfinity 的 IC 也是处于活跃的一种一级市场的例子，再比如像 Bitcoin SV 是在做 “支持智能合约的比特币” 这样的事情。</p><p>还有很多创业团队是在做二级市场，比如有从 360 出来的去做智能合约的安全，把合约扫描一遍报出安全漏洞给你；还有做 NFT 的交易协议，去定制一些类似 NFT 交易所的 API，想建立通用的交易网络；还有炒元宇宙概念的，给虚拟人物定制不同样式的衣服；也有基于 IC 做去中心化邮箱的等等。以太坊的各种扩容方案当然也算二级市场，OP 前段时间还发行 token 了。</p><p>从商业角度没有什么高下之分，从技术角度也不好说简单和难，不过我觉得还是一级市场更基础一些，但是技术上的发展相对缓慢，花样没那么多。具体倾向于哪一种看个人意愿了，这里想提醒的是，Web 3.0 开发是统称，要了解这些不同层级市场的区别。</p><h3 id="去中心化是历史的倒退"><a href="#去中心化是历史的倒退" class="headerlink" title="去中心化是历史的倒退"></a>去中心化是历史的倒退</h3><p>刚才说希望 Web 3.0 是有前景的方向，这个部分想说的是 Web 3.0 的前景也没有那么好。</p><p>想到这个话题是在关心钱包安全的时候，意识到一个问题，就是账户的私钥一旦泄露，你就永远失去了对账户资产的控制权，或者说别人永远拥有了你账户资产的控制权。</p><p>因为我们知道，账户地址是可以从私钥解码出来的，私钥就是你的资产，在备份钱包的时候，备份的就是私钥。你的私钥泄露，就相当于把金钱摆到别人手里，至于别人会不会及时拿走，你能不能在对方动手之前抢回来，那就是另外的问题了。</p><p>这个和传统的账户模型是不一样的，你不可能说你的用户密码是你的财产，因为中心化账户是基于 KYC 的，你只要能证明自己的身份，身份证或者指纹或者长相，都可以找回你的财产，因为财产是和你绑定在一起，而不是你的账户，你的账户密码是可以修改的，即使泄露，别人也只能在短时间内拥有你账户的控制权，你把密码改掉，别人就没办法了。</p><p>私钥是不可能更改的，你能做的，就是及时把资产转移到另外的私钥。去中心化的世界有意区分了身份和数字身份的概念，增加了数字身份的主权，但同时也削弱了身份对数字身份的控制能力。</p><p>这里衍生出的问题就是，去中心化的资产安全吗？把钱拿在自己手里，比把钱存到银行，更加安全吗？考虑到比特币诞生的背景，是出于对中心化机构的不信任，才有了去中心化的理念。</p><p>想想吧！一开始就是没有中心化机构的，人们以物换物，打一开始，就是去中心化的世界。后来为了降低个人保护自己财产的成本，为了增加对坏人作恶更有力的惩罚机制，人们共同组建起中心化机构，保护大部分人的利益。</p><p>现在炒作去中心化的理念，不正是一种历史的倒退吗？去中心化并非新产生的事物，而是早就已经存在的、被人们选择性抛弃的东西。</p><p>不过现在的去中心化和以前的去中心化，最大的不同就是现在的技术手段更为先进，有可能做到之前做不到的事情，把世界推到一种新的愿景上。但是也要注意现在的技术不是那么先进，还远没有发展到那种程度，区块链的技术瓶颈非常多。</p><p>所以我的观点是，现在的去中心化理念不是中心化世界的演进，而是中心化世界的补充。在接下来的时间，中心化和去中心化会同时存在。</p><p>要注意的是，去中心化不等于 Web 3.0，Web 3.0 是 Web 2.0 的演进，因为版本号增加了。Web 3.0 将是中心化和去中心化同时存在的时代。 </p><h3 id="LUNA-归零"><a href="#LUNA-归零" class="headerlink" title="LUNA 归零"></a>LUNA 归零</h3><p>前段时间有一件搞笑的事情，有一天，LUNA 的价格早上还是 80 美元一个，晚上的时候就跌到 1 美元一个了。在接下来的三四天，LUNA 的价格从 1 美元，跌到了 0.00001 美元。几天之内，近万倍的跌幅。曾经号称前十的加密货币，突然归零了。</p><p>我粗浅的理解是，UST 有一个交易池，当短时间有大量卖出的时候，交易池会有小幅的倾斜。当时先是小幅的波动，然后随着社交媒体的传播，大量散户失去了对 UST 的信任，开始大幅卖出，越卖价格越低。Terra 团队是有 5 万个比特币作为储备的，当时也及时打进去想把平衡拉回来，结果比特币也在跌，质押进去的比特币在结算的时候已经不值预期那么多钱了，没能把价格拉回来，后来 Terra 团队也放弃了，任由价格下跌。</p><p>LUNA 的事情发生后不久，看到有的人讨论说，LUNA 还有机会吗？有一种机会是，Terra 团队还有 20 亿，等 UST 的价格跌到总市值小于 20 亿的时候，Terra 团队可以把市场上所有 UST 都买下来，销毁掉多余的 UST，只留 20 亿个，UST 的价格就可以回到 1 美元了。不过 Terra 团队可能没打算那么做，后来发行了新的 LUNA。</p><p>LUNA 的失败不意味着算法稳定币的失败，有的团队也在研发新的算法稳定币，据说是想把美联储的运行模式，用算法模拟出来，正在写白皮书。</p><p>在 Web 3.0 宏大的时代背景下，LUNA 的事情就算是先行的笑料吧。</p><h3 id="Web5"><a href="#Web5" class="headerlink" title="Web5"></a>Web5</h3><p>最近新出一个 Web5 的概念，就是 Web 5.0 的意思。提出这个概念的人<a href="https://blog.web3labs.com/web5">说</a>，跳过 Web4 是因为 Web2 + Web3 &#x3D; Web5。好家伙，不愧是 Web5，提出 Web5 的能是一般人吗？但凡对软件工程有了解的敢这么说？</p><p>简而言之，我的结论是，Web5 一定不会成功。不管它叫 Web5 还是 Web6、Web7，它的理念还是围绕去中心化、SSI 那一套，还在我理解的 Web 3.0 的范畴之内。如果认真了解过 DIDs 的理念，就知道现阶段所谓的 Web5 完全是噱头了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;Web-3-0-开发&quot;&gt;&lt;a href=&quot;#Web-3-0-开发&quot; class=&quot;headerlink&quot; title=&quot;Web 3.0 开发&quot;&gt;&lt;/a&gt;Web 3.0 开发&lt;/h3&gt;&lt;p&gt;Web 3.0</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="Web3" scheme="https://smallyu.net/tags/Web3/"/>
    
  </entry>
  
  <entry>
    <title>GitBook 好用吗？</title>
    <link href="https://smallyu.net/2021/11/21/GitBook%E5%A5%BD%E7%94%A8%E5%90%97%EF%BC%9F/"/>
    <id>https://smallyu.net/2021/11/21/GitBook%E5%A5%BD%E7%94%A8%E5%90%97%EF%BC%9F/</id>
    <published>2021-11-21T06:21:20.000Z</published>
    <updated>2025-11-09T09:22:13.308Z</updated>
    
    <content type="html"><![CDATA[<p>10 月 15 日，我写下<a href="https://smallyu.net/micro-blog/#2021-84">一句话</a>：</p><blockquote><p>用 cloudflare cloud 的 DNS，把子域名 gub 从原来 CNAME 到 gitbook 改为指向到 github，到现在已经超过 72 小时，开启 DNS proxy 的情况下依然跳转到 gitbook，看样子是一个 302 forward。</p><p>不是 cacheing 的问题，已经很多次打开 dev mode 并且 purge 所有内容了。猜测 cloudflare cloud proxy 服务对于 forward 记录的更新非常慢，甚至有 bug。现在经过的时长一定超过 TTL 了。</p></blockquote><p>一个月过去了，问题无意间得到了解决。想展开详细描述一下我遇到的问题。</p><h3 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h3><p>这里会涉及到两个概念，DNS（Domain Name System）和 CDN（Content Delivery Network）。如果你不太接触 Web service 领域，可以先了解一下它们的联系和区别。</p><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>一开始的时候，我计划写一本开源书，选择 GitBook 作为写作平台。GitBook 名声在外，又有 GitbookIO&#x2F;gitbook 那样广为人知的开源渲染工具，是开源书的不二选择。经过短暂的试用后，在平台的使用上没有感觉到异常。我创建了 Workspace，然后在 GitHub 上新建仓库，把仓库关联到 GitBook 上，一切都很顺利。我简单测试了一下 GitBook 和 GitHub 自动同步的能力，有可能会出现一点点冲突，但还是容易解决的。</p><p>我在 GitBook 上绑定了自定义的域名。<code>smallyu.net</code> 这个域名托管在 Cloudflare 上，子域名 <code>gub.smallyu.net</code> 也是在 Cloudflare 上设置 DNS 记录。全世界都知道，Cloudflare 会提供免费的 CDN 服务，只要在 DNS 记录上打开 Proxy 的橙色按钮开关就可以了：</p><img src="1.png"><p>当时在解析到 GitBook 的时候，开关是打开的。之后没几天，正好遇到了 GitBook 改版大升级，写作界面完全改变了。改版后一两天，我想要更新一些页面的内容，发现改版后的 GitBook 操作流程反直觉、bug 满天飞，每次修改都相当于 Git的 Pull Request，而且每次点编辑按钮，都会新增一个 Pull Request 的条目。当同时存在多个 Pull Request 记录时，页面状态会完全不可控，这个是 1、那个是 2、另一个是 3，还不能增量合并，因为你无法区分两个 PR 之间，一些内容是没修改过还是被删除了。重点在于，PR 没有删除选项，稍微有点强迫症都受不了。网速不好的时候，多刷新两下编辑页面，草稿箱就会多出好几个 PR 的条目，还不知道哪个是刚刚修改的。包括一些其他使用体验上的小问题，当时我还吐槽说：</p><blockquote><p>现在 gitbook 的在线编辑难用过头了，不能删除 commit，不能新建文档，光标会自动跳转……他们是怎么对用户负责的。</p></blockquote><p>后来决定放弃 GitBook，换成了 docsify，页面部署在 GitHub Pages 上，<code>sub.smallyu.net</code> 域名的解析也换到了 GitHub 上。更改 DNS 的解析记录后，发现解析没有生效（Cloudflare 上的 CDN Proxy 开着），访问 <a href="https://gub.smallyu.net/">https://gub.smallyu.net</a> 总是跳转到原来的 GitBook 页面上。</p><p>一开始怀疑是 DNS TTL 的问题，因为在 Proxied 的状态下，TTL 的值只能是 Auto。毕竟 Cloudflare 的 CDN 节点多，我的域名访问量又低，可能 DNS 记录更新比较慢。幸幸苦苦等了 3 天，这个时间足够长了，发现解析依然不生效，因为域名还是跳转到了旧的页面。</p><p><code>dig</code> 域名的记录，是这样的结果：</p><pre><code>gub.smallyu.net.300INA104.21.81.212gub.smallyu.net.300INA172.67.146.253</code></pre><p>此时域名是查不到 CNAME 记录的。对比之后，发现这就是 Cloudflare CDN 的 IP。域名已经解析到了 CDN 上，问题是 CDN 没有返回预期的新页面的内容。</p><p>然后偶然发现，把 CDN Proxy 关了，域名解析正常了，A 记录是 GitHub 的，CNAME 也是 GitHub 的，页面是新的。</p><p>是什么问题呢？Cloudflare 的 CDN 没有刷新内容。</p><p>Cloudflare 有一个 Caching 的配置，也提供了 Purge 的能力：</p><img src="2.png"><p>在点过很多次 Purge Everything 的按钮后，CDN 内容仍然没有刷新，即使打开其他人都说有效的开发者模式，也是徒劳：</p><img src="3.png"><p>包括自定义页面规则，不走任何缓存，也无济于事：</p><img src="4.png"><p>甚至为了让内容更新生效，我在 GitBook 上删除了原有的 Workspace，还注销了账号。仍然没有用。</p><p>之后就不了了之了，只要不开 Proxy，域名解析就是能用的。不过，我当时认为可能是 CDN 的 bug，也许 GitBook 用了 302 forward 之类的记录，CDN 不能正确刷新这种类型的记录。</p><h3 id="原因"><a href="#原因" class="headerlink" title="原因"></a>原因</h3><p>最近，在访问 <a href="https://gub.smallyu.net/">https://gub.smallyu.net</a> 的时候页面稍微卡顿了一下，想起这个页面是没有走 CDN 的，想到了 Cloudflare 上的这条不正常的 DNS 记录。顺手 Google 了一下相关问题，没想到这次找到了有用的信息。之前遇到问题的时候也在网上搜过，搜出来的全是更新缓存之类，这次却找到了不一样的内容。</p><p>Cloudflare 有一些 partners，这些 partners 有着控制 Cloudflare DNS 的权力，Cloudflare 的域名在解析到 GitBook 上后，CDN 的 DNS 就受 GitBook 控制了，在 Cloudflare 上的配置优先级低于 GitBook 上的配置。</p><p>相关问题的链接：</p><ul><li><a href="https://community.cloudflare.com/t/dns-subdomain-no-longer-works-nor-redirects-to-anything/240984/7">DNS subdomain no longer works nor redirects to anything</a></li><li><a href="https://community.cloudflare.com/t/subdomain-cname-does-not-update/280696/2">Subdomain CNAME does not update</a></li></ul><p>我发邮件给 GitBook Support：</p><img src="11.png"><p>没想到 GitBook Supoort 一天之内就回复并解决了问题：</p><img src="12.png"><p>经过测试，现在一些正常，确实是那样的原因。</p><h3 id="教训"><a href="#教训" class="headerlink" title="教训"></a>教训</h3><p>谁能想到，Cloudflare 如此广泛使用的服务提供商，会把域名在 CDN 上的解析权限交给 partners。</p><p>谁能想到，GitBook 的产品即使用户删除了 Workspace 注销了账户，在系统内的域名解析记录都不会被删除。</p><p>这件事情可以带来的启发是，我们应该从普通的用户思维转变为开发者思维。也许在某种观念的影响下，因为所谓 “官网”、“权威” 的概念，当使用一些平台的时候，我们习惯于首先质疑自己的使用方法和操作错误，却很少质疑平台的问题。即使明确是平台的问题，也不会优先试图联系平台方解决问题。从这个点其实可以发散出很多内容，以后有机会再展开。</p><h3 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h3><p>最后现在用 Rust 团队开发的 mdbook 了。GitBook 稍微有点过时、处于不怎么维护的状态。docsify 也有问题，docsify 更适合项目文档，除了样式不那么凸显文字外，页面和页面之前是没有关联的，没有上一页下一页的跳转链接，不太像是一本书。虽然 mdbook 的样式没有很时尚，但是功能齐全完整、编译速度能感受到的快，好用就行。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;10 月 15 日，我写下&lt;a href=&quot;https://smallyu.net/micro-blog/#2021-84&quot;&gt;一句话&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用 cloudflare cloud 的 DNS，把子域名 gub 从原来 CNAME</summary>
        
      
    
    
    
    
    <category term="工具" scheme="https://smallyu.net/tags/%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>一种区块链节点存储扩容的方式</title>
    <link href="https://smallyu.net/2021/11/10/%E4%B8%80%E7%A7%8D%E5%8C%BA%E5%9D%97%E9%93%BE%E8%8A%82%E7%82%B9%E5%AD%98%E5%82%A8%E6%89%A9%E5%AE%B9%E7%9A%84%E6%96%B9%E5%BC%8F/"/>
    <id>https://smallyu.net/2021/11/10/%E4%B8%80%E7%A7%8D%E5%8C%BA%E5%9D%97%E9%93%BE%E8%8A%82%E7%82%B9%E5%AD%98%E5%82%A8%E6%89%A9%E5%AE%B9%E7%9A%84%E6%96%B9%E5%BC%8F/</id>
    <published>2021-11-10T14:25:28.000Z</published>
    <updated>2025-06-19T03:30:33.160Z</updated>
    
    <content type="html"><![CDATA[<p>区块链是天然支持水平扩容的系统，节点扩展能力首屈一指。</p><img src="1.png" width="50%"><p>但区块链的垂直扩展能力还是一个经常被讨论的课题。单个节点的硬盘容量总是有限，如果节点拥有全部的数据，对单机性能要求会比较高；如果节点没有全量数据，就不能认为是 P2P 网络的节点之一。</p><p>最直接的办法就是用分布式数据库，数据库本身就支持扩容，区块链节点的存储模块就也算是支持扩容了。（如果区块链在立场上和数据库没有冲突的话。）</p><p>这里描述一种简单的实现思路的设想。</p><p>节点的垂直扩展，是想用多个节点合力代替原有的一个节点的位置，整体形式上一个集群提供了和单个节点一样的输入输出。</p><img src="2.png" width="80%"><p>节点完全可以将块数据分散储存在不同的子节点上，比如按照数据库分库分表的经典思路，对块号取模，或者随机分发也行。</p><img src="3.png" width="50%"><p>节点可以区分为索引节点和存储节点，索引节点只记录块号和子节点的对应关系，子节点集群就作为索引节点的储存模块。索引节点同时负责发送和接收块等操作。除了网络延迟带来的存取速度的降低，似乎没有大碍。</p><p>对于节点类型的问题，是必须要有不只一种类型的节点吗？有没有办法实现只要一份源代码、只有一种二进制程序、只用一种类型的节点，就能实现所有的功能？当然，不是说把三种类型的节点打包到一起就行了。由于功能侧重点的不同，尤其是节点 “身份” 的不同，可能节点不得不区分类型。一个节点对外提供能力和一个集群合作对外提供能力，集群内的节点和单个节点应该是一样的地位吗？</p><p>将块数据分散开后，对于 “世界状态” 一类的数据，可以全部储存在索引节点上。</p><img src="4.png" width="50%"><p>如果状态数据也想扩容，同样可以只在索引节点上保留索引数据，然后将状态数据也分散到储存节点上。</p><img src="5.png" width="50%"><p>这样的方案也许过于简单了，万一行之有效呢。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;区块链是天然支持水平扩容的系统，节点扩展能力首屈一指。&lt;/p&gt;
&lt;img src=&quot;1.png&quot;</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>一种基于“自我中心主义”的共识机制</title>
    <link href="https://smallyu.net/2021/10/29/%E4%B8%80%E7%A7%8D%E5%9F%BA%E4%BA%8E%E2%80%9C%E8%87%AA%E6%88%91%E4%B8%AD%E5%BF%83%E4%B8%BB%E4%B9%89%E2%80%9D%E7%9A%84%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    <id>https://smallyu.net/2021/10/29/%E4%B8%80%E7%A7%8D%E5%9F%BA%E4%BA%8E%E2%80%9C%E8%87%AA%E6%88%91%E4%B8%AD%E5%BF%83%E4%B8%BB%E4%B9%89%E2%80%9D%E7%9A%84%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/</id>
    <published>2021-10-29T14:28:52.000Z</published>
    <updated>2025-06-19T03:30:33.145Z</updated>
    
    <content type="html"><![CDATA[<h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>“自我中心主义” 的含义是，对于每个人来说，世界的大小取决于他能够接触到多大的世界，世界很大，和我无关，世界很小，全和我有关。一个人认识的人、了解的事、接收到的信息，无论是否命中注定，一定是有限的，你不可能认识世界上所有的人，知道世界上所有的事。才学渊博、见多识广，又怎样呢。</p><p>在区块链中，共识机制是用来保证数据一致性的关键手段，也给区块链带来了最核心的去中心化的特点。共识机制是强一致性的，或者是在拥有一定容错能力的情况下，达到大多数一致的效果。有没有一种可能，存在一种共识机制，不以数据一致为首要目标呢？</p><p>世界本就是复杂的，试图将所有节点同步到仅仅一种数据状态，其实是违反直觉的。而且，无论是不需要授权的大范围共识，还是基于身份授权的小范围共识，最终实现数据一致的方法，都是 “多点变单点”，也就是同一时间只有一个节点在处理数据，其他节点可能是在共识后接受满足条件的数据，也可能在确认数据前投票是否同意对数据的操作，总之都需要有一个 “英雄” 一样的节点，在关键的变更数据的时刻，做一些事情。</p><p>有的英雄实力强大，先斩后奏，改过数据后过来跟你说，“我改了数据”，你一开始不满意，但是接触后发现英雄确实厉害，能做出你没有解决的难题，于是你就认可了英雄的行动。</p><p>还有的英雄被公众授予权力，行动之前作为代表被选举出来，行动的时候会万分小心，挨个问民众，“改动这里的数据，你同意吗？” 如果大多数人同意，英雄就会行动。</p><p>当然，每个人都有平等地享有做英雄的机会，虽然有的人天生神力，有的人八面玲珑，但机会总还是有的，题放在那里，你做不出来，怪谁？每个人就可以被选举，别人不选你，怪谁？</p><p>所以，为什么我们不能做自己的英雄？为什么我们要屈就于别人的光环之下？每个人都是自己的英雄，在我们的世界里，在大小受限于个人接触范围的世界里。一种共识机制，节点的边界受限于其触及的网络规模。</p><h3 id="网络概况"><a href="#网络概况" class="headerlink" title="网络概况"></a>网络概况</h3><p>在非结构化的点对点网络中，路由表是必不可少的组成部分，节点能够接触到的网络大小，就取决于路由表中存着多少 “联系方式”。共识对数据的处理，就以路由表中的节点为依据，路由表中有 10 个节点，就争取和这 10 个节点达成一致，路由表中有 10,000 个节点，就和 10,000 个节点达成一致。也没有必要使用分布式路由表，就普通的数组就可以。在这种情况下，网络中的节点会是这种样子：</p><img src="21.png" width="40%"><p>以当前节点为中心，连接到的节点数量可多可少，有的很远，有的很近。弱水三千，只取一瓢。在路由发现的问题上，节点也是需要种子地址的，比如节点启动的时候先解析种子地址的记录：</p><pre><code>lookup(&quot;seek.domain&quot;)     -&gt; 127.0.0.1    -&gt; 127.0.0.2</code></pre><p>然后依次请求解析出来的节点地址，去得到他们路由表中的内容，将其添加到自己的路由表。这其实是常规做法，不过这样有可能引起的后果是，节点会瞬间获取到整个网络的路由信息。这并非不好，只是感觉有点快了，我们认识一个人是需要时间的，和人交谈也是需要时间的，你无法同时和三个人交谈，或者无法同时听三个人说话。即使拿到了很多人的联系方式，也没办法 “多线程” 联系每个人。我们处理信息的 “带宽” 有限，节点也一样。我们甚至可以对网络发现的速度稍微做一点限制，比如串行处理路由表新增记录的动作，先与节点建立连接然后再添加信息。</p><p>让路由发现慢一点，似乎听起来不太正常，难道是想让网络处于不同步的状态吗？很多共识的瓶颈就在网络带宽上，就在协议交互的复杂上。如果降低节点间的交流成本，共识的容错能力也会随之降低。网络可能会被划分为不同的区域，可能形成大大小小的圈子。</p><img src="22.png" width="50%"><p>对于共识算法来说，脑裂是要尽可能避免的问题，但其实网络分割是再正常不过的事情，是自然存在的情况。我们人类的思想是分裂的，有可能是对立的，但是经过一些事件后又可能达成一致。所以在一个网络中出现割裂是完全允许的情形，形成的小规模网络可能是互联互通开放的，也可能是保守封闭与外界隔离的。重点是要有一种机制能够 “修正” 这种分裂，也就是在某种条件下，割裂的两个网络可以相互合并。</p><h3 id="同步数据"><a href="#同步数据" class="headerlink" title="同步数据"></a>同步数据</h3><h4 id="主动"><a href="#主动" class="headerlink" title="主动"></a>主动</h4><p>这里保留区块链创世块的概念，所有节点的第一个块内容是相同的。新加入网络的节点，会从创世块开始启动，此时其他节点的块高度已经有很多了。比如当前节点的块高度是 2，想要从网络中同步第 3 个块高度的内容，节点的路由表中有其他节点的地址，其他节点块高度均等于 3。当前节点会发起一个对块高度 3 的请求，依次到其他节点。</p><img src="1.png" width="50%"><p>请求过后，发现有 2 个节点块高度为 3 的块一样，块的内容一样、块哈希一样、前块哈希也一样，那当前节点就把这个多数节点都存在的块作为第 3 个块。</p><img src="2.png" width="50%"><p>如果请求的时候发现有的节点的块高度已经大于 3 了，那么是不是应该块高度最高的优先呢？如果考虑时间尺度的话，就会觉得事物发展是需要遵次序的，你不能跳过 3 岁直接过起 4 岁的人生，区块链的数据也应该有先后次序。况且，当前节点需要的块高度是 3，请求的块高度是 3，管你有没有其他高度的块？你的块高度再高，我就要 3 的，你说你多高有什么用？</p><p>那么如果请求过所有节点，发现每个节点的块都不一样呢？该信谁？</p><img src="3.png" width="50%"><p>总得挑一个吧。如果不能确定哪个节点或者哪个内容可信的话，就随机选吧。最好是选择最后一个请求的节点，因为错过的节点就已经错过了，此时最后一个节点是距离你最近的节点，并且在此之前你并不能判断，是否存在块内容相同的节点。所以在放弃之前的节点后，最后一个节点就是你不可以放手的选择。</p><p>对于主动请求块数据的情况，很关键的地方是，请求一定是按照路由表顺序依次进行的，在得到第 1 个节点的响应之前，绝不向第 2 个节点发起请求，做人不能太三心二意了。如果有节点就刻意加速、同时请求多组数据呢？其实也无伤大雅，毕竟只是同步数据，有的人喜欢快点，有的人喜欢慢点，有的人喜欢快生活，有的人享受慢生活。</p><h4 id="被动"><a href="#被动" class="headerlink" title="被动"></a>被动</h4><p>除了主动请求某一高度的块数据，节点也会收到其他节点的广播消息，比如当前节点的块高度是 2，收到了来自其他节点的内容分别是 3、4、4 的块。</p><img src="4.png" width="50%"><p>按大多数一致的原则，是不是应该选择内容是 4 的块放在自己的第 3 个块高度上？但是这样存在一个问题是，你无法预测自己会收到多少个块，没办法计算块内容的总量和占比。所以对于被动接收的块，可以以第一个收到的块内容为准。</p><p>人生的出场顺序很重要，如果正好需要的块高度是 3，接收到广播的块高度也是 3，那就它吧，遇到哪个算哪个。实在不行后面遇到更合适的再换。如果有节点很激进，为了自己的块能够被大范围接受，把同一个块标记为从 1 到很大块高度广播出去，就为了碰运气，让正好缺块的节点接收，那也就随他吧。因为节点在主动同步块的时候，是按照高度获取内容的，这种激进一点的做法并不能带来很好的收益。</p><h3 id="新增数据"><a href="#新增数据" class="headerlink" title="新增数据"></a>新增数据</h3><p>网络中的数据由谁产生？为了解决这个问题，可以先定义为，每个节点都可以产生数据。一种极端的情况是，每个人都只相信自己的数据，各玩各的，整个网络就变成单机版了。所以节点也需要将自己产生的数据散播出去，发送给其他节点。对于其他节点来说，就是 “被动同步数据” 的情况了。</p><h4 id="有节点需要"><a href="#有节点需要" class="headerlink" title="有节点需要"></a>有节点需要</h4><p>主动广播块数据分两种情况，一种是有节点正好需要块，你正好发送给他了。</p><img src="5.png" width="50%"><p>当前节点接收到内容的请求，新增了块高度为 4 的块，这时会直接把块持久化到主链上。接着开始对块高度为 4 的块进行广播，广播按照路由表依次进行，在广播结束之前，当前节点不会打包下一个块。广播开始后，有节点块高度为 3，说明你是第一个发送给他块高度 4 的节点，它一定会接收你的块，同时给你一个响应消息。在收到响应后你就可以知道，当前网络至少有一个节点接收了你的块，你可以继续处理下一个块了。当然，在路由表遍历结束之前，节点即使收到响应消息也不会停止这一轮广播，这是理所当然的，希望有更多节点可以接收块内容。</p><p>如果对方节点在收到块后，发现块内容是 5，前块哈希是 3，并不对应它自己的前块哈希 4，对方依然会接收这个块，并且依次替换自己之前的块，直到哈希一致。</p><img src="51.png" width="50%"><img src="52.png" width="50%"><p>这种机制有可能带来的风险是，接收到一个块，然后把整条链都替换掉了，这是非常严重的不能接受的开销。但确实存在这样的可能，你遇到了一个坏人，这个坏人乘虚而入，他的思想颠覆了你的人生观，让你误入歧途，六亲不认。倒是你需要反思一下，你的路由表里为什么会有这样的坏人。而且这样的坏人多吗？如果一个恶意节点用一个块替换了你的整条链，但是接下来会有很大概率有好人来把你的整条链置换到大多数一致的情况。</p><p>这里暴露出了一点问题，接收到第一个块就认可，是不是太草率了？如果是坏人怎么办？为了增加节点作恶的难度，在接收到块内容为 5 发现前块哈希对不上的时候，应该不止向第一条链请求块内容，而是走完整的 “主动同步数据” 的逻辑，根据块高度把路由表里的所有节点都请求一遍。如果块内容为 5 的块，前块哈希和大多数节点不一致，就直接把 5 抛弃掉。如果一致，就说明它不是有害内容。</p><h4 id="没有节点需要"><a href="#没有节点需要" class="headerlink" title="没有节点需要"></a>没有节点需要</h4><p>节点新增块后，可能遇到没有节点需要当前块高度的情况，</p><img src="6.png" width="50%"><p>其他节点的块高度都大于等于广播出去的块。这种时候，当前节点就有必要做一点点妥协，为了让别人接收自己，为了让其他节点接收自己的块数据，只好先从其他节点同步数据，和其他节点保持一致。</p><img src="7.png" width="50%"><p>在块高度为 4 的块上，当前节点广播了一圈发现没有节点愿意接收这个块，那当前节点就把最后一个访问的节点的，当前块高度的块，请求过来。为什么是最后一个请求的节点？这里也可以走一遍完整的 “主动同步数据” 的流程，但为了提高效率，减少网络交互，可以先随意接收一个块内容，再做后续的判断。选择最后一个节点，是因为离得近。在错过了万丈红尘纷纷扰扰之后，恍然回首，发现最后一个节点是你此时最亲近的伙伴。</p><img src="8.png" width="50%"><img src="9.png" width="50%"><p>收到最后一个节点的块内容是 7 的块后，当前节点继续广播块内容为 5 的块。</p><p>如果不幸遇到了其他节点的块高度都远高于自己的情况，那说明自己确实落后了，先把其他节点的内容都同步过来再说。想要创新，想要新增内容，至少要先到达某一种顶端，</p><img src="10.png" width="50%"><p>不一定是整个网络的顶端，至少是某种圈子的顶端。</p><h3 id="交换数据"><a href="#交换数据" class="headerlink" title="交换数据"></a>交换数据</h3><h4 id="类型"><a href="#类型" class="headerlink" title="类型"></a>类型</h4><p>在目前的机制下，网络可能是混乱、不同步的。虽然对数据同步的速度预设是慢的，但如果有的节点就喜欢快呢，用快的计算、大的网络带宽，就是要达到整个网络的最前沿。</p><img src="33.png" width="50%"><p>也就是大多数节点慢，少数节点快的情况。每个节点都是按照块高度平行更新内容的，也都是按照块高度广播内容的，在一定程度上会缓解这种问题。你想快就快，和我们没有关系，我们慢的自成一派，我们遵循大多数一致的原则，不是谁块高度高就听谁的。你想内卷就尽力去卷，我们不跟你玩。</p><img src="32.png" width="50%"><p>另一种是一半节点慢，一半节点快的情况，也没有什么好担心的，最坏就是形成两个网络，无关痛痒。</p><img src="31.png" width="50%"><p>至于少数节点慢，多数节点快，属于最正常的情况了。</p><h4 id="融合"><a href="#融合" class="headerlink" title="融合"></a>融合</h4><p>在一半节点慢，一个节点快的情况下，很容易造成这样数据对立的情况，即使块高度一致，也是两种数据。</p><img src="34.png" width="50%"><img src="35.png" width="50%"><p>这个时候就不得不有一方妥协了。如果两个网络想要融合，就必须有一方做出一些牺牲。在块高度一致的情况下，假设路由表互通，产生新的块数据后，其实就是 “主动广播数据” 的过程，当前节点先产生一个块：</p><img src="36.png" width="50%"><p>例如最后一个节点在收到块后，发现前块哈希和自己的对不上：</p><img src="37.png" width="50%"><p>就去其他节点请求上一个块高度的内容：</p><img src="38.png" width="50%"><img src="39.png" width="50%"><p>发现上上个块的哈希对不上：</p><img src="40.png" width="50%"><p>继续请求其他节点对应块高度的内容；</p><img src="41.png" width="50%"><img src="42.png" width="50%"><p>依次类推，直到整条链完全相同。</p><p>被替换掉的块内容可以放到一个缓存队列，作为新块的内容，继续向外广播，减少节点内容的丢失。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这是一种没有经过实践考验的、也无法简单用公式来建模的共识机制的设想，共识以自身立场为出发点，关心自己如何应对网络中其他节点的不同行为，而不是从整个网络的角度 “上帝式” 地设计交互协议。这样的机制会给网络带来不确定性，但也会带来很多可能性。我们只能考虑节点基本的行为规则，就像我们学习生活规则一样，我们很难预测整个网络的走向，就像我们无法预测世界会向什么趋势发展。这种共识机制并不是而且也许不能解决特定的问题，比如建立电子现金系统或者提供图灵完备的运行平台，它关注在更基础一点的层面，提供一种实现数据一致性的方法和思路。</p><p>计算机通过网络组成的虚拟世界，一定也很精彩。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;简介&quot;&gt;&lt;a href=&quot;#简介&quot; class=&quot;headerlink&quot; title=&quot;简介&quot;&gt;&lt;/a&gt;简介&lt;/h3&gt;&lt;p&gt;“自我中心主义”</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="共识机制" scheme="https://smallyu.net/tags/%E5%85%B1%E8%AF%86%E6%9C%BA%E5%88%B6/"/>
    
  </entry>
  
  <entry>
    <title>链表常见算法题及解析</title>
    <link href="https://smallyu.net/2021/10/27/%E9%93%BE%E8%A1%A8%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E9%A2%98%E5%8F%8A%E8%A7%A3%E6%9E%90/"/>
    <id>https://smallyu.net/2021/10/27/%E9%93%BE%E8%A1%A8%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95%E9%A2%98%E5%8F%8A%E8%A7%A3%E6%9E%90/</id>
    <published>2021-10-27T03:04:20.000Z</published>
    <updated>2025-11-09T09:22:13.536Z</updated>
    
    <content type="html"><![CDATA[<p>目录：</p><ul><li><a href="#%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8">翻转链表</a></li><li><a href="#%E5%88%A4%E6%96%AD%E9%93%BE%E8%A1%A8%E6%98%AF%E5%90%A6%E6%9C%89%E7%8E%AF">判断链表是否有环</a></li><li><a href="#%E9%93%BE%E8%A1%A8%E5%A6%82%E6%9E%9C%E6%9C%89%E7%8E%AF%EF%BC%8C%E6%89%BE%E5%88%B0%E7%8E%AF%E7%9A%84%E8%B5%B7%E7%82%B9">链表如果有环，找到环的起点</a></li><li><a href="#%E5%88%A4%E6%96%AD%E4%B8%A4%E4%B8%AA%E9%93%BE%E8%A1%A8%E6%98%AF%E5%90%A6%E7%9B%B8%E4%BA%A4">判断两个链表是否相交</a></li><li><a href="#%E9%93%BE%E8%A1%A8%E5%A6%82%E6%9E%9C%E7%9B%B8%E4%BA%A4%EF%BC%8C%E6%89%BE%E5%88%B0%E4%BA%A4%E7%82%B9">链表如果相交，找到交点</a></li><li><a href="#%E5%90%88%E5%B9%B6%E4%B8%A4%E4%B8%AA%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8">合并两个有序链表</a></li></ul><br><h2 id="翻转链表"><a href="#翻转链表" class="headerlink" title="翻转链表"></a>翻转链表</h2><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>对于一个这样的链表：</p><img src="4.png" width="50%"><p>希望经过函数处理后，变成这样：</p><img src="5.png" width="50%"><h3 id="链表结构定义"><a href="#链表结构定义" class="headerlink" title="链表结构定义"></a>链表结构定义</h3><p>节点的定义为：</p><pre><code class="go">type Node struct &#123;    Value int    Next  *Node&#125;</code></pre><p>构造链表方法为：</p><pre><code class="go">func createLinkedList(n int) *Node &#123;    head := &amp;Node&#123;Value: 0&#125;    node := head    for i := 0; i &lt; n; i++ &#123;        if i &lt; n &#123;            node.Next = &amp;Node&#123;Value: i + 1&#125;        &#125;        node = node.Next    &#125;    return head&#125;</code></pre><p>函数会返回一个链表的指针。使用指针而不是结构体类型是因为，Go 语言的某些关于变量的设计，无法使用 <code>Node&#123;&#125; == nil</code> 的形式判断变量是否为空，因为理论上 <code>Node&#123;&#125;</code> 不是 <code>nil</code>。这就造成了如果使用<code>Node&#123;&#125;</code>作为链表头部的变量类型，在遍历的时候找不到一个合理的结束时机，只能使用类似 <code>Node&#123;&#125;.Next == nil</code> 这样的形式，还会遗漏掉最后一个节点。</p><h3 id="迭代翻转链表"><a href="#迭代翻转链表" class="headerlink" title="迭代翻转链表"></a>迭代翻转链表</h3><p>这里不能使用直接改变节点值的方式，比如遍历一次后把链表节点的值按照顺序储存到数组中，然后再遍历一次，一次修改链表节点的值。这个违背了数据结构的意义。可以使用递归完成翻转链表的操作。</p><img src="6.png" width="50%"><p>比如第一个节点，使用 <code>temp</code> 变量储存翻转前的下一个节点的位置，然后把 <code>head.Next</code> 指向翻转后应该有的节点位置，第一个节点的下一个节点是<code>空节点</code>，第二个节点的下一个节点是<code>节点 1</code>。完成 <code>head.Next </code>的指向后，<code>head</code> 要指向 <code>temp</code> 也就是原来的下一个节点用以完成遍历。这时还需要要用一个 <code>curr</code> 变量来储存<code>head</code> 跳转前的位置，方便下一次 <code>head.Next</code> 指向上一个节点的位置。这应该是一个简单的过程。</p><pre><code class="go">func reverseLinkedList(head *Node) *Node &#123;    curr := new(Node)    for head != nil &#123;        temp := head.Next        head.Next = curr        curr = head        head = temp    &#125;    return curr&#125;</code></pre><ul><li>时间复杂度：O(n)</li><li>空间复杂度：O(1)</li></ul><h3 id="执行"><a href="#执行" class="headerlink" title="执行"></a>执行</h3><p>执行程序后结果和预期一致：</p><pre><code class="go">func main() &#123;    head := createLinkedList(4)    head = reverseLinkedList(head)    for head != nil &#123;        fmt.Println(head.Value)        head = head.Next    &#125;&#125;</code></pre><p><br><br></p><h2 id="判断链表是否有环"><a href="#判断链表是否有环" class="headerlink" title="判断链表是否有环"></a>判断链表是否有环</h2><h3 id="问题-1"><a href="#问题-1" class="headerlink" title="问题"></a>问题</h3><p>链表有环是指链表中“最后”的一个节点，它的下一个节点指向了链表中位于它之前的节点。</p><img src="9.png" width="50%"><p>当程序遍历这个链表，会发现进入了死循环，永远找不到出口了。怎么判断一个链表，是否存在这样的环呢？</p><h3 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h3><p>常用的解决思路是双指针。设想一个在赛道上的场景，两个人 A 和 B 处于同样的起点，但是他们跑步的速度并不相同，A 的速度 v<sub>1</sub>&#x3D;1，B 的速度 v<sub>2</sub>&#x3D;2，也就是 B 比 A 快。在这样的情况下，只要他们不停，B 一定会超过 A 一圈然后再次追上 A，这是一种生活中的常识。</p><img src="7.png" width="50%"><p>在一个圈里，一快一慢的两个点一定会再次相遇，而且他们经过的路程是可以计算的，路程 s<sub>1</sub> 和 s<sub>2</sub> 应该满足这样的关系：</p><pre><code>s2 - s1 = nR</code></pre><p>R 是圆圈的周长，n 是正整数，他们位于出发点时 n&#x3D;0，第一次相遇的时候 B 比 A 多跑了一圈，多出了 1 倍周长的路程，n&#x3D;1。</p><p>和链表的情景相比较，赛道的场景还少了开始的一段距离，在进入赛道之前，A 和 B 会先从赛道外的小路进入赛道，然后再开始绕圈跑步。他们的起点在赛道外，为了便于计算，他们的速度从始至终不发生变化，那么当他们进入赛道之后，就已经不是同样的起点了。</p><img src="8.png" width="50%"><p>在这种情况下，他们经过的路程 s<sub>1</sub> 和 s<sub>2</sub> 还有规律可循吗？设圆形赛道外的直道距离为 d，相比上面的关系式，他们在圆圈内的路径依然满足 n 倍的周长 R，只不过现在的表达式不同了：</p><pre><code>(s2 - d) - (s1 - d) = nR    s2 - d - s1 + d = nR            s2 - s1 = nR</code></pre><p>结果表达式在相互抵消路径 d 之后，和之前的一样。</p><p>A 的路程 s<sub>1</sub>&#x3D;v<sub>1</sub>t，B的路程 s<sub>2</sub>&#x3D;v<sub>2</sub>t，时间 t 是一样的，速度 v<sub>1</sub> 和 v<sub>2</sub> 是已知的 1 和 2，有：</p><pre><code>    s2 - s1 = nR  v2t - v1t = nR     2t - t = nR          t = nR</code></pre><p>取 n &#x3D; 1，<code>t = R</code>。</p><h3 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h3><p>回到链表的问题，其实我们只要用快慢指针就可以判断链表是否有环了，并不需要知道他们具体相遇的点在哪儿，不过计算路径关系的公式可以辅助我们验证结果的正确性。</p><p>回到这个链表，用两个指针 A 和 B 从节点 1 分别以速度 1 和 2 出发：</p><img src="9.png" width="50%"><p>他们的位置关系将会是：</p><table><thead><tr><th>时间 t</th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th></tr></thead><tbody><tr><td>A 的位置</td><td>节点 1</td><td>节点 2</td><td>节点 3</td><td>节点 4</td><td><strong>节点 5</strong></td></tr><tr><td>B 的位置</td><td>节点 1</td><td>节点 3</td><td>节点 5</td><td>节点 3</td><td><strong>节点 5</strong></td></tr></tbody></table><p>在第 4 个时间点的时候，A 和 B 相遇了，环的周长正好等于 4，满足 <code>t = R</code> 的关系。</p><p><br><br></p><h2 id="链表如果有环，找到环的起点"><a href="#链表如果有环，找到环的起点" class="headerlink" title="链表如果有环，找到环的起点"></a>链表如果有环，找到环的起点</h2><h3 id="问题-2"><a href="#问题-2" class="headerlink" title="问题"></a>问题</h3><p>这个问题是上一个问题的延伸，在判断链表已经有环的基础上，找到环的起点。比如这样的一个链表，环的起点是节点 3。</p><img src="20.png" width="50%"><h3 id="分析-1"><a href="#分析-1" class="headerlink" title="分析"></a>分析</h3><p>（1）</p><p>在判断链表是否有环的问题中，我们得到了一个至关重要的结论：</p><pre><code>t = R</code></pre><p>两个快慢指针将会在等于环长度的时间点相遇。对于上图的链表，快慢指针的位置关系是这样：</p><table><thead><tr><th>时间 t</th><th>0</th><th>1</th><th>2</th><th>3</th><th>4</th><th>5</th><th>6</th></tr></thead><tbody><tr><td>A 的位置</td><td>节点 1</td><td>节点 2</td><td>节点 3</td><td>节点 4</td><td>节点 5</td><td>节点 6</td><td><strong>节点 7</strong></td></tr><tr><td>B 的位置</td><td>节点 1</td><td>节点 3</td><td>节点 5</td><td>节点 7</td><td>节点 3</td><td>节点 5</td><td><strong>节点 7</strong></td></tr></tbody></table><p>我们可以观察到，环的长度是 6，快慢指针也会在第 6 秒相遇，他们交点位置是节点 7:</p><img src="21.png" width="50%"><p>（2）</p><p>根据上面提到的之前的结论，按照慢指针 v<sub>1</sub> &#x3D; 1 的速度，它经过的路程和时间是一样的，也就是说，从出发点到两指针相遇的路径长度，根据 <code>t = R</code>，此刻的时间是 t，正好是环的长度 R：</p><img src="22.png" width="50%"><p>（3）</p><p>做一个假设，慢指针保持着这个长度为 R 的走过的路径，向前移动一步，会变成这样：</p><img src="23.png" width="50%"><p>再走一步，变成了这样：</p><img src="24.png" width="50%"><p>（4）</p><p>到这里似乎还不知道我们要干什么。现在对路径设一个变量，从 <code>出发点</code> 到 <code>环的起点</code> 之间的距离设为 l<sub>1</sub>，整个链表的长度设为 l，环的长度仍然为 R。</p><img src="25.png" width="50%"><p>这 3 个变量将满足这样的关系：</p><pre><code>l - l1 = R</code></pre><p>这是太显而易见的事情。</p><p>（5）</p><p>记得我们一开始的结论吗？从 <code>出发点</code> 到 <code>快慢指针的交点</code> 之间的距离，等于环的长度 R：</p><img src="22.png" width="50%"><p>变量 l 和 l<sub>1</sub> 保持不变，图就成了这样：</p><img src="26.png" width="50%"><p>此时的 l 仍然等于 <code>l1 + R</code>，不同的是，<code>l1</code> 和 <code>R</code> 重合了。</p><p>（6）</p><pre><code>l - l1 = R</code></pre><p>重合之后，等式关系还成立吗？当然成立，因为整个链表没有变，变量的大小没有变。但好像又觉得哪里奇怪。</p><p>现在新设一个变量，设从 <code>快慢指针的交点</code> 到 <code>环的起点</code> 的距离为 l<sub>2</sub>：</p><img src="27.png" width="50%"><p>此时：</p><pre><code>l - l2 = R</code></pre><p>（7）</p><p>经过这样一些比较，发现 <code>l1 == l2</code>，也就是从 <code>出发点</code> 到 <code>环的起点</code> 的距离，等于 <code>快慢指针的交点</code> 到 <code>环的起点</code> 的距离。</p><h3 id="解决-1"><a href="#解决-1" class="headerlink" title="解决"></a>解决</h3><pre><code>出发点 -&gt; 环的起点 == 快慢指针的交点 -&gt; 环的起点</code></pre><p>这是一个很重要的结论，因为我们此时的快慢指针就在 <code>快慢指针的交点</code> 上，在节点 7 的位置。</p><p>如果这个时候在新增一个指针 p<sub>3</sub>，在快慢指针相交的时刻，从整个链表的 <code>出发点</code> 1 出发（速度为 1），那么 p<sub>3</sub> 和慢指针一定会相交，因为 p<sub>3</sub> 到 <code>环的起点</code> 的距离等于慢指针到 <code>环的起点</code> 的距离。p<sub>3</sub> 遇到慢指针的位置，就是环的起点。</p><img src="28.png" width="50%"><p><br><br></p><h2 id="判断两个链表是否相交"><a href="#判断两个链表是否相交" class="headerlink" title="判断两个链表是否相交"></a>判断两个链表是否相交</h2><h3 id="问题-3"><a href="#问题-3" class="headerlink" title="问题"></a>问题</h3><p>存在两个链表，分别在某一个节点指向了同一个节点作为下个节点：</p><img src="10.png" width="50%"><p>这里有两个链表：</p><pre><code>1 -&gt; 2 -&gt; 3 -&gt; 4     5 -&gt; 3 -&gt; 4</code></pre><p>怎么判断两个链表是否相交？</p><h3 id="分析-2"><a href="#分析-2" class="headerlink" title="分析"></a>分析</h3><p>一种简单的做法是，分别遍历每条链表到最后一个节点，判断最后一个节点是否相同。如果两个链表在中间节点相交，则最后一个节点一定相同。</p><p><br><br></p><h2 id="链表如果相交，找到交点"><a href="#链表如果相交，找到交点" class="headerlink" title="链表如果相交，找到交点"></a>链表如果相交，找到交点</h2><h3 id="问题-4"><a href="#问题-4" class="headerlink" title="问题"></a>问题</h3><p>对于这样两个链表：</p><img src="31.png" width="50%"><img src="32.png" width="50%"><p>如何找到第一个交点 3 ？</p><h3 id="分析-3"><a href="#分析-3" class="headerlink" title="分析"></a>分析</h3><p>一种简单的解决思路是，把这个链表的尾节点和任意一个链表的头节点连起来：</p><img src="33.png" width="50%"><p>可以是链表 1 的尾节点到链表 2 的头节点，或者链表 2 的尾节点到链表 2 的头节点，总之连起来以后，问题就转变成了，找到链表环的起点。</p><p><br><br></p><h2 id="合并两个有序链表"><a href="#合并两个有序链表" class="headerlink" title="合并两个有序链表"></a>合并两个有序链表</h2><h3 id="问题-5"><a href="#问题-5" class="headerlink" title="问题"></a>问题</h3><img src="11.png" width="50%"><p>给出两个有序链表，将两个链表合并为一个有序链表。</p><h3 id="分析-4"><a href="#分析-4" class="headerlink" title="分析"></a>分析</h3><p>思路暴力简单，同时迭代两个链表，按照顺序依次合并就可以了。控制好边界条件。</p><img src="12.png" width="50%"><img src="13.png" width="50%"><h3 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h3><p>node 结构定义：</p><pre><code class="go">type Node struct &#123;    Value int    Next  *Node&#125;</code></pre><p>构建两条链表：</p><pre><code class="go">func main() &#123;    root1 := &amp;Node&#123;        Value: 1,    &#125;    root1.Next = &amp;Node&#123;        Value: 1,    &#125;    root1.Next.Next = &amp;Node&#123;        Value: 3,    &#125;    root1.Next.Next.Next = &amp;Node&#123;        Value: 5,    &#125;    root2 := &amp;Node&#123;        Value: 1,    &#125;    root2.Next = &amp;Node&#123;        Value: 2,    &#125;    root2.Next.Next = &amp;Node&#123;        Value: 4,    &#125;    root := merge(root1, root2)    for root != nil &#123;        fmt.Println(root.Value)        root = root.Next    &#125;&#125;</code></pre><p>合并链表：</p><pre><code class="go">func merge(root1 *Node, root2 *Node) *Node &#123;    var root *Node    var temp *Node    if root1.Value &lt;= root2.Value &#123;        root = root1        temp = root2    &#125; else &#123;        root = root2        temp = root1    &#125;    p1 := root    p2 := p1.Next    for &#123;        if p2 == nil || temp == nil &#123;            break        &#125;        if p2.Value &lt;= temp.Value &#123;            p1.Next = p2            p1 = p1.Next            p2 = p2.Next        &#125; else &#123;            p1.Next = temp            p1 = p1.Next            temp = temp.Next        &#125;    &#125;    return root&#125;</code></pre>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8&quot;&gt;翻转链表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a</summary>
        
      
    
    
    
    
    <category term="链表" scheme="https://smallyu.net/tags/%E9%93%BE%E8%A1%A8/"/>
    
  </entry>
  
  <entry>
    <title>基于 Multi-Linked List 的区块链设想</title>
    <link href="https://smallyu.net/2021/10/17/%E5%9F%BA%E4%BA%8EMulti-Linked-List%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E8%AE%BE%E6%83%B3/"/>
    <id>https://smallyu.net/2021/10/17/%E5%9F%BA%E4%BA%8EMulti-Linked-List%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E8%AE%BE%E6%83%B3/</id>
    <published>2021-10-17T03:27:17.000Z</published>
    <updated>2025-11-09T09:22:13.413Z</updated>
    
    <content type="html"><![CDATA[<p>区块链有没有可能不再是线性的结构，而是有多条链路的、图式的数据结构呢，比如从 singly-linked list 进化到 multi-linked list。假设是 multi-linekd list 的形式，会给区块链带来什么样的变化，或者说，可行吗？</p><p>首先是正向的 multi-linked list，像是这样：</p><img src="1.png" width="50%"><p>然后是适用于区块链的、反向的 multi-linked list，像这样：</p><img src="2.png" width="50%"><p>对于 multi-lineked list，一种是固定数量的情况，比如每个节点的后续节点一定是 2 个，或者是不固定数量的 multi-linked list，每个节点可以指向任意多个子节点，它们的区别不大。</p><p>还有一个分歧是要不要保留块高度的概念，块高度是用来表示块顺序的索引，在多个节点之间同步数据的时候起到很重要的作用，在 multi-linked list 中，如果按照树结构层数来定义块高度，会是这样：</p><img src="3.png" width="50%"><p>或者可以按照块写入的顺序定义，也就是初始的标号意味着块高度：</p><img src="2.png" width="50%"><p>对于区块链来说，很重要的是确认数据的完整性，当只有一个子节点的时候，其他节点很容易同步一个块的数据，并且根据 hash values 校验块的有效性。对于 multi-linked list 的情形，这个校验块数据的步骤会复杂一些，但也并非无法处理，只要把原先校验一个块的操作，多几次循环，多校验几次就可以了。</p><p>正向和反向 multi-linked list 最大的区别是，一种是向外发散的，会随着层级的增大，每一层容纳的节点数不断增大，指数级增长。另一种是收敛的，每一层的节点数会不断减少，最终只剩下一个。也就是正着的树和倒着的树的区别。</p><img src="6.png" width="50%"><p>区块链中有一个创始块的概念，如果是正向的 multi-linked list，创世块还是一个保持不变，但是子节点逐渐膨胀以后，会越来越不可控。对于保留块高度的情况，其实除了程序处理多几个步骤，并没有无法实现的理由。对于不保留块高度或者说用索引值代替块高度的情况，块仍然可以一个一个地出，也没有在技术上实现不了的理由，但其实还是链式结构的处理方式。单纯数据结构从 linked list 转为 multi-linked list，似乎没有明确的理由，因为无法想象到，这样做可以带来什么样的好处。</p><img src="7.png" width="50%"><p>如果是反向的 multi-linked list，一个很大的问题是创世块怎么办，反向的树结构子节点会越来越少，最后只剩一个，难不成创世块规定足够大数量的节点数，然后后期逐步趋于1？这非常不合理，相当于给程序设置了一个停机条件，到达一定块高度后就无法在增加内容了，显然是不可取的做法。</p><p>由于树结构的发散或者收敛，multi-linked list 的设想是有问题的。那么这样呢，使用平行的 multi-linked list，可行吗？</p><img src="4.png" width="50%"><p>因为必须有多个输入和输出，并且输入和输出的数量必须一样，所以使用这样带有重复指向的方式。</p><p>同样地，在技术上可行，问题在于，有什么好处呢？这样的数据结构带来了什么？除了程序复杂程度的增加，还有什么？</p><p>好像还真有，这样的数据结构，允许同时出两个块。两个节点只要使用相同的父节点，即使没有先后，同一时刻产生了内容，也可以将块累加到链后面，作为子节点。</p><p>不过这样又回带来很多问题，比如怎么防止 double-spending ？同一个账户交易分别出现在了两个块里，以哪个为准？分布式系统的数据同步，一定是多点变单点的过程，并行处理的程序也会有一个资源竞争的问题，同时出两个块后还是需要某种机制单点处理数据，同时出块就没有意义了，并不能加快整个系统的数据处理速度。</p><p>而且，其实平行的 multi-linked list，可以认为是这样：</p><img src="5.png" width="50%"><p>这样来看，平行的 multi-linked list 就更没有意义了。</p><p>总的来说，区块链有可能基于 multi-linked list 的数据结构吗？似乎没有必要。singly-linked list 是简单的数据结构，但又确实是很适合于区块链的数据结构。</p><p><br><br></p><h3 id="背景知识"><a href="#背景知识" class="headerlink" title="背景知识"></a>背景知识</h3><h4 id="Singly-linked-list"><a href="#Singly-linked-list" class="headerlink" title="Singly-linked list"></a>Singly-linked list</h4><img src="10.png" width="50%"><p>Singly-linked list 是一种线性的数据结构， 每个节点都包含一个指向下个节点的 field，用于显式表示节点之间的关联关系，一般在程序中会用对象的引用地址来填充这个字段。当然直接使用节点的值也可以，只是为了在数据类型上有明显的区分。</p><pre><code class="go">type Node struct &#123;    Value int    Next  *Node&#125;func main() &#123;    node1 := Node&#123;Value: 1&#125;    fmt.Printf(&quot;%p\n&quot;, &amp;node1) // 0x14000104210    node2 := Node&#123;Value: 2&#125;    fmt.Printf(&quot;%p\n&quot;, &amp;node2) // 0x14000104220    node1.Next = &amp;node2    fmt.Println(node1) // &#123;1 0x14000104220&#125;    fmt.Println(node2) // &#123;2 &lt;nil&gt;&#125;&#125;</code></pre><p>node1 的内存地址为 <code>0x14000104210</code> ，node2 的内存地址为 <code>0x14000104220</code> ，然后将 node2 的内存地址赋值给 node1 的 next 变量，这个时候只要访问 node1，就可以通过 next 得到 node2 的内容了。以此类推，即使有很多很多 node，只需要知道起始位置 node1 的内容，就可以访问到整个 Linked list 的所有节点了。</p><h4 id="Doubly-linked-list"><a href="#Doubly-linked-list" class="headerlink" title="Doubly-linked list"></a>Doubly-linked list</h4><img src="11.png" width="50%"><p>Doubly-linked list 在 Singly-lined list 的基础上，多了一个字段，用于保存指向上一个节点的信息。在这样的数据结构下，获得一个节点的内容时，不但可以知道下一个节点的位置，还可以知道上一个节点的位置。</p><pre><code class="go">type Node struct &#123;    Value int    Next  *Node    Prev  *Node&#125;</code></pre><h4 id="Doubly-linked-list-Singly-linked-list"><a href="#Doubly-linked-list-Singly-linked-list" class="headerlink" title="(Doubly-linked list) - (Singly-linked list)"></a>(Doubly-linked list) - (Singly-linked list)</h4><p>Singly-linked list 时保留了下一个节点的信息，Doubly-linked list 上一个和下一个节点的信息都保留了，那么有没有一种数据结构，是只保留上一个节点的信息呢？比如这样：</p><pre><code class="go">type Node struct &#123;    Value int    Prev  *Node&#125;</code></pre><p>为什么会需要以保留上一个节点信息的形式，构造 Linked list 呢？因为存在这样一种场景：在创建当前节点的时候，下一个节点的内容和引用地址还不确定或者不存在。</p><p>当下一个节点内容确定后，还要更改上一个节点的内容吗？Demo 代码改起来是容易的，如果是在一个海量数据的数据库中呢？update 的成本是很高的；如果是在一个分布式系统中呢？网络交互、数据一致性的成本也很高。</p><img src="12.png" width="50%"><p>也许这样的数据结构并不差劲，它既没有牺牲 Linked list 的特性，又可以在不改动之前节点数据的基础上，使列表不断延长。只不过访问顺序和 Singly-lined list 是相反的，需要按照从最后一个节点向前的顺序才能遍历所有节点，就像是 Singly-linked list 倒过来了。其实倒过来的 Singly-linked list 也是 Singly-linked list。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;区块链有没有可能不再是线性的结构，而是有多条链路的、图式的数据结构呢，比如从 singly-linked list 进化到 multi-linked list。假设是 multi-linekd list</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>理解哈希函数与序列化</title>
    <link href="https://smallyu.net/2021/10/13/%E7%90%86%E8%A7%A3%E5%93%88%E5%B8%8C%E5%87%BD%E6%95%B0%E4%B8%8E%E5%BA%8F%E5%88%97%E5%8C%96/"/>
    <id>https://smallyu.net/2021/10/13/%E7%90%86%E8%A7%A3%E5%93%88%E5%B8%8C%E5%87%BD%E6%95%B0%E4%B8%8E%E5%BA%8F%E5%88%97%E5%8C%96/</id>
    <published>2021-10-13T03:47:31.000Z</published>
    <updated>2025-11-09T09:22:13.461Z</updated>
    
    <content type="html"><![CDATA[<h3 id="Hash-function"><a href="#Hash-function" class="headerlink" title="Hash function"></a>Hash function</h3><p>Hash function 用于处理数据和其 hash values 的映射关系，hash values 是数据类似唯一标识的东西，可以用内存比较小的形式标识数据。hash function 有各种各样的实现，可以认为是一个黑盒子，进去的是 data，出来的是 hash values。</p><img src="7.png" width="50%"><p>比如，我们可以把字符的 ASCII 码作为字符的 hash values：</p><pre><code>HASH(&quot;a&quot;) = 97HASH(&quot;b&quot;) = 98HASH(&quot;c&quot;) = 99HASH(&quot;d&quot;) = 100</code></pre><p>对于 2 个字符的 data，就把两个字符的 ASCII 相加，作为 hash values：</p><pre><code>HASH(&quot;ab&quot;) = 97 + 98 = 195HASH(&quot;cd&quot;) = 99 + 100 = 199</code></pre><p>但是这样很容易发现存在问题，<code>HASH(&quot;ad&quot;) == HASH(&quot;bc&quot;) == 197</code>。对于 3 个、4 个甚至更多字符的情形，hash values 重复的可能性更大。</p><p>hash values 是允许重复的，但如果存在大量重复，hash function 也就失去了它的作用和使用场景：如果全部都一样，无法区分，还用 hash values 干嘛？</p><p>不幸的是，目前最好的 hash function 也无法避免 hash values 重复的问题，只能尽可能减少 hash values 重复的概率，比如用类似数据库分库分表的方式，给每个字符足够的余量。</p><p>我们可以重新设计一下我们的 hash function，在只有 1 个字符的时候，仍然使用 ASCII 作为输出。在有 2 个字符的时候，让 第 1 个字符乘以一个基数，再加上第 2 个字符。由于第 1 个字符在乘以基数后会足够大，无论第 2 个字符是什么，在其基础上加上第 2 个字符的 ASCII 码，应该不会重复。</p><pre><code>HASH(&quot;ab&quot;) = 97 * 1000 + 98 = 97098HASH(&quot;cd&quot;) = 99 * 1000 + 100 = 99100HASH(&quot;ad&quot;) = 97 * 1000 + 100 = 97100HASH(&quot;cd&quot;) = 98 * 1000 + 99 = 98099</code></pre><p>这样至少解决了 2 个字符 hash values 重复的问题。</p><p>推广到更一般的场景，在面对可能很多字符的情况下，基数使用质数以避免累加造成的重复，为了保证基数足够大，使用质数的不同次方分别作为每个字符的基数，公式为：</p><pre><code>hashCodes = char1 * base^(l-1) + char2 * base^(l-2) + ...</code></pre><p><code>hashCodes</code> 是输出的 hash values，<code>char1</code> 是第 1 个字符，<code>char2</code> 是第 2个字符，<code>base</code> 是基数，<code>l</code> 指字符串的长度。对于 3 个字符长度的字符串，第 1 个字符的基数就是质数的 2 次方，第 2 个字符的基数是质数的 1 次方，第 3 个字符是 0 次方，以此类推。</p><p>如果质数选择为 31，hash function 的实现为：</p><pre><code class="java">public static int hashCode(byte[] value) &#123;    int h = 0;    for(int i = 0; i &lt; value.length; ++i) &#123;        h = 31 * h + value[i];    &#125;    return h;&#125;</code></pre><p>也许具体的代码不是完全符合直觉，但你可以相信，和上面描述的公式是一致的。</p><pre><code>hashCode(&quot;a&quot;) = 97hashCode(&quot;ab&quot;) = 97 * 31 + 98 = 3105hashCode(&quot;abc&quot;) = 97*31^2 + 98*31 + 99 = 96354</code></pre><p>这就是 JDK (Java Development Kit) 中 hashCode 的实现方式。</p><h3 id="Cryptographic-hash-function-CHF"><a href="#Cryptographic-hash-function-CHF" class="headerlink" title="Cryptographic hash function (CHF)"></a>Cryptographic hash function (CHF)</h3><p>不难发现的是， hash function 比较容易根据 hash values 反推出原始的 data 是什么。我们可以写出这样的程序，假设我们已经知道字符长度是 2，由于字符使用 ASCII 编码，范围在 0 ~ 255，因此设 x 和 y 两个变量，枚举所有符合目标 hash values 的情况：</p><pre><code class="java"> public static String deHashCode(int code) &#123;    for (int x = 0; x &lt;= 255; x++) &#123;        int y = code - 31 * x;        if (y &lt; 0 || y &gt; 255) &#123;            continue;        &#125;        System.out.println(((char) x)+&quot;,&quot;+((char) y));    &#125;    return &quot;&quot;;&#125;</code></pre><p>比如当 <code>hashCode = 3105</code>，得到的输出是：</p><pre><code>\,ý],Þ^,¿_, `,a,bb,Cc,$d,</code></pre><p>原始数据 <code>ab</code> 就出现在了为数不多不多的可能性中。</p><p>那么有没有办法减少 hash values 推出原始 data 的方法？在 Public-key cryptography 中 <code>%</code> 可是起到了很大的作用。hash function 也可以与一些加密算法的原理结合。</p><p>cryptographic 是 hash function 的修饰词，即使用了加密算法的 hash function。</p><img src="8.png" width="50%"><p>md5 是使用非常广泛也接近过时的一种 cryptographic hash function，可以把任意长度的 data 计算输出为 128 bit 的 hash values。</p><pre><code>md5(&quot;a&quot;) = 0cc175b9c0f1b6a831c399e269772661md5(&quot;ab&quot;) = 187ef4436122d1cc2f40dc2b92f0eba0</code></pre><p>md5 的加密原理步骤很多，是一种不可逆的、单向的 hash function，无法轻易根据 hash values 得到 data。md5 的输入可以是任意大小的，1 GB 的二进制文件也可以hash 为 128 bit 的字符串。</p><p>md5 之外，SHA-1 的安全性更高，BLAKE2 的计算速度更快，它们都是典型的 cryptographic hash function。</p><br><h3 id="Serialization"><a href="#Serialization" class="headerlink" title="Serialization"></a>Serialization</h3><p>序列化是编程中很常见的一种操作，主要用于把复杂格式的数据转化成易于在不同环境中统一处理的格式，类似于定义一种接口格式，便于网络传输。</p><img src="9.png" width="50%"><p>把数据转换为统一的过程称为 serialization，从统一格式转换为特殊格式的过程为 deserialization。JSON stringify 的过程也可以认为是一种序列化：</p><pre><code class="javascript">let object = &#123;    field1: &quot;abc&quot;,    field2: 123&#125;let str = JSON.stringify(object)    print(str)    // &#123;&quot;field1&quot;:&quot;abc&quot;,&quot;field2&quot;:123&#125;</code></pre><h3 id="Serialization-CHF"><a href="#Serialization-CHF" class="headerlink" title="Serialization + CHF"></a>Serialization + CHF</h3><p>可以明确的是，JSON stringify 的结果是一个字符串，这个时候就可以和之前的 cryptographic hash function 结合起来用了：</p><pre><code>md5(str) = d79152b724c5f1e52e6bd4bfaf6e1532</code></pre><p>只要定义过数据的 serialization 方法，我们就可以得到任意数据格式的 hash values。</p><h3 id="Serialization-CHF-Linked-List"><a href="#Serialization-CHF-Linked-List" class="headerlink" title="Serialization + CHF + Linked List"></a>Serialization + CHF + Linked List</h3><p>Linked list 之间的关联关系常用变量的引用地址表示，但指针不是惟一的方式，数据结构的含义也可以扩展到更大的范围。我们完全可以用节点数据的 hash values 作为关联：</p><img src="10.png" width="50%"><p><code>98</code> 是 <code>b</code> 的 hash values，表明值为 <code>a</code> 的节点，下一个节点的 hash values 为 <code>98</code>，也就是值为 <code>b</code> 的节点。</p><p>我们也可有使用反向的 linked-list：</p><img src="11.png" width="50%"><p><code>a</code> 的 hash values 是 97，表明值为 <code>b</code> 的节点，上一个节点的 hash values 为 <code>97</code>。</p><p>当然，这里的值可以是更复杂的数据结构，只要定义好 serialization 格式，也可以应用到更复杂的 hash function 上，比如这样正向的 linked-list：</p><pre><code class="go">type Node struct &#123;    Value int    Next  string&#125;node1 = Node&#123; Value: &quot;a&quot; &#125;node1_str = JSON.stringify(node1)   // &#123; &quot;Value&quot;: &quot;a&quot; &#125;node1_hash = md5(node1_str)         // 9ad06e8a44d0daf821f110794fb012c7node1.Next = node1_hash</code></pre><p>这就构建好了一个节点，以此类推。</p><p>另一种也许更好或者更适用于某种特定场景的形式是，将其改为反向的 linked-list：</p><pre><code class="go">type Node struct &#123;    Prev string    Value int&#125;node1 = Node&#123; Value: &quot;a&quot; &#125;node1_str = JSON.stringify(node1)   // &#123; &quot;Value&quot;: &quot;a&quot; &#125;node1_hash = md5(node1_str)         // 9ad06e8a44d0daf821f110794fb012c7node2 = Node&#123; Value: &quot;b&quot; &#125;node2_str = JSON.stringify(node2)   // &#123; &quot;Value&quot;: &quot;b&quot; &#125;node2_hash = md5(node2_str)         // 7e332b78dbaac93a818a6ab639f5a71bnode2.Prev = node1_hash</code></pre><p>这种反向的 linked-list 就是区块链的基础数据结构。 </p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;Hash-function&quot;&gt;&lt;a href=&quot;#Hash-function&quot; class=&quot;headerlink&quot; title=&quot;Hash function&quot;&gt;&lt;/a&gt;Hash function&lt;/h3&gt;&lt;p&gt;Hash function 用于处理数据和其</summary>
        
      
    
    
    
    
    <category term="加密" scheme="https://smallyu.net/tags/%E5%8A%A0%E5%AF%86/"/>
    
  </entry>
  
  <entry>
    <title>联盟链比公有链差在哪儿</title>
    <link href="https://smallyu.net/2021/09/29/%E8%81%94%E7%9B%9F%E9%93%BE%E6%AF%94%E5%85%AC%E6%9C%89%E9%93%BE%E5%B7%AE%E5%9C%A8%E5%93%AA%E5%84%BF/"/>
    <id>https://smallyu.net/2021/09/29/%E8%81%94%E7%9B%9F%E9%93%BE%E6%AF%94%E5%85%AC%E6%9C%89%E9%93%BE%E5%B7%AE%E5%9C%A8%E5%93%AA%E5%84%BF/</id>
    <published>2021-09-29T06:27:43.000Z</published>
    <updated>2025-11-09T09:22:13.536Z</updated>
    
    <content type="html"><![CDATA[<p>中文语境下的 “公有链” 和 “联盟链” 并没有明确标准的定义。2018 年，美国国家标准与技术研究院（NIST, National Institute of Standards and Technology）在 <em><a href="http://vpb.smallyu.net/[Tech]%20blockchain/Blockchain%20Technology%20Overview%20-%20NIST.IR.8202.pdf">Blockchain Technology Overview</a></em> 中将区块链分为 Permissionless blockchain 和 Permissioned blockchain，但那样的分类方式并不严格对应公有链和联盟链。也许公有链和联盟链的区别在于节点网络规模的大小，也许区别在于区块链面向的范围是公共互联网还是私有局域网。无论是怎样的定义，我们至少可以大概区分出公有链和联盟链。</p><p>公有链和联盟链的好坏，不单纯在于技术或者某些评价指标的比较，也许会有人下意识地认为，公有链面对比联盟链更复杂的网络环境和用户体量，但其实技术上的差距总是有办法弥补，联盟链也有少数好于公有链的技术特性。</p><p>有一个段子《<a href="https://baike.baidu.com/item/%E7%9A%87%E5%B8%9D%E7%9A%84%E9%87%91%E9%8B%A4%E9%A0%AD/23725819">皇帝的金锄头</a>》：</p><blockquote><p>古代有两个老农民畅想皇帝的奢华生活，一个说：“我想皇帝肯定天天吃白面馍吃到饱！” 另一个说：“不止不止，我想皇帝肯定下地都用的金锄头！”</p></blockquote><p>联盟链就是在用区块链做传统行业的业务，甚至可以说是打着区块链的幌子到处骗钱。联盟链的问题就在于，格局小了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;中文语境下的 “公有链” 和 “联盟链” 并没有明确标准的定义。2018 年，美国国家标准与技术研究院（NIST, National Institute of Standards and Technology）在 &lt;em&gt;&lt;a</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    <category term="公有链" scheme="https://smallyu.net/tags/%E5%85%AC%E6%9C%89%E9%93%BE/"/>
    
    <category term="联盟链" scheme="https://smallyu.net/tags/%E8%81%94%E7%9B%9F%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>在 Dijkstra 算法中保存路径</title>
    <link href="https://smallyu.net/2021/09/18/%E5%9C%A8Dijkstra%E7%AE%97%E6%B3%95%E4%B8%AD%E4%BF%9D%E5%AD%98%E8%B7%AF%E5%BE%84/"/>
    <id>https://smallyu.net/2021/09/18/%E5%9C%A8Dijkstra%E7%AE%97%E6%B3%95%E4%B8%AD%E4%BF%9D%E5%AD%98%E8%B7%AF%E5%BE%84/</id>
    <published>2021-09-18T11:00:45.000Z</published>
    <updated>2025-11-09T09:22:13.317Z</updated>
    
    <content type="html"><![CDATA[<p>区块链的 Layer 2 中有一种 State Channels 的扩容方案，其中会需要搜索距离最近的路由节点。</p><h3 id="Dijkstra-算法思路"><a href="#Dijkstra-算法思路" class="headerlink" title="Dijkstra 算法思路"></a>Dijkstra 算法思路</h3><p>Dijkstra 算法能够解决 single-source 的最短路径问题，算法本身只输出一个点到其他点的最短距离。比如在这样一个图中，起点是 A，想知道到 D 点的最短距离是多少：</p><img src="g1.png" width="50%" /><p>Dijkstra 算法实质是动态规划的贪心算法的结合，要寻找最短路径，就去遍历所有的点，每到一个点更新最短距离的记录，直到走过所有的点，就可以确信拿到了可靠的最短距离的记录。初始化的状态集合为：</p><table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>0</td><td>-</td><td>-</td><td>-</td></tr></tbody></table><p>此时位于 A 点，未出发的状态，到自身的距离为 0，到其余点的距离未知。</p><p>从 A 点出发后，发现 A 点可以到达 B 点和 C 点，距离分别为 4 和 2，那么就更新状态集合为：</p><table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>0</td><td>-</td><td>-</td><td>-</td></tr><tr><td></td><td>4</td><td>[2]</td><td>-</td></tr></tbody></table><p>中括号的含义是在当前这一轮中距离最短的点，哪个距离最短，下一步就到哪个点。到 C 点的距离比到 B 点的距离短，所以下一轮到 C 点：</p><img src="g2.png" width="50%" /><p>到 C 点以后，发现 C 点可以到达 A、B、D 三个点，这个时候意识到，其实 A 点已经走过了，不会再往回走的。于是需要另一个集合记录走到过哪些点，以避免下一步重复。定义 <code>prev = []</code>，因为 A 和 C 已经走过了，就把这两个点放到集合里， <code>prev = [A, C]</code>。</p><p>在这一步的时候，到达 B 点的距离从 4 变成了 3，<code>A -&gt; C -&gt; B</code> 的距离小于 <code>A -&gt; B</code> 的距离，更新状态集合，同时因为已经能够到 D 点了，更新到 D 点的距离：</p><table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>0</td><td>-</td><td>-</td><td>-</td></tr><tr><td></td><td>4</td><td>[2]</td><td>-</td></tr><tr><td></td><td>[3]</td><td></td><td>5</td></tr></tbody></table><p>这一轮中，到达 B 点的距离小于到达 D 点的距离，中括号选中 3，并且下一步到 B 点：</p><img src="g3.png" width="50%" /><p>此时 <code>prev = [A, C, B]</code>，状态集合更新为：</p><table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>0</td><td>-</td><td>-</td><td>-</td></tr><tr><td></td><td>4</td><td>[2]</td><td>-</td></tr><tr><td></td><td>[3]</td><td></td><td>5</td></tr><tr><td></td><td></td><td></td><td>[5]</td></tr></tbody></table><p>中括号只剩一个选择，只有 D 点没去过了：</p><img src="g4.png" width="50%" /><p><code>prev = [A, C, B, D]</code>，所有点遍历结束，最终结果为：</p><table><thead><tr><th>A</th><th>B</th><th>C</th><th>D</th></tr></thead><tbody><tr><td>0</td><td>3</td><td>2</td><td>5</td></tr></tbody></table><p>现在就可以知道从 A 点到 D 点的最短距离为 5.</p><h3 id="最短路径跟踪"><a href="#最短路径跟踪" class="headerlink" title="最短路径跟踪"></a>最短路径跟踪</h3><p>算法结束后，可以得到从 A 点到其他点的最短距离数据。可是如果不只想要距离值，还想要具体路径，比如从 A 点到 D 点的最短路径，该怎么处理？</p><h4 id="正向贪心算法"><a href="#正向贪心算法" class="headerlink" title="正向贪心算法"></a>正向贪心算法</h4><p>可以判断出，从 A 到 D 的最短路径是 <code>A -&gt; C -&gt; D</code>，而上面的 <code>prev</code> 集合为 <code>A, C, B, D</code>。因为从 C 直接到 D 比 <code>C -&gt; B -&gt; D</code> 的距离要短，所以在路径中抛弃了 B 点。</p><p>按照这样的现象进行对比，是不是只要在 <code>prev</code> 的基础上，在合适时候抛弃某些点，就可以得到正确路径了？比如上面从 B 到 D，存在 4 种情况：</p><ul><li>B 可以到达 D</li><li>B 不可以到达 D</li><li>通过 B 到达 D 是状态集合中到达 D 距离最短的方案</li><li>通过 B 到达 D 不是状态集合中到达 D 距离最短的方案</li></ul><p>这 4 中情况中，只有 <code>B 可以到达 D</code> 并且 <code>通过 B 到达 D 是状态集合中到达 D 距离最短的方案</code> 的时候，才会保留 B 这个点到路径中。否则就应该去掉 B 点。</p><p>中括号每选择到一个点，就把点放到路径中，如果不满足上面的条件，就从路径中去掉这个点，也就是不放到路径里面。这样的话，即使有其他捣乱的点存在，程序也可以应对，比如：</p><img src="g5.png" width="50%" /><p>在选中 B 点后，发现 B 点不满足条件，此时路径由 <code>path = [A, C, B]</code> 回退到了 <code>path = [A, C]</code>。如果下一轮最小的点选中了 E，<code>path = [A, C, E]</code>，但是 E 点不满足条件，<code>path = [A, C]</code>。直到最小的点选中目标点 D，整个程序结束。</p><p>或者这样的，也可以处理，E 点不会被放到路径中：</p><img src="g6.png" width="50%" /><p>那么这样的思路存在问题吗？当然有问题，这样的程序是不能处理这种情况的：</p><img src="g7.png" width="50%" /><p>假如最短路径是 <code>[A, E, C, D]</code>，E 点是不满足上面被放进路径的条件的，E 点无法直接到达 D 点，但是又必须被包含在路径里。去掉 <code>可以直接到达 D 点</code> 的限制？那上上图的 E 点也会被放到路径里。</p><p>也就是说，需不需要能够直接到达目标点，取决于对于最终的路径，被选中的点是不是倒数第二个点。这样的条件在一个未知的图中是无法判断的，谁能知道一个点是最终路径的倒数第几个点？</p><p>正向的贪心算法试图每一次都把距离最小并且在最终路径上的点记录下来，但其实很难做到，因为根本无法判断一个点是不是在最终的路径上。</p><h4 id="反向贪心算法"><a href="#反向贪心算法" class="headerlink" title="反向贪心算法"></a>反向贪心算法</h4><p>当 D 点被中括号选中，作为本轮距离最小的点，就已经能够确定从 A 点到 D 点最短距离了。那么只要知道这一步是从哪个点过来的，来源的点就一定是最短路径的倒数第二个点。依次类推，只要层层回推到出发的点，整条路径就出来了。</p><img src="g4.png" width="50%" /><p>假如在到达 D 点后，能够知道是从 C 点而不是 B 点过来，在 C 点的时候，能够知道是从 A 点而不是 B 点过来，整个路径就很清晰了。</p><p>问题是怎么在 D 点的时候，知道是从 C 点而不是 B 点过来的？选中最小距离点的顺序可是 <code>[A, C, B, D]</code>，按照最小点的顺序显然是不行的。</p><p>这看起来不是一件难事，在 DFS 或者树的遍历中，经常会前后进入多个路径然后在适当的时候返回以修正路径。换个角度看，其实在 DFS 中维护最短距离，也可以达到目的。<code>维护了距离状态的 DFS</code> &#x3D;&#x3D; <code>Dijkstra algorithm</code> 吗？显然不是。</p><p><strong>递归 vs 尾递归</strong></p><p>Dijkstra 适合写成循环的形式：</p><pre><code class="go">for &#123;&#125;</code></pre><p>更适合写成尾递归的形式：</p><pre><code class="go">func recursion() &#123;        recursion()&#125;</code></pre><p>总之，程序会是单向的循环。适合写成递归的形式吗？</p><pre><code class="go">func recursion() &#123;    for &#123;        recursion()    &#125;&#125;</code></pre><p>当遇到分支情况的时候，用 for 循环 “同时” 进入多个路径，寻找最合适的那个。比如到 C 点的时候，for 循环前后进入 <code>C -&gt; B -&gt; D</code> 和 <code>C -&gt; D</code> 的路径，每次循环将只保留一条路径，找到最合适的直接终止递归就可以。</p><p>这样的写法存在问题吗？问题在于，怎么确定在哪个节点进行分叉。在 C 点分叉？为什么是 C 点？为什么不是 B 点？如果是 B 点，路径上就会多出 B 点。为什么不是 A 点？如果是 A 点，到了 C 点的时候需不需要继续分叉？是每一个点都需要分叉吗？想象一下那会造成多么大的冗余……为什么树可以同时遍历？因为树的节点不会交叉。</p><h4 id="第二个动态规划"><a href="#第二个动态规划" class="headerlink" title="第二个动态规划"></a>第二个动态规划</h4><p>第一个动态规划是指算法本身距离数据的维护。第二个动态规划可以维护一个路径数据的状态：</p><pre><code class="js">pathList = &#123;    A: [],    B: [],    C: [],    D: []&#125;</code></pre><p>路径状态保存从源点到达每个节点在当前阶段的最短路径，在一开始的时候，因为 A 点已经可以到达 B 和 C：</p><img src="g1.png" width="50%" /><pre><code class="js">pathList = &#123;    A: [A],    B: [A, B],    C: [A, C],    D: []&#125;</code></pre><p>选择并到达 C 点，这个时候因为 C 点可以到达 B 点并且 <code>A -&gt; C -&gt; B</code> 的距离小于 <code>A -&gt; B</code>，所以更新路径状态数据为 <code>pathList[C].push(B)</code>。D 点也可以到达了，更新路径状态。（更新路径状态数据发生在进入下一个点之前，甚至发生在选择下一个节点之前。可以想一想为什么这样做。）</p><img src="g2.png" width="50%" /><pre><code class="js">pathList = &#123;    A: [A],    B: [A, C, B],    C: [A, C],    D: [A, C, D]&#125;</code></pre><p>这一轮在距离的状态数据上，会把 B 点选中为最小距离的节点，判断到达 D 的路径 <code>A -&gt; C -&gt; B -&gt; D</code> 大于目前已有的距离记录 <code>A -&gt; C -&gt; D</code>，所以不更新路径状态。（判断距离是否大于已有距离是根据距离的状态数据，也就是表格的数据。）</p><img src="g3.png" width="50%" /><p>最终进入目标 D 结束，路径状态不更新。</p><img src="g4.png" width="50%" /><p>得到路径 <code>A -&gt; C -&gt; D</code>。</p><p>路径的状态数据可以为了节省空间，只维护到达目标点的路径吗？不可以，因为更新下一个点的路径需要依赖当前点的路径，路径的状态必须是全量的。</p><h3 id="非最短路径跟踪"><a href="#非最短路径跟踪" class="headerlink" title="非最短路径跟踪"></a>非最短路径跟踪</h3><p>Dijkstra 算法包含了贪心算法的思维，每一步选出的都是距离最短的点。如果需要保存不是最短路径的路径，Dijkstra 算法也许可以做到，但是就已经不需要 Dijkstra 算法了。DFS&#x2F;BFS 更合适一点。</p><p><br><br></p><h3 id="补充（2025-05-11）"><a href="#补充（2025-05-11）" class="headerlink" title="补充（2025.05.11）"></a>补充（2025.05.11）</h3><p>这个 Dijkstra 相关的工作，是当时在一个 State Channels 的项目 <a href="https://github.com/saveio-backup/pylons">pylons</a> 上，用来在多个通道之间寻找最短路径用的，原本是 <a href="https://github.com/saveio-backup/pylons/blob/master/route/dfs.go">DFS</a>，后来我加了一个 <a href="https://github.com/saveio-backup/pylons/blob/master/route/dijkstra.go">Dijkstra</a>，带有黑名单的功能，以及把手续费作为路径距离的计算依据。</p><p>现在把 route 部分的代码单独拆分出一个仓库 <a href="https://github.com/smallyunet/dijkstra-demo/">smallyunet&#x2F;dijkstra-demo</a> 留作纪念。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;区块链的 Layer 2 中有一种 State Channels 的扩容方案，其中会需要搜索距离最近的路由节点。&lt;/p&gt;
&lt;h3 id=&quot;Dijkstra-算法思路&quot;&gt;&lt;a href=&quot;#Dijkstra-算法思路&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="算法" scheme="https://smallyu.net/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>为什么数字货币使用区块链是政治问题</title>
    <link href="https://smallyu.net/2021/04/18/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%95%B0%E5%AD%97%E8%B4%A7%E5%B8%81%E4%BD%BF%E7%94%A8%E5%8C%BA%E5%9D%97%E9%93%BE%E6%98%AF%E6%94%BF%E6%B2%BB%E9%97%AE%E9%A2%98/"/>
    <id>https://smallyu.net/2021/04/18/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%95%B0%E5%AD%97%E8%B4%A7%E5%B8%81%E4%BD%BF%E7%94%A8%E5%8C%BA%E5%9D%97%E9%93%BE%E6%98%AF%E6%94%BF%E6%B2%BB%E9%97%AE%E9%A2%98/</id>
    <published>2021-04-18T15:33:54.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<p>本来不想再专门提区块链。由于工作相关，接触相关话题比较频繁。</p><p>最近，一个高级别的技术管理吐槽我 “数字货币不可能用区块链” 完全是外行的观点，即使只是把四大行的大额交易记录到区块链上，也是很好的一件事情，而且可以用分层交易的技术架构，顺势解决小额支付在区块链上性能受限的问题……</p><p>我的逻辑很简单。</p><p>假如区块链在世界上从来没有出现过，没有存在过。在这种情况下，如果“上面”要求各大银行的交易数据必须同步一致可追溯。下面的人能做到吗？</p><p>不但能做到，而且可以做得很好。</p><p>区块链能解决的问题，不用区块链也能解决。这几乎是众所周知的事情。这也是为什么有人说 “区块链没有新技术” 的原因。</p><p>当然，这里的 “数字货币” 特指中国的数字货币，“区块链” 指——在讨论区块链怎么用之前，是不是应该先把 “区块链是什么” 搞清楚？奇怪的是，似乎没有人关心这个问题。</p><p>在这个方面上，和区块链形成对比的是人工智能。人工智能能做的事情，如果技术跟不上，就确实做不到。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;本来不想再专门提区块链。由于工作相关，接触相关话题比较频繁。&lt;/p&gt;
&lt;p&gt;最近，一个高级别的技术管理吐槽我 “数字货币不可能用区块链”</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>区块链：下一代数字身份认证体系的基石</title>
    <link href="https://smallyu.net/2020/12/08/%E5%8C%BA%E5%9D%97%E9%93%BE%EF%BC%9A%E4%B8%8B%E4%B8%80%E4%BB%A3%E6%95%B0%E5%AD%97%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E4%BD%93%E7%B3%BB%E7%9A%84%E5%9F%BA%E7%9F%B3/"/>
    <id>https://smallyu.net/2020/12/08/%E5%8C%BA%E5%9D%97%E9%93%BE%EF%BC%9A%E4%B8%8B%E4%B8%80%E4%BB%A3%E6%95%B0%E5%AD%97%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E4%BD%93%E7%B3%BB%E7%9A%84%E5%9F%BA%E7%9F%B3/</id>
    <published>2020-12-08T12:02:38.000Z</published>
    <updated>2025-11-09T09:22:13.316Z</updated>
    
    <content type="html"><![CDATA[<p>如何在互联网的世界中，证明“我是我”？在 A 网站认证过了身份信息，到了 B 网站又需要认证一次？手持身份证拍照上传、人工审核，流程太繁琐？多个账户密码记不清、容易混，管理起来困难？自主主权的数字身份（Self-soverieign identity, SSI）正是可以解决这些问题的理念。</p><p>SSI 是数字身份运动中的观念，指只有用户自己拥有全部的、完整的数字身份信息，没有其他管理者和组织参与的数字身份体系。在 SSI 的理念中，用户拥有属于自己的去中心化的惟一身份标识（Decentralized identifiers, DIDs），用户可以完全控制自己的身份信息，可以在任何时候使用、更新或者彻底删除信息。用户可以创建并管理自己的可验证证明（Verifiable Credentials），自主决定在什么时候使用和分享自己的证明信息，而不需要请求其他中心化的机构、通过机构授权来使用自己的个人数据。</p><p>使用 SSI 的系统，所有的密钥信息都可以通过数字身份钱包进行管理，使用一个账号就可以登录所有的网站。在钱包终端中，用户可以随时向权威机构申请签发证明，包括身份证、驾驶证、居住证等各种形式的证件，都将以数字证明的形式储存在手机或电脑上。数字证明拥有机器可读、机器可验证的特性，不但可以放心地展示给第三方应用，第三方应用还可以在没有人工干预的情况下，直接验证证明的有效性，不需要签发机构的参与。</p><p>得益于区块链技术的不断发展，SSI 理念的实现逐渐成为可能。区块链系统本身就是点对点网络，天然拥有去中心化的特性，结合独特的数据结构设计和密码学技术的应用，加上共识算法在多节点数据同步方面的优秀能力，区块链不但能够保护数据的隐私安全，而且数据一旦写入系统便任何人无法篡改，为数据提供了极高可信度的储存环境。在 SSI 系统的建设中，将区块链作为可验证数据的数据中心（Verifiable data registry）无疑是最好的选择。</p><p>SSI 目前已经有诸多先例。2017 年，Sovrin 基金会发布了世界上首个公开的用于自主主权的数字身份的分布式账本网络，整个系统运行在开放标准以及公开源码的 Sovrin 协议之上，由 Linux 基金会的 Hyperledge Indy 项目维护。Sovrin 在 2018 年公布的白皮书中自问自答，“为什么网络世界中没有像物理世界一样可以用来证明身份的证书？直到区块链技术的出现，我们解决了这个问题！”结合 W3C 的 DIDs，Sovrin 提出了完整的数字身份和证明的解决方案。Sovrin 的主意一直都很明确，就是一定要构建和使用公开的、任何人都可以访问的、像比特币和以太坊一样的区块链网络。</p><p>eSSIF-Lab（European Self-Sovereign Identity Lab）是另一个案例。欧洲区块链联盟提出的 EBSI（The European Blockchain Services Infrastructure）是一个横跨欧洲的分布式节点网络，提供跨境的公共服务，有 28 个成员国签署了相关声明。eSSIF-Lab 项目是 EBSI 的一部分，由欧盟委员会资助，旨在促进 SSI 成为下一代开放、可信、安全的数字身份解决方案。欧盟曾在 2014 年 7 月 23 日建立了针对欧盟共同市场电子交易的电子身份识别和可信服务的法规 eIDAS（electronic IDentification, Authentication and trust Services），2019 年 5 月，eIDAS 宣布支持基于 W3C 相关规范的自主主权的数字身份。</p><p>微软在相关领域也表现活跃，2018 年 10 月，微软发布《去中心化的身份》白皮书，介绍了基于区块链的去中心化数字身份系统建设的技术方案，包括 DIDs 规范、去中心化的数据系统、DID 用户终端、DID 通用解析器、DID 身份中心、DID 认证系统、去中心化的客户端和服务等核心模块，详细说明了各模块组件以及各种角色在系统中的交互流程，为 SSI 系统的建设提供了非常好的模板。目前，微软已经提供公开的服务平台，可以体验相关的产品和能力。</p><p>此外，构建在以太坊和 IPFS 网络上的 uPort、使用自研区块链和支持第三方 DApp 的 Blockstack、能够适配比特币网络的 ShoCard 等都是优秀的案例。国内的厂商和机构也在进行相关的工作，如蚂蚁链提供的分布式身份服务 DIS（Decentralized Identity Service）、腾讯云的数字身份标识解决方案、微众银行的基于区块链的分布式多中心的技术解决方案 WeIdentity 等，都利用了区块链去中心化、数据高度可信的技术特点，构建了可靠的数字身份标识和认证体系。</p><p>区块链是一项极具潜力的先进技术，具有非常广阔的发展前景和应用空间，无论是国家政策的支持还是实际案例的应用，都体现出区块链未来的无数种可能。我们也在积极探索和推进区块链相关的技术发展和场景落地，将区块链与同态加密、联邦学习、多方计算、零知识证明等前沿技术结合起来，使用最优秀的技术能力，促进下一个互联网时代的到来。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;如何在互联网的世界中，证明“我是我”？在 A 网站认证过了身份信息，到了 B 网站又需要认证一次？手持身份证拍照上传、人工审核，流程太繁琐？多个账户密码记不清、容易混，管理起来困难？自主主权的数字身份（Self-soverieign identity,</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>网页技术能实现 3D 建模吗？</title>
    <link href="https://smallyu.net/2020/09/20/%E7%BD%91%E9%A1%B5%E6%8A%80%E6%9C%AF%E8%83%BD%E5%AE%9E%E7%8E%B03D%E5%BB%BA%E6%A8%A1%E5%90%97%EF%BC%9F/"/>
    <id>https://smallyu.net/2020/09/20/%E7%BD%91%E9%A1%B5%E6%8A%80%E6%9C%AF%E8%83%BD%E5%AE%9E%E7%8E%B03D%E5%BB%BA%E6%A8%A1%E5%90%97%EF%BC%9F/</id>
    <published>2020-09-20T14:58:39.000Z</published>
    <updated>2025-11-09T09:22:13.532Z</updated>
    
    <content type="html"><![CDATA[<p>网页技术（HTML5、CSS3、JavaScript）能实现效果炫酷的 3D 建模甚至是 3D 动画效果吗？我暂时认为是不可以的。比如期望这样的页面效果：</p><img src="preview.png" class="no-shadow" width="100%"><p>d3.js 是不用考虑的，它仅仅是一个数据可视化的工具，和 3D 建模是两个领域。</p><p>three.js 似乎是目前比较流行的 3D 建模库。假如 three.js 可以做到的话，应该怎么做呢？首先的想法是画这样一个正方体出来：</p><img src="cube_target.png" class="no-shadow" width="20%"><p>3d 建模里的立方体相当于编程世界的 hello world，很容易就能出来：</p><img src="cube_1.png" class="no-shadow" width="20%"><p>给场景加上灯光，正方体就不是黑漆漆的了。然后给正方体加上颜色，改一下场景的背景色，再把灯光调到正方体的上面，正方体就能像样子一点了：</p><img src="cube_2.png" class="no-shadow" width="20%"><p>目标正方体的边缘是发光的，而且是渐变色。怎么给正方体加一个边缘线呢，正方体本身是没有这种属性的，只能用线性材料（three.js 里的 LineBasicMaterial，正方体用的是 MeshPhongMaterial）再画一个正方体出来，套在实体正方体上：</p><img src="cube_3.png" class="no-shadow" width="20%"><p>怎么让线性的正方体发光呢？线性材料（LineBasicMaterial）是不能使用渐变色的，只有着色器材料（ShaderMaterial）可以使用渐变色。着色器材料可以实现多彩的效果，比如这样（来自 <a href="https://stackoverflow.com/questions/52614371/apply-color-gradient-to-material-on-mesh-three-js">StackOverflow</a>），：</p><img src="line_1.png" class="no-shadow" width="20%"><p>但是到这里遇到问题了。在 three.js 里，渲染一个物体需要两个参数，一个是 geometry（几何体），一个是 material（材料），线性材料和着色器材料都是材料的种类。(TorusKnotGeometry 是上图用到的几何形状)</p><pre><code>线条正方体 = EdgesGeometry + LineBasicMaterial渐变曲线条 = TorusKnotGeometry + ShaderMaterial</code></pre><p>现在想要线性材料和着色器材料（LineBasicMaterial 和 ShaderMaterial）组合是不合逻辑的，我没有找到实现发光的正方体边缘效果的方法。把着色器用在正方体的效果是这样的（颜色从 0x215ec9 到 0x000000）：</p><img src="cube_4.png" class="no-shadow" width="20%"><p>所以然后呢？我意识到即使实现了一个好看的正方体，离渲染出整张图还差的太多。比如这样的文字效果怎么做？</p><img src="part_1.png" class="no-shadow" width="30%"><p>three.js 的 Texture 本身效果是不错的，可是怎么把文字安安稳稳的放到正方体上，还带透明的黑色背景框？再比如这五彩斑斓的线条，以及准确的箭头指向：</p><img src="part_2.png" class="no-shadow" width="15%"><p>还有整个图上十多种元素的位置布局、动画效果。</p><p>我相信 three.js （WebGL）在技术能力上是可以实现这样效果的，甚至官方的 example 里网页游戏都有，不过假如要实现一个网页游戏，一定会用到图像素材，素材从哪儿来呢？还是得回到 PS、AI 之类的工具上，如果用上了那样的生产力工具，就没有必要用 js 来写布局和动画了。单纯的网页技术似乎很难完全解决 3D 建模的问题。H5 动画也是类似的情况。</p><p>单纯写代码来 3D 建模的另一个问题是不直观，代码是违反直觉和视觉的，写个网页、APP界面似乎还可以（二维的）。如果可以在一个画布上直接放置正方体和线条，然后鼠标拖动改变位置、调整颜色，以及添加各种其他元素，像玩游戏（比如我的世界）一样操作简便，不就比写代码好多了吗……那不就是 Adobe Animate 吗？</p><p>可惜 Adobe Animate 没有 Linux 版本，而且 Linux 下的替代品 Blender 有点性能问题。</p><img src="blender.png" class="no-shadow" width="100%"><p>回到一开始期望的效果图上，图片出自一个大屏 UI 的 <a href="https://www.zcool.com.cn/work/ZMjg2NTA1Njg=.html">设计演示</a>，其实原效果不是三维的，作者使用的工具是 PS、AI。那么在仅需要二维效果的前提下，网页技术能实现吗？如果要用代码实现各种图形，就依然还是三维建模的问题。最简单的方式是拿个背景图，把文字贴到上面。背景图从哪儿来呢？</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;网页技术（HTML5、CSS3、JavaScript）能实现效果炫酷的 3D 建模甚至是 3D 动画效果吗？我暂时认为是不可以的。比如期望这样的页面效果：&lt;/p&gt;
&lt;img src=&quot;preview.png&quot; class=&quot;no-shadow&quot;</summary>
        
      
    
    
    
    
    <category term="工作" scheme="https://smallyu.net/tags/%E5%B7%A5%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>给区块链一个定义</title>
    <link href="https://smallyu.net/2020/08/09/%E7%BB%99%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%80%E4%B8%AA%E5%AE%9A%E4%B9%89/"/>
    <id>https://smallyu.net/2020/08/09/%E7%BB%99%E5%8C%BA%E5%9D%97%E9%93%BE%E4%B8%80%E4%B8%AA%E5%AE%9A%E4%B9%89/</id>
    <published>2020-08-09T08:06:45.000Z</published>
    <updated>2025-11-09T09:22:13.532Z</updated>
    
    <content type="html"><![CDATA[<p>一直以来，区块链似乎都没有一个明确的定义，伴随区块链出现的词语经常是去中心化、溯源、不可篡改、以信用为基础、下一代价值互联网之类，这些都是区块链的特性，不是区块链的组成，这些词都在说区块链有什么，没有说区块链为什么会有那些，以及为什么要有那些。</p><p>感觉上很多东西都没有明确的定义，比如，计算机是什么？都知道是那么一个东西，可以打游戏上网，稍微专业点的会说是基于冯诺依曼体系结构的、有 5 个组成部分的什么什么机器。其实计算机可以认为是“做计算的机器”，就这么简单。冰箱是什么？“一个放东西的柜子，有一些冷冻的功能”，就可以了。设计模式是什么？一种软件程序设计的范式。微服务是什么？一种软件架构的模式。所以区块链是什么？奇怪的是，区块链（blockchain）这个词不知从何而起，从来没有人明确提出这个概念，比特币白皮书里也只是提到 “chain of block”。</p><p>我以前对区块链有过一些不成熟的认识，虽然好像也没什么错，但不够清晰，尤其是没搞清楚一个问题，区块链是什么？现在来看，区块链的定义应该是：</p><blockquote><p>区块链是一种数据协同软件，或者说，区块链是一种用来同步数据的软件。</p></blockquote><p>数据协同软件决定用什么样的数据结构通过什么样的通信机制同步哪些数据。区块链不是数据库，区块链不负责储存数据，储存数据的事情会交给真正的数据库来做，区块链并不关心数据是怎么存在磁盘上的，不关心储存结构是否合理，利用率高不高，处理速度快不快。区块链关心数据以什么样的方式同步到其他的机器上，如何及时同步，以及其他机器同步过来的数据有没有问题。可以说，区块链是对数据协同软件的一种实现。</p><p>因为是数据协同软件，所以区块链多节点、去中心化，这显而易见。</p><p>溯源是指交易可溯源，只要数据之间有关联关系就可以，这是数据模型决定的，比如 UTXO。</p><p>链式的数据结构，是为了方便数据协同软件校验数据的完整性，类似用 md5 判断文件是否完整。这种数据结构并不是必要的，数据的全量对比也可以实现目的，只是效率非常低下。所以采用加密算法做摘要然后放到下一部分数据里的做法，相当于保证了一大块数据是完整的，仅此而已。</p><p>至于不可篡改，其实是数据协同软件带来的特性。区块链的不可篡改，并不是数据不能修改，而是改了之后其他节点不认可。这是不一样的，数据不能更改是技术问题，比如不提供更新数据的接口，用户就没有修改数据的渠道，通过技术手段可以控制。改了之后其他节点不接受，是一种机制，这种机制问题已经脱离技术领域。</p><p>区块链目前的发展受技术限制吗？计算机的计算理论包含两个主要部分，可计算性理论和复杂度理论。可计算性理论判断一个问题能不能用算法解决，复杂度理论意在提高算法的效率。和区块链有关系吗？退一步说，区块链需要计算吗？不需要，没有关系，不受限制。有个有趣的脑洞问题，如果把全世界的人都拉到一个微信群里，会发生什么？起码屏幕上的消息肯定刷不过来了。如果全世界的数据共用一条区块链，会发生什么？所以区块链最终还是机制的问题，不是技术问题。</p><p>比特币和区块链是两个概念，比特币是一种使用了区块链做数据同步的交易系统，比特币首先是一个交易系统，其次才需要的数据同步。这也是我以前犯的概念上的错误，把区块链等同于比特币了。很多对区块链概念比较模糊的人，提到区块链也都会往比特币之类的数字货币上想。记得去年参加过一个分享会，主讲人是某知名交易所总监，分享标题是区块链和国家政策什么的，整个会议下来，讲的却全是比特币的趣闻轶事。</p><p>区块链是比特币的组成部分。比特币的作者看到了比特币的价值，把软件和白皮书发布出来了。为什么比特币的作者没有把区块链的概念抽离出来，发个通用软件和说明书？是水平不够没有意识到区块链潜在的巨大价值吗？不是。区块链的提出，是因为人们看到了比特币的价值，想要复制比特币的成功，所以把比特币的技术组成提取出来，叫做区块链。可惜比特币是一个设计巧妙的系统，单独把某些技术特点拿出来难以产生预期的价值，这也是区块链的现状。这是现代版技术圈的东施效颦。</p><p>智能合约（Smart Contract）早在 1997 年由一位金融、法律从业者提出，“智能”是指和纸质合同相比，智能合约达到某一条件时就会自动执行某些操作，确实比纸质合同智能了一点，尤其在那个年代，数字化还没有普及，描述这是一种智能并不为过。而且，作者明确说，智能合约没有用到人工智能。</p><p>智能合约抽象一下，达到某一条件自动执行一些动作，不就类似编程语言的条件语句吗，事实上现在的智能合约大多是用图灵完备的编程语言实现的。用编程语言来描述合约的致命问题在于，编程语言的表达能力比自然语言弱太多了，如果试图用编程语言来重写保险说明书里的所有条文，“发生什么，就赔偿多少……”，这种改写的成本太高了，而且很多时候法律条文需要专业律师、法官解释和判断，现实世界的逻辑远比程序逻辑复杂，编程语言是搞不定的。</p><p>一种数据同步软件不应该被推崇，区块链被神化、妖魔化了。也因此，不能说区块链没有价值，因为区块链是且只是一种工具软件。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;一直以来，区块链似乎都没有一个明确的定义，伴随区块链出现的词语经常是去中心化、溯源、不可篡改、以信用为基础、下一代价值互联网之类，这些都是区块链的特性，不是区块链的组成，这些词都在说区块链有什么，没有说区块链为什么会有那些，以及为什么要有那些。&lt;/p&gt;
&lt;p&gt;感觉上很多东西</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>从 Erlang 开始了解 Actor 模型</title>
    <link href="https://smallyu.net/2020/03/31/%E4%BB%8EErlang%E5%BC%80%E5%A7%8B%E4%BA%86%E8%A7%A3Actor%E6%A8%A1%E5%9E%8B/"/>
    <id>https://smallyu.net/2020/03/31/%E4%BB%8EErlang%E5%BC%80%E5%A7%8B%E4%BA%86%E8%A7%A3Actor%E6%A8%A1%E5%9E%8B/</id>
    <published>2020-03-31T04:47:59.000Z</published>
    <updated>2025-11-09T09:22:13.310Z</updated>
    
    <content type="html"><![CDATA[<p>Actor Model是一个宽泛的概念，早在上个世纪就被提出来，它将Actor视作一个整体，可以是原子变量，也可以是一个实体，也可以代表一个线程，Actor之间相互通信，每个Actor都有自己的状态，在接收到其他Actor的消息后可以改变自己的状态，或者做一些其他事情。一般提到Actor，会用Erlang、Elixir或Akka来举例，它们都在一定程度上实现了Actor模型。</p><p>前端的MVVM框架React、Vue等都有各自的数据流管理框架，比如Redux和Vuex，这些数据流管理框架中有几个类似的概念，Action、Reducer、State之类，这些概念有时候会让人感到迷惑。现在前端变得越来越复杂，其中有一些东西可能是借鉴后端的，像TypeScript的类型系统。我好奇这些前端框架里的Action和后端的Actor模型在概念上是否有相似的地方。</p><p>其实Action的本质是简单的，甚至代码的原理也是简单的，reducer里面用switch判断不同的操作类型，去调不同的方法。最简化的形式就是一个方法Action改变了全局变量state的值。Redux文档里说它的设计来自Flux架构，Flux架构的来源暂时不得而知，但也不太可能说是受到了Actor模型的启发。</p><pre><code class="JavaScript">let state = nullfunction action(val) &#123;  state = val&#125;</code></pre><p>Erlang是一门古老的编程语言，也是一门典型的受Actor Model启发的编程语言。单纯去理解概念是空泛的，从具体的、特定的语言入手也许能帮助我们探索这些理论。就像学习FP，选择Haskell要好过Java很多倍。Elixir是基于Erlang虚拟机的一门语言，与Erlang的关系类似Scala和Java的关系，也因此Erlang的语法相对简单和干净一点。</p><h3 id="Erlang"><a href="#Erlang" class="headerlink" title="Erlang"></a>Erlang</h3><p>Erlang的代码块以<code>.</code>结尾，代码块可能只有一行，也可以有多行，<code>.</code>的作用类似于<code>&#125;</code>，只是Erlang里没有<code>&#123;</code>。代码块内的语句以<code>,</code>结尾，意味一个语句的结束，相当于一些语言的<code>;</code>。</p><p>Erlang将一个程序文件定义为一个模块，在命令行中使用<code>c(test).</code>可以加载模块。模块名称必须和文件名称一致：</p><pre><code class="Erlang">-module(test).</code></pre><p>文件头部需要定义程序export的函数，这是模块的出口：</p><pre><code class="Erlang">-export([start/0, ping/3, pong/0]).</code></pre><p>这里导出了3个函数，方括号和其他语言一样表示数组，函数名称后面的<code>/0</code>、<code>/3</code>指函数参数的个数。start函数将作为程序的主入口，负责启动整个程序，ping负责发送消息，pong负责接收消息并做出响应。</p><p>Erlang里面有个<code>process</code>的概念，它不是线程，也不是指计算机层面的进程，它就是<code>process</code>，或者也能把它当做线程，但是要明白它和线程不一样。我们将启动两个process，一个负责ping，一个负责pong，模拟消息的传输和交互。可以类比启动了两个线程，一个负责生产，一个负责消费。</p><pre><code class="Erlang">ping(0, Pong_PID, StartTime) -&gt;     Pong_PID ! &#123;finished, StartTime&#125;;</code></pre><p>这是ping函数的第一部分，是ping函数的一个分支，接收3个参数，如果第一个参数是0，就会执行这个函数中的语句。第二个参数<code>Pong_PID</code>指包含pong的process，第三个参数指程序启动的时间，用于记录程序的运行时长。函数体内只有一个语句，<code>!</code>是发送消息的意思，意为将数据<code>&#123;finished, StartTime&#125;</code>发送到id为<code>Pong_PID</code>的process中，其中finished是一个<code>Atom</code>，作为标识发送到pong那里。Atom是Erlang的数据类型之一，相当于……不需要声明的常量。</p><pre><code class="Erlang">ping(N, Pong_PID, StartTime) -&gt;     Pong_PID ! &#123;ping, self()&#125;,    receive        pong -&gt;             io:format(&quot;~w~n&quot;, [N])    end,    ping(N - 1, Pong_PID, StartTime).</code></pre><p>这是ping函数的第二部分，如果函数接收到的第一个参数不等于0，就会执行这个函数内的语句。这一部分函数在接收到请求后，首先会做和分支一同样的事情，就是把数据<code>&#123;ping, self()&#125;</code>发送给pong，区别在于这里的标识为<code>ping</code>而不是<code>finished</code>，pong那里会根据这个标识做不同的操作，至于第二个参数，<code>self()</code>会返回当前process的id，也就是把ping的id传给了pong，用以pong回复消息。pong会选择性的使用第二个参数。</p><p>把数据发送到pong之后，有一个<code>receive ... end</code>的代码段，这个代码段会阻塞当前程序的执行，直到当前process接收到数据。代码段里是一个简单的模式匹配，<code>pong</code>是一个Atom类型的变量，如果接收到pong这样的标识，就会执行<code>-&gt;</code>后面的语句。<code>io:format</code>是一个简单的格式化输出，把N的值打印到屏幕上。</p><p>receive结束之后，马上又调了一下ping自己，递归……直到N为0，也就是说ping和pong的交互会持续N次，<code>io:format</code>那里会把交互次数打印出来。这是ping函数的两个分支，pong函数和ping函数的程序类似：</p><pre><code class="Erlang">pong() -&gt;    receive        &#123;finished, StartTime&#125; -&gt;             io:format(&quot;The End&quot;);            io:format(&quot;~w~n&quot;, [erlang:timestamp()]);            io:format(&quot;~w~n&quot;, [StartTime]);        &#123;ping, Ping_PID&#125; -&gt;            Ping_PID ! pong,            pong()    end.</code></pre><p>pong函数在入参层面没有分支，但是receive里有两种匹配，如果接收到了结束标识finished，会把开始时间和结束时间都打印出来，然后程序结束。如果接收到的标识是ping而不是finished，首先给Ping_PID也就是ping的process一个pong的响应，然后调了一遍自己，相当于先发了一个消息出去，接着自己等待消息的回复，如果没有收到回复，它就一直等着。</p><pre><code class="Erlang">start() -&gt;    Pong_PID = spawn(test, pong, []),    spawn(test, ping, [10, Pong_PID, erlang:timestamp()]).</code></pre><p>最后是start函数，程序的入口函数，spawn了两个process，这两个process分别单独地运行。当传入ping的第一个参数为10，ping和pong的交互将持续10次。</p><h3 id="交互速率"><a href="#交互速率" class="headerlink" title="交互速率"></a>交互速率</h3><p>以前听到过一个所谓的“大牛”讲，我们现在想要提高计算机的速率，瓶颈是什么呢，我们应该往哪个方向努力呢，应该是CPU的利用率，Actor是很快的，为什么快呢，因为一个Actor就是一个整体，一个Actor只在一个内核中运行，连CPU内核之间的交互都省了……这种说法的正确性可能有待验证，不过Actor是否真的快呢，我有点好奇，也因此萌生了测试一下Actor速度的想法。</p><p>必须要说明的是，我也相当清楚，这种测试方法很不靠谱。</p><p>在Erlang程序里启动两个process，两个process之间相互通信，测试不同数量级的通信次数，记录下程序执行所花费的时间。与Erlang作为对比，在Java里启动两个线程，用线程的睡眠和唤醒实现线程间的通信。同样的，在Go语言里用两个协程通信。至于Akka……其实也是Actor的代表。下表是测试之后的结果，次数从1到1亿，时间单位为毫秒。</p><table class="table"><thead><tr><th style="text-align:right">次数</th><th style="text-align:right">Erlang</th><th style="text-align:right">Java</th><th style="text-align:right">Go</th><th style="text-align:right">Akka</th></tr></thead><tbody><tr><td style="text-align:right">1</td><td style="text-align:right">0</td><td style="text-align:right">0</td><td style="text-align:right">0</td><td style="text-align:right">3</td></tr><tr><td style="text-align:right">10</td><td style="text-align:right">0</td><td style="text-align:right">1</td><td style="text-align:right">0</td><td style="text-align:right">7</td></tr><tr><td style="text-align:right">100</td><td style="text-align:right">3</td><td style="text-align:right">4</td><td style="text-align:right">1</td><td style="text-align:right">17</td></tr><tr><td style="text-align:right">1,000</td><td style="text-align:right">26</td><td style="text-align:right">30</td><td style="text-align:right">4</td><td style="text-align:right">83</td></tr><tr style="background:#ffff0008;"><td style="text-align:right">10,000</td><td style="text-align:right;"><span style="color:#0000ff99;">610</span></td><td style="text-align:right">168</td><td style="text-align:right">42</td><td style="text-align:right"><span style="color:#0000ff99;">225</span></td></tr><tr style="background:#ffff0008;"><td style="text-align:right">100,000</td><td style="text-align:right">2783</td><td style="text-align:right"><span style="color:#0000ff99;">1295</span></td><td style="text-align:right">404</td><td style="text-align:right"><span style="color:#0000ff99;">674</span></td></tr><tr style="background:#ffff0008;"><td style="text-align:right">1,000,000</td><td style="text-align:right">27,085</td><td style="text-align:right">11,300</td><td style="text-align:right"><span style="color:#0000ff99;">4489</span></td><td style="text-align:right;"><span style="color:#0000ff99;">3515</span></td></tr><tr><td style="text-align:right">10,000,000</td><td style="text-align:right">273,912</td><td style="text-align:right">107,673</td><td style="text-align:right">40,335</td><td style="text-align:right">29,368</td></tr><tr><td style="text-align:right">100,000,000</td><td style="text-align:right">2,851,680</td><td style="text-align:right">1,092,879</td><td style="text-align:right">482,196</td><td style="text-align:right">300,228</td></tr></tbody></table><p>本来尝试用Echarts之类渲染一下这些数据，方便对比，后来发现这些数据绘制出来的折线图并不友好。</p><p>总的来看，Erlang的速度是最慢的，这可能和Erlang历史悠久有关，也许是因为没有得到足够的优化，相信Elixir的速度会好一些。相较之下，Java的速度胜过Erlang，Go语言的速度胜过Java，这似乎是意料之中的事情。Java的耗时是Erlang的1&#x2F;3，Go语言的耗时是Java的1&#x2F;2。</p><p>最让人惊讶的在于，Akka的Actor速度竟然比Go语言的协程还要快。在交互1000次之前，Akka的速度比Erlang还要慢，在10K数量级的时候，它的速度超过了Erlang，在100K数量级的时候，速度超过了Java，直到1M数量级的时候，Akka超过了Go语言，并且一直保持领先。这是一个令人难以置信的结果，同样是运行在JVM上，Akka的耗时是Java的1&#x2F;3，可能Java线程间的交互确实带来了很大的开销。</p><p>没有用Elixir做测试是一个遗憾。关于Akka为什快，和Actor模型有没有关系，有多大的关系，还需要进一步探索。</p><p>(The End)</p><h3 id="Akka"><a href="#Akka" class="headerlink" title="Akka"></a>Akka</h3><p>用来做测试的Akka程序是Akka官方的Hello Wrold程序，能看到明显的Actor模型的影子，尤其是<code>!</code>运算符和<code>receive</code>方法。</p><pre><code class="Scala">import akka.actor.typed.ActorRefimport akka.actor.typed.ActorSystemimport akka.actor.typed.Behaviorimport akka.actor.typed.scaladsl.Behaviorsimport GreeterMain.SayHello</code></pre><p>这是导入部分，如果使用VS Code之类的编辑器，这段代码还是很重要的。和Erlang的程序类似，有一个发消息的Greeter和一个接收并回复消息的GreeterBot，另外还有一个主方法。</p><pre><code class="Scala">object Greeter &#123;  final case class Greet(whom: String, replyTo: ActorRef[Greeted])  final case class Greeted(whom: String, from: ActorRef[Greet])  def apply(): Behavior[Greet] =     Behaviors.receive &#123; (context, message) =&gt;      message.replyTo ! Greeted(message.whom, context.self)      Behaviors.same    &#125;&#125;</code></pre><p>这是发消息的Greeter，当Greeter作为函数被调用，会自动执行apply中的代码。apply方法是一个receive，和Erlang的receive一样会阻塞程序直到Actor接收到消息。replyTo是GreeterBot的”pid”，Greeter接收到消息后会回复消息给GreeterBot。</p><pre><code class="Scala">object GreeterBot &#123;  var startTime = System.currentTimeMillis()  def apply(max: Int) = &#123;    bot(0, max)  &#125;  private def bot(greetingCounter: Int, max: Int): Behavior[Greeter.Greeted] =    Behaviors.receive &#123; (context, message) =&gt;      val n = greetingCounter + 1      context.log.info(&quot;&#123;&#125;&quot;, n)      if (n &gt;= max) &#123;        context.log.info(&quot;The End | &#123;&#125;&quot;, System.currentTimeMillis() - startTime)        Behaviors.stopped      &#125; else &#123;        message.from ! Greeter.Greet(message.whom, context.self)        bot(n, max)      &#125;    &#125;&#125;</code></pre><p>这是GreeterBot，和Erlang简洁的代码比起来，Scala冗长的类型声明可能显得有些……烦杂。GreeterBot接收到来自Greeter的消息后，判断n是否为max，如果已经执行够次数了，就停止，否则调用自己进行递归。</p><pre><code class="Scala">object GreeterMain &#123;  final case class SayHello(name: String)  def apply(): Behavior[SayHello] =    Behaviors.setup &#123; context =&gt;      val greeter = context.spawn(Greeter(), &quot;greeter&quot;)      Behaviors.receiveMessage &#123; message =&gt;        val replyTo = context.spawn(GreeterBot(max = 10), message.name)        greeter ! Greeter.Greet(message.name, replyTo)        Behaviors.same      &#125;    &#125;&#125;object AkkaQuickstart extends App &#123;  val greeterMain = ActorSystem(GreeterMain(), &quot;AkkaQuickStart&quot;)  greeterMain ! SayHello(&quot;Charles&quot;)&#125;</code></pre><p>最后是主方法，看着可能也有点……长。继承于App的类是能够运行的主类，向Actor系统中注册了GreetMain，同时GreetMain的apply方法被执行了一次。GreetMain里spawn了两个process，和Erlang的程序行为是类似的。</p><h3 id="Go"><a href="#Go" class="headerlink" title="Go"></a>Go</h3><p>Go语言的程序真的要简洁很多，这是程序头部：</p><pre><code class="Go">package mainimport(  &quot;fmt&quot;  &quot;time&quot;)var maxCount = 100000000var startTime = time.Now().UnixNano() / 1e6</code></pre><p>定义了两个变量，一个是程序执行次数，一个是程序开始时间。</p><pre><code class="Go">func main() &#123;  ch := make(chan bool)  exit := make(chan bool)  go func() &#123;    for i := 0; i &lt; maxCount; i++ &#123;      fmt.Println(i)      &lt;- ch      ch &lt;- true    &#125;  &#125;()  go func() &#123;    defer func() &#123;      timeUsed := time.Now().UnixNano() / 1e6 - startTime      fmt.Println(&quot;The End | &quot;, timeUsed)      close(ch)      close(exit)    &#125;()    for i := 0; i &lt; maxCount; i++ &#123;      ch &lt;- true      &lt;- ch    &#125;  &#125;()  &lt;- exit&#125;</code></pre><p>两个协程，从channel中取数据和向channel中写数据交替。Go语言的程序看着清爽太多了，Scala扎眼睛。</p><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3><p>Java的冗长程度不比Scala轻。</p><pre><code class="Java">public class Test&#123;    public static void main(String[] args) &#123;        Object lock = new Object();        Thread sender = new Sender(lock);        Thread receiver = new Receiver(lock);        sender.start();        receiver.start();    &#125;&#125;</code></pre><p>主方法里启动了两个线程，锁是共享资源。</p><pre><code class="Java">class Message &#123;    static long MAX_COUNT = 100000000;    static String status = new String(&quot;init&quot;);    static long count = 0;    static long startTime = 0;    public static void send() &#123;        System.out.println(count);        status = &quot;sent&quot;;        count++;        if (count == 1) &#123;            startTime = System.currentTimeMillis();        &#125;        if (count &gt;= MAX_COUNT) &#123;            status = &quot;stop&quot;;            long time = System.currentTimeMillis() - startTime;            System.out.println(&quot;The End | &quot; + time);        &#125;    &#125;    public static void receive() &#123;        status = &quot;received&quot;;    &#125;    public static String getStatus() &#123;        return status;    &#125;&#125;</code></pre><p>Message是临界资源，储存消息的内容。消息内容变更时做了一点其他的事情，把需要的日志打印到屏幕上。</p><pre><code class="Java">class Sender extends Thread &#123;    Object lock = null;    public Sender(Object lock) &#123;        this.lock = lock;    &#125;    @Override    public void run() &#123;        while (!Message.getStatus().equals(&quot;stop&quot;)) &#123;            synchronized (lock) &#123;                if (Message.getStatus().equals(&quot;init&quot;)                   || Message.getStatus().equals(&quot;received&quot;)) &#123;                    Message.send();                    lock.notify();                    try &#123;                        lock.wait();                    &#125; catch (Exception e) &#123;                        e.printStackTrace();                    &#125;                &#125;            &#125;        &#125;    &#125;&#125;class Receiver extends Thread &#123;    Object lock = null;    public Receiver(Object lock) &#123;        this.lock = lock;    &#125;    @Override    public void run() &#123;        while (!Message.getStatus().equals(&quot;stop&quot;)) &#123;            synchronized (lock) &#123;                if (Message.getStatus().equals(&quot;sent&quot;)) &#123;                    Message.receive();                    lock.notify();                    try &#123;                        lock.wait();                    &#125; catch (Exception e) &#123;                        e.printStackTrace();                    &#125;                &#125;            &#125;        &#125;    &#125;&#125;</code></pre><p>Sender和Receiver的程序类似，Sender先发送消息，然后wait，等着接收Receiver的消息，Receiver用while不停地判断有没有收到消息，如果有则回复消息，并且唤醒Sender，通知它该处理消息了，叫醒Sender后自己wait，等着Sender的反馈。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Actor</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>一种侧边导航栏的交互方式</title>
    <link href="https://smallyu.net/2020/03/21/%E4%B8%80%E7%A7%8D%E4%BE%A7%E8%BE%B9%E5%AF%BC%E8%88%AA%E6%A0%8F%E7%9A%84%E4%BA%A4%E4%BA%92%E6%96%B9%E5%BC%8F/"/>
    <id>https://smallyu.net/2020/03/21/%E4%B8%80%E7%A7%8D%E4%BE%A7%E8%BE%B9%E5%AF%BC%E8%88%AA%E6%A0%8F%E7%9A%84%E4%BA%A4%E4%BA%92%E6%96%B9%E5%BC%8F/</id>
    <published>2020-03-21T09:59:37.000Z</published>
    <updated>2025-06-19T03:30:33.027Z</updated>
    
    <content type="html"><![CDATA[<p>最近看到几个管理系统的演示项目，结合开发过程中不顺手的地方，发现大多数网站的侧边导航栏都是点击展开，点击关闭。</p><img src="classical.gif" width="20%"><p>感觉这样的交互方式稍微有点繁琐：</p><ul><li>在不知道子菜单位置的情况下，需要一个一个点开才能找到需要的页面</li><li>在知道子菜单位置的情况下，也需要点击一下父级菜单，才能看到想要的子菜单</li><li>不一个一个点开，就无法知道子菜单有些什么</li><li>子菜单展开之后，需要一次一次点击父菜单才能收起</li></ul><p>后来就想，能不能把点击事件换成悬浮事件呢？只要鼠标放上去，菜单就会自动展开，不用点一下的操作了。但是单纯的悬浮展开，需要考虑菜单长度不一致的问题，如果下一个菜单的长度比当前菜单短，鼠标离开当前菜单，当前菜单收回，鼠标所在的位置会直接越过下一长度较短的菜单。</p><img src="problem.gif" width="20%"><p>像图片中这样，栏目二的长度是4，栏目三的长度是2，当鼠标从栏目二向下移动，离开栏目二的瞬间栏目二收回内容，鼠标在没有移动的情况下跳过了栏目三，悬浮在栏目四上，这其实是不合理的，会违背用户的预期。栏目二之后是栏目三，这是最正常的逻辑。</p><p>为了应对这一问题，也许可以将交互设计成这样，当鼠标离开栏目二后，栏目二不收回，直到鼠标离开整个导航栏，子菜单才自动折叠。如果子菜单展开时用户点击了某一父菜单，那这个父菜单即使鼠标离开导航栏也不收回。</p><img src="solution.gif" width="20%"><p>下面是一个demo页面，通过iframe嵌入到这里，可以对比两种侧边导航栏的交互方式（移动端没有鼠标悬浮事件）。我偏爱灵活一点的交互，第二种方式单击父菜单也可以展开收起列表，相当于在方式一的基础上加入了鼠标悬浮自动展开的能力。</p><div align="center"><iframe src="/html/sider_bar_demo.html" width="400px" height="580px" frameborder="0" scrolling="yes" style="border: 5px double #e4e4e4;"> </iframe></div><p>相较于鼠标悬浮自动展开不收回的方式，更进阶一点的做法是，当鼠标从上往下移动时，子菜单自动展开但不收回，当鼠标从下往上移动时，子菜单自动展开并且自动收回。因为子菜单要不要自动收回取决于对用户接下来的操作有没有影响。不过这样的效果实现起来有些复杂了，对于网页上的一个导航栏来说，需要不断监听鼠标的坐标，开发和和维护的成本有点高。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;最近看到几个管理系统的演示项目，结合开发过程中不顺手的地方，发现大多数网站的侧边导航栏都是点击展开，点击关闭。&lt;/p&gt;
&lt;img src=&quot;classical.gif&quot;</summary>
        
      
    
    
    
    
    <category term="设计" scheme="https://smallyu.net/tags/%E8%AE%BE%E8%AE%A1/"/>
    
  </entry>
  
  <entry>
    <title>Rust 的 ownership 是什么？</title>
    <link href="https://smallyu.net/2019/12/21/Rust%E7%9A%84ownership%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F/"/>
    <id>https://smallyu.net/2019/12/21/Rust%E7%9A%84ownership%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F/</id>
    <published>2019-12-21T15:06:28.000Z</published>
    <updated>2025-06-30T13:36:00.071Z</updated>
    
    <content type="html"><![CDATA[<p>Rust是内存安全的。Facobook的Libra使用Rust开发，并推出了新的编程语言Move。Move最大的特性是将数字资产作为资源（Resource）进行管理，资源的含义是只能够移动，无法复制，就像纸币一样，以此来保证数字资产的安全。其实Move的这种思想并不是独创的，Rust早已使用这样的方式来管理内存，因此Rust是内存安全的。Rust中的内存由ownership系统进行管理。</p><h3 id="Java的引用计数"><a href="#Java的引用计数" class="headerlink" title="Java的引用计数"></a>Java的引用计数</h3><p>垃圾回收有很多种方式，ownership是其中之一。Java使用的是引用计数，引用计数法有一个广为人知的缺陷，无法回收循环引用涉及到的内存空间。引用计数的基本规则是，每次对内存的引用都会触发计数加一，比如实例化对象，将对象赋值给另一个变量，等。当变量引用被取消，对应的计数就减一，直到引用计数为0，才释放空间。</p><pre><code class="Java">class Test &#123;    Test ref = null;&#125;Test a = new Test(); // a的计数加一Test b = new Test(); // b的计数加一// 此时a的计数是1，b的计数是1a.ref = b;           // a的计数加一，因为ref是a的类变量b.ref = a;           // b的计数加一，因为ref是b的类变量// 此时a的计数是2，b的计数是2a = null;            // a的计数减一，因为a的引用被释放b = null;            // b的计数减一，因为b的引用被释放// 此时a的计数是1，b的计数是1</code></pre><p>因此，在a和b的引用被释放时，它们的计数仍然为1。想要a.ref的计数减一，就要将a.ref指向nulll，需要手动操作指定为null吗？当然不需要，Java从来没有手动释放内存空间的说法。一般情况下，a.ref执行的对象也就是b的空间被释放（计数为0）时，a.ref的计数也会自动减一，变成0，但此时因为发生了循环引用，b需要a的计数变为0，b的计数才能变成0，可a要想变成0，需要b先变成0。相当于死锁。</p><p>这和Rust的ownership有关系吗？当然，没有关系……</p><h3 id="ownership"><a href="#ownership" class="headerlink" title="ownership"></a>ownership</h3><p>ownership有三条基本规则：</p><ul><li>每个值都拥有一个变量<code>owner</code></li><li>同一时间只能有一个<code>owner</code>存在</li><li>当<code>owner</code>离开作用域，值的内存空间会被释放</li></ul><p>作用域多数情况由<code>&#123;&#125;</code>界定，和常规的作用域是一样的概念。</p><pre><code class="Rust">&#123;                       // s还没有声明    let s = &quot;hello&quot;;    // s是可用的&#125;                       // s已经离开作用域</code></pre><p>Rust的变量类型分简单类型和复杂类型，相当于普通变量和引用变量，因为ownership的存在，简单类型发生赋值操作是，值是被复制了一份的，但复杂类型是将引用直接重置到新的引用变量上，原先的变量将不可用。</p><pre><code class="Rust">let x = 5;let y = x;                        // y是5，x还是5let s1 = String::from(&quot;smallyu&quot;);let s2 = s1;                      // s2是&quot;smallyu&quot;，s1已经不可用</code></pre><p>赋值过程中，s2的指针先指向string，然后s1的指针被置空，这也就是移动（Move）的理念。如果想要s1仍然可用，需要使用<code>clone</code>复制一份数据到s2，而不是改变指针的指向。</p><pre><code class="Rust">let s1 = String::from(&quot;smallyu&quot;);let s2 = s1.clone();              // s1仍然可用</code></pre><h3 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h3><p>目前提到的有两个概念，一是ownership在离开作用域后会释放内存空间，二是复杂类型的变量以移动的方式在程序中传递。结合这两个特点，会发生这样的情况：</p><pre><code class="Rust">fn main() &#123;    let s = String::from(&quot;smallyu&quot;);    takes(s);             // s被传递到takes函数                          // takes执行结束后，s已经被释放    println!(&quot;&#123;&#125;&quot;, s);    // s不可用，程序报错&#125;fn takes(s: String) &#123;     // s进入作用域    println!(&quot;&#123;&#125;&quot;, s);    // s正常输出&#125;                         // s离开作用域，内存空间被释放</code></pre><p>如果把s赋值为简单类型，比如5，就不会发生这种情况。对于复杂类型的变量，一旦离开作用域空间就会释放，这一点是强制的，因此目前可以使用函数的返回值来处理这种情况：</p><pre><code class="Rust">fn main() &#123;    let s = String::from(&quot;smallyu&quot;);    let s2 = takes(s);     println!(&quot;&#123;&#125;&quot;, s2);&#125;fn takes(s: String) -&gt; String &#123;     println!(&quot;&#123;&#125;&quot;, s);     s&#125; </code></pre><p>takes把变量原封不动的返回了，但是需要一个变量接住takes返回的值，这里重新声明一个变量s2的原因是，s是不可变变量。</p><h3 id="引用变量"><a href="#引用变量" class="headerlink" title="引用变量"></a>引用变量</h3><p>引用变量不会触发ownership的drop方法，也就是引用变量在离开作用域后，内存空间不会被回收：</p><pre><code class="Rust">fn main() &#123;    let s = String::from(&quot;smallyu&quot;);    takes(&amp;s);    println!(&quot;&#123;&#125;&quot;, s);&#125;fn takes(s: &amp;String) &#123;    println!(&quot;&#123;&#125;&quot;, s);&#125;</code></pre><h3 id="可变变量"><a href="#可变变量" class="headerlink" title="可变变量"></a>可变变量</h3><p>引用变量仅属于可读的状态，在takes中，s可以被访问，但无法修改，比如重新赋值。可变变量可以解决这样的问题：</p><pre><code class="Rust">fn main() &#123;    let mut s = String::from(&quot;smallyu&quot;);    takes(&amp;mut s);    println!(&quot;&#123;&#125;&quot;, s);&#125;fn takes(s: &amp;mut String) &#123;    s.push_str(&quot;, aha!&quot;);&#125;</code></pre><p>可变变量也存在限制，同一个可变变量同一时间只能被一个其他变量引用：</p><pre><code class="Rust">let mut s = String::from(&quot;smallyu&quot;);let r1 = &amp;mut s;let r2 = &amp;mut s;println!(&quot;&#123;&#125;, &#123;&#125;&quot;, r1, r2);</code></pre><p>程序会报错，这是容易理解的，为了保证内存安全，一个变量只能存在一个可变的入口。如果r1和r2同时有权力更改s的值，将引起混乱。也因此，如果是<code>r1 = &amp;s</code>而不是<code>r1 = &amp;mut s</code>，程序会没有问题，只能存在一个引用针对的是可变变量的引用变量。</p><h3 id="返回值"><a href="#返回值" class="headerlink" title="返回值"></a>返回值</h3><p>函数的返回值类型不可以是引用类型，这同样和ownership的规则有关，返回普通变量相当于把函数里面的东西扔了出来，如果返回引用变量，引用变量指向的是函数里面的东西，但函数一旦执行结束就会销毁内部的一切，所以引用变量已经无法引用到函数。</p><pre><code class="Rust">fn dangle() -&gt; &amp;String &#123;    let s = String::from(&quot;smallyu&quot;);    &amp;s;&#125; // 到这里s的内容空间已经释放，返回值无法引用到这里</code></pre><h3 id="？"><a href="#？" class="headerlink" title="？"></a>？</h3><p>没有更多内容了。</p><p>最近看了一部能够让人振奋的美剧《硅谷》，编剧给主角挖了很多坑，感觉他们倒霉都是自己作的，编剧也给观众留了很多坑，剧情跌宕起伏到想给编剧寄刀片。抛开那些情节，剧中渲染的geek真的很帅，很帅！当然，神仙打架，凡人也参与不了。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Rust是内存安全的。Facobook的Libra使用Rust开发，并推出了新的编程语言Move。Move最大的特性是将数字资产作为资源（Resource）进行管理，资源的含义是只能够移动，无法复制，就像纸币一样，以此来保证数字资产的安全。其实Move的这种思想并不是独创的</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>Haskell 中的 Monad 是什么？</title>
    <link href="https://smallyu.net/2019/11/26/Haskell%E4%B8%AD%E7%9A%84Monad%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F/"/>
    <id>https://smallyu.net/2019/11/26/Haskell%E4%B8%AD%E7%9A%84Monad%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F/</id>
    <published>2019-11-26T05:34:02.000Z</published>
    <updated>2025-06-30T13:35:47.359Z</updated>
    
    <content type="html"><![CDATA[<p>第一次听说Monad是在一个Scala Meetup上，后来试着了解Monad的概念，却头疼于Haskell的各种大部头的书和教程。再后来看到阮一峰在2015年发表的《<a href="http://www.ruanyifeng.com/blog/2015/07/monad.html">图解 Monad</a>》，虽然清晰易懂，但是脱离了Haskell，图片的表意和语言中的概念对不上。阮一峰的文章译自《<a href="http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html">Functors, Applicatives, And Monads In Pictures</a>》，我阅读了原文。</p><h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>计算机程序用于控制计算机进行运算，程序操作的对象是各种不同类型的值，比如数值。这是一个简单的值<code>2</code>：</p><img src="1.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>用函数对值进行一些处理，可以返回函数执行的结果，比如：</p><img src="2.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>除了简单的数值类型，值也有可能被包含在一些上下文环境中，组成更复杂的值类型。可以把上下文环境想象成盒子，数值放在盒子里面，这个盒子整体作为一个值，描述为<code>Just 2</code>，也就是带盒子的<code>2</code>：</p><img src="3.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>如果对Java有过了解，可以将这个盒子理解为包装类，比如Integer和int，对应带盒子的2和不带盒子的2。</p><h3 id="Functors"><a href="#Functors" class="headerlink" title="Functors"></a>Functors</h3><p>面对带盒子的<code>2</code>，我们无法直接把<code>+3</code>的函数作用在它上面：</p><img src="4.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>这时需要一个函数<code>fmap</code>来操作。fmap会先从<code>Just 2</code>中取出数值2，然后和3相加，再把结果5放回盒子里，返回<code>Just 5</code>：</p><img src="5.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>fmap怎么知道该如何解析<code>Just</code>？换一个其他像<code>Only</code>之类的类型，还能解析吗？所以就需要Functor（函子）来完成<code>定义</code>的的操作。</p><p>Functor是一种数据类型：</p><img src="6.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>Functor定义了fmap的行为：</p><img src="7.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>fmap有两个入参和一个出参，入参分别是一个函数和一个带盒子的值，出参是一个带盒子的值，可以这样使用：</p><pre><code class="Haskell">fmap (+3) (Just 2)-- Just 5</code></pre><p>回到Haskell，在Haskell的“系统类库”中有一个<code>Functor</code>的实例<code>Maybe</code>，<code>Maybe</code>中定义了<code>fmap</code>的行为，指定了面对<code>Just</code>类型的入参时对值进行操作：</p><pre><code class="Haskell">instance Functor Maybe where  fmap func (Just val) = Just (func val)  fmap func Nothing = Nothing</code></pre><p>表达式<code>fmap (+3) (Just 2)</code>的整个过程类似这样：</p><img src="8.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>同理，从<code>Maybe</code>的定义中能看出，如果传入fmap的第二个参数是<code>Nothing</code>，函数将返回<code>Nothing</code>，事实确实如此：</p><img src="9.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><pre><code class="Haskell">fmap (+3) Nothing-- Nothing</code></pre><p>现在假设一个Java的场景，用户使用工具类Request发起一个向服务器的请求，请求返回的类型是Response，Response是一个实体类，可能包含所需数据data也可能不包含：</p><pre><code class="Java">Response res = Request.get(url);if (res.get(&quot;data&quot;) != null) &#123;  return res.data;&#125; else &#123;  return null;&#125;</code></pre><p>使用Haskell中fmap的写法就变成了：</p><pre><code class="Haskell">fmap (get(&quot;data&quot;)) (Response res)</code></pre><p>当然Haskell不存在<code>get(&quot;data&quot;)</code>这样的写法，可以将由Response获取Response.data的操作封装为函数getData，然后传入fmap作为第一个参数。</p><p>Haskell提供了fmap函数的语法糖<code>&lt;$&gt;</code>简化fmap的写法：</p><pre><code class="Haskell">getData &lt;$&gt; (Response res)</code></pre><p>再来想一个问题，Haskell的函数是如何对列表进行操作的？函数会对列表的每一个元素都进行计算，然后返回列表：</p><img src="10.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>其实列表也是Functions，这是列表的定义：</p><pre><code class="Haskell">instance Functor [] where  fmap = map</code></pre><h3 id="Applicatives"><a href="#Applicatives" class="headerlink" title="Applicatives"></a>Applicatives</h3><p><code>Applicatives</code>是另一个概念，我们之前说数据被放在盒子里，如果函数也被放在盒子里呢？</p><img src="11.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>Haskell的系统提供了操作符<code>&lt;*&gt;</code>用于处理盒子里的函数：</p><img src="12.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>例如：</p><pre><code class="Haskell">Just (+3) &lt;*&gt; Just 2 == Just 5</code></pre><p>使用<code>&lt;*&gt;</code>还可以完成一些有趣的操作，比如分别让列表中的元素*2和+3：</p><pre><code class="Haskell">[(*2), (+3)] &lt;*&gt; [1, 2, 3]-- [2, 4, 6, 4, 5, 6]</code></pre><img src="13.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><h3 id="Monads"><a href="#Monads" class="headerlink" title="Monads"></a>Monads</h3><p>函数的执行是使用<code>带入参</code>的<code>函数</code>处理<code>值</code>，涉及到三个角色。<code>Functors</code>是被处理的<code>值</code>放在盒子里，<code>Applicatives</code>是<code>函数</code>放在盒子里，<code>Monads</code>则是将函数的<code>入参</code>放在盒子里。Monads有一个操作符<code>&gt;&gt;=</code>来实现Monads的功能。假设现在有一个函数<code>half</code>的入参是数值，如果是偶数就除以2，否则返回Nothing:</p><pre><code class="Haskell">half x = if even x  then Just (x `div` 2)  else Nothing</code></pre><img src="14.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>想要给<code>half</code>传一个<code>Just</code>类型的值怎么办？</p><img src="15.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p><code>&gt;&gt;=</code>可以解决这个问题：</p><pre><code class="Haskell">Just 3 &gt;&gt;= half-- Nothing</code></pre><p><code>&gt;&gt;=</code>操作符把<code>Just 3</code>变成了<code>3</code>放在<code>half</code>中进行计算。<code>Monad</code>是一个数据类型，定义了<code>&gt;&gt;=</code>的行为：</p><pre><code class="Haskell">class Monad m where  (&gt;&gt;=) :: m a -&gt; (a -&gt; m b) -&gt; m b</code></pre><img src="16.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><p>这里的<code>Maybe</code>是一个<code>Monad</code>（和上文的Maybe同时存在）:</p><pre><code class="Haskell">instance Monad Maybe where  Nothing &gt;&gt;= func = Nothing  Just val &gt;&gt;= func = func val</code></pre><p><code>&gt;&gt;=</code>还支持链式的操作：</p><pre><code class="Haskell">Just 20 &gt;&gt;= half &gt;&gt;= half &gt;&gt;= half-- Nothing</code></pre><img src="17.png" style="box-shadow: 0 0 0 #fff; margin-left: 0;" /><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>虽然Haskell的Monad比较有名，但实际上涉及到三个概念<code>Functors</code>、<code>Applicatives</code>和<code>Monads</code>，可能Monad的应用比较广泛一点。在数据处理上，FP并不比OOP高级，逻辑是相似的，只是写法不同。面对同样的问题使用不同的思维方式和表达方式去解决，对应了不同的编程思想和编程范式。世界上有很多精妙的理论等待我们探索。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;第一次听说Monad是在一个Scala Meetup上，后来试着了解Monad的概念，却头疼于Haskell的各种大部头的书和教程。再后来看到阮一峰在2015年发表的《&lt;a</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>浅析 Libra 背后的区块链技术</title>
    <link href="https://smallyu.net/2019/11/20/%E6%B5%85%E6%9E%90Libra%E8%83%8C%E5%90%8E%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF/"/>
    <id>https://smallyu.net/2019/11/20/%E6%B5%85%E6%9E%90Libra%E8%83%8C%E5%90%8E%E7%9A%84%E5%8C%BA%E5%9D%97%E9%93%BE%E6%8A%80%E6%9C%AF/</id>
    <published>2019-11-20T09:21:18.000Z</published>
    <updated>2025-11-09T09:22:13.451Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>前段时间国家领导人曾公开表示鼓励区块链技术的研究，称要把区块链作为核心技术自主创新的突破口。Libra的发行计划是区块链发展史上一座重要的里程碑，本文从合约语言、数据库协议、逻辑数据模型、数据结构、共识协议等方面简要介绍Libra区块链的技术方案。</p></blockquote><p>2019年5月，Facebook首次确认推出加密货币的意向，“全球币”、“脸书币”的消息不胫而走。6月18日，Facebook正式宣布将推出名为Libra的加密货币，预计2020年上半年针对性发布。Facebook宣称，Libra建立在安全、可靠、可扩展的区块链上，采用链外资产抵押的模式，锚定一篮子法定货币作为资产担保，由独立的Libra协会治理。Facebook在全球拥有27亿用户，Libra的愿景是建立一套简单、无国界的货币，为数十亿人提供金融服务。</p><p>Facebook正式宣布Libra后，同步上线了Libra的官网、白皮书和测试网络等内容。白皮书中提到，Libra网络希望未来以公有链的形式运作，但由于目前没有成熟的技术能在公有链上支撑大规模的交易，Libra将以联盟链的形式起步，计划三到五年内展开由联盟链过渡为公有链的研究。</p><p>Libra官网公布了三篇论文详细说明Libra使用的技术方案，这三篇论文分别是《Libra区块链》、《Move：一种具有可编程资源的语言》和《Libra区块链中的状态机复制》。Libra为满足高度安全、足够灵活、吞吐量极高等要求，设计了新的编程语言Move，选择BTF共识机制，采用广泛使用的Merkle Tree作为数据结构。本文简要介绍Libra区块链的相关技术。</p><h3 id="Move"><a href="#Move" class="headerlink" title="Move"></a>Move</h3><p>当现实世界的资产进入Libra储备，系统会创建相应的数字资产Libra货币，这些数字资产在不同账户之间流通，当现实中的资产离开Libra储备，对应的数字资产也随之销毁。Fackbook设计了新的编程语言Move用于对Libra中的数字资产进行管理，Libra在Move中将数字资产表示为资源（resource）。</p><p>Move是带有类型的字节码语言，资源是Move的类型之一，程序在执行前会先经过字节码验证器检验，然后由解释器执行。Move中的资源仅支持copy和move两种操作，可以理解为copy移出资源，move移入资源，也就是说资源和普通的变量类型不同，资源的值可以赋给普通变量，但资源本身只能在地址间移动，不能复制或丢弃。如果在程序中使用了违反规则的copy和move操作，比如copy一次move两次，程序将无法通过字节码验证器，因为在第一次move后原先的资源就已经不可访问了。</p><p>使用Move可以编写自定义的交易逻辑和智能合约，相比现有的合约语言要更加强大。比特币脚本提供了简洁优雅的设计用于表达花费比特币的策略，但是比特币脚本不支持自定义数据类型和程序，不是图灵完备的。以太坊虚拟机倒是支持流程控制、自定义数据结构等特性，但太过自由的合约让程序的漏洞随之增多，发生过多起安全事件。Move的静态类型系统为数字资产的安全性提供了保障。</p><p>为了配合静态验证工具的验证，Move在设计上采取了一些措施：没有动态调度，让验证工具更容易分析程序；限制可变性，每一次值的变化都要通过引用传递，临时变量必须在单个脚本中创建和销毁，字节码验证器使用类似Rust的”borrow checking”机制保证同一时间变量只有一个可变引用；模块化，验证工具可以从模块层面对程序进行验证而不需要关心具体实现细节，等等。Move的这些特性都使得静态验证工具更加高效可靠。</p><pre><code>public main(payee: address, amount: u64) &#123;  let coin: 0x0.Currency.Coin = 0x0.Currency.withdraw_from_sender(copy(amount));  0x0.Currency.deposit(copy(payee), move(coin));&#125;</code></pre><p>这是一段交易脚本的示例程序，是Move语言的中间表示（IR），IR更适合程序员阅读和编写。程序实现了一个转移资源的函数，main方法是脚本的入口，包含两个入参：目标地址和金额。程序先从0x0地址Currency模块中移出amount个资源暂存到coin变量，然后将coin的资源移动到payee的地址上。</p><p>交易脚本是为Move提供灵活性的一个方面，另一方面来自安全的模块化设计。交易脚本让交易逻辑更加自由，模块化设计则让保证了脚本程序的多样化。模块的类型是<code>module</code>，主要包含Move程序，一个模块可以包含任意个资源，也就是声明另个或多个资源类型的变量，modules&#x2F;resources&#x2F;procedures相当于面向对象语言中的classes&#x2F;object&#x2F;methods，不同的是Move中并没有self、this之类的概念。</p><h3 id="Libra协议"><a href="#Libra协议" class="headerlink" title="Libra协议"></a>Libra协议</h3><p>Libra区块链是一个需要经过密码学认证的分布式数据库，用于储存可编程资源，比如Libra货币就是可编程资源，在Move中表现为资源。Libra协议中有两种实体类型，验证节点（validators）共同参与维护数据库，客户端（client）通常发起向数据库的请求。Libra协议会在执行过程中选举出leader接收客户端的请求，然后leader将请求同步到其他验证节点执行，其他验证节点执行结束后把结果返回给leader，leader再把请求的最终结果返回客户端。</p><p>Libra的交易会经过很多步骤，包括验签、运行先导程序、验证交易脚本和模块程序的正确性、发布模块、执行交易脚本、运行结尾程序等。为了使合约交易的计算能力可计量，Libra吸收了以太坊中Gas的概念，消耗Gas作为交易的费用。</p><img src="a.png" width="100%" style="box-shadow: 0 0 0px #fff;"><p>这张图详细展示了交易请求在Libra的网络组件中流转的过程，客户端发起请求到权限控制层，权限验证后将请求数据转给虚拟机进行预处理，同时数据也会进入内存池中，内存池负责将请求同步到其他节点，共识协议在请求同步的过程中发挥作用，节点同步结束后虚拟机执行真正的交易程序，程序执行完毕对结果持久化，基本流程结束。</p><h3 id="逻辑数据模型"><a href="#逻辑数据模型" class="headerlink" title="逻辑数据模型"></a>逻辑数据模型</h3><p>Libra区块链上所有的数据都保存在有版本号标识的数据库中，版本号是64位无符号整数。每个版本的数据库都包含一个元组 (T, O, S)，T代表交易，O代表交易的输出，S代表账本的状态。当我们说执行了一个Apply操作，表示为Apply(S, T) -&gt; (O, S)，意思是在S状态下执行了T交易，产生了O输出并且账本的状态变为S。</p><p>账户是资源的拥有者，可以使用账户内的资源进行交易。账户地址是一个256位的值，创建新账户需要一个验证&#x2F;签名的键值对（vk, sk），新的账户地址a由vk经过公钥加密计算得到，a &#x3D; H(vk)。具体来说Libra使用SHA3-256实例化哈希函数，使用wards25519椭圆曲线做变量的EdDSA公钥进行数字签名。交易过程中由已经存在的账户调用create_account(a) 指令即可生成新账户。</p><img src="b.png" width="80%" style="box-shadow: 0 0 0px #fff;"><p>上图所示有四个以0x为前缀的账户地址，矩形框表示模块，椭圆形表示资源，箭头表示依赖关系。图中0x12账户中的Currency.T在Currency模块中声明，Currency模块的代码储存在0x56地址上。同理，0x34的StatChannel.T声明自0x78的StateChannel模块。当客户端想要访问0x12下的Currency.T，请求资源的路径应写作0x12&#x2F;resources&#x2F;0x56.Currency.T。</p><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>Libra交易区块中包含各节点签名的数据，交易前会对签名数据进行校验，根据这一集体签名客户端可以相信请求的数据库版本是完整有效的，也因此客户端可以请求任意节点甚至是第三方数据库副本进行查询。Libra协议中的数据结构主要基于默克尔树。</p><img src="c.png" width="100%" style="box-shadow: 0 0 0px #fff;"><p>如图所示，账本历史数据的根哈希用来验证系统的完整状态，账本数据由默克尔树累加形成，虚线表示数据累积的过程。账本历史数据的每一个节点都包含交易签名、事件树和账本状态，事件树也是基于默克尔树，账本状态则是基于稀疏默克尔树，账本状态的每个叶子节点都包含有账户数据。</p><p>Libra协议中验证节点V会对数据D的根哈希a进行校验。例如不受信任的节点在获取到数据D后使用函数f对数据进行运算，希望得到结果r，同时还需要一个用于验证函数结果正确性的数据π，协议会要求节点把(a, f, r, π)都传到验证节点V处进行验证，如果f(D) &#x3D; r则通过验证。</p><img src="d.png" width="70%" style="box-shadow: 0 0 0px #fff;"><p>在上图中，数据D &#x3D; {0:s0, 1:s1, 2:s2, 3:s3}。假设f是获取第三项数据的函数，也就是要获取h2的数据，期望结果f(D) &#x3D; h2，此时h2就是r，r &#x3D; h2，用于验证计算结果正确性的数据π &#x3D; [h3, h4]，根哈希a &#x3D; H(h4||h5) &#x3D; H(h4 || H(H(2 || r) || h3))，验证节点将执行Verify(a, f, r, π)对计算结果进行验证。</p><h3 id="共识协议"><a href="#共识协议" class="headerlink" title="共识协议"></a>共识协议</h3><p>Libra选择使用的是拜占庭容错共识，实现了一种HotStuff共识的变体LibraBFT，简称LBFT。LBFT协议的主要作用是让提交的块在同一个序列上，或者说避免分叉。每三次提交为一轮操作，每一次提交验证节点都会投票选举出下一轮的leader，同时这些投票的集合形成一个法定证书（QC），每一轮的第一个块记为preferred_round，下一个块写入时对preferred_round的QC进行验证，也就是preferred_round后的第一、二、三个块都与preferred_round校验，第四个块将是第二轮的preferred_round，依次更迭。</p><img src="e.png" width="50%" style="box-shadow: 0 0 0px #fff;"><p>如图所示，k是preferred_round，如果在k处出现了分叉，并且有2f + 1个验证节点投票给了k，k+1将接在k的后面，k+2依次写入，k左侧的分叉失效。这时假如k+3的leader超时了，k+4成为新的leader并写入在preferred_round（k）的后面，会引起新的分叉。如果k+4获得2f+1个投票，k+5会按照规则写入在k+4后面。</p><p>在k+4分叉后，当超时的k+3再次被选为leader并重新提交，验证节点会对k+3的preferred_round（k）的QC进行校验，校验通过，k+3写入在了k+2的后面，这符合规则。再然后，下一个leader（k+6）的preferred_round实际上是k+4，准备提交块到k+3后面时发现QC对不上，k及其后的k+1、k+2、k+3都会被删除，k+4后的链成为主链。</p><p>LBFT基本上是对链式HotStuff的实现，并没有太多创新，Libra团队更多的是在共识协议中做出一种选择，对BFT的选择是好是坏还存在争议，需要时间来验证，LBFT算是Libra从联盟链到公有链过渡前的方案，预计至少支持100个节点，上限大概是1000个左右。和HotStuff一样，LBFT最多容忍三分之一的不诚实节点。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Libra协议目前还处于比较早期的阶段，性能上并不算惊艳，支持每秒1000笔交易，每次提交的交易确认时间大概是10秒钟。Libra协议在设计上很多也是兼顾了性能，比如每三次提交进行一次共识，每一轮操作内不需要等待就可以进行投票，这减少了客户端和验证节点之间的网络延时；考虑到并行和分片的思想，稀疏默克尔树的使用使得账户的身份数据可以跨数据库进行验证，也支持并行更新等等。</p><p>Libra选择了众多成熟的技术构建Libra系统，使用内存安全的Rust编写核心程序、使用容易验证的Merkle Tree作数据结构、基于Chained HotStuff实现共识协议等等。Move是Libra在技术上最大的亮点之一，在语言层面保证了数字资产的安全性，Move本身是一种字节码语言，难以阅读，所以提供了Move的中间表示IR，用于编写交易脚本和智能合约。Libra作为一种在金融领域的创新实验，基于区块链提出了世界货币的愿景，其社会意义可能要远大于在技术创新上所带来的意义。Libra协会目前拥有16个成员组织，涵盖支付业、电信业、区块链业、风险投资业等领域，已经在世界范围引起广泛关注。</p><p>Libra预计2020年上半年针对性发布，让我们拭目以待！</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;前段时间国家领导人曾公开表示鼓励区块链技术的研究，称要把区块链作为核心技术自主创新的突破口。Libra的发行计划是区块链发展史上一座重要的里程碑，本文从合约语言、数据库协议、逻辑数据模型、数据结构、共识协议等方面简要介绍Libra区块链的技术方案。</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>对区块链的理性认识</title>
    <link href="https://smallyu.net/2019/11/05/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E7%90%86%E6%80%A7%E8%AE%A4%E8%AF%86/"/>
    <id>https://smallyu.net/2019/11/05/%E5%AF%B9%E5%8C%BA%E5%9D%97%E9%93%BE%E7%9A%84%E7%90%86%E6%80%A7%E8%AE%A4%E8%AF%86/</id>
    <published>2019-11-05T05:53:58.000Z</published>
    <updated>2025-11-09T09:22:13.447Z</updated>
    
    <content type="html"><![CDATA[<p>曾以为区块链是革命性的、颠覆性的技术，毕竟区块链和人工智能、大数据并列互联网前沿技术。但是，人工智能达到真正的智能暂时还是梦，大数据也实现不了像《复联》里一样的精准分析。前几天国家领导人对区块链的讲话振奋人心，可过去几年的物联网、互联网+，不也都无疾而终了吗。</p><h3 id="公有链无法脱离货币"><a href="#公有链无法脱离货币" class="headerlink" title="公有链无法脱离货币"></a>公有链无法脱离货币</h3><p>有的观点认为，核反应最初的目的是建造核弹，而核反应现在也作为电能的来源服务于人民，区块链最初的目的是支持比特币的运作，现在我们想要区块链应用于其他方向。也就是说，观点认为核反应的位置和区块链是对等的。</p><pre><code>核反应 -&gt; 核弹区块链 -&gt; 比特币</code></pre><p>可是仔细想想，到底该如何对比这几乎完全不一样的两种事物。如果你看过几本区块链相关的书，会发现讲的东西并不会特别新鲜，观点也完全够不上所谓革命、创新。比特币诞生至今不过10年左右，并没有克服技术上的难题，只是不同机制的组合，它应该和P2P或者某种电子游戏处在同一地位。比特币的价值在于进行电子交易，而不是货币本身。</p><pre><code>核反应 -&gt; 核能量 -&gt; 大爆炸区块链 -&gt; 比特币 -&gt; 电子交易</code></pre><p>因为核反应，才能产出核能量，有了核能量，才能够产生大爆炸。因为区块链，才有了比特币，有了比特币，才能够进行电子交易。所以这样来看，核能量是离不开核反应的，比特币是离不开区块链的。</p><p>那么，区块链能够离开比特币单独应用于场景吗？或者说，其实区块链和比特币是一体的，就像核反应产生出的核能量，有价值的是能量而不是反应，区块链产出比特币，运作于区块链上的比特币才有所谓匿名、不可篡改、可溯源等特性，有价值的是比特币而不是区块链。</p><p>几天前广州市政府发布补贴区块链企业的细则（<a href="http://www.gz.gov.cn/gzswjk/2.3.6.3/201910/7beb40281dde4fa2beee0da2b16aa6dc.shtml">实施细则</a>），明确要求“无币”公有链项目。这项政策，一方面是国家不允许发币，另一方面，公有链无币其实是区块链最理想的情况，一般来说，理想是难以实现的。</p><p>比特币解决的是交易中的信任问题，解决方式归根结底是数据存在哪儿。两个人进行交易，如果由交易发起方记账，或者交易接受方记账，或者两个人都记账，无论谁记，只要有一方说谎，甚至不说谎，他就是记错了，都会产生争执，不认账怎么办！这时就需要第三方机构介入，通过银行记账，通过律所解决纠纷。要是连银行、政府、法律都不相信，就不用活了。</p><p>如果真的不信任中心化的机构，比特币提出的办法是，让全世界的人都为你记账，全世界的人都会记住两个人的交易记录，谁给谁转账多少，这样无论如何都不再会有差错，除非全世界一半以上的人都犯了同样的错误。所以问题在于，凭什么让全世界的人为两个人的个人交易记账？人家为什么要记？于是将比特币作为奖励，谁记账了，并且被系统认为记的账是有效的，谁就可以得到奖金。</p><p>没有奖励，世界上的人不会主动为你记账，分布式账本还怎么维持运行？</p><h3 id="比特币并非去中心化"><a href="#比特币并非去中心化" class="headerlink" title="比特币并非去中心化"></a>比特币并非去中心化</h3><p>有人认为分布式记账、分布式数据库就已经是去中心化了，但这一定不是去中心化的最终形态。比如，比特币程序的开发、维护和升级？数据确实天下共享，但程序还是要有人制定规则，有人开发，有人发布，出bug了要有人修复，有更好的点子了要迭代升级，分布式的数据全部经由中心化的程序发布中心发布的程序处理。目前解决程序上信任的方式是将程序开源……这一点暂且可行，但是程序升级带来的困难就要大多了，要么确保向下兼容，要么确保所有人更新程序。</p><p>另外，比特币的数据冗余是个极大的问题，每个节点都需要备份全量数据，而且大多数是不相关的历史数据。如果单个节点不保留全部数据，就无法保证分布式数据的可靠性，但如果保留全部数据，又是对资源很大的浪费。中心化系统一份数据就可以解决的问题，为了能够相互信任，就多出来几十亿份数据？就好比我不相信银行，就自己造一个银行，自己管自己？</p><p>可以畅想一下，在牺牲去中心化概念的情况下，能够有哪些可能。</p><p>一、全球共用一个数据库，数据库只承担储存数据的任务，分布式程序只解决共识问题。数据库非常安全，数据容量非常大，但是写入规则严格，需要全球一半以上的人认可，或者通过其他的共识机制准入。任何人可以随意查询，可溯源，历史数据不能修改。共识程序是必要的，决定了哪些数据可以写入，比如判断余额是否足够，而且是全世界的人一起判断，如果有坏人想要写入非法数据，需要买通全球一半以上的人……这样数据冗余最少。</p><p>二、每个节点只保留一半数据，数据拆分为历史的一半和当前的一半。一个人储存最新的一半数据，另一个人储存旧的一半数据，旧数据只需要负责储存，当新数据过多时同步到旧数据这里。新数据负责接收广播、写入数据，功能等同于现在的节点，如果遇到需要查询历史数据的情况，就从旧数据的一半查。相当于两人合作完成一个节点，新旧节点随时随机搭配，节点的新旧由系统平均分配。至于安全性，因为全网的节点随机配对，应该不会低于比特币，最坏的情况是一半的节点全部挂掉。同理，可将两份数据扩展到多份数据的情况。</p><p>三、每个节点只保留一半数据或者更多份，数据对半拆分。就是同一条数据，按照一定规则拆分为多个数据包，分别储存在不同的节点，参考HDFS的储存方式，存在一定冗余，但又节省了不少空间。再激进一点，数据可以实现自验证，网络中的每个节点储存的数据大小是随机的，当用户查询某一条数据时，从全网的节点中搜寻可以组成所需数据的节点，然后从中取出数据。也就是说整个节点网络的数据都混杂在一起，难点变成了如何给数据包设计自验证机制。</p><h3 id="数字货币和区块链没有关系"><a href="#数字货币和区块链没有关系" class="headerlink" title="数字货币和区块链没有关系"></a>数字货币和区块链没有关系</h3><p>有的人谈到区块链的应用，会把央行关于数字货币的研究给扯上，甚至某交易所知名总监，以区块链为主题的演讲，却把比特币和Libra的趣闻轶事说了一遍。很多人都在忽略概念上的区别，这无关紧要，也至关重要。央行说有发行数字货币的计划，也说过区块链可以作为技术选择之一，但区块链从不是必须的技术。区块链对于国家的意义，是“以去中心化之名，行中心化之实”，意在一统国内互联网，方便监管。即使没有区块链，国家也有能力实现各种应用，只是借势上了区块链的船而已。</p><p>过去的区块链指支撑比特币运行的技术体系，未来的区块链将几乎约等于联盟链。</p><p>华为区块链白皮书中的观点很客观，区块链是互联网的补充，它不会脱离传统数据库，离不开TCP，只是在特定场合下发挥独特的作用。对于国家来说，链上的数据清晰可见，没有人能暗箱操作；对于企业来说，可以方便的实现制衡，几家企业合作共享一组数据，区块链则是打开大门的钥匙。如果没有区块链，可能说不上来数据共享是个什么样子，区块链诞生了，并且比特币在世界范围稳定运行了十多年，所以这是可信的、有前途的技术方向，大家都争先恐后创新、落地。</p><p>可以预见，未来区块链的开发会分为两类，一类底层开发，一类应用层开发。底层开发的技术要求更高，开发者素质更高，应用层开发则类似于现在的Web开发。会先后出现一些区块链应用提供商，也会相应的出现一些SDK，开发者调用区块链储存数据、进行交易，类似于现在调用数据库提供的API、请求支付机构的接口。</p><h3 id="所以"><a href="#所以" class="headerlink" title="所以"></a>所以</h3><p>区块链会被广泛应用到我们的网络中，但不足以改变世界。（不要笑）</p><h3 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h3><p>无意间发现了分布式网络 <a href="https://github.com/HelloZeroNet/ZeroNet">ZeroNet</a> ，是一个早在2015年就发布的项目，它几乎满足了所有我对区块链储存系统的想象，而且功能完备，可以基于这个网络搭建博客、论坛、邮箱、共享文件等。当然，我曾想到的、应该存在的问题，ZeroNet也一个都没有解决，算是对我的一些想法的验证，惟一不同的是我希望将分布式网络对接到公网，但ZeroNet的做法是建立了一套自治的网络系统，包括.bit域名也只能作为URI的后缀，这无疑限制了该网络无法被更加广泛传播使用。另外，由于点对点文件系统难以监管，GWF将ZeroNet列入名单，这虽然是特殊现象，但ZeroNet和IPFS等网络似乎可以说明，区块链最适合也只能应用于金融领域或者受限制的互联网中。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;曾以为区块链是革命性的、颠覆性的技术，毕竟区块链和人工智能、大数据并列互联网前沿技术。但是，人工智能达到真正的智能暂时还是梦，大数据也实现不了像《复联》里一样的精准分析。前几天国家领导人对区块链的讲话振奋人心，可过去几年的物联网、互联网+，不也都无疾而终了吗。&lt;/p&gt;
&lt;h</summary>
        
      
    
    
    
    
    <category term="区块链" scheme="https://smallyu.net/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
  </entry>
  
  <entry>
    <title>Rust 基础语法概述</title>
    <link href="https://smallyu.net/2019/08/19/Rust%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E6%A6%82%E8%BF%B0/"/>
    <id>https://smallyu.net/2019/08/19/Rust%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E6%A6%82%E8%BF%B0/</id>
    <published>2019-08-19T14:12:25.000Z</published>
    <updated>2025-06-30T13:35:29.557Z</updated>
    
    <content type="html"><![CDATA[<p>Rust是复杂度和应用场景都对标C++的语言，一起学习吧！</p><p>最近，我开始思考像本文这样类型的内容算什么，编程语言的教程？内容不全面；对语言的评价？够不着；学习笔记？如果是，那绝非我本意。我倾向于认为这是一个探索的过程，无论对于我自己还是对于别人，我希望可以表现出来的是，你看，新的编程语言没什么神秘的，它如此简单！有的程序员终其一生，都将某种语言作为自己职业头衔的前缀，“Java程序员”或是“后端开发”，我们该跳出这种怪圈。</p><h3 id="语句"><a href="#语句" class="headerlink" title="语句"></a>语句</h3><p>Rust必须以<code>;</code>结尾。</p><h3 id="常量和变量"><a href="#常量和变量" class="headerlink" title="常量和变量"></a>常量和变量</h3><p>Rust使用<code>let</code>定义常量，使用<code>let mut</code>定义变量。这样的写法可能稍微有点奇怪：</p><pre><code class="Rust">fn main() &#123;  let x = 1;  println!(&quot;&#123;&#125;&quot;, x);  let mut y = 2;  println!(&quot;&#123;&#125;&quot;, y);  y = 3;  println!(&quot;&#123;&#125;&quot;, y);&#125;</code></pre><p>不同于其他语言的是，Rust允许在同一作用域中多次声明同一常量。也就是说，Rust里的常量虽然不可以被第二次赋值，但是同一常量名可以被多次定义。我们虽然能在系统层面明白常量和变量的区别，但是写法上稍微有点容易引起混淆。我多次给同一组符号赋值，这个符号不就是变量吗？</p><pre><code class="Rust">fn main() &#123;  let x = 1;  println!(&quot;&#123;&#125;&quot;, x);  let x = 2;  println!(&quot;&#123;&#125;&quot;, x);&#125;</code></pre><p>另一个有点奇怪的地方是，Rust的变量不允许重复定义。我们无法推测语言设计者的初衷，这明显不是为了允许重复定义而允许。也许，Rust中只存在常量，<code>mut</code>关键字的作用就是给常量一个可以被多次赋值的接口。没有mut，常量就是个常量，有了mut，常量就有了获得新值的“入口”。至于变量重复定义的问题，要啥自行车？</p><pre><code class="Rust">fn main() &#123;  let mut x = 1;  let mut x = 2;&#125;// warning: variable does not need to be mutable</code></pre><h3 id="控制流"><a href="#控制流" class="headerlink" title="控制流"></a>控制流</h3><p>Rust的条件部分不需要写小括号，和Go语言一样。谁先谁后呢？</p><pre><code class="Rust">fn main() &#123;  let number = 2;  if number == 1 &#123;    println!(&quot;1&quot;)  &#125; else if number == 2 &#123;    println!(&quot;2&quot;)  &#125; else &#123;    println!(&quot;3&quot;)  &#125;&#125;</code></pre><p>由于if语句本身是一个表达式，所以也可以嵌套进赋值语句中，实现类似其他语言三目运算符的功能。（Rust是强类型的语言，所以赋值类型必须一致。）</p><pre><code class="Rust">fn main() &#123;  let number = if true &#123;    3  &#125; else &#123;    4  &#125;;  println!(&quot;&#123;&#125;&quot;, number);&#125;</code></pre><p>与Go语言简洁的多功能for循环相比，Rust支持多种类型的循环：</p><pre><code class="Rust">fn main() &#123;  loop &#123;    // ...  &#125;  while true &#123;    // ...  &#125;  let a = [1, 2, 3];  for item in a.iter() &#123;    println!(&quot;&#123;&#125;&quot;, item);  &#125;&#125;</code></pre><h3 id="函数与值的传递"><a href="#函数与值的传递" class="headerlink" title="函数与值的传递"></a>函数与值的传递</h3><p>Rust似乎不存在值传递与引用传递的区别，因为Rust中全都是引用传递，或者分类为常量的传递与变量的传递。对比Java中字符串的创建，Rust中创建字符串也可以使用“声明对象”的方式：</p><pre><code class="Rust">fn main() &#123;  // 常量传递  let a = String::from(&quot;a&quot;);  testa(&amp;a);  // 变量传递  let mut b = String::from(&quot;b&quot;);  testb(&amp;mut b);  println!(&quot;&#123;&#125;&quot;, b);&#125;fn testa(a: &amp;String) &#123;  println!(&quot;&#123;&#125;&quot;, a);&#125;fn testb(b: &amp;mut String) &#123;  b.push_str(&quot; b&quot;);&#125;</code></pre><p>函数当然也是可以有返回值的，Rust中函数的返回值用<code>-&gt;</code>定义类型，默认将函数最后一行的值作为返回值，也可以手动return提前结束函数流程。需要注意的是，在最后一行用来作为返回值的表达式，记得不要加封号……</p><pre><code class="Rust">fn main() &#123;  let mut a = test();  println!(&quot;&#123;&#125;&quot;, a);  a = test2();  println!(&quot;&#123;&#125;&quot;, a);&#125;fn test() -&gt; u32 &#123;  1&#125;fn test2() -&gt; u32 &#123;  return 2;&#125;</code></pre><h3 id="结构体"><a href="#结构体" class="headerlink" title="结构体"></a>结构体</h3><p>结构体的基本用法比较常规，没有<code>new</code>关键字，直接“实例化”就可以使用：</p><pre><code class="Rust">struct Foo &#123;  a: String,  b: i32&#125;fn main() &#123;  let t = Foo &#123;    a: String::from(&quot;a&quot;),    b: 1,  &#125;;  println!(&quot;&#123;&#125;, &#123;&#125;&quot;, t.a, t.b);&#125;</code></pre><p>同样可以给结构体添加方法：</p><pre><code class="Rust">struct Foo &#123;  a: String,  b: i32&#125;impl Foo &#123;  fn test(&amp;self) -&gt; i32 &#123;    self.b + 1  &#125;&#125;fn main() &#123;  let t = Foo &#123;    a: String::from(&quot;a&quot;),    b: 1,  &#125;;  println!(&quot;&#123;&#125;, &#123;&#125;, &#123;&#125;&quot;, t.a, t.b, t.test());&#125;// a, 1, 2</code></pre><h3 id="列表与模式匹配"><a href="#列表与模式匹配" class="headerlink" title="列表与模式匹配"></a>列表与模式匹配</h3><p>下面的例子创建了包含3个元素的向量，然后将第0个元素赋值给常量one。之后使用模式匹配判断列表的第0个元素是否等于one的值，如果相等则输出字符串”one”，否则为”none”。Rust的模式匹配中，Some()和None都是内置的关键字：</p><pre><code class="Rust">fn main() &#123;  let v = vec![1, 2, 3];  let one = &amp;v[0];  println!(&quot;&#123;&#125;&quot;, one);  match v.get(0) &#123;    Some(one) =&gt; println!(&quot;one&quot;),    Some(2) =&gt; println!(&quot;two&quot;),    None =&gt; println!(&quot;none&quot;),  &#125;&#125;</code></pre><h3 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h3><p>panic函数用于抛出异常：</p><pre><code class="Rust">fn main() &#123;  panic!(&quot;new Exception&quot;);&#125;// thread &#39;main&#39; panicked at &#39;new Exception&#39;, test.rs:4:3// note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.</code></pre><p>针对错误处理，Rust提供了两个简写的方法，用于便捷的处理错误信息。unwrap()函数会自动抛出panic，如果不使用unwrap()，程序则会跳过发生panic的代码。这在某种程度上与Java的异常处理逻辑相反，因为Java如果不对异常进行处理，程序就无法继续运行。而Rust如果使用unwrap()对panic进行处理，程序将不再继续执行，同时打印出错误信息。</p><pre><code class="Rust">use std::fs::File;fn main() &#123;  let f = File::open(&quot;hello.txt&quot;);  println!(&quot;a&quot;);  let f2 = File::open(&quot;hello.txt&quot;).unwrap();  println!(&quot;b&quot;);&#125;// a// thread &#39;main&#39; panicked at &#39;called `Result::unwrap()` on an `Err` value: Os &#123; code: 2, kind: NotFound, message: &quot;系统找不到指定的文件。&quot; &#125;&#39;, src\libcore\result.rs:999:5// ...</code></pre><p>另一个简写的方法是expect()，可用于替代unwrap()。它与unwrap()的区别在于，unwrap()使用系统内置的panic信息，而expect()可以传入参数作为panic的错误信息。仅此而已。</p><pre><code class="Rust">use std::fs::File;fn main() &#123;    let f = File::open(&quot;hello.txt&quot;).expect(&quot;Failed to open hello.txt&quot;);&#125;// thread &#39;main&#39; panicked at &#39;Failed to open hello.txt: ...// ...</code></pre><h3 id="Lambda表达式"><a href="#Lambda表达式" class="headerlink" title="Lambda表达式"></a>Lambda表达式</h3><p>Rust中的Lambda表达式使用<code>|</code>作为入参的界定符，即使用<code>||</code>来代替<code>()</code>。此外Lambda的公用和其它语言是相同的：</p><pre><code class="Rust">fn main() &#123;  let test = |num| &#123;    num == 1  &#125;;  println!(&quot;&#123;&#125;, &#123;&#125;&quot;, test(1), test(2));&#125;// true, false</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>Rust的语言特性远不止此，尤其是Rust与众不同的内存管理机制，以及让Rust新手得其门不得其道的概念”ownership”，都需要我们不断前行。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Rust是复杂度和应用场景都对标C++的语言，一起学习吧！&lt;/p&gt;
&lt;p&gt;最近，我开始思考像本文这样类型的内容算什么，编程语言的教程？内容不全面；对语言的评价？够不着；学习笔记？如果是，那绝非我本意。我倾向于认为这是一个探索的过程，无论对于我自己还是对于别人，我希望可以表现</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>基于 Java 的爬虫框架 WebCollector</title>
    <link href="https://smallyu.net/2019/08/10/%E5%9F%BA%E4%BA%8EJava%E7%9A%84%E7%88%AC%E8%99%AB%E6%A1%86%E6%9E%B6WebCollector/"/>
    <id>https://smallyu.net/2019/08/10/%E5%9F%BA%E4%BA%8EJava%E7%9A%84%E7%88%AC%E8%99%AB%E6%A1%86%E6%9E%B6WebCollector/</id>
    <published>2019-08-10T13:03:41.000Z</published>
    <updated>2026-08-04T11:43:17.000Z</updated>
    
    <content type="html"><![CDATA[<p>Long, Long Ago，网络上出现大量Python爬虫教程，各种培训班借势宣扬Python，近几年又将噱头转向人工智能。爬虫是一个可以简单也可以复杂的概念，就好比建造狗屋和建筑高楼大厦都是在搞工程。</p><p>由于工作的缘故，我需要使用WebCollector爬取一些网页上的数据。其实宏观上，爬虫无非就是访问页面文件，把需要的数据提取出来，然后把数据储存到数据库里。难点往往在于，一是目标网站的反爬策略，这是让人比较无奈的斗智斗勇的过程；二是目标网页数量大、类型多，如何制定有效的数据爬取和数据分析方案。</p><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>这是一张简略的概念图，受屏幕宽度限制，可能无法看清内容，请在新标签页打开图片，或者直接点击 <a href="/2019/08/10/%E5%9F%BA%E4%BA%8EJava%E7%9A%84%E7%88%AC%E8%99%AB%E6%A1%86%E6%9E%B6WebCollector/WebCollector.png">这里</a>。这张图片并不是完美的，甚至还包含不完全正确的实现方式，具体内容会在后面阐述。</p><img src="WebCollector.png" width="95%" height="100%"><p>我将目标网页分为4种类型：</p><ol><li>静态的网页文档，curl就可以加载到</li><li>需要自定义HTTP请求的页面，比如由POST请求得到的搜索结果页面，或者需要使用Cookie进行鉴权的页面</li><li>页面中包含由JavaScript生成的数据，而我们需要的正是这部分数据。由于js是加载后才执行的，就像CSS加载后由浏览器进行渲染一样，这样的数据无法直接得到</li><li>页面中包含由JavaScript生成的数据，且需要自定义HTTP请求的页面</li></ol><h3 id="测试环境"><a href="#测试环境" class="headerlink" title="测试环境"></a>测试环境</h3><p>为了便于测试，在本地使用Node.js启动一个简单的服务器，用于接收请求，并返回一个页面作为响应。server.js的内容如下：</p><pre><code class="JavaScript">var http = require(&#39;http&#39;)var fs = require(&#39;fs&#39;)var server = http.createServer((req,res) =&gt; &#123;  // 返回页面内容  fs.readFile(&#39;./index.html&#39;, &#39;utf-8&#39;, (err,data) =&gt; &#123;    res.end(data);  &#125;);  // 打印请求中的Cookie信息  console.log(req.headers.cookie)&#125;)server.listen(9000) </code></pre><p>index.html的内容更加简单，只包含一个title和一个p标签：</p><pre><code class="HTML">&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;    &lt;title&gt;This is a title&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;</code></pre><h3 id="静态页面"><a href="#静态页面" class="headerlink" title="静态页面"></a>静态页面</h3><p>这是一个最简版的爬虫程序，在构造方法中调用父类的有参构造方法，同时添加url到待爬取队列中。visit是消费者，每一个url请求都会进入这个方法被处理。</p><pre><code class="Java">public class StaticDocs extends BreadthCrawler &#123;    public StaticDocs(String crawlPath, boolean autoParse) &#123;        super(crawlPath, autoParse);        this.addSeed(&quot;http://127.0.0.1:9000/&quot;);    &#125;    @Override    public void visit(Page page, CrawlDatums next) &#123;        System.out.println(page.doc().title();        // This is a title    &#125;    public static void main(String[] args) throws Exception &#123;        StaticDocs crawler = new StaticDocs(&quot;crawl&quot;, true);        crawler.start(1);    &#125;&#125;</code></pre><h3 id="Cookie鉴权"><a href="#Cookie鉴权" class="headerlink" title="Cookie鉴权"></a>Cookie鉴权</h3><p>需要在header中带cookie请求同样简单，在构造方法中添加相应配置就可以，node.js的命令行会打印出cookie的内容：</p><pre><code class="Java">public CookieDocs(String crawlPath) &#123;    super(crawlPath, true);    // 设置请求插件    setRequester(new OkHttpRequester() &#123;        @Override        public Request.Builder createRequestBuilder(CrawlDatum crawlDatum) &#123;            return super.createRequestBuilder(crawlDatum)                    .header(&quot;Cookie&quot;, &quot;name=smallyu&quot;);        &#125;    &#125;);    this.addSeed(&quot;http://127.0.0.1:9000/&quot;);&#125;// name=smallyu</code></pre><h3 id="JavaScript生成的数据"><a href="#JavaScript生成的数据" class="headerlink" title="JavaScript生成的数据"></a>JavaScript生成的数据</h3><p>测试js生成数据的情况需要做一点准备，修改index.html，在body标签中加入这样几行代码：</p><pre><code class="JavaScript">&lt;div id=&quot;content&quot;&gt;1&lt;/div&gt;&lt;script&gt;  document.getElementById(&#39;content&#39;).innerHTML = &#39;2&#39;&lt;/script&gt;</code></pre><p>可以预见，请求中直接返回的div内容是1，然后js经由浏览器执行，改变div的内容为2。访问静态页面的爬虫程序只能进行到第1步，也就是直接获取请求返回的内容。修改StaticDocs.java的visit方法，打印出div的内容看一下，可以确信是1：</p><pre><code class="Java">System.out.println(page.select(&quot;div&quot;).text());// 1</code></pre><p>这是一个官方提供的Demo，用于获取js生成的数据。WebCollector依赖于Selenium，使用HtmlUnitDriver运行js：</p><pre><code class="Java">public class JsDocs &#123;    public static void main(String[] args) throws Exception &#123;        Executor executor = (CrawlDatum datum, CrawlDatums next) -&gt; &#123;            HtmlUnitDriver driver = new HtmlUnitDriver();            driver.setJavascriptEnabled(true);            driver.get(datum.url());            WebElement divEle = driver.findElement(By.id(&quot;content&quot;));            System.out.println(divEle.getText());            // 2        &#125;;        //创建一个基于伯克利DB的DBManager        DBManager manager = new RocksDBManager(&quot;crawl&quot;);        //创建一个Crawler需要有DBManager和Executor        Crawler crawler = new Crawler(manager, executor);        crawler.addSeed(&quot;http://127.0.0.1:9000/&quot;);        crawler.start(1);    &#125;&#125;</code></pre><p>如果你看过WebCollector的主页，一定可以注意到这个Demo和其他Demo的明显不同。在不需要js生成的数据时，新建的类继承自BreadthCrawler，而BreadthCrawler继承自AutoParseCrawler，AutoParseCrawler又继承自Crawler。现在获取js数据的Demo，直接跳过BreadthCrawler和AutoParseCrawler，实例化了Crawler。</p><img src="uml.png" width="50%" height="100%"><p>为什么要这样做呢？再次强调，这是官方提供的Demo。</p><h3 id="Cookie鉴权后JavaScript生成的数据"><a href="#Cookie鉴权后JavaScript生成的数据" class="headerlink" title="Cookie鉴权后JavaScript生成的数据"></a>Cookie鉴权后JavaScript生成的数据</h3><p>根据官方提供的用例，显然是无法设置cookie的，因为Crawler类并没有提供自定义Header的方法。这个自定义Header的方法继承自AutoParseCrawler类。那么如何做到既可以添加Cookie又可以使用HtmlUnitDriver？</p><p>其实结果很简单，我在看过WebCollector的代码后发现AutoParseCrawler实现了Executor接口，并且在构造方法中将this赋值给了父类的executor。也就是说，AutoParseCrawler本身就是一个Executor。下面的代码用以表示它们的关系：</p><pre><code class="Java">public class Crawler &#123;    protected Executor executor;    public Crawler(DBManager dbManager, Executor executor) &#123;        // ...    &#125;&#125;public class AutoParseCrawler extends Crawler implements Executor &#123;    public AutoParseCrawler(boolean autoParse) &#123;        // 这里的executor指向父类        this.executor = this;    &#125;&#125;</code></pre><p>new Crawler时传入一个executor，相当于直接new一个AutoParseCrawler。BreadthCrawler继承自AutoParseCrawler，所以BreadthCrawler本身也是个Executor。再看官方关于自定义Cookie的Demo，如何在其中使用HtmlUnitDriver呢？重写Executor的execute方法。</p><p>所以，在定义cookie后获取js生成的数据，使用继承BreadthCrawler的类，然后重写execute就可以。这是一个完整的Demo：</p><pre><code class="Java">/** * @author smallyu * @date 2019.08.11 12:18 */public class JsWithCookieDocs extends BreadthCrawler &#123;    public JsWithCookieDocs(String crawlPath) &#123;        super(crawlPath, true);        // 设置请求插件        setRequester(new OkHttpRequester() &#123;            @Override            public Request.Builder createRequestBuilder(CrawlDatum crawlDatum) &#123;                return super.createRequestBuilder(crawlDatum)                        .header(&quot;Cookie&quot;, &quot;name=smallyu&quot;);            &#125;        &#125;);        this.addSeed(&quot;http://127.0.0.1:9000/&quot;);    &#125;    // 直接重写execute即可    @Override    public void execute(CrawlDatum datum, CrawlDatums next) throws Exception &#123;        super.execute(datum, next);        HtmlUnitDriver driver = new HtmlUnitDriver();        driver.setJavascriptEnabled(true);        driver.get(datum.url());        WebElement divEle = driver.findElement(By.id(&quot;content&quot;));        System.out.println(divEle.getText());        // 2        // 同时，node.js的命令行中打印出cookie内容    &#125;    // 重写execute就不需要visit了    public void visit(Page page, CrawlDatums crawlDatums) &#123;&#125;    public static void main(String[] args) throws Exception &#123;        JsWithCookieDocs crawler = new JsWithCookieDocs(&quot;crawl&quot;);        crawler.start(1);    &#125;&#125;</code></pre><h3 id="外部代理"><a href="#外部代理" class="headerlink" title="外部代理"></a>外部代理</h3><p>也许还没有结束。在一开始概述的图片上，同时定义cookie以及获取js生成的数据，实现方式是内部Selenium + 外部browsermob-proxy。假设没有上述重写execute的方法（官方也确实没有提供类似的Demo），该如何实现想要的效果？一种实践是本地启动一个代理，给代理设置好cookie，然后让Selenium的WebDriver通过代理访问目标页面，就可以在带header的情况下拿到js生成的数据。这是在JsDocs.java的基础上，使用代理的完整实现：</p><pre><code class="Java">public class JsWithProxyDocs &#123;    public static void main(String[] args) throws Exception &#123;        Executor executor = (CrawlDatum datum, CrawlDatums next) -&gt; &#123;            // 启动一个代理            BrowserMobProxy proxy = new BrowserMobProxyServer();            proxy.start(0);            // 添加header            proxy.addHeader(&quot;Cookie&quot; , &quot;name=smallyu&quot;);            // 实例化代理对象            Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);            // 由代理对象生成capabilities            DesiredCapabilities capabilities = new DesiredCapabilities();            capabilities.setCapability(CapabilityType.PROXY, seleniumProxy);            // 内置，必须设置            capabilities.setBrowserName(&quot;htmlunit&quot;);            // 使用capabilities实例化HtmlUnitDriver            HtmlUnitDriver driver = new HtmlUnitDriver(capabilities);            driver.setJavascriptEnabled(true);            driver.get(datum.url());            WebElement divEle = driver.findElement(By.id(&quot;content&quot;));            System.out.println(divEle.getText());   // 2        &#125;;        //创建一个Crawler需要有DBManager和Executor        Crawler crawler = new Crawler(new RocksDBManager(&quot;crawl&quot;), executor);        crawler.addSeed(&quot;http://127.0.0.1:9000/&quot;);        crawler.start(1);    &#125;&#125;</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>对于WebCollector我已经没有兴趣了解更多，倒是在注意到框架的包名<code>cn.edu.hfut</code>后有种豁然开朗的感觉。凌乱的代码风格，随处可见不知所以的注释，毫无设计美感的代码架构，倒也符合国内不知名大学的开源软件水平，距离工业级的框架，可能还需要N个指数倍的时间。至于使用过程中遇到depth含义不明、线程非法结束、next.add失效等问题，就这样吧，也在情理之中，整个框架都像是赶工的结果，或者说是学生们拿来练手的项目。我在WebCollector的GitHub上PR了关于重写execute的问题，从开发者回复的只言片语中，我怀疑开源者自己都没有把里面的东西搞清楚 :P</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Long, Long</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>Kotlin：简化版的 Scala</title>
    <link href="https://smallyu.net/2019/07/06/Kotlin%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84Scala/"/>
    <id>https://smallyu.net/2019/07/06/Kotlin%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84Scala/</id>
    <published>2019-07-06T02:18:22.000Z</published>
    <updated>2025-07-19T15:57:56.734Z</updated>
    
    <content type="html"><![CDATA[<p>行走江湖的剑客，必然要有一柄趁手的宝剑。好的程序语言就像一把好剑，重量合适，拿着舒服，挥舞起来优雅，杀伤力过关。Kotlin官方对待Kotlin和Scala的关系是，“如果你玩Scala很happy，那你就不需要Kotlin。”</p><h3 id="脚本化"><a href="#脚本化" class="headerlink" title="脚本化"></a>脚本化</h3><p>Scala执行的基本单位和Java一样是类，而Kotlin允许文件中的main方法直接运行，不需要类。Java的入口函数定义在类中：</p><pre><code class="Java">public class Java &#123;    public static void main(String[] args) &#123;&#125;&#125;</code></pre><p>Scala的入口函数定义在样本类而不是普通的类中：</p><pre><code class="Scala">object Scala &#123;  def main(args: Array[String]): Unit = &#123;&#125;&#125;</code></pre><p>Kotlin的入口函数则直接定义在.kt文件中，相应的，Kotlin的类仅相当于一种数据结构，类中无法定义入口函数：</p><pre><code class="Kotlin">fun main(args: Array&lt;String&gt;) &#123;&#125;</code></pre><h3 id="构造函数与单例模式"><a href="#构造函数与单例模式" class="headerlink" title="构造函数与单例模式"></a>构造函数与单例模式</h3><p>Kotlin的构造函数同Scala一样写在类定义处，因此也无法像Java的构造函数一样直接写入初始化代码。Kotlin中使用init代码块来执行初始化程序：</p><pre><code class="Kotlin">class Test(arg: String) &#123;    init &#123;        println(&quot;This string is $&#123;arg&#125;&quot;)    &#125;&#125;fun main(args: Array&lt;String&gt;) &#123;    val test = Test(&quot;smallyu&quot;)&#125;// This string is smallyu</code></pre><p>如果需要第二个构造函数，就要使用类似ES6的constructor函数，或者类似Scala的辅助构造器。这实在是丑陋的写法，相比之下Java真的友善多了。</p><pre><code class="Kotlin">class Test(arg1: String) &#123;    init &#123;        println(&quot;This string is $&#123;arg1&#125;&quot;)    &#125;    constructor(arg2: Int): this(&quot;smallyu2&quot;) &#123;        println(&quot;This int is $&#123;arg2&#125;&quot;)    &#125;&#125;fun main(args: Array&lt;String&gt;) &#123;    val test = Test(1)&#125;// This string is smallyu2// This int is 1</code></pre><p>Kotlin的构造函数是需要用constructor关键字定义的，默认可以省略，但如果要加权限修饰符自然就不能省了。在Kotlin中实现单例模式的思路与Java相同，让构造器私有，然后通过静态方法暴露实例：</p><pre><code class="Kotlin">class Test private constructor() &#123;    companion object Factory &#123;        fun create(): Test = Test()    &#125;&#125;fun main(args: Array&lt;String&gt;) &#123;    val test = Test.Factory.create()&#125;</code></pre><p>Kotlin中的object定义静态代码块，companion允许在类内部定义静态代码块，因此compaion object定义了类外部可以访问的方法create()。</p><h3 id="getter和setter"><a href="#getter和setter" class="headerlink" title="getter和setter"></a>getter和setter</h3><p>Kotlin另一个有趣的玩意儿是getter和setter。前端框架React或Vue实现数据双向绑定的原理即使用Object.defineProperty()定义对象的getter和setter，使得对象的变化可以实时同步到页面上。Kotlin提供了对属性getter和setter的支持：</p><pre><code class="Kotlin">var test: Int    get() &#123;        println(&quot;There is test getter&quot;)        return 2    &#125;    set(arg) &#123;        println(&quot;The setter arg is $&#123;arg&#125;&quot;)    &#125;fun main(args: Array&lt;String&gt;) &#123;    println(test)    test = 3&#125;// There is test getter// 2// The setter arg is 3</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>开始对Kotlin感兴趣是因为发现Kotlin竟然支持协程，如果Kotlin真的有语言级别的协程支持，加上运行在Jvm上的特点，以及能够开发多平台应用包括Server Side、Android、JavaScript、Native，那Kotlin无疑是异常强大的编程语言。然而事实上Kotlin的协程只是一个扩展包，甚至还需要使用编译工具来引入，对协程的支持还是Go语言独大。用于JavaScript平台也是个幌子，并没有比TypeScript好用，至于Android和Native本身也是Java的应用场景……</p><p>Kotlin提供了许多语法糖，看似可以简化程序员的代码量，但是为了熟练应用Kotlin的特性，使用者又不得不搞清楚类似data class的概念，就像Scala的case class一样。Kotlin的学术性弱于Scala，工程能力又不比Java有大的优势。Go语言虽然另辟蹊径，语言特性上有广为诟病的地方，但是看着爽，写着也爽。所以Kotlin和Scala一样，并不会有广泛的应用前景。也就是说，它并不会是下一个很流行的编程语言。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;行走江湖的剑客，必然要有一柄趁手的宝剑。好的程序语言就像一把好剑，重量合适，拿着舒服，挥舞起来优雅，杀伤力过关。Kotlin官方对待Kotlin和Scala的关系是，“如果你玩Scala很happy，那你就不需要Kotlin。”&lt;/p&gt;
&lt;h3 id=&quot;脚本化&quot;&gt;&lt;a</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>JavaScript 有关联数组吗？</title>
    <link href="https://smallyu.net/2019/05/18/JavaScript%E6%9C%89%E5%85%B3%E8%81%94%E6%95%B0%E7%BB%84%E5%90%97%EF%BC%9F/"/>
    <id>https://smallyu.net/2019/05/18/JavaScript%E6%9C%89%E5%85%B3%E8%81%94%E6%95%B0%E7%BB%84%E5%90%97%EF%BC%9F/</id>
    <published>2019-05-18T10:50:32.000Z</published>
    <updated>2025-06-30T13:35:02.794Z</updated>
    
    <content type="html"><![CDATA[<p>如果你接触过PHP，那你对关联数组一定不陌生。C或Java中数组下标都是从0开始的数值，而PHP除了数值，还可以用字符串作为数组的下标。用数值做下标的数组叫做索引数组，用字符串做下标的数组叫做关联数组，他们都是合法的数组。</p><pre><code class="PHP">&lt;?php$arr[0] = 1;        // 索引数组$arr[&quot;a&quot;] = &quot;b&quot;;    // 关联数组echo $arr[0];       // 1echo $arr[&quot;a&quot;];     // b</code></pre><p>在JavaScript中，同样可以使用字符串来作为数组的下标：</p><pre><code class="JavaScript">let arr = []arr[0] = 1arr[&#39;a&#39;] = &#39;b&#39;</code></pre><p>昨天，我和漂亮同事在使用JavaScript中用字符串做下标的数组时，遇到了令人困惑的问题。</p><h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>在Express.js框架的路由处理中，用res.json()返回数组，下标为数值的数组可以正常返回，下标使用字符串的数组却始终返回空。这是一段最简代码，可以用来描述该过程：</p><pre><code class="JavaScript">app.get(&#39;/&#39;, (req, res) =&gt; &#123;  let arr = []  arr[&#39;a&#39;] = &#39;b&#39;  console.log(arr)  // [a: &#39;b&#39;]  res.json(arr)     // []&#125;)</code></pre><p>预期返回的数组arr包含1个元素，console.log()直接在命令行打印的文本内容是<code>[a: &#39;b&#39;]</code>，和预期一致，然而如果通过页面请求路由，返回的内容是<code>[]</code>，这是匪夷所思的，也就是说res.json()把数组的内容吞掉了。</p><h3 id="探寻"><a href="#探寻" class="headerlink" title="探寻"></a>探寻</h3><p>为了寻找问题的真实原因，我在框架的中找到res.json()方法的定义：</p><pre><code class="JavaScript">res.json = function json(obj) &#123;  var val = obj;  // ...  var body = stringify(val, replacer, spaces, escape)  // ...  return this.send(body);&#125;;</code></pre><p>返回内容body经过了stringify()方法处理，stringify()方法调用的是JavaScript中JSON标准库的方法JSON.stringify()：</p><pre><code class="JavaScript">function stringify (value, replacer, spaces, escape) &#123;  var json = replacer || spaces    ? JSON.stringify(value, replacer, spaces)    : JSON.stringify(value);  // ...&#125;</code></pre><p>那么就说明，JSON.stringify()方法的返回值，会忽略用字符串做下标的数组。为了证实这一现象，用简单的Demo测试一下：</p><pre><code class="JavaScript">let arr1 = [], arr2 = []arr1[0] = 1arr2[&#39;a&#39;] = &#39;b&#39;JSON.stringify(arr1)    // &quot;[1]&quot;JSON.stringify(arr2)    // &quot;[]&quot;</code></pre><p>所以问题又来了，JavaScript标准库中的JSON.stringify()方法，为什么要忽略数组中下标为字符串的元素？是有意为之，官方不赞成使用字符串做下标，还是无奈之举，存在不可抗拒的原因无法实现？为了找到问题的根源，我试着从Chrome解析JavaScript的 <a href="https://github.com/v8/v8">V8引擎</a> 中寻找JSON.stringify()的定义。</p><p>V8引擎是用C++写的，关于JSON.stringify()的定义应该是这一段代码：</p><pre><code class="JavaScript">// ES6 section 24.3.2 JSON.stringify.BUILTIN(JsonStringify) &#123;  HandleScope scope(isolate);  JsonStringifier stringifier(isolate);  Handle&lt;Object&gt; object = args.atOrUndefined(isolate, 1);  Handle&lt;Object&gt; replacer = args.atOrUndefined(isolate, 2);  Handle&lt;Object&gt; indent = args.atOrUndefined(isolate, 3);  RETURN_RESULT_OR_FAILURE(isolate,                           stringifier.Stringify(object, replacer, indent));&#125;</code></pre><p>可以推测出，object即JSON.stringify()处理并返回的内容，返回之前使用args.atOrUndefined()方法进行包装。这里atOrUndefined()被反复调用，传入两个参数，可以理解为，第一个参数isolate保存有完整的参数信息，第二个参数是数据的索引，结合起来便是atOrUndefined()方法要处理的完整数据。</p><p>然后看atOrUndefined()的定义，在下面的代码中，tOrUndefined()调用了at()方法，at()方法又调用了Arguments的at方法：</p><pre><code class="C++">Handle&lt;Object&gt; atOrUndefined(Isolate* isolate, int index) &#123;  if (index &gt;= length()) &#123;    return isolate-&gt;factory()-&gt;undefined_value();  &#125;  return at&lt;Object&gt;(index);&#125;Handle&lt;S&gt; at(int index) &#123;  DCHECK_LT(index, length());  return Arguments::at&lt;S&gt;(index);&#125;</code></pre><p>Arguments::at()方法中，指针value获取了待处理参数的内存地址，然后使用reinterpret_cast对value的值进行类型强转。</p><pre><code class="C++">Handle&lt;S&gt; at(int index) &#123;  Object** value = &amp;((*this)[index]);  // This cast checks that the object we&#39;re accessing does indeed have the  // expected type.  S::cast(*value);  return Handle&lt;S&gt;(reinterpret_cast&lt;S**&gt;(value));&#125;</code></pre><p>到这里值就返回了，但是并没能解释为什么使用字符串做下标的数组内容会被忽略。只要是同一个数组，它的值就会保存在一段连续的地址空间中，即使reinterpret_cast处理的是指针变量，也应该无论多少都照常输出才是。</p><h3 id="真相"><a href="#真相" class="headerlink" title="真相"></a>真相</h3><p>最后，通过Google找到了一个关于数组使用字符串做下标的问题和答案（<a href="https://stackoverflow.com/questions/10326635/string-index-in-js-array">String index in js array</a>），我才明白为什么字符串做下标的数组如此特殊，因为JavaScript里压根就没有关联数组！</p><pre><code class="JavaScript">let arr1 = [], arr2 = []arr1[0] = 1arr2[&#39;a&#39;] = &#39;b&#39;arr1.length     // 1arr2.length     // 0</code></pre><p>给一个数组使用字符串作为下标赋值后，数组的长度不会改变，赋的值并没有作为数组元素储存到数组里。使用字符串作为下标能够正常对数组取值赋值的原因是，JavaScript将字符串作为数组的属性进行了储存。</p><pre><code class="JavaScript">let arr = []arr[&#39;a&#39;] = &#39;b&#39;arr.hasOwnProperty(&#39;a&#39;)   // true</code></pre><p>因此，JSON.stringify()处理的是数组的内容，reinterpret_cast也只是基于指针对数组内容进行类型转换，属性什么的，当然不会有输出！</p><h3 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h3><ol><li><p>为什么console.log()可以将数组的属性也输出？对于要输出的内容，它是怎么定义的？</p></li><li><p>为什么JavaScript中<code>typeof []</code>的值是<code>&quot;object&quot;</code>，也就是数组的类型是对象，但对象的属性会被处理，而数组不会？</p></li></ol>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;如果你接触过PHP，那你对关联数组一定不陌生。C或Java中数组下标都是从0开始的数值，而PHP除了数值，还可以用字符串作为数组的下标。用数值做下标的数组叫做索引数组，用字符串做下标的数组叫做关联数组，他们都是合法的数组。&lt;/p&gt;
&lt;pre&gt;&lt;code</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>主流编程语言的异常处理机制</title>
    <link href="https://smallyu.net/2019/04/24/%E4%B8%BB%E6%B5%81%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6/"/>
    <id>https://smallyu.net/2019/04/24/%E4%B8%BB%E6%B5%81%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6/</id>
    <published>2019-04-24T02:19:04.000Z</published>
    <updated>2025-06-19T03:30:33.084Z</updated>
    
    <content type="html"><![CDATA[<p>学习编程语言应该从语言特性入手，而不是编程语言本身。这里尝试对各种编程语言的异常和错误处理机制做一个横向的、简单的了解。涉及到的编程语言包括C、C++、Go、Java、Scala、Kotlin、Ruby、Rust、JavaScript、PHP、Python、Lisp。</p><h3 id="C"><a href="#C" class="headerlink" title="C"></a>C</h3><p>C语言没有异常捕获机制。程序在发生错误时会设置一个错误代码errno，该变量是全局变量。C语言提供了perror()和strerror()函数来显示与errno相关的描述信息。perror()函数可以直接调用，入参是一个字符串，输出<code>入参: 错误文本</code>。strerror()函数入参是一个数字（错误码），返回一个指针，指针指向错误码对应的文本。</p><pre><code class="C">#include &lt;stdio.h&gt;#include &lt;errno.h&gt;#include &lt;string.h&gt;void main ()&#123;  // 打开一个不存在的文件，会发生错误  fopen (&quot;unexist.txt&quot;, &quot;rb&quot;);  // 2  printf(&quot;%d\n&quot;, errno);  // No such file or directory  perror(&quot;&quot;);  // No such file or directory  printf(&quot;%s\n&quot;, strerror(errno));&#125;</code></pre><h3 id="C-1"><a href="#C-1" class="headerlink" title="C++"></a>C++</h3><p>C++支持异常捕获机制。C++可以抛出或捕获两种内容，一种是int或char*之类的内容，程序可以捕获并抛出，这一点和Java相比有差异，因为Java并不支持直接抛出基本类型的异常：</p><pre><code class="C">#include &lt;iostream&gt;#include &lt;exception&gt;using namespace std;int main () &#123;    try    &#123;        throw &quot;error&quot;;    &#125;    catch(const char* msg)    &#123;        cout &lt;&lt; msg &lt;&lt; endl;    &#125;&#125; // error</code></pre><p>另一种内容就是类，可以是内置的标准异常类，或是自定义的异常类：</p><pre><code class="C">#include &lt;iostream&gt;#include &lt;exception&gt;using namespace std;int main () &#123;    try    &#123;        throw exception();    &#125;    catch(std::exception&amp; e)    &#123;        cout &lt;&lt; e.what() &lt;&lt; endl;    &#125;&#125; // std::exception</code></pre><h3 id="Go"><a href="#Go" class="headerlink" title="Go"></a>Go</h3><p>Go语言作为非OOP派系的编程语言，并不支持try-catch的语法，但仍然具有类似抛出和捕获的特性。Go语言有3个错误相关的关键字，panic()、recover()和defer。可以理解为，panic()函数抛出异常，recover()函数捕获异常，defer关键字定义最后也就是finally执行的内容：</p><pre><code class="Go">package mainimport &quot;fmt&quot;func main() &#123;  defer func() &#123;    err := recover()    fmt.Println(err)  &#125;()  panic(&quot;error&quot;)&#125;// error</code></pre><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3><p>Java是纯粹的OOP语言，仅支持对象的抛出和捕获：</p><pre><code class="Java">public class ErrorTest &#123;    public static void main(String[] args) &#123;        try &#123;            throw new Exception();        &#125; catch (Exception e) &#123;            System.out.println(e);        &#125;    &#125;&#125;// java.lang.Exception</code></pre><h3 id="Scala"><a href="#Scala" class="headerlink" title="Scala"></a>Scala</h3><p>Scala和Java是一个流派，同样仅支持对象的抛出和捕获，除了语法上和Java稍有差异，概念上基本是一jian样rong的：</p><pre><code class="Scala">object ErrorTest &#123;  def main(args: Array[String]): Unit = &#123;    try &#123;      throw new Exception()    &#125; catch &#123;      case e: Exception =&gt; print(e)    &#125;  &#125;&#125;// java.lang.Exception</code></pre><p>另外，Scala抛出的是Java的异常，也许Scala不能算作是独立的编程语言，而是依附于Java、为Java提供语法糖的编程语言。这一点值得深入思考和探究。</p><h3 id="Kotlin"><a href="#Kotlin" class="headerlink" title="Kotlin"></a>Kotlin</h3><p>Kotlin和Scala是一种性质的语言，默认抛出的同样是Java的异常：</p><pre><code class="Kotlin">fun main(args: Array&lt;String&gt;) &#123;  try &#123;    throw Exception()  &#125; catch (e: Exception) &#123;    print(e)  &#125;&#125;// java.lang.Exception</code></pre><h3 id="Ruby"><a href="#Ruby" class="headerlink" title="Ruby"></a>Ruby</h3><p>Ruby使用关键字raise和rescue代替try和catch来实现异常的抛出和捕获。Ruby同样支持try-catch关键字，这里暂不讨论，因为我没搞清楚它的用法。</p><pre><code class="Rbuy">begin  raise &quot;error&quot;   rescue Exception =&gt; e    puts eend// error</code></pre><h3 id="Rust"><a href="#Rust" class="headerlink" title="Rust"></a>Rust</h3><p>Rust没有try-catch的语法，也没有类似Go的错误处理函数，而是用对错误处理进行过包装的<code>Option&lt;T&gt;</code>或Option的加强版<code>Result&lt;T, E&gt;</code>进行错误处理。Rust的模式匹配和Scala类似：</p><pre><code class="Rust">fn main() &#123;  match find() &#123;    None =&gt; println!(&quot;none&quot;),    Some(i) =&gt; println!(&quot;&#123;&#125;&quot;, i),  &#125;&#125;fn find() -&gt; Option&lt;usize&gt; &#123;  if 1 == 1 &#123;    return Some(1);  &#125;  None&#125;// 1</code></pre><h3 id="JavaScript"><a href="#JavaScript" class="headerlink" title="JavaScript"></a>JavaScript</h3><p>脚本语言在变量类型上不做强制约束，捕获时也就不能按照异常类型来做区分。抛出错误的内容还是相对自由的：</p><pre><code class="JavaScript">try &#123;  throw 1&#125; catch (e) &#123;  console.log(e)&#125;// 1try &#123;  throw new Error(&#39;&#39;)&#125; catch (e) &#123;  console.log(e)&#125;// Error</code></pre><h3 id="PHP"><a href="#PHP" class="headerlink" title="PHP"></a>PHP</h3><p>PHP的try-catch和Java类似，并没有特殊之处：</p><pre><code class="PHP">&lt;?php  try &#123;      throw new Exception(&quot;error&quot;);  &#125; catch (Exception $e) &#123;      echo $e-&gt;getMessage();  &#125;</code></pre><h3 id="Python"><a href="#Python" class="headerlink" title="Python"></a>Python</h3><p>Python在语法上能找到Ruby的影子，raise触发异常，execpt捕获异常：</p><pre><code class="Python">try:  raiseexcept:  print(&quot;error&quot;)</code></pre><h3 id="Lisp"><a href="#Lisp" class="headerlink" title="Lisp"></a>Lisp</h3><p>Lisp整体较复杂，Lisp捕获处理异常的内容暂时留坑。以下是Common Lisp触发错误的情形之一，declare会声明函数入参类型，传入错误参数将引发错误：</p><pre><code class="Lisp">(defun df (a b)  (declare (double-float a b))  (* a b))  (df &quot;1&quot; 3)// *** - *: &quot;1&quot; is not a number</code></pre><h3 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h3><p>原先想梳理这些语言的大部分异常和错误处理相关概念，然而真正开始后发现比较困难，并且之前我没能区分”exception”和”checked exception”，以致从立意到标题到内容可能都有偏差。这次就先提及”exception”，之后讨论关于”checked exception”的内容。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;学习编程语言应该从语言特性入手，而不是编程语言本身。这里尝试对各种编程语言的异常和错误处理机制做一个横向的、简单的了解。涉及到的编程语言包括C、C++、Go、Java、Scala、Kotlin、Ruby、Rust、JavaScript、PHP、Python、Lisp。&lt;/p</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>Go 语言基本语法</title>
    <link href="https://smallyu.net/2019/03/15/Go%E8%AF%AD%E8%A8%80%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/"/>
    <id>https://smallyu.net/2019/03/15/Go%E8%AF%AD%E8%A8%80%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/</id>
    <published>2019-03-15T13:18:11.000Z</published>
    <updated>2025-06-30T13:34:55.278Z</updated>
    
    <content type="html"><![CDATA[<p>Go语言虽然在语言设计上不被王垠看好，但它如此简洁的代码结构确实让人着迷。</p><h3 id="语句"><a href="#语句" class="headerlink" title="语句"></a>语句</h3><p>Go语言语句结尾不需要<code>;</code>。</p><h3 id="变量和常量"><a href="#变量和常量" class="headerlink" title="变量和常量"></a>变量和常量</h3><p>使用<code>var</code>声明变量。当变量需要初始化时，可以使用赋值符号<code>:=</code>代替<code>=</code>以省略var关键字。</p><pre><code class="go">var a intvar b stringvar c int = 10var d = &quot;golang&quot;  // 编译器自动推断类型d := 10</code></pre><p>与C语言或Java不同，Go语言的类型声明在变量右侧。需要注意的是，如果程序中声明的变量未经使用，程序将无法通过编译。Go语言是一种工程化的语言，因此它的一些特性让人感觉不可理喻，但又会在实际工程中提高效益。</p><p>Go语言的变量赋值支持一些炫酷的写法，比如要交换变量x和y的值，可以使用这种违反直觉的写法：</p><pre><code class="go">x, y = y, x</code></pre><p>Go语言中使用<code>const</code>定义常量，<code>true</code>、<code>false</code>和<code>iota</code>是预定义常量。其中iota稍显特殊，iota会在每一个const关键字出现时重置为0，然后在下一次const出现前，每出现一次iota，iota的值加1。</p><pre><code class="go">const a = iota   // 0const b = iota   // 0const (  c = iota       // 0  d = iota       // 1)</code></pre><h3 id="数组和切片"><a href="#数组和切片" class="headerlink" title="数组和切片"></a>数组和切片</h3><p>声明一个元素个数为3的数组，并初始化：</p><pre><code class="go">array := [3]int&#123;0, 1, 2&#125;array[0] = 3fmt.Println(array)</code></pre><p>和其他语言一样，Go语言在声明数组后并不能改变数组的大小。所以Go语言提供了像Python一样的切片。切片可以从数组中产生，也可以使用make()函数新建。</p><pre><code class="go">array := [3]int&#123;0, 1, 2&#125;slice1 := array[:2]       // 从数组中创建slice2 := make([]int, 3)  // 直接创建fmt.Println(slice1)       // [0 1]fmt.Println(slice2)       // [0 0 0]</code></pre><p>除切片外，映射也是使用make函数创建，映射的类型全称是<code>var myMap map[string] int</code>，意为声明变量myMap，key为string，value为int。</p><h3 id="流程控制"><a href="#流程控制" class="headerlink" title="流程控制"></a>流程控制</h3><p>Go语言允许if-else语句的条件表达式不加小括号，当然加上也无妨。</p><pre><code class="go">a := 1if a == 1 &#123;  print(1)&#125; else if (a == 2) &#123;  print(2)&#125; else &#123;  print(3)&#125;</code></pre><p>选择语句的条件表达式同样不需要小括号，另外也不需要break，其他匹配项并不会执行，这一点和Scala相同。对选择语句的优化貌似已经是不约而同的做法。</p><pre><code class="go">i := 0switch i &#123;case 0:  print(0)case 1:  print(1)&#125;</code></pre><p>循环结构的条件表达式依然不需要小括号。Go语言只支持for循环。同时对无限循环的场景也做了优化，不再需要for(;;)的写法。</p><pre><code class="go">for &#123;  print(1)&#125;</code></pre><h3 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h3><p>Go语言诞生自C语言的派系，因此Go语言从一开始就不是OOP或FP的语言，没有类、对象等概念。函数是程序中的一等公民。和C语言相同，（main包下的）main函数是整个程序的入口。</p><pre><code class="go">func add(a int, b int) (int, int) &#123;  return a + b, a - b&#125;func main() &#123;  x, y := add(1, 2)  print(x, y)&#125;</code></pre><p>Go语言的语句简洁高效，函数名后的第一个括号为入参，第二个括号是出参。函数支持多返回值。如果参数类型相同，可以将类型声明合并到一起，如<code>(a, b int)</code>。</p><h3 id="结构体"><a href="#结构体" class="headerlink" title="结构体"></a>结构体</h3><p>刚才提到Go语言没有类、对象等概念，但是Go语言有类似C语言的结构体，并且能力强大。这里定义一个Person结构体，包含两个属性name和age，并为Person添加一个方法getInfo，用于输出Person对象的信息：</p><pre><code class="go">type Person struct &#123;  name string  age int&#125;func (p Person) getInfo() &#123;  print(p.name, p.age)&#125;func main() &#123;  smallyu := new(Person)  smallyu.name = &quot;smallyu&quot;  smallyu.age = 1  smallyu.getInfo()&#125;</code></pre><p>用OOP的思想理解这样的程序并不违和。除了结构体，Go语言还保留有指针的概念。Java程序员对指针可能稍感陌生，关于指针在结构体方法中的应用，可以通过一个简单的例子来了解：</p><pre><code class="go">type Person struct &#123;  name string&#125;func (p Person) setName() &#123;  p.name = &quot;set name&quot;&#125;func (p *Person) setName2() &#123;  p.name = &quot;set name&quot;&#125;func main() &#123;  smallyu := &amp;Person&#123;&quot;smallyu&quot;&#125;  smallyu.setName()  fmt.Println(smallyu)        // &amp;&#123;smallyu&#125;  bigyu := &amp;Person&#123;&quot;bigyu&quot;&#125;  bigyu.setName2()  fmt.Println(bigyu)          // &amp;&#123;set name&#125;&#125;</code></pre><p>使用值类型定义的结构体方法，入参为形参；使用引用类型定义的结构体方法，入参为实参。<code>&amp;&#123;&#125;</code>是初始化对象的方法之一，等同于<code>new()</code>。</p><h3 id="匿名结合"><a href="#匿名结合" class="headerlink" title="匿名结合"></a>匿名结合</h3><p>Go语言中匿名结合的概念，相当于OOP语言的继承。一个结构体可以继承另一个结构体的属性和方法，大致是这样。</p><pre><code class="go">type Father struct &#123;  name string&#125;func (f Father) getName() &#123;  print(f.name)&#125;type Son struct &#123;  Father&#125;func main() &#123;  smallyu := &amp;Son&#123;&#125;  smallyu.name = &quot;smallyu&quot;  smallyu.getName()       // smallyu&#125;</code></pre><p>Son并没有定义name属性，也没有定义getName()方法，它们均继承自Father。</p><h3 id="接口"><a href="#接口" class="headerlink" title="接口"></a>接口</h3><p>Go语言的接口是非侵入式的，结构体只要实现了接口中的所有方法，程序就会认为结构体实现了该接口。</p><pre><code class="go">type IPerson interface &#123;  getName()&#125;type Person struct &#123;  name string&#125;func (p Person) getName() &#123;  print(p.name)&#125;func main() &#123;  var smallyu IPerson = &amp;Person&#123;&quot;smallyu&quot;&#125;  smallyu.getName()&#125;</code></pre><h3 id="协程"><a href="#协程" class="headerlink" title="协程"></a>协程</h3><p>使用协程的关键字是<code>go</code>，从命名就能看出协程对于Go语言的重要性、协程是轻量级的线程，启动一个协程非常简单：</p><pre><code class="go">func f(msg string) &#123;  println(msg)&#125;func main() &#123;  f(&quot;直接调用方法&quot;)  go f(&quot;协程调用方法&quot;)&#125;</code></pre><p>运行程序，你会发现程序只打印出”直接调用方法”几个字。这种情况是不是似曾相识？go启用了另一个”线程”来打印消息，而main线程早已结束。在程序末尾加上<code>fmt.Scanln()</code>阻止main线程的结束，就能看到全部的打印内容。</p><h3 id="通道"><a href="#通道" class="headerlink" title="通道"></a>通道</h3><p>通道即协程之间相互通信的通道。</p><pre><code class="go">func main() &#123;  message := make(chan string)  go func() &#123;    message &lt;- &quot;ping&quot;  &#125;()  msg := &lt;-message  println(msg)&#125;</code></pre><p>make函数返回一个chan string类型的通道，在匿名函数中将字符串”ping”传入通道，之后将通道中的数据输出到变量msg，最后打印出msg的值为”ping”。</p><h3 id="错误处理"><a href="#错误处理" class="headerlink" title="错误处理"></a>错误处理</h3><p>Go语言在错误处理部分有两个函数较为常用，panic函数和defer函数。panic函数会打印错误消息，并终止整个程序的执行，类似Java的Throw Exception；defer函数会在当前上下文环境执行结束前再执行，类似try catch后的finally；panic函数虽然会终止整个程序，但不会终止defer函数的执行，可以将defer函数用于打印日志。这是一个简单的例子：</p><pre><code class="go">func main() &#123;  println(&quot;beginning&quot;)  defer func() &#123;    println(&quot;defer&quot;)  &#125; ()  println(&quot;middle&quot;)  panic(&quot;panic&quot;)  println(&quot;ending&quot;)&#125;</code></pre><p>来分析一下程序的执行结果。首先beginning被打印；然后遇到defer，暂不打印；middle在defer之前被打印；遇到panic，程序将终止，打印defer和panic。</p><p>这里要注意，defer是在程序结束前执行，而不是在其他语句结束后执行，这是有区别的。就像这里，panic函数引起了当前程序的结束，所以defer会在panic函数前执行，而不是panic后。程序的执行结果如下：</p><pre><code class="Go">beginningmiddledeferpanic: panicgoroutine 1 [running]:main.main()    D:/go/src/awesomeProject/main.go:12 +0x7f</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>除此之外Go语言还有很多语言特性，也提供了非常多实用的工具包。Go语言是一种值得我们尝试去使用的语言。关于协程和通道，后续会单独探讨这一重要特性。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><p>《Go语言编程》</p></li><li><p><a href="https://gobyexample.com/">Go by Example</a></p></li></ul>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Go语言虽然在语言设计上不被王垠看好，但它如此简洁的代码结构确实让人着迷。&lt;/p&gt;
&lt;h3 id=&quot;语句&quot;&gt;&lt;a href=&quot;#语句&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>Scala 语法基础</title>
    <link href="https://smallyu.net/2018/12/17/Scala%E8%AF%AD%E6%B3%95%E5%9F%BA%E7%A1%80/"/>
    <id>https://smallyu.net/2018/12/17/Scala%E8%AF%AD%E6%B3%95%E5%9F%BA%E7%A1%80/</id>
    <published>2018-12-17T03:06:03.000Z</published>
    <updated>2025-06-30T13:34:43.044Z</updated>
    
    <content type="html"><![CDATA[<p>Scala语法较复杂，参考软件的增量开发，学习一门编程语言也应先找到一种能够驾驭的表达方式，之后再逐步添枝加叶。Scala同时支持面向对象和函数式编程，是其语法复杂的原因之一。一些教程非常全面，但也因为全面，导致难以抽丝剥茧，抓住主干。</p><p>以下内容关注最简单的基础语法，希望根据这些内容，可以尝试编写面向对象风格的Scala代码。</p><h3 id="语句"><a href="#语句" class="headerlink" title="语句"></a>语句</h3><p>Scala允许语句结尾不加<code>;</code>，这一点类似JavaScript。</p><h3 id="变量定义"><a href="#变量定义" class="headerlink" title="变量定义"></a>变量定义</h3><p>val定义不可变变量（常量），var定义可变变量：</p><pre><code class="Scala">val msg1 = &quot;Hello World&quot;var msg2 = &quot;Hello Wrold&quot;val msg3: String = &quot;Hello World&quot;</code></pre><p>定义变量时，类型声明在变量右侧，而且是可选的，可以不声明，编译器会自动推断。Scala中的基本类型包括：</p><blockquote><p>Byte、Short、Int、Long、Char、String、Float、Double、Boolean</p></blockquote><h3 id="函数定义"><a href="#函数定义" class="headerlink" title="函数定义"></a>函数定义</h3><p>函数即方法，下面是定义函数的例子：</p><pre><code class="Scala">def max(x: Int, y: Int): Int = &#123;  if (x &gt; y) &#123;    return x  &#125; else &#123;     return y  &#125;&#125;</code></pre><p>与Java中方法定义的显著区别有三处：一是使用def关键字定义函数；二是类型声明在变量右侧，上文已提及；三是函数声明和函数体中间使用<code>=</code>连接。</p><p>注意函数声明的参数必须明确定义类型，编译器无法自动推断入参类型。返回类型则是可选的，除非函数使用了递归。另外，return关键字也是可选的，如果没有显式的返回语句，程序会将最后一次运算结果作为返回。</p><p>当然if后是单个语句也可以不使用大括号，因此该函数还可以这样描述：</p><pre><code class="Scala">def max2(x: Int, y: Int) = if (x &gt; y) x else y</code></pre><h3 id="选择结构"><a href="#选择结构" class="headerlink" title="选择结构"></a>选择结构</h3><p>上面的示例已经用到了if语句，Scala的if语句并无特殊之处，不过与其他语言相比，Scala用模式匹配的概念代替传统的switch结构：</p><pre><code class="Scala">val a = 1a match &#123;  case 1 =&gt; println(1)  case 2 =&gt; println(2)  case _ =&gt;&#125;</code></pre><p><code>_</code>通配符匹配所有值，用于捕获默认情况。匹配表达式中，备选项永远不会掉到下一个case，因此不需要break或return。（如果将_放到首句，程序不会继续向下执行）。但是要小心，如果程序没有匹配到选项，会抛出MatchError。</p><h3 id="循环结构"><a href="#循环结构" class="headerlink" title="循环结构"></a>循环结构</h3><p>while循环并不是Scala推荐的代码风格：</p><pre><code class="Scala">var i = 0while (i &lt; 5) &#123;  println(i)  i += 1&#125;</code></pre><p>似乎并没有难以理解的地方，这就是典型的while循环。与指令式语言相比，Scala没有<code>++</code>运算符，只能使用<code>i += 1</code>这样的语句。</p><p>提起while，就一定会想到for。Scala中的for循环与指令式语言有一些差异，简单的示例如下，程序会从0打印直到5（不包括5）。</p><pre><code class="Scala">for (i &lt;- 0 until 5) &#123;  println(i)&#125;</code></pre><p>Scala不推荐while循环，而更倾向于函数式的编程风格，用于遍历的foreach方法就是其一：</p><pre><code class="Scala">&quot;abc&quot;.foreach(c =&gt; println(c))</code></pre><p>程序会依次换行打印出a b c三个字符。如果函数体只有一行语句并只有一个参数，这行代码还可以更简洁：</p><pre><code class="Scala">&quot;abc&quot;.foreach(println)</code></pre><h3 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h3><p>Scala的数组并不在语言层面实现，可以实例化Array类来使用。相应的，数组下标使用小括号（也就是方法参数）表示：</p><pre><code class="Scala">val greet  = new Array[String](3)greet(0) = &quot;a&quot;greet(1) = &quot;b&quot;greet(2) = &quot;c&quot;greet.foreach(println)</code></pre><p>实例化对象时，也可以直接传入默认参数。Array确实只是一个普通的类，下面的书写方式并没有黑魔法，只是用到了样本类。关于样本类，后文有提及。</p><pre><code class="Scala">val greet2 = Array(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;)greet2.foreach(println)</code></pre><h3 id="类"><a href="#类" class="headerlink" title="类"></a>类</h3><p>类使用class关键字定义，类中也包含字段和方法，即典型的面向对象。与Python不同，Scala仍然支持权限控制：</p><pre><code class="Scala">class Accumulator &#123;  private var sum = 0  def add(b: Byte): Unit = &#123;    sum += b    println(sum)  &#125;&#125;</code></pre><h3 id="单例对象"><a href="#单例对象" class="headerlink" title="单例对象"></a>单例对象</h3><p>单例对象（Singleton对象）相当于Java中的静态类，使用object替代class关键字定义。单例对象由程序共享，可直接调用。单例对象可以作为程序入口，即将main方法定义在单例对象中。下面的程序从上面定义的Accumulator类中实例化出对象c，并调用其add方法，最终程序打印1：</p><pre><code class="Scala">object Run &#123;  def main(args: Array[String]): Unit = &#123;    val a = new Accumulator    a.add(1)  &#125;&#125;</code></pre><p>在同一源文件中，当单例对象和类同名时，称单例对象为类的伴生对象，类为单例对象的伴生类。类可以访问其伴生对象的私有属性和方法。</p><h3 id="构造方法"><a href="#构造方法" class="headerlink" title="构造方法"></a>构造方法</h3><p>Scala中构造方法的规则比Java要严格。Scala通过类参数的概念来实现构造方法：</p><pre><code class="Scala">class Accumulator(a: Int, b: Int)</code></pre><p>如果类没有主体，大括号是可以省略的。实例化这个类时，就需要传入参数。在Java中的构造方法重载，对应Scala中的辅助构造器，它看起来像这样：</p><pre><code class="Scala">class Accumulator(a: Int, b: Int) &#123;  def this(c: Int) = this(c, 1)&#125;</code></pre><p>这时类拥有两个构造方法：</p><pre><code class="Scala">val a1 = new Accumulator(1)val a2 = new Accumulator(1, 2)</code></pre><p>Scala构造器的严格之处就在于，第二个构造器只能借助第一个或超类的构造器。</p><h3 id="继承与重写"><a href="#继承与重写" class="headerlink" title="继承与重写"></a>继承与重写</h3><p>Scala的继承与Java没有明显差异，只是方法重写必须要使用override关键字：</p><pre><code class="Scala">class A(a: Int) &#123;  def test = println(&quot;a&quot;)&#125;class B(b: Int) extends A(b) &#123;  override def test = println(&quot;b&quot;)&#125;</code></pre><h3 id="特质"><a href="#特质" class="headerlink" title="特质"></a>特质</h3><p>特质（trait）和单例对象相像，除了定义时使用的关键字不同，其余和普通的类一样，可以包含字段和方法。特质的意义在于，支持混入（Mixins），并且允许混入多个特质。这一特性经常和多重继承进行对比。</p><pre><code class="Scala">trait A &#123;  def aMethod = println(&quot;A&quot;)&#125;trait B &#123;  def bMethod = println(&quot;B&quot;)&#125;class C extends A with B</code></pre><p>这样C的实例就可以调用aMethod和bMethod：</p><pre><code class="Scala">val c = new Cc.aMethodc.bMethod</code></pre><h3 id="样本类"><a href="#样本类" class="headerlink" title="样本类"></a>样本类</h3><p>样本类的定义要在class前加case关键字，即类在定义时用case修饰。这种修饰可以让Scala编译器自动为类添加一些便捷设定：1. 实例化可以省略new关键字；2. 自动将参数作为类字段；3. 自动为类添加toString、hashCode和equals：</p><pre><code class="Scala">case class A(a: Int) &#123;  def aMethod = println(a)&#125;object Run &#123;  def main(args: Array[String]): Unit = &#123;    val a = A(1)    a.aMethod     // 1    println(a)    // A(1)    println(a.a)  // 1  &#125;&#125;</code></pre><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>与Java相比，Scala支持抽象类，但不支持接口，抽象类使用abstract定义，接口则由特质代替。Scala同样支持泛型、注解等语法。</p><h3 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h3><p>以上内容并不全面，也许并不够用。使用一种编程语言，除了掌握它的基本语法外，还要熟悉它的惯用写法，尤其像Scala这种多范式的编程语言。之后会持续修改完善此篇内容，也将继续讨论Scala的其他语言特性。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;Scala语法较复杂，参考软件的增量开发，学习一门编程语言也应先找到一种能够驾驭的表达方式，之后再逐步添枝加叶。Scala同时支持面向对象和函数式编程，是其语法复杂的原因之一。一些教程非常全面，但也因为全面，导致难以抽丝剥茧，抓住主干。&lt;/p&gt;
&lt;p&gt;以下内容关注最简单的基</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>用 Scala 改写 Java 浅度实践</title>
    <link href="https://smallyu.net/2018/12/14/%E7%94%A8Scala%E6%94%B9%E5%86%99Java%E6%B5%85%E5%BA%A6%E5%AE%9E%E8%B7%B5/"/>
    <id>https://smallyu.net/2018/12/14/%E7%94%A8Scala%E6%94%B9%E5%86%99Java%E6%B5%85%E5%BA%A6%E5%AE%9E%E8%B7%B5/</id>
    <published>2018-12-14T04:36:36.000Z</published>
    <updated>2025-06-30T13:34:34.273Z</updated>
    
    <content type="html"><![CDATA[<h3 id="起源"><a href="#起源" class="headerlink" title="起源"></a>起源</h3><p>想要用Java实现Markdown解析器，目前只完成了多级标题的解析。其实也就是正则匹配之后替换掉相应内容，程序暂时比较简单，大致流程如下：</p><img src="mainProcess.png" width="80%"><h3 id="改写"><a href="#改写" class="headerlink" title="改写"></a>改写</h3><p>按照同样的流程，用Scala来实现该功能，之后也将使用Scala继续完成开发。首先读取文件内容，IO操作参考《Scala Cookbook》，只需一行代码即可：</p><pre><code class="scala">var srcLines = Source.fromFile(srcFile).getLines().toList</code></pre><p>与冗长的Java相比，Scala确实精简了不少。这是之前使用Java读取文件封装的方法：</p><pre><code class="java">/**  * 读取文件内容  *  * @param src 读取文件路径  * @return 读取文件内容  */private static String readFile(String src) throws IOException &#123;    StringBuffer content = new StringBuffer();    InputStream is = null;    BufferedReader reader = null;    is = new FileInputStream(src);    reader = new BufferedReader(new InputStreamReader(is));    String line = reader.readLine();    while (line != null) &#123;        content.append(line);        content.append(&quot;\n&quot;);        line = reader.readLine();    &#125;    if (reader != null) &#123;        reader.close();    &#125;    if (is != null) &#123;        is.close();    &#125;    return content.toString();&#125;</code></pre><p>至于Scala版本将字符串改为列表操作的原因在于，Scala和Java在使用正则匹配替换的API上有差异。Java使用Matcher对象进行迭代，Matcher对象拥有查找、替换等方法：</p><img src="replaceProcess.png" width="80%"><p>而Scala的Regex对象虽然拥有findAllMatchIn、replaceAllIn等方法，但在find中的对象仅用于查找，replace方法中又无法定位匹配项的内容。因此在Scala中，将文件读入列表，使用如下方式带索引遍历文本内容：</p><pre><code class="scala">List.range(0, srcLines.size).foreach(index =&gt; &#123;  srcLines = srcLines.updated(index, regexReplace)&#125;)</code></pre><p>无论是否含有匹配项，循环内都对列表执行一次updated，更新原内容为正则替换后的内容。这样做可能稍微欠妥，关于性能问题将持续关注并整改。可以看到的是，Scala的程序思想与Java典型的OOP确实存在些许差异。</p><p>最后关于文件写入，SDK中没有提供专门的操作对象，可使用JDK中的PrintWriter：</p><pre><code class="scala">val pw = new PrintWriter(new File(outFile))pw.write(outString)pw.close()</code></pre><h3 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h3><p>“Scala是一门会伴随开发者成长的语言”，我将用它完成我的毕业设计。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;h3 id=&quot;起源&quot;&gt;&lt;a href=&quot;#起源&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>Python 获取海贼王更新信息</title>
    <link href="https://smallyu.net/2018/12/02/Python%E8%8E%B7%E5%8F%96%E6%B5%B7%E8%B4%BC%E7%8E%8B%E6%9B%B4%E6%96%B0%E4%BF%A1%E6%81%AF/"/>
    <id>https://smallyu.net/2018/12/02/Python%E8%8E%B7%E5%8F%96%E6%B5%B7%E8%B4%BC%E7%8E%8B%E6%9B%B4%E6%96%B0%E4%BF%A1%E6%81%AF/</id>
    <published>2018-12-02T10:08:00.000Z</published>
    <updated>2025-06-30T13:34:19.282Z</updated>
    
    <content type="html"><![CDATA[<p>12月2日，晴，海贼王停更。</p><h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>做为一个合格的肥宅，海贼王和妖精的尾巴每周必追。这两部动漫播放源都在爱奇艺，都是VIP内容。每周末看动漫，都要在YouTube或其它网站上找资源。问题是，资源网站的更新往往不及时，常常需要Google“动漫名称 + 最新集数”，比如“海贼王 864”。</p><p>每个星期都精确的记住一部动漫应该更新的最新一集集数是多少，恐怕不是正常肥宅会做的事情，况且两部。这样，每次搜索资源前，都需要进入爱奇艺，搜索海贼王，看到最新的一集集数，关闭页面，进入Google搜索。妖精的尾巴也要同样的操作来一次。</p><p>而且，在爱奇艺里看到最新一集集数的瞬间，无法判断它是否停更，还需要在复杂的PC页面中找到“更新时间”这一标签，看更新状态是否正常，才可以做出判断。至于手机页面或APP，更是没有途径可以查看动漫的更新状态。</p><p>另一个获取动漫最新集数的方式是，百度直接搜索动漫名称，首页就倒序显示最新几集的列表（这一点百度好于谷歌），但也无法判断更新是否正常。再者，浏览器默认为Google，百度搜索需要先输入baidu，按TAB切换至百度搜索引擎，再输入要搜索的内容敲回车，步骤同样繁琐。</p><h3 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h3><p>写一段简单的Python脚本，从爱奇艺页面上抓取信息，自己直接访问程序便能知晓动漫的更新情况。引入工具包：</p><pre><code class="python">import urllib.requestfrom bs4 import BeautifulSoupfrom wsgiref.simple_server import make_server# 海贼王页面链接url = &quot;http://www.iqiyi.com/a_19rrhb3xvl.html?vfm=2008_aldbd&quot;</code></pre><p>urllib用于发送http请求，并接收页面数据；bs4用于解析页面，更轻易获取内容；wsgiref用于建立http服务器，提供网络服务。url是全局变量，储存海贼王页面的链接地址。</p><pre><code class="python"># 从页面获取数据def reciveData(url):  # 获取页面内容  response = urllib.request.urlopen(url)  html = response.read()  # 解析器  soup = BeautifulSoup(html, &quot;html.parser&quot;, from_encoding=&quot;utf-8&quot;)  # 更新时间  p = soup.find(&#39;p&#39;, class_=&quot;episodeIntro-update&quot;)  # 最新集数  i = soup.find(&#39;i&#39;, class_=&quot;title-update-num&quot;)  return p, i</code></pre><p>这几行代码发送了请求，并从页面中获取信息。这里更新时间和最新一集集数的信息就已经拿到了。接着要创建一个http服务器，让程序输出内容到页面：</p><pre><code class="python"># 服务器环境的处理函数def application(environ, start_response):  # 获取数据  p, i = reciveData(url)  # 拼接出页面内容  start_response(&#39;200 OK&#39;, [(&#39;Content-Type&#39;, &#39;text/html&#39;)])  content = (&#39;&lt;h3&gt;海贼王&lt;/h3&gt;&#39;            + &#39;msg: &#39; + p.contents[2].get_text().strip()             + &#39;&lt;br&gt;&#39;            + &#39;num: &#39; + i.get_text())  return [bytes(content, encoding = &quot;utf-8&quot;)]</code></pre><p>最后启动一个本地服务器，访问8010端口即可看到页面。可将程序部署到服务器，之后直接访问服务器：</p><pre><code class="python"># 启动服务器httpd = make_server(&#39;&#39;, 8010, application) httpd.serve_forever()</code></pre><p>运行结果如图：</p><img src="preview.png" width="30%"><h3 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h3><p>从这一想法出发，可以扩展程序。一种是从各大网站获取全面的动漫更新信息，主动提供服务；再一种是根据用户的输入，提供自定义的动漫更新信息；或者将两者结合，提供一种大而全的、可收藏、可定制的服务。虽然这种想法毫无意义。</p><h3 id="更正"><a href="#更正" class="headerlink" title="更正"></a>更正</h3><p>之前的代码犯了一个低级错误，程序只会在首次运行时发起网络请求，之后由于网络服务一直处于启动状态，返回网页的内容始终都是初始数据。解决这个问题也很容易，将请求网络的操作封装到一个函数中，再到application函数中调用该函数即可。（代码已更正，为保证简洁，去掉了妖尾部分的代码和控制台的日志输出）</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;p&gt;12月2日，晴，海贼王停更。&lt;/p&gt;
&lt;h3 id=&quot;问题&quot;&gt;&lt;a href=&quot;#问题&quot; class=&quot;headerlink&quot;</summary>
        
      
    
    
    
    
    <category term="编程语言" scheme="https://smallyu.net/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"/>
    
  </entry>
  
  <entry>
    <title>HTML5 音乐可视化</title>
    <link href="https://smallyu.net/2017/05/17/html5%E9%9F%B3%E4%B9%90%E5%8F%AF%E8%A7%86%E5%8C%96/"/>
    <id>https://smallyu.net/2017/05/17/html5%E9%9F%B3%E4%B9%90%E5%8F%AF%E8%A7%86%E5%8C%96/</id>
    <published>2017-05-16T16:00:00.000Z</published>
    <updated>2025-11-18T12:30:47.852Z</updated>
    
    <content type="html"><![CDATA[<img src="201705141494765899218386.png" /><p>下面梳理一下HTML5实现音乐可视化的流程。</p><p><strong>搭建静态页面</strong></p><p>静态页面的结构分三部分，header，left，right。header为语义化标签，left和right都用div来实现。</p><img src="201705141494770946657494.png" /><p>类似这样，然后把背景颜色改为黑色，字体改成白色，加上或不加边框线，给一定padding或margin，静态页面就搭建好啦。</p><p>不过重点不是页面布局。可以在左侧栏有一个曲目列表，点击切歌。这里就不那么麻烦，直接后台加载指定的歌曲。歌曲是许嵩的烟花笑，左侧栏显示部分歌词。</p><p><strong>创建音频文件对象</strong></p><p>AudioContext对象用于获取音频文件源。</p><blockquote><p>（节点）AudioContext：包含各个AudioNode对象以及它们的联系的对象，可以理解为上下文对象。绝大多数情况下，一个document中只有一个AudioContext。</p><p>（方法）createBufferSource()：创建audioBufferSourceNode对象。</p></blockquote><p>可以这样写：</p><pre><code>var ac = new window.AudioContext();    // 实例化ac为一个音频对象var bs = ac.createBufferSource();      // 用bs来保存音频文件资源</code></pre><p>有了音频资源，是否就可以播放音频了呢？当然不可以，因为还没有指定文件，告诉对象播放哪一个音频文件。</p><p><strong>获取音频数据</strong></p><p>创建bufferSource后，到了AudioBufferSourceNode这个节点。这个节点有一个属性值buffer，用来指定文件资源。这个属性值需要一个buffer类型的数据。</p><blockquote><p>（节点）AudioBufferSourceNode：表示内存中的一段音频资源，其音频数据存在于AudioBuffer中（其buffer属性）。</p><p>（属性）buffer：AudioBuffer对象，表示要播放的音频资源数据。</p></blockquote><p>获取buffer有两种方式，一种是ajax设置返回值类型为arraybuffer，然后解码，一种是用FileReader读取文件，获得blob值。后一种方式多用input上传文件后获得文件，再进行解析。前面一种好用一点。（留坑）  </p><p>ajax的原生写法：</p><pre><code>var url=&quot;...&quot;;var xhr = new XMLHttpRequest();xhr.abort();xhr.open(&quot;GET&quot;, url);xhr.responseType = &quot;arraybuffer&quot;;xhr.onload = function()&#123;    return xhr.response;&#125;xhr.send();</code></pre><p>onload触发的函数中xhr.response就是我们想要的值。</p><blockquote><p>（方法）decodeAudioData(arrayBuffer, succ(buffer), err) ：异步解码包含在arrayBuffer中的音频数据。</p></blockquote><p>将arraybuffer解码：</p><pre><code>ac.decodeAudioData(arraybuffer, function(buffer)&#123; ... &#125;,function(err)&#123; ... &#125;);bs.buffer = buffer;    // 回调函数中的buffer就是想要的buffer</code></pre><p><strong>播放音乐</strong>  </p><blockquote><p>（方法）start：开始播放音频</p></blockquote><p>这个时候调用start方法：</p><pre><code>bs.start(0);</code></pre><p>打开页面，音乐开始播放。</p><p><strong>音量控制</strong></p><blockquote><p>（节点）GainNode：改变音频音量的对象，会改变通过它的音频数据所有的sample frame的信号强度。</p><p>（方法）createGain()：创建GainNode对象。</p><p>（属性）value：可以改变音频信号的强弱，默认为1，最小值为0。</p></blockquote><p>音乐播放还不行，要添加一个音量控制控件，音量控制用GainNode节点：</p><pre><code>var gainNode = accreateGain();    // 创建GainNode对象gainNode.connect(ac.destination);    // 将gainNode连接到AudioDestinationNode节点</code></pre><p>这样就有了一个音量控制的对象。</p><pre><code>gainNode.gain.value= ... ;</code></pre><p>gain.value用于控制输入信号的强弱，也就是音量的大小。HTML中用type为range的input，把值传递给对象，就可以实时控制音量了。  </p><p><strong>得到音频解析数据</strong></p><blockquote><p>（节点）AnalyserNode：音频分析对象，他能实时的分析音频资源的频域和时域信息，但不会对音频流做任何处理。</p><p>（方法）createAnalyser()：创建AnalyserNode对象。</p></blockquote><blockquote><p>（属性）fftsize：设置FFT（是离散傅里叶变换的快速算法，用于将一个信号变换到频域）值的大小，用于分析得到频域，为32 ~ 2048之间2的整数次倍，默认为2048，实时得到的音频频域的数据个数为fftSize的一半。</p><p>（属性）frequencyBinCount，FFT值的一半，即实时得到的音频频域的数据个数</p></blockquote><blockquote><p>（方法）getByteFrequencyData(Uint8Array)，复制音频当前的频域数据（数量是frequencyBinCount）到Uint8Array（8位无符号整型类型化数组）中。</p></blockquote><p>先创建AnalyserNode对象，然后设置fftsize的值。FFT用于数字信号的处理，会把音频文件流输入的信号变换到频域，用getByteFrequencyData()方法得到经系统处理之后的频域数据。</p><pre><code>var analyser = ac.createAnalyser();    // 实例化对象analyser.fftSize = 32;                 // 设置fftsizevar arr = new Uint8Array(analyser.frequencyBinCount);    // getByteFrequencyData()方法要求参数为8位无符号整型类型化数组analyser.getByteFrequencyData(arr);    //    arr为所需频域数据</code></pre><p>这样得到的数组arr就是用于可视化效果绘制的数据。</p><blockquote><p>（方法）window.requestAnimationFrame()：告诉浏览器您希望执行动画，并请求浏览器调用指定的函数在下一次重绘之前更新动画。该方法将在重绘之前调用的回调作为参数。</p></blockquote><p>一次解析只能得到一组数据，所以需要requestAnimationFrame不断解析，不断得到arr。</p><p><strong>前端界面绘制</strong></p><p>前端界面使用canvas绘制，实现音乐可视化的效果。</p><pre><code>var box = document.getElementById(&quot;right&quot;);    // 获取right区域的dombox.appendChild(canvas);                       // 创建canvas节点var ctx = canvas.getContext(&quot;2d&quot;);             // 实例化canvas画笔</code></pre><p>接下来就是使用for循环和ctx画矩形、圆圈，填充渐变的操作了。（留坑吧，估计不填了）  </p><p>案例用到web Audio API的关系：</p><img src="201705141494773947635812.png" /><p>虚线是可以跳过的节点，直接播放音频文件。好吧有错。</p>]]></content>
    
    
      
      
        
        
    <summary type="html">&lt;img src=&quot;201705141494765899218386.png&quot;</summary>
        
      
    
    
    
    
    <category term="HTML5" scheme="https://smallyu.net/tags/HTML5/"/>
    
  </entry>
  
</feed>
