阅读视图

发现新文章,点击刷新页面。
🔲 ☆

利息,套利,交易策略,金融市场

资金费率套利

资金费率套利是一种没有风险、保本的套利方式。我一开始尝试自己写脚本来运行策略,后来发现 Binance 直接提供了 bot 来进行这种操作:

这里的 Arbitrage Bot 就是资金费率套利的 Bot,点开可以看到有很多币种可以选,包括按照资金费率来排序等。而且 Binance 的 Bot 做的很智能,如果资金费率是正的,就开正向的套利策略(使用 USDT),如果资金费率是负的,就开反向的策略(使用代币)。

资金费率套利的获利空间很小,尤其是 BTC 等主流币种,APR 在 1% 以下,当前低迷的市场环境下,大概持有 300 天以上才能磨平手续费损耗。实际上是很不划算的方式。而如果按照资金费率排序,选最高的一个代币,APR 能相对高一点,也许能达到 40% 甚至更多,当然高收益也就意味着高的波动和风险:

资金费率套利是 Detla 中性的,假如代币突然暴涨,把 2 倍的做多合约给爆仓了,会不会导致损失一半本金呢?不会,因为现货涨了 2 倍后卖掉正好抵消了爆仓的损失,所以本金没有变化,整个过程就是赚了这段时间的资金费率。

所以根据资金费率高的代币排序,然后开套利 Bot,是我唯一尝试过想赚点利息的方式。

保本赚币

Simple Earn 很简单,存到 Binance 里赚利息。问题在于 APR 并不高,USDC 也就 6%,而且有非常高的额度限制,只支持 200 USDC。几乎没什么用。

双币投资

双币投资(Dual Investment)的策略整体看下来,关键在于你心里要有一个预期的价位。比如 BTC 到 60000 就愿意买入,无论它以后会不会继续跌,都以 60000 的价格买入(卖出同理)。

双币投资更像是期货交割的逻辑,对于普通散户来说,同样没什么用。为什么双币投资的利息高呢?因为风险也高,有卖飞或者接刀子的风险。风险高,利息就高了。

如果双币投资设定一个非常离谱的价格呢,比如在 100000 的时候买入 BTC,岂不是可以达不到条件一直赚利息了?但实际上利息是由风险决定的,市场看到风险小,也就没有人愿意付高额利息,所以价格离谱的市场本身就不存在。

开合约、加杠杆

别作死。

Lido

这 2.3% 的 APR 实在是太低了。用某个人的话来说,对于普通人,低于 5% 的 APR 根本不需要考虑,得至少 20 年才能赚钱。

Aave

Aave 的 APR 也很低,甚至最高也低于 2%,几乎等于没有。

Uniswap

乍一看还挺吓人的,USDC/ETH 有 76% 的 APR。问题在于提供流动性是存在无常损失的,你投入 1000 ETH 和 1000 USDC,随着池子中不断进行交易,最终你提出来可能就剩 300 ETH 和 1700 USDC。代币的总量不变,但是 ETH 的价格在剧烈波动,你的投入就完全不划算了。而 Uniswap 上的稳定币交易对 USDT/USDC 这种池子的收益率自然很低,在 1% 左右。

BTC 5 分钟猜涨跌

Polymarket 上线了 BTC 5 分钟猜涨跌的玩法。那么在这种市场有获利的机会吗?结论是没有。这是一种负期望值的游戏,散户、低频等同于纯粹的赌博。甚至可以认为所有的事件预测市场都是赌博。别提什么稳定套利的机会了。

Polymarket 上的 free money

一方面,Polymarket 上有一些整活市场,比如 耶稣会不会在 2027 年回归?。这种市场的 no 价格大约 96.2c,意味着如果持仓一年等待事件结算后,持仓的年化收益大约 4%。非要和传统银行以及现金 APR 比的话这个收益率也还不错。

另一方面,Polymarket 有一个机制,对于某些 符合要求 的市场,只要 holding 就会一直分发年化 4% 的 shares。有一些市场是显然知道结局的,所以可以大胆买入,比如某个市场的 no 价格是 91.1c,当事件结算后会获得接近 9% 的年化收益,再累加上 4% 的持仓收益,实际上是一个还算不错的理财选择。

MEV 攻击、链上科学家、黑暗森林

快省省吧。

金融市场

经过这么多年的发展以及几年前的 DeFi Summer,无论是 Cex 利息还是 DeFi,随着 ETF 的推出、华尔街巨头的介入,只要市场上有低风险的套利机会,就会有几亿的资金以及高频低延迟的机器人入场。普通玩家的机会非常少。

在金融市场里怎么赚钱?

  1. 拿时间换空间(撸空投)
  2. 拿本金换利息(大额本金)
  3. 拿认知、精力换超额收益(现货定投、寻找 Alpha)

除此之外,别无他法。

散户之所以是韭菜,是因为:

  1. 没有无限的本金
  2. 缺乏顶级的认知
  3. 缺乏极度的耐心

反正什么都没有。有的只有无知,和赚钱的欲望。

定投策略

李笑来经历过那么多项目,最后总结出的终极交易策略是定投。《定投改变命运》这本书其实写的非常好,信息密度很高。当我们对于某些高频交易策略动心的时候,不妨回顾一下这本书里提到的要点,而定投正是需要极度耐心的一种策略。

当然定投策略以及李笑来的理论,也有让人觉得扯淡的地方。比如书里到最后,给大家的建议是提高 “场外赚钱的能力”。这基本上是非常正确的废话,谁不知道场外赚钱重要……

定投是一种与人性做对抗的策略,确实很难严格落实,尤其是对于从业者来说,天天会看到行情和市场情绪,不可能做到情绪高的时候还定投,也做不到情绪低的时候管得住手。

想要赚钱,还是得做出点别人做不到的事情才行。

Restaking

刚才提到了定投,那么假如坚持定投 BTC 的话,BTC Restaking 能不能在持有 BTC 的基础上再赚点利息?

Restaking 赛道唯一一个真正做 BTC Restaking 的项目 Babylon,提供的 APR 至高 0.87%,这确实是不值一提。毕竟 2B 的资金量压在协议里,APR 又不能凭空产生,项目收益再高,一分到 2B 的质押量上,APR 就不可能高了。所以 BTC Restaking 几乎不值得去操作。

场外赚钱

我在思考 “场外赚钱” 这个问题。我发现 “定投+持有” 的过程无聊至极。

金融市场里有一个很奇怪的逻辑,明明你就是场外赚钱能力有限,才想要到金融领域通过投资手段寻找高杠杆、高回报率的新机会。而投资界的大佬都会告诉你,场外赚钱的能力才是最重要的,你要有源源不断的现金流才行。

而事实上,金融领域从来只放大资金收益,而不负责基础资金的来源。在金融领域的不可能三角是:

  • 本金
  • 收益率
  • 时间

如果你有足够的本金,即使收益率低也有很好的回报,100 万美元年化 10% 一年下来就是 10 万美元。如果你愿意承担极度高的风险,土狗币百倍币就是高收益最直接的渠道,收益率是对承担风险的回报。

那么如果本金也不够大,风险也不愿意承担呢?剩下能投入的只有时间。

🔲 ☆

continuation 教程: 理解 CPS

这是一个 continuation 系列教程:

  1. continuation 教程:理解 CPS
  2. continuation 教程:用 yield 实现协程调度
  3. continuation 教程:用 call/cc 实现协程调度
  4. continuation 教程:用 shift/reset 实现协程调度
  5. continuation 教程:体验 Racket 语言
  6. continuation 教程:实现抢占式协程调度

我们来由浅入深地系统学习下 continuation 的原理以及应用场景。这个系列教程的内容和王垠的 continuation 专项班无关,是我自己学习和研究的成果,所以不会有版权问题。不过当然正是因为我学习了基础班,打下了坚实的基础,才知道该如何去自学和理解 continuation 这个概念。这篇文章会少量透露出基础班学到的技能,毕竟 continuation 属于基础班的进阶内容,无法跳过基础技能去理解。

递归

首先用递归的形式写一个阶乘函数 fact,我们已经很熟悉它的写法,不需要过多解释:

function fact(n){  if (n === 0)   {    return 1;  }  else  {    return n * fact(n - 1);  }}console.log("fact1=", fact(1)); // 1console.log("fact3=", fact(3)); // 6console.log("fact5=", fact(5)); // 120

尾递归

接着把 fact 函数改为尾递归的形式。尾递归会比递归多一个参数,新参数用来保存每个调用计算后的值:

function factTail(n, prod){  if (n == 0)  {    return prod;  }  else  {    return factTail(n-1, prod*n);  }}console.log("factTail1=", factTail(1, 1)); // 1console.log("factTail3=", factTail(3, 1)); // 6console.log("factTail5=", factTail(5, 1)); // 120

CPS 形式

我们基于 fact 函数的尾递归形式,再新增一个参数 k,这个 k 是一个函数,fact 不直接返回计算后的值,而是结果值对 k 函数的调用,像这样:

function factTailCPS(n, prod, k){  if (n == 0)  {    return k(prod);  }  else  {    return factTailCPS(n-1, prod*n, k);  }}factTailCPS( 1, 1, x => console.log("factTailCPS1=", x) ); // 1factTailCPS( 3, 1, x => console.log("factTailCPS3=", x) ); // 6factTailCPS( 5, 1, x => console.log("factTailCPS5=", x) ); // 120

这个 k 就是 continuation,意味着告诉 fact 函数,你执行完了计算出结果之后,应该如何进行下一步延续。不用怀疑,这个函数完全符合 CPS(Continuation-Passing-Style)的形式。

典型 CPS

但是用尾递归结合 continuation 参数的形式,显然不够简洁,并不算典型的 CPS 形式。典型的 CPS 形式比较难理解,所以不需要自己思考出来,直接看这个现成的例子,我们对递归形式的 fact 函数改进一下:

function factCPS(n, k){  if (n == 0)  {    return k(1);  }  else  {    return factCPS(n-1, r => k(n * r));  }}

可能看着有点懵,不要慌,我们拆解一下其中的内容。首先 k 仍然代表 continuation,并且 k 是一个函数。然后我们这样来调用:

let factCPS1 = factCPS(0, x => x);console.log("factCPS1=", factCPS1); // 1let factCPS3 = factCPS(3, x => x);console.log("factCPS3=", factCPS3); // 6let factCPS5 = factCPS(5, x => x);console.log("factCPS5=", factCPS5); // 120

关键在于调用的时候,传入函数的第二个参数是 x => x,如果结合函数内部的 r => k(n * r),也许一下子就糊涂了。

这确实是最难理解的部分。我们以计算 2 的阶乘为例,写一个拆解 factCPS 函数调用步骤的过程。这里用到的技巧是在基础班第一课就学过的 单步替换,对于理解递归非常有帮助。如果在基础班经过训练并且打好基础,确实会有助于理解更复杂的东西,比如这里的 CPS 调用:

let factCPS2 = factCPS(2, x => x);console.log("factCPS2=", factCPS2); // 2// n=2, k=x=>x, return factCPS(1, r => k(2 * r));  // n=1, k=r=>(x=>x)(2*r), return factCPS(0, r => k(1 * r));    // n=0, k=r=>(r=>(x=>x)(2*r)(1*r)), return k(1);      // k(1) = r=>(x=>x)(2*r)(1*1)      //      = (x=>x)(2)      //      = 2

虽然我已经按照正确的思路拆解出了正确的步骤,但是从阅读者的角度,这仍然会非常难理解,可以自己拆解一下试试,逐步理解典型 CPS 的调用过程。理解这些步骤也许需要几个小时的时间,这是正常的。

总结来说,CPS 的每一次调用,都是在用闭包来储存当前步骤计算的值。尾递归是直接用参数传递值,而 CPS 是在用闭包传递给下个步骤值,就是这样的关系。当然理解这一点的前提是,知道闭包是什么,这个也是基础班学习的重点内容,尤其是会在实现解释器环节,自己实现闭包的语句,对于闭包的理解会很透彻。

fib 函数的 CPS

计算阶乘的函数 fact 特点是只在函数体内进行一次递归调用,我们再来看计算斐波那契数列的 fib 函数,它会在函数体内进行两次递归调用,CPS 该怎么处理这个情况。

fib 函数的递归形式的定义是这样:

function fib(n){  if (n == 0)  {    return 0;  }  else if (n == 1)  {    return 1;  }  else   {    return fib(n-1) + fib(n-2);  }}console.log("fib(2)=", fib(2)); // 1console.log("fib(5)=", fib(5)); // 5

这里直接给出 fib 函数的 CPS,然后理解一下 fib 函数的运作过程:

function fibCPS(n, k){  if (n == 0)  {    return k(0);  }  else if (n == 1)  {    return k(1);  }  else  {    return fibCPS(n-1, r1 => fibCPS(n-2, r2=>k(r1+r2)) );  }}

可以看到,对于需要两次递归调用的情况,CPS 是把另一次递归调用,写在了原本的 r => k(r) 函数里,让第二次内部调用成为了递归调用 fib 时候的子调用。这句话有点绕,可以结合代码理解一下。

CPS 形式的 fib 函数这样来调用:

let fibCPS1 = fibCPS(1, x=>x);console.log("fibCPS1=", fibCPS1); // 1let fibCPS2 = fibCPS(2, x=>x);console.log("fibCPS2=", fibCPS2); // 1let fibCPS4 = fibCPS(4, x=>x);console.log("fibCPS4=", fibCPS4); // 3let fibCPS5 = fibCPS(5, x=>x);console.log("fibCPS5=", fibCPS5); // 5

我们以计算 3 的斐波那契数为例,拆解一下具体的执行步骤。要注意的是,这个过程非常复杂,比 fact 函数还要复杂很多,只有自己亲自写一下才能搞清楚:

let fibCPS3 = fibCPS(3, x=>x);console.log("fibCPS3=", fibCPS3); // 1+1=2// n=3, k=x=>x,        // return fibCPS(2, r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) );// n=2, k= r1 => fibCPS(1, r2=>(x=>x)(r1+r2)),        // return fibCPS(1, r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)) );// n=1, k= r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)),        // return k(1)       // return ( r1 => fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(r1+r2)) )(1)       // return fibCPS(0, r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2))          // n=0, k= r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2)              // return k(0)              // return ( r2 => ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) )(1+r2) )(0)              // return ( r1 => fibCPS(1, r2=>(x=>x)(r1+r2)) ) (1+0)              // return fibCPS(1, r2=>(x=>x)(1+r2))                  // n=1, k = r2=>(x=>x)(1+r2)                  // return k(1)                  // return (x=>x)(1+1)                  // return 2

那么经过了 factfib 函数的训练,我们就已经知道 CPS 的形式是什么,以及具体的执行步骤是怎样了。理解 CPS 只是开始,接下来还会利用 continuation 实现更多有趣的程序。

练习题

已知一个递归形式的 sumFrom 函数,接收两个参数 ab,函数的功能是计算 a+(a+1)+...+(b-1)+b 的值,例如参数是 14,则计算 1+2+3+4 的结果:

function sumFrom(a, b){  if (a == b)   {    return a;  }  else  {    return b + sumFrom(a, b-1);  }}console.log(sumFrom(1, 3));   // 6console.log(sumFrom(2, 5));   // 14

练习的内容是,将 sumFrom 函数修改为 CPS 形式,补充 sumFromCPS 函数空白处的代码,让程序可以满足测试用例中的输出结果:

function sumFromCPS(a, b, k){  // ____}sumFromCPS(1, 3, x => console.log("sumFromCPS(1, 3)=", x));   // 6sumFromCPS(2, 5, x => console.log("sumFromCPS(2, 5)=", x));   // 14

延伸阅读

我们已经体验了手动将递归程序转变为 CPS 形式的过程,实际上存在能将代码自动转变为 CPS 形式的方法,也就是传说中 “王垠 40 行代码” 在干的事情。可以参考这两个链接查看更多内容:

因为 “自动 CPS 变换” 的难度比较大,我自己并不打算深入学习和实现。

🔲 ☆

我从王垠的计算机科学视频班学到了什么

为了行文方便,这篇文章不使用 “王垠老师” 这样的尊称,直接称呼名字 “王垠”。

大约 2 个月前,我开始报名学习王垠的 计算机科学视频班(基础班),经过 1 个月的学习后毕业,大概用了 120 个小时的学习时长。“120 个小时” 这个数字是经过认真估算的,包含了观看视频的时间和做练习题的时间。因为视频课的学习节奏由自己把控,毕业速度因人而异,所以 “1 个月” 这样的描述不够直观,用小时数更加容易理解。 120 个小时意味着,如果一天学 2 个小时的话,需要 60 天。

王垠并不认识我,但我早就认识王垠;我以前不是王垠的学生,但王垠早就是我的老师了。我关注王垠的博客多年,已经从他的博客文章中受益很多。而这次系统学习了视频课的课程,像是打开了新世界的大门。这并不夸张,我可以负责任地站在学习过课程的立场上说,王垠没有吹牛,他的课程真的有他说的那么好。你可以不赞同王垠的观点,也可以不喜欢王垠的人生态度,但是不可能怀疑王垠在计算机科学,尤其是编程语言领域的研究水平,也完全不需要担心王垠这样对自己和世界都如此较真的人,会拿一些没有含量的课程出来忽悠人。

现在毕业后经过半个月时间的沉淀,我想分享一下 “从课程中能学到什么” 这个话题。没有在毕业当天做总结,是因为怕有点浮燥总结不全,或者掺杂太多个人经历。半个月的时间其实也不够,我没有太多时间复习,课程也远远没来得及发挥出应有的成果,但是现在做总结并不妨碍以后对课程内容有进一步的感悟。王垠曾经有一篇文章《爱因斯坦谈教育》,里面提到爱因斯坦说 “被传授的知识应该被当成宝贵的礼物”,我在学完课程后无意间翻到这篇文章,看到这句话后,突然对这句话有了切实的理解。

因为课程内容是保密的,我不会透露课程具体的学习内容,只基于公开的招生说明中的内容进行解释。对于计算机初学者来说,从课程中能学到的最直接的知识,就是函数、链表、二叉树等基础的编程概念,内容涵盖了大学本科阶段的整个过程。而第 6 课关于解释器的部分,属于课程的 “最终成果”,对应一些美国顶尖大学本科高年级,至硕士低年级的阶段。为什么王垠在招生说明里的描述是 “大学博士阶段才可能学到的内容” 呢?因为中国的大学没有编程语言专业,本科课程没有教解释器的,即使到了研究生阶段,lambda 演算也属于选修课,博士早期才会接触到解释器的实现是正常现象。所以王垠的描述真的没有夸大。

解释器这种内容在中国的教育体系里本来就很稀有,比较高级和精练的教程更是少见。举例来说,当你学完了王垠的课,然后去 bilibili 上搜一些解释器相关的教程,你就会明白这些公开教程里的解释器有多么差劲,不但一眼就能看出它们缺什么要素、存在什么问题,还知道如何改进、如何用最简洁的代码写出最可靠的实现。为什么解释器这个东西重要呢?同样举个例子,以太坊的虚拟机(EVM)就是一个解释器,只不过 EVM 并不是在对编程语言做解释,而是在对以太坊的操作码(Opcodes)做解释,每个操作码都对应在栈结构上的一个动作。所以学过了解释器之后,对 EVM 的原理会有不一样的理解。

对于有计算机经验的学生来说,从课程上可以学到的,就不只是表面上的知识了。比如,课程只用到非常少的编程要素,就表达了第 1 课到第 6 课的全部内容,如果王垠不是对计算机理论有非常深刻的理解,不可能做到这种地步的深入简出。从学习者的角度,一方面可以思考一下为什么课程内容能如此精致,组织这些课程内容的思路是什么,这种高度抽象的思维背后,需要怎样的功底。另一方面由于课程内容自成一体,学习者完全有可能做到自己复刻整个课程内容,就像是手里的一个精致的玩物,随时可以拿出来复习把玩。

王垠最近在微博上评论 AI 编程的时候提到,AI 无法写出 “王垠级别” 的代码。什么是 “王垠级别” 的代码呢?上过课就知道了。我在做练习题的时候,被助教提醒最多次的问题,就是 “代码复杂”,有时候是写法上的复杂,有时候是复杂度上的复杂,但是每一次把代码写到符合课程标准之后,又不禁感叹原来代码可以如此精巧。我已经有多年的编程经验,让代码运行出练习题的结果并不难,但是把代码写的足够漂亮却不容易。假如以前给公司写的都是这种质量的代码,那公司可就太占便宜了。

还不止这些,也许有人看到招生说明会怀疑,一节选修课真能让人学会一种新的编程语言吗?我想提醒的是,不要忘了给你讲课的人是谁,是真正的编程语言专家。

🔲 ☆

很多人以为远程工作是“下限”

比如有的人想肉身回老家,首先琢磨在老家没有好的事业,其次就说 “要不找个远程也行”。

省省吧,这种心态上想回老家的人,专业能力必然不过关。远程工作首先要的就是自驱力、学习能力。而有自驱力的人,本来就不在乎工作形式,无论是坐班还是远程都会做出相应的成绩。而且想回老家的人心态上就是想要“安逸”、“懈怠”,别指望这种人能做出什么工作成绩。

其次就是这种 “如果A不行,就B也行” 的心态。为什么会认为 “远程工作” 的难度和要求比 “在老家找个谋生的事业” 更简单?为什么会认为 “远程工作” 比坐班更简单?这种认知本身就不对,在错误的认知下不可能遇到正确的机会。坐班,你的竞争对手是同一个城市的人。远程,你的竞争对手是整个世界的人。

这些人竟然把远程工作看作一种降级而不是升级,这就很奇怪。他们不会说,“我在小公司上班不舒服,实在不行去大厂吧。” 为什么?因为他们明白自己能力不行。为什么到了远程工作这里,有些人会认为远程工作更简单?因为他们的实际工作是肉身在场熬日子,而不是工作结果的产出,才会以为只要不在办公室被 “盯着” 就会轻松。

还有一种是觉得坐班辛苦,通勤占用了娱乐时间,整体工作时间太长,想要借助远程工作 work life balance 的。这种也是痴人说梦,在你自己能力不变的前提下,为什么工作形式的转换就会让你有更多休闲娱乐的时间?为什么得到这种 “好处” 的人是你而不是别人?

这些人的心态是 “在不付出更多努力的前提下,得到更多”。然而天底下哪有那样的好事?

🔲 ☆

远程 web3 工作溢价的原因

远程工作溢价的原因:

  1. 没有劳动合同、制度性保障、基本的法律保障
  2. 没有公积金、五险一金
  3. 没有周末、病假、年假、节假日等正常假期
  4. 没有设备补贴、年度体检、年会抽奖、团建等活动
  5. 没有节假日福利礼物、下午茶等
  6. 没有高档办公室、免费水电、人类同事
  7. 不能摸鱼(没有意义)
  8. 创业项目节奏快、成败迅速、工作不稳定性更高

综上,只有得到相比于坐班工作一定程度的溢价工资,远程工作才合理。

如果你觉得,远程工作的好处是:

  1. 随时休息,不需要固定的工作时间、工作更轻松

别扯了,但凡远程工作过就明白,没有固定工作时间意味着什么。

  1. 可以当数字游民,全世界各地旅居

别扯了,牛马住在哪儿都一样。你一天工作8小时,窗外再美的风景都跟你无关。

  1. 可以居住在低消费地区、按照高消费地区的工资要价

别扯了,老板会按照高消费地区的工资给你开价吗?老板会根据你所在的地区来估价。

注意,我说的是远程工作,不特指远程 web3 的工作。如果从事 web3 的工作,你就要额外承担:

  1. 项目违法追回你全部工资的风险
  2. 极端情况亲身去喝茶的风险
  3. 冻卡导致全部银行卡被封的风险
  4. 一旦从事 web3 项目就不可能回到 web2 的代价

所以 web3 工作相比于 web2 工作一般也会有一定的溢价。

当然,现实中的情况更为复杂。以上都是打工人的视角,而对于招聘方的视角:

  1. 远程工作本来就是为了节省成本
  2. 你不干有的是人干

所以对于可替代性强的职位,远程 web3 的工资比坐班 web2 还要低,而且招聘方没有任何裁员成本。

无论 web2 还是 web3,无论坐班还是远程,自己的竞争力始终是第一位的。

🔲 ☆

默认参数引起的以太坊节点运行错误

大概是两年前的事情了。在工作中,我们用以太坊节点搭建了一条网络,上线后发现网络中不能新增 Validator,也就是质押者的 32 ETH 没有生效。

原因是我们用了多种共识层客户端,包括 Prysm 和 Lighthouse 等。为什么会突然出问题呢?因为 Prysm 有一个叫 --contract-deployment-block 的命令行参数,其他客户端比如 Lighthouse 和 Teku,这个参数的默认值都是 0(参数名字在不同客户端可能不同,但是含义类似),而 Prysm 的这个参数默认值是 11184524,这个数字是以太坊从 PoW 转向 PoS 后质押机制开始生效的区块高度。

这个参数在干什么呢?它会从这个参数配置的区块高度开始,去扫描 Deposit Contract(主网是 0x00000000219ab540356cbb839cbe05303d7705fa )上质押者的存款记录。我们平时说的质押 32 ETH,就是质押到这个合约里了,或者说就是把 32 ETH 转账给这个合约。这个合约在收到你的 ETH 之后,Consensus Client 比如 Prysm,就能根据合约的记录知道某一个地址确实给合约转了 32 ETH,因此认可这个人成为出块节点。

以太坊整个网络可能有几千个节点,每一个节点都在执行同样的操作,就是从 Deposit Contract 上扫描质押者列表、然后维护到自己本地的数据库状态中。直到轮到自己出块的时候,就把这个一直在维护的质押者列表的数据,计算一个总数 Deposit Count 和哈希根 Deposit Root,作为 eth1_data 字段的值,提交到区块数据中:

这个截图来自以太坊主网的区块 24374562,意味着当前以太坊一共有 204 万个质押者。要注意质押者不等于物理服务器上运行的节点数量,一个节点可以运行几千个质押者(Staker / Validator),所以推测以太坊实际上的物理节点数量大概在几千个左右。

回到 Prysm 配置的问题,如果不配置 --contract-deployment-block 参数,默认值是 11184524,那么对于一条新启动的链来说(Chain ID 不是 1 那种),Prysm 就不去扫描 Deposit Contract 合约在块高度 11184524 之前的质押记录了,本地数据库里没有质押者的数据,在出块的时候自然也不会带上 eth1_data 的字段数据。

以太坊的 协议设计 中要求,eth1_data 的数据必须要超过半数节点一致才可以生效。(注意这里的比例是 1/2,和其他地方用到的魔法比例 2/3 不一样)。

所以如果你新启动的网络中有超过一半的节点用了 Prysm,同时这些 Prysm 节点没有可以设置 --contract-deployment-block 参数,网络就会异常、不能正确处理新加入的质押节点。

我们一般认为软件的默认参数是相对安全可靠的,如果刻意设置才表示我们有特殊需求。而在以太坊 Prysm 客户端的这种语境下,不刻意设置默认值反而是危险的。

🔲 ☆

打工人

随着年龄、工作经验、技术能力的增长,我发现:

  1. 比如我现在在做某一份工作
  2. 我无法同时做另外的工作,因为时间不允许,即使我有能力完成其他工作
  3. 也就是说,我的时间同一时间只能出售一份
  4. 我的技能无法落实为书籍或者课程,因为很多工作问题其实并不通用、没有技术含量、很多领域内问题。工作并不是因为你特有的技能,而是因为你花时间精力把事情给做了。
  5. 所以对于个人来说,在有能力的情况下,同时只能做一份工作是一种损失,但是工作本身的性质决定了无法复利。
  6. 简称机会成本、沉没成本
  7. 另一方面,对于大多数人来说,工作是唯一的收入来源
  8. 在没有工作或者哪怕工作有空闲时间的情况下,是无法产生收入的

非要再补充两条道貌岸然的道理的话:

  1. 年龄越大,精力越少,但是挣钱的欲望和生活上实际的对钱需求越大。也就是说,打工人的收益天然是负增长。
  2. AI 技术的发展首先取代的,就是花费时间但内在逻辑不需要顶尖的打工人的工作。
🔲 ☆

为什么维护人设很难

想起来点事情。在之前的工作中,我的 Leader 就属于需要在工作中维持自己专业形象的人,直白一点说,就是时刻在 “端着”。

这种 “端着” 的状态,是可以理解的:

  1. 来自大厂,需要维护与 “大厂” 这一标签匹配的专业感与高级感
  2. 工资较高(币权占比大)
  3. 职位定位较高(Tech Leader)

由于需要维护这些外部的光环,以及需要 “镇得住” 团队内的开发人员,所以在实际的工作中,处处表现出 “端着” 的状态,比如:

  1. 全员会议上,用到 “范式” 这个词描述某个业务逻辑
  2. 内部会议上,凭头脑中的想象 debug 线上数据索引慢的问题,说什么属于计算密集还是 IO 密集之类
  3. 交给我负责某个项目的时候,给我发了一些程序设计的参考文章和文档。关键在于,发给我的是一些 Java 面向对象的设计模式,大学生学编程的时候看的那种。(他不是程序员出身。因为程序员出身大学都接触过……)
  4. 我刚入职的时候,发给我一本 Cosmos 原理方面的书籍让我参考。问题在于,是一本中文的书。正经做技术的都知道,不要看中文技术书。
  5. 有一次内部会议,我发现他对其他某个人进行 “说教” 的时候,用的是我博客上的观点
  6. 在我离职的时候,我们的对话中,他也用到了我博客中提到的观点

这种 “端着” 的状态,是可以被真正懂技术或者经验丰富的人看出来的,而且偶尔无论是在会议还是工作中,都难免会 “露怯”。想要时刻维护一种高级感的形象是很难,也很累的。

当然这种刻意的形象维护,也有一些好处,能带来高薪、高职位。

世界上也有不需要 “刻意” 维护就能高光的人设,比如李星野,身家过亿这一条就够了,比如王垠,清华退学这一条就够了。

刻意维护的人设,早晚会被揭穿。

不要误会,以上内容看起来像是在贬低别人,以达到抬高自己的目的。我肯定承认自己不如人家,比如包装自己的能力…… 技术能力的话,我已经把能写的都写出来了,透明公开。另外,这个博客实际上等同于是 “匿名” 的,抬高自己毫无用处。

我为什么会想到这个话题呢?因为我注意到现在的博客过于简洁,尤其缺少两个关键的东西:

  1. 一句话简介,说明我是谁,我的来历、技术栈、职业定位
  2. 其他平台的社交账号链接

我不知道怎么定义自己,所以写不出一句话简介。也没有什么高光的经历,不能拿来做人设。至于其他平台的账号,维护起来就更累了。

从运营和营销角度看,这个博客是失败的,读者需要凭借一篇篇技术内容去拼凑出一个形象作为记忆点,而且我相信大多数人不是特别理解一些技术概念。所以这方面很难做。但是另一方面,实在是不想 “运营”…… 再一方面,也许可以认为我开始逐渐有市场营销的意识了,也许以后会 “变好”。

🔲 ☆

硬件钱包与资产安全

加密货币的资金管理,从最安全到最不安全的方式排名是:

  1. 家族信托
  2. 硬件钱包
  3. MPC钱包
  4. 交易所托管
  5. 热钱包

如果你不在乔布斯排行榜上,家族信托就跟你没关系。对于个人而言,硬件钱包是目前最安全的方案。

这里没有提多签钱包、助记词+Passphrase 之类的方案,因为那些方案在技术手段上复杂到你都用不明白,复杂的方案是无法长期留存的,反而会增加遗忘的风险,更不可靠。

那么有几个需要更正的对于硬件钱包的认识:

  1. 硬件钱包是钥匙,不是保险柜
  2. 钱是存在助记词上,不是硬件钱包上
  3. 硬件钱包输错密码会直接清空数据
  4. 助记词不是打开保险柜的备用钥匙,助记词是保险柜本身

而助记词安全也有几个有意思边界:

  1. 你无法验证助记词是否已经泄露
  2. 如果你不把助记词拿出来,就无法验证助记词是否正确
  3. 你把助记词拿出来的行为本身,就增加了助记词暴露的风险

所以去中心化资产在给你带来控制权的同时,也要求你自己承担资产安全的风险。

🔲 ☆

BitDoge

BitDoge 是一个部署在以太坊主网(Chain ID=1)的 ERC-20 代币,按照这样的规则运行:

  1. 代币名称:BitDoge (BITDOGE)
  2. 代币合约地址:0x000000001994bb7b8ee7d91012bdecf5ec033a7f
  3. 代币的总供应量是 2100 万,没有初始流通量
  4. 代币的释放规则是,每一个以太坊区块最多产生 1 BitDoge
  5. 获取代币的方式是,每次向代币的合约地址转账 0 ETH,就可以获得 1 BitDoge
  6. 转账的金额可以大于 0 ETH,但是不会因为带有金额而获得更多 BitDoge,仍然只能得到 1 BitDoge。而且转账的金额会被永久锁定在合约内,无法取出
  7. 合约中没有任何 Owner 权限,也没有其他特殊权限,合约源码 已经在 Etherscan 验证
  8. 代币的创世区块(Genesis Block)是以太坊的区块高度 24444444(2026年2月12日),在此之前合约会拒绝交易
  9. 每一个区块的代币奖励是 1 BitDoge,每 4 年减半一次,也就是 10512000 个以太坊区块之后,每个区块只能获得 0.5 BitDoge
  10. 如果当前区块没有地址交互,代币奖励会打入黑洞地址 0x000000000000000000000000000000000000dEaD
  11. 每一个区块只有第一个与合约交互的地址可以获得代币奖励
  12. 按照每 4 年一次的减半周期,BitDoge 的区块奖励大概会在 140 年之后(2166年)归零

合约源码全文:

// SPDX-License-Identifier: MITpragma solidity ^0.8.20;import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";/** * @title BitDoge (The 140-Year Social Experiment) * @notice A deflationary token that rigorously simulates Bitcoin's halving mechanics on Ethereum. * @dev  * - Total Supply: 21,000,000 (Strict Cap) * - Block Time: ~12 seconds (Ethereum) * - Halving: Every ~4 years (10,512,000 blocks) * - Mechanism:  * 1. Pure PVP: First come, first served per block. No Cooldowns. No Limits. * 2. Entropy Rule: If a block is missed (no interaction), its reward is BURNED forever. * 3. Genesis Launch: Mining is strictly locked until block #24,444,444. */contract BitDoge is ERC20 {    // ==========================================    //              COSMIC CONSTANTS    // ==========================================    // Strict cap: 21 Million coins.    uint256 public constant MAX_SUPPLY = 21000000 * 1e18;         // Initial reward: 1 Coin per block.    // Ethereum produces ~7200 blocks/day.     // This matches Bitcoin's early issuance (~7200 BTC/day) perfectly.    uint256 public constant INITIAL_REWARD = 1 * 1e18;        // Halving interval: ~4 Years (based on 12s block time).    uint256 public constant HALVING_BLOCKS = 10512000;         // Standard burn address (The Black Hole).    address public constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;    // ==========================================    //           GENESIS CONFIGURATION    // ==========================================    // The Sequence of Death and Entropy.    // Mining starts precisely at this block height.    // Approx. 23 days from now (based on current height ~24.27M).    uint256 public constant GENESIS_BLOCK = 24444444;     // ==========================================    //              STATE VARIABLES    // ==========================================    // Tracks the last block that was processed (mined or burned).    uint256 public lastMinedBlock;               // Total ETH sacrificed to the contract (forever locked).    // Serves as the "floor value" or monument of the experiment.    uint256 public totalSacrificed;     // ==========================================    //                 EVENTS    // ==========================================    event Minted(address indexed user, uint256 userReward, uint256 burnedReward, uint256 blockNumber);    event Sacrifice(address indexed user, uint256 amount);    /**     * @dev Constructor     * Sets the state pointers. No parameters needed as Genesis is hardcoded.     */    constructor() ERC20("BitDoge", "BITDOGE") {        require(GENESIS_BLOCK > block.number, "Genesis must be in the future");                // Initialize state so the first valid mineable block is GENESIS_BLOCK        lastMinedBlock = GENESIS_BLOCK - 1;     }    /**     * @dev Main interaction point.      * Send 0 ETH (to just mint) or sacrifice ETH (to mint + donate).     * WARNING: No Cooldowns. No Max ETH Limits. Pure Gas War.     */    receive() external payable {        // 1. Genesis Check: Is it time yet?        require(block.number >= GENESIS_BLOCK, "BitDoge loading... Wait for Block #24444444!");        // 2. Competition Check: Has this block already been mined?        // Only one winner per block.        require(block.number > lastMinedBlock, "Block already mined");                // 3. Bot Protection: Only allow EOAs (Externally Owned Accounts).        // This prevents smart contracts from batch-mining, ensuring fairness.        require(msg.sender == tx.origin, "Humans only");                // 4. Hard Cap Check.        require(totalSupply() < MAX_SUPPLY, "Minting ended (Year 2160+)");        _processMining(msg.sender, msg.value);    }    /**     * @dev Internal logic to calculate rewards and burns.     */    function _processMining(address user, uint256 ethAmount) internal {        // --- Step 1: Calculate Current Reward Rate ---        // Based on time passed since Genesis.        uint256 blocksPassed = block.number - GENESIS_BLOCK;        uint256 era = blocksPassed / HALVING_BLOCKS;                 // Bitwise shift for halving. Returns 0 after 64 halvings.        uint256 currentRate = (era >= 64) ? 0 : (INITIAL_REWARD >> era);        // --- Step 2: Distribute Rewards ---                // A. User Reward:         // The user only gets the reward for the CURRENT block.        uint256 userReward = currentRate;                // B. Black Hole Reward (Entropy):        // All blocks missed between the last mine and now are burned.        // "Use it or lose it."        uint256 missedBlocks = block.number - lastMinedBlock - 1;        uint256 burnReward = missedBlocks * currentRate;                // --- Step 3: Supply Cap Protection ---        uint256 totalRequired = userReward + burnReward;        if (totalSupply() + totalRequired > MAX_SUPPLY) {            uint256 remaining = MAX_SUPPLY - totalSupply();                        // Priority given to the user. Burn the rest.            if (remaining <= userReward) {                userReward = remaining;                burnReward = 0;            } else {                burnReward = remaining - userReward;            }        }        // --- Step 4: Update State ---        lastMinedBlock = block.number;                // Handle ETH Sacrifice (locked forever)        if (ethAmount > 0) {            totalSacrificed += ethAmount;            emit Sacrifice(user, ethAmount);        }        // --- Step 5: Minting ---        if (userReward > 0) {            _mint(user, userReward);        }                // Direct mint to Dead address (Auto-Burn)        if (burnReward > 0) {            _mint(BURN_ADDRESS, burnReward);        }        emit Minted(user, userReward, burnReward, block.number);    }}
🔲 ☆

预测市场比币圈更赌场

  1. 预测市场不存在长期主义
  2. 预测市场一定会有明确的结果
  3. 输的归零,风险不低于合约,高于现货,合约可以加仓,预测市场没商量
  4. 所以预测市场无法用于投资
  5. 预测市场的技术实现不是去中心化的,所以跟 web3 没关系,定位上类似于币安等 CEX
  6. 所以预测市场比土狗盘还残酷
🔲 ☆

为什么去中心化的跨链桥不可能实现

准确来说,是 Trustless 的跨链桥不可能实现。因为一般认为去中心化就意味着 Trustless,虽然实际上去中心化是在信任一个 Permissonless 的群体。

不纠结这些具体的名词。现在的各种跨链桥方案,以及未来的跨链桥方案,一定都做不到纯粹的、工具性质的、数学证明的、轻量级运营的、去中心化的形式,能够满足物理隔离的、异步的、异构链之间的跨链。

先列举一下现在的跨链桥方案,以 BTC 到 ETH 的跨链为例:

  1. 公证人模式,也就是中心化的模式,把 BTC 打给一个中心化机构,机构再把 ETH 打给你指定的地址。安全性依赖中心化机构,没什么可说的。现在大多数跨链桥都是这种传统的模式,最多就是机构内部多签。

  2. TSS 密钥分片、多签,相对去中心化。把 BTC 转给一个第三方的链比如 Thorchain,在 Thorchain 中,有多个节点分别持有 ETH 链上资产的密钥分片,每个节点分别扫描和验证 BTC 的交易信息,达到一定阈值则多签的签名生效、在 ETH 链上放行资产。

    这种模式把公证人给去中心化了,有多个节点在共同验证来源交易是否正确。这是目前相对来说比较靠谱的、依赖社区博弈的方案。因为 Thorchain 本身就是一条链,想成为持有密钥分片的节点就需要质押 Token 等。TSS 的签名算法比较消耗计算资源,做不到同时分发太多的密钥分片,所以持有分片的节点数量在一定范围内。像所有的 PoS 链一样,表面上是去中心化的,实际上是 Permissioned 的,只不过是权限的门槛高低不同。

    TSS 确实把跨链桥去中心化了,但是引入了 BTC 和 ETH 之外的第三条链,而第三条链的安全性又依赖于它自己的经济学博弈,本身就是风险。所以这种模式可靠但不够纯粹。

  3. 合并挖矿,特指资产的来源方是 PoW 链,让 BTC 的矿工在挖矿的时候,把跨链桥的某种证明信息给带上,一起挖到 BTC 的区块信息里。那么 BTC 的区块信息本身就包含了跨链信息。这种方式非常可靠但是不可行,因为矿工不可能配合跨链桥干这种事情。

  4. HTLC,本质上是资金的对敲,对实时性要求比较高,必须双方同时在 BTC 和 ETH 在线交换密钥,而且 HTLC 只能做到资金的交换而不是跨链,涉及到汇率的问题。

  5. 全验证轻客户端,也就是在 ETH 上运行 BTC 的轻客户端,比如让每个 Geth 节点都可以同步和验证 BTC 的全部区块信息,那么 Geth 本身就是一个轻量级的 BTC 客户端,自然也能够实时地、无需第三方信任地把 BTC 交易信息同步到 ETH 的智能合约上。但是这样显然成本太高了,相当于一个客户端同时兼容了两条链。

  6. ZK 跨链桥,运行一个 BTC 的轻客户端,把所有区块信息生成电路证明,然后在 ETH 上部署一个 Verifier,只需要验证很小的证明就可以相信来源信息是正确的。那么这种 ZK 模式的问题在哪里?ZK 并没有黑魔法,它的问题在于,需要在 ETH 上部署一个 Verifier。也就是说,你不需要关心是谁、是怎么生成证明的,但是你必须相信在 ETH 上的 Verifier。那么 Verifier 的代码本身、背后的开发者、来源是否可信任又是一个问题。

  7. TEE 硬件设备签名。比如在一个 Intel 的服务器上,同步 BTC 的区块信息,然后用 TEE 内置的私钥签名,证明这个信息一定来源于这台设备。然后在 ETH 上,只需要判断来源签名是否来自于那台硬件设备,就可以证明信息是否安全。TEE 方案最大的问题自然是单点风险。

各种方案对比下来,似乎根本找不到一种让人满意的方式。为什么会是这样?

  1. 分布式系统中的两军问题(Two Generals’ Problem),这是计算机科学史上第一个被证明无解的问题。这个问题具体的定义不重要,重要的是它早已被科学界证实无解。

  2. 计算理论中的预言机问题(The Oracle Problem),每一条区块链本身都是一个确定性的状态机,它是封闭的、可被验证的、可被重复计算的。如果要和外部通信,就必然需要引入信任模型、经济学博弈、密码学证明等,这些内容会让跨链方案不再纯粹。

所以去中心化的跨链桥这个问题,已经触及到了计算机科学的理论边界,不是工业界无能,而是这个问题本身无解。

  1. 有趣的是,Vitalik 早在 2022 年就发表过一个 观点,未来的区块链是多链的(multi-chain),而不是跨链的(corss-chain)。因为即使存在可靠的跨链工具解决了通信问题,ETH 仍然需要信任 BTC 的共识机制。假如 BTC 网络分叉,ETH 不可能跟着 BTC 的分叉去重新分配资金。这同样是一个无解的问题。

这种跨链桥方面的技术边界可以带给我们启发:

  1. 不存在无需信任的技术方案,只有信任转移、最小化信任
  2. 工程架构不可能完美,大多数时候需要权衡
🔲 ☆

我对于 AI 时代的答案

当我的工资从每天 ___人民币上升到每天 ___美元的时候,我开始思考是什么让工资增长。

得到的结论是:

  1. 技术能力的提升

然后我继续思考这几个问题:

  1. 什么样的技术能力算是好
  2. 什么样的技术能力是稀缺、长久的
  3. 如何提高技术能力

所以多年以来,贯彻我人生的思路就是:如何提高技术能力。最明显的体现是,如果工作不能让我成长,就换工作。

那么对于 “如何提高技术能力” 这个问题,我的答案是:

  1. 关注底层原理和实现,而不是应用层的框架和接口
  2. 需要具备在复杂代码中找到并调试关键流程的能力
  3. 实际参与一些比较前沿的、像样子的项目
  4. 自己模仿实现一些比较底层和看起来硬核的项目
  5. 关注技术理念而不是具体实现
  6. 知道王垠课程所代表的 “计算的本质”

事实一

但是紧接着,我发现了一个令人不安的事实:

  1. 工资的高低、工作是否稳定,和技术能力的好坏没有必然的联系

得到这个结论有这样几个原因:

  1. 我不认为自己技术能力不合格,但是我遭遇的面试结果为不合适的情况,非常非常多
  2. 我不认为自己技术能力不合格,但是我亲眼看见水平很一般的人在做领导、面试官
  3. 我不认为自己技术能力不合格,但是我的工作很不稳定
  4. 有的人出生就在罗马

事实二

与此同时,我发现另一个令人不安的事实是,AI 在改变游戏玩法:

  1. 普通人和知识渊博的专家之间,差距只是一个 Gemini
  2. 程序员不再需要手写代码

AI 引起的变化非常大,直接改变 “如何提高技术能力” 这个问题的答案:

  1. 写代码的能力完全不重要,掌握多编程语言的能力完全不重要
  2. 在领域内的经验不再需要积累,一问 AI 全是标准答案

复盘一下我之前犯的错误:

  1. EchoEVM,在半年前,开发这样的东西似乎是有意思的。半年后的今天,AI 可以轻易开发出完整的项目。所以 EchoEVM 不再有意义。
  2. EthBFT,在 2 个月前,开发这样的东西也许可以看到 AI 能力的局限性。但是现在,AI 在逐渐突破以前的局限。所以 EthBFT 也不再有意义。

EchoEVM 和 EthBFT 的共同特点,是偏低层、侧重技术的实现,试图用硬核项目来证明自己的技术能力。然而在拥有 AI 的今天,这种硬核的代码能力恰恰是 AI 最擅长、最先取代的。


新的问题

结合这两个令人不安的事实,需要回答的新问题是:

  1. 如何提高自己的竞争力、稀缺性、硬实力、挣更多钱、不被时代淘汰

新的答案

那么对于新的问题,我的答案是:

  1. 提高发现问题的能力

对答案的解释

你也许会说,这不废话吗,自古以来,发现问题的能力都是重要的。

不,这不一样,在没有 AI 的时候,你可以不需要有判断力,不需要能够发现问题,哪怕只是听产品经理的话来实现功能,也就是干好程序员的活,就可以活下去。

但是 AI 取代了这种只会听话干活的人。

“发现问题” 同时涵盖技术领域和非技术领域,在技术领域,发现代码有没有问题、功能设计是否存在漏洞、业务的边界条件是否缺少约束;在非技术领域,发现用户有哪些实际的需求,发现市场有哪些比较大的空缺。

那么为什么没有把 “提高判断力” 放到答案中?因为判断明天的股市涨跌也算判断,这种能力是无法验证以及无法通过努力提高的。

进一步问题

还没完,对于新的答案,有两个问题:

  1. 如何提高自己发现问题的能力
  2. 与代码能力不同,发现问题的能力该如何量化、与他人比较

对于 “如何提高自己发现问题的能力” 的问题:

  1. 只有见过更好的,才能知道现在看到的有什么不足。所以要事事都向上看齐

对于 “发现问题的能力该如何量化” 的问题:

  1. 技术方面,把发现的问题落实到技术文档、设计文档、架构文档上
  2. 非技术方面,把发现的问题记录下来,比如博客、日记,文字可以记录思考的过程
🔲 ☆

不要投资任何隐私币

  1. 除了试图犯罪以外,真正需要协议级隐私的场景很少
  2. 协议级隐私带来的是技术上的极度复杂、使用体验差
  3. 假如 ZEC 使用 100% 的 z 地址,CEX 首先会下架 ZEC
  4. 任何代币只要全过程匿名,将要面对的不只是CEX下架,还有各国政府的封禁
  5. 大型机构绝不会也不可以投资隐私币
  6. 隐私币绝不会成为国家储备
  7. 任何代币,只要警察对你进行物理监禁,技术型隐私就毫无意义
  8. 你需要生活在真实的世界中,而不是网络中
🔲 ☆

HTML5 音乐可视化

下面梳理一下HTML5实现音乐可视化的流程。

搭建静态页面

静态页面的结构分三部分,header,left,right。header为语义化标签,left和right都用div来实现。

类似这样,然后把背景颜色改为黑色,字体改成白色,加上或不加边框线,给一定padding或margin,静态页面就搭建好啦。

不过重点不是页面布局。可以在左侧栏有一个曲目列表,点击切歌。这里就不那么麻烦,直接后台加载指定的歌曲。歌曲是许嵩的烟花笑,左侧栏显示部分歌词。

创建音频文件对象

AudioContext对象用于获取音频文件源。

(节点)AudioContext:包含各个AudioNode对象以及它们的联系的对象,可以理解为上下文对象。绝大多数情况下,一个document中只有一个AudioContext。

(方法)createBufferSource():创建audioBufferSourceNode对象。

可以这样写:

var ac = new window.AudioContext();    // 实例化ac为一个音频对象var bs = ac.createBufferSource();      // 用bs来保存音频文件资源

有了音频资源,是否就可以播放音频了呢?当然不可以,因为还没有指定文件,告诉对象播放哪一个音频文件。

获取音频数据

创建bufferSource后,到了AudioBufferSourceNode这个节点。这个节点有一个属性值buffer,用来指定文件资源。这个属性值需要一个buffer类型的数据。

(节点)AudioBufferSourceNode:表示内存中的一段音频资源,其音频数据存在于AudioBuffer中(其buffer属性)。

(属性)buffer:AudioBuffer对象,表示要播放的音频资源数据。

获取buffer有两种方式,一种是ajax设置返回值类型为arraybuffer,然后解码,一种是用FileReader读取文件,获得blob值。后一种方式多用input上传文件后获得文件,再进行解析。前面一种好用一点。(留坑)

ajax的原生写法:

var url="...";var xhr = new XMLHttpRequest();xhr.abort();xhr.open("GET", url);xhr.responseType = "arraybuffer";xhr.onload = function(){    return xhr.response;}xhr.send();

onload触发的函数中xhr.response就是我们想要的值。

(方法)decodeAudioData(arrayBuffer, succ(buffer), err) :异步解码包含在arrayBuffer中的音频数据。

将arraybuffer解码:

ac.decodeAudioData(arraybuffer, function(buffer){ ... },function(err){ ... });bs.buffer = buffer;    // 回调函数中的buffer就是想要的buffer

播放音乐

(方法)start:开始播放音频

这个时候调用start方法:

bs.start(0);

打开页面,音乐开始播放。

音量控制

(节点)GainNode:改变音频音量的对象,会改变通过它的音频数据所有的sample frame的信号强度。

(方法)createGain():创建GainNode对象。

(属性)value:可以改变音频信号的强弱,默认为1,最小值为0。

音乐播放还不行,要添加一个音量控制控件,音量控制用GainNode节点:

var gainNode = accreateGain();    // 创建GainNode对象gainNode.connect(ac.destination);    // 将gainNode连接到AudioDestinationNode节点

这样就有了一个音量控制的对象。

gainNode.gain.value= ... ;

gain.value用于控制输入信号的强弱,也就是音量的大小。HTML中用type为range的input,把值传递给对象,就可以实时控制音量了。

得到音频解析数据

(节点)AnalyserNode:音频分析对象,他能实时的分析音频资源的频域和时域信息,但不会对音频流做任何处理。

(方法)createAnalyser():创建AnalyserNode对象。

(属性)fftsize:设置FFT(是离散傅里叶变换的快速算法,用于将一个信号变换到频域)值的大小,用于分析得到频域,为32 ~ 2048之间2的整数次倍,默认为2048,实时得到的音频频域的数据个数为fftSize的一半。

(属性)frequencyBinCount,FFT值的一半,即实时得到的音频频域的数据个数

(方法)getByteFrequencyData(Uint8Array),复制音频当前的频域数据(数量是frequencyBinCount)到Uint8Array(8位无符号整型类型化数组)中。

先创建AnalyserNode对象,然后设置fftsize的值。FFT用于数字信号的处理,会把音频文件流输入的信号变换到频域,用getByteFrequencyData()方法得到经系统处理之后的频域数据。

var analyser = ac.createAnalyser();    // 实例化对象analyser.fftSize = 32;                 // 设置fftsizevar arr = new Uint8Array(analyser.frequencyBinCount);    // getByteFrequencyData()方法要求参数为8位无符号整型类型化数组analyser.getByteFrequencyData(arr);    //    arr为所需频域数据

这样得到的数组arr就是用于可视化效果绘制的数据。

(方法)window.requestAnimationFrame():告诉浏览器您希望执行动画,并请求浏览器调用指定的函数在下一次重绘之前更新动画。该方法将在重绘之前调用的回调作为参数。

一次解析只能得到一组数据,所以需要requestAnimationFrame不断解析,不断得到arr。

前端界面绘制

前端界面使用canvas绘制,实现音乐可视化的效果。

var box = document.getElementById("right");    // 获取right区域的dombox.appendChild(canvas);                       // 创建canvas节点var ctx = canvas.getContext("2d");             // 实例化canvas画笔

接下来就是使用for循环和ctx画矩形、圆圈,填充渐变的操作了。(留坑吧,估计不填了)

案例用到web Audio API的关系:

虚线是可以跳过的节点,直接播放音频文件。好吧有错。

🔲 ☆

区块链技术世界的三大真理

有这样两个事实:

  1. 王垠是编程语言理论、计算机科学理论方面的专家
  2. 在区块链技术理论方面,目前看不到这种级别的专家,尤其是愿意公开发声、开课、讲授知识的

这样的事实背后是有原因的:

  1. 计算机科学经过了几十年发展,区块链大概十几年
  2. 区块链本身、从诞生之初就是工程化集成的产物,而不是理论创新

这会带来不同的现象:

  1. 掌握计算机科学的基础理论,lambda 演算、图灵机、计算模型,理解计算的本质后,在编程语言方面可以长久复用、不会过时,无论上层语言、框架如何变化,计算的核心不会变化
  2. 区块链工程似乎没有基础理论,没有什么技术是不会变的,也没有什么技术是需要长期积累的。从业者年龄小、新人多、工作内容以系统集成、调 SDK 为主

所以区块链的技术世界中,有没有什么理论性质的 “真理”,是长久不变、可以复用、无论上层框架如何变化都不需要担心的?

区块链技术世界的三大真理:

  1. 共识。如何解决拜占庭将军问题。
  2. 加密。以数学为根基的不可篡改、验证。
  3. 激励。社会学博弈引擎,让共识长久运转。

掌握了这三个部分的技术,无论区块链形式上怎么推陈出新,无论行业热点如何变化,都不用担心,因为区块链本质上就是在解决这些问题。

怎么样才算是掌握了 “真理”?我看懂了、我理解了,算是我会了吗?算是我掌握了吗?

掌握真理的标准是,可以根据真理,从头构建出知识。

在计算机科学的世界里,假如世界毁灭了,给你一张纸和笔,你可以从头实现 lambda 演算、实现数据结构、实现一个解释器、实现一种编程语言,甚至构造出更多东西,不依赖于教材、框架、API,这叫掌握真理。

真理的意义在于,让你明白知识为何必须如此存在。——这也是王垠的课程在试图教会你的东西,王垠不教知识,只教 “王垠式真理”。所以我一直认为王垠的课程好、价值高。

类似的,在区块链的世界里,如果你可以从脚本写起,实现共识、加密、激励,不一定重建全部细节,但一定要理解现有系统为何那样设计,就差不多了。

要注意,智能合约的编程语言不在真理的范畴之内,无论是比特币脚本、Solidity、Move、Cairo,都只是表达交易逻辑的 DSL,都是在用不同形式,定义区块链执行交易的规则,很重要但是还没到 “真理层”。

非要说智能合约的真理层,可能可以表达为一个确定性的状态转移函数,无论语言如何变化,这个 “真理” 都始终存在:

State_t+1 = f(State_t, Transaction)

下一个状态来自于上一个状态加上一些交易引起的状态变化,简单吧。但我们这篇文章重点关注区块链世界中的 “王垠式真理”,所以依然是三大真理:共识、加密、激励。

🔲 ☆

以太坊 AA 钱包的致命问题是什么

到目前为止,最新的 AA(Account Abstraction)钱包规范仍然是基于 EIP-4337 实现的。

对于 AA 钱包存在的问题,像操作繁琐、Bundler 中心化、可用性低、生态支持不完善、合约安全风险高等表面上的问题就不多说了。

一句话描述 AA 钱包在干什么事情:AA 钱包能实现 “对账户资金的授权” 与 “把交易广播到链上” 这两个行为的分离。

AA 钱包的功能

AA 钱包的功能,体现到具体的交易行为上,就是如果没有 AA 钱包,你得自己发交易。有了 AA 钱包,你可以只签名,不发交易,让其他人代替你把交易发到链上就行。

为什么 AA 钱包能做到这一点?因为 AA 钱包本质上就是一个合约,所以你会发现,AA 钱包的大多数 “优点”,其实是智能合约本身就具备的功能,像什么社交恢复、批量操作等。唯一能带来特殊体验的,只有 “代付手续费” 这个特性。

那为什么 AA 钱包能实现代付手续费这个功能?因为 AA 钱包的所有操作实际上不是交易,而是 UserOperation,有一个链下的 Bunlder 程序会把这些用户操作,通过发交易批量提交到链上。

为什么以太坊需要 AA 钱包

为什么以太坊需要 AA 钱包?因为以太坊的共识层协议要求,交易必须由一个 EOA 地址来发起。这个 EOA 地址,就是交易结构中的 from 字段,以太坊节点会从这个 from 地址计算手续费、扣手续费、验证交易有效性等。

合约没有私钥,交易不可能由合约发起。在这样的规则约束下,就导致以太坊所有的链上行为,都必须由某一个 EOA 地址来发起交易。

你可能觉得不对,部署一个合约,然后让合约来验证 data 里得签名数据就好了。data 里的签名数据,未必需要和发起交易的地址一致。

没错,事实上,AA 钱包的发展链路是:meta-transcations -> EIP-2771 -> EIP-4337。

这些方案在解决的问题本质上都是:如何让使用资金的权限,与发起链上交易的行为分离。

而引起这一系列复杂协议的根源,来自于以太坊 “交易必须由一个 EOA 地址来发起” 的规则。

为什么以太坊有这个规则

为什么以太坊要有 “交易必须由一个 EOA 地址来发起” 这个规则?

因为以太坊的账户模型,是账户-余额模型。协议必须要知道,一笔交易的手续费从哪里扣。

比特币不存在这个问题

比特币的 PSBT 交易格式,可以实现原生的多签。功能是多个钱包只负责签名,最终由另外一个钱包把交易广播出去就可以。

多签交易,就是典型的把对资金的授权,与广播交易行为分离开的场景。

为什么比特币不存在以太坊的这个问题?因为比特币使用的是 UTXO 模型,交易根本没有 from 地址,有的是多个输入脚本,节点只需要校验交易是否符合脚本的解锁规则,而不需要考虑手续费从哪里扣的问题。

AA 钱包的定位

我们梳理一下这个链条:以太坊使用账户-余额模型 -> 交易必须由 EOA 地址发起 -> 需要 AA 钱包。

AA 钱包在干的事情,实际上是在给以太坊的账户模型打补丁,为了修补账户-余额模型相比 UTXO 模型的不足,才有了 AA 钱包这个东西。AA 钱包是在不涉及以太坊协议变更的前提下,诞生出的一种 workaround 方案。

从地位上来说,AA 钱包对于以太坊的地位,类似于铭文/符文/RGB 对于比特币的地位。在比特币生态里,因为没有图灵完备的脚本,所以在不触及比特币协议变更的前提下,搞出了铭文/符文/RGB 这些 workaround方案。

AA 钱包需要链下的 bundler 来提交交易,与符文需要链下的索引器来维护符文的数据状态,是不是一个意思,都严重依赖于链下的程序?

而事实上我们都知道,比特币生态的玩法,至今都还没有被主流社会认可。

假如 AA 钱包未来有一天能被社会大众认可,那也就意味着 workaround 方案在区块链世界中是可行的。对于整个生态的叙事都将引起巨大的改变。

AA 钱包的未来

综上所述,我们能得出的结论是,以太坊永远不可能支持 “原生” 的 AA 的钱包(在协议层面支持)。

总结

这些结论,对于技术人员的指导意义在于:

  1. 对 AA 钱包祛魅,不要以为 AA 钱包是高级、先进的技术。
  2. 可以学习、使用、研究 AA 钱包,但是千万不要真的 “相信” AA 钱包的技术理念。
🔲 ☆

基于 AI 语义执行的 MCP 区块链的设计

MCP 区块链

MCP(Modal Content Protocol)是 AI Agent 在使用的一种协议规范,用于 AI 和外部的工具进行交互,MCP Server 则是具体执行外部交互的组件。

MCP 区块链的含义是,首先它是一条区块链,然后每个节点都原生支持 MCP 协议的 RPC 接口,可以直接接受来自 AI Agent 的请求。每一个 MCP 请求,都是一笔交易(智能合约调用),这也就意味着,区块链会记录下所有的 MCP 交互历史。

节点内部的 MCP 执行引擎,功能分两部分,对于内部交互(EVM 在干的事情),只需要维护好内部的状态转移,把结果写入本地的 KV 数据库就可以了。对于外部交互,则只记录下要执行的请求本身,先不做外部调用。那么实际上对于每一个请求,都会改变本地的状态,所以这些交易是可以重放的,也就可以根据哈希值来确认节点数据的完整性。

至于对外部的执行请求,可以由外部的执行节点(一种角色)来完成。每一个外部请求都包含一些详细的参数,比如需要几个执行者、结果如何验证。交易会把一部分手续费作为执行费用,奖励给执行节点。如果执行节点作恶,自然也会有相应的惩罚机制。

对于外部的调用,关键在于如何验证外部执行的结果,这个问题可以交给调用者来定义,比如要创建一个 GitHub 仓库,验证方式就是,可以通过 API 查询到这个仓库的信息。

这就是 MCP 区块链的大体思路。

设计来源

解释一下这个想法的来源。MCP 区块链并不是想要 “把 MCP Server 去中心化”,而是想要 “给区块链带来与 AI 交互的能力”。这两种动机是截然不同的。

这个想法背后的逻辑很简单,比特币其实有脚本,只不过是生硬的操作码形式。以太坊干了一件很厉害的事情,给操作码加上了编译器,让开发者可以用编程语言来表达操作码。那么如今的 AI,很厉害的一点在于,打通了从自然语言到编程语言的路径。也就是说,未来的区块链,也许可以做到自然语言直接与状态机交互,而不需要经过 自然语言 -> 编程语言 -> 状态机 这样的路径。明显编程语言是一个中间层,MCP 区块链的设想在于消除这个中间层。

另一个边界在于,让区块链完全按照自然语言的意图执行是不切实际的,因为哪怕是人类,也需要书面形式的合同这种东西,所以代码本身不会消失,状态转移也不会消失。目前能够实现自然语言和状态机直接交互的技术方式,就是 MCP。

区块链的技术趋势

最近在思考一个问题,区块链的下一个技术趋势是什么?能明确的几个事实是:

  1. 大家已经不再怀疑加密货币是一种支付手段这件事情
  2. 依靠开发区块链发币的路线,已经完全没有叙事空间
  3. 下一个技术趋势,绝不会是以太坊路线图的扩展

行业内已经提过的方向也都不大有机会,DeAI、ZK、Layer2、DeFi、跨链、RWA、GameFi、BaaS、NFT、元宇宙、DAO、DID 等等,都是陈词滥调了。

所以区块链在技术趋势上,需要的一个原语级别的新叙事。

🔲 ☆

理解哈希函数与序列化

Hash function

Hash function 用于处理数据和其 hash values 的映射关系,hash values 是数据类似唯一标识的东西,可以用内存比较小的形式标识数据。hash function 有各种各样的实现,可以认为是一个黑盒子,进去的是 data,出来的是 hash values。

比如,我们可以把字符的 ASCII 码作为字符的 hash values:

HASH("a") = 97HASH("b") = 98HASH("c") = 99HASH("d") = 100

对于 2 个字符的 data,就把两个字符的 ASCII 相加,作为 hash values:

HASH("ab") = 97 + 98 = 195HASH("cd") = 99 + 100 = 199

但是这样很容易发现存在问题,HASH("ad") == HASH("bc") == 197。对于 3 个、4 个甚至更多字符的情形,hash values 重复的可能性更大。

hash values 是允许重复的,但如果存在大量重复,hash function 也就失去了它的作用和使用场景:如果全部都一样,无法区分,还用 hash values 干嘛?

不幸的是,目前最好的 hash function 也无法避免 hash values 重复的问题,只能尽可能减少 hash values 重复的概率,比如用类似数据库分库分表的方式,给每个字符足够的余量。

我们可以重新设计一下我们的 hash function,在只有 1 个字符的时候,仍然使用 ASCII 作为输出。在有 2 个字符的时候,让 第 1 个字符乘以一个基数,再加上第 2 个字符。由于第 1 个字符在乘以基数后会足够大,无论第 2 个字符是什么,在其基础上加上第 2 个字符的 ASCII 码,应该不会重复。

HASH("ab") = 97 * 1000 + 98 = 97098HASH("cd") = 99 * 1000 + 100 = 99100HASH("ad") = 97 * 1000 + 100 = 97100HASH("cd") = 98 * 1000 + 99 = 98099

这样至少解决了 2 个字符 hash values 重复的问题。

推广到更一般的场景,在面对可能很多字符的情况下,基数使用质数以避免累加造成的重复,为了保证基数足够大,使用质数的不同次方分别作为每个字符的基数,公式为:

hashCodes = char1 * base^(l-1) + char2 * base^(l-2) + ...

hashCodes 是输出的 hash values,char1 是第 1 个字符,char2 是第 2个字符,base 是基数,l 指字符串的长度。对于 3 个字符长度的字符串,第 1 个字符的基数就是质数的 2 次方,第 2 个字符的基数是质数的 1 次方,第 3 个字符是 0 次方,以此类推。

如果质数选择为 31,hash function 的实现为:

public static int hashCode(byte[] value) {    int h = 0;    for(int i = 0; i < value.length; ++i) {        h = 31 * h + value[i];    }    return h;}

也许具体的代码不是完全符合直觉,但你可以相信,和上面描述的公式是一致的。

hashCode("a") = 97hashCode("ab") = 97 * 31 + 98 = 3105hashCode("abc") = 97*31^2 + 98*31 + 99 = 96354

这就是 JDK (Java Development Kit) 中 hashCode 的实现方式。

Cryptographic hash function (CHF)

不难发现的是, hash function 比较容易根据 hash values 反推出原始的 data 是什么。我们可以写出这样的程序,假设我们已经知道字符长度是 2,由于字符使用 ASCII 编码,范围在 0 ~ 255,因此设 x 和 y 两个变量,枚举所有符合目标 hash values 的情况:

 public static String deHashCode(int code) {    for (int x = 0; x <= 255; x++) {        int y = code - 31 * x;        if (y < 0 || y > 255) {            continue;        }        System.out.println(((char) x)+","+((char) y));    }    return "";}

比如当 hashCode = 3105,得到的输出是:

\,ý],Þ^,¿_, `,a,bb,Cc,$d,

原始数据 ab 就出现在了为数不多不多的可能性中。

那么有没有办法减少 hash values 推出原始 data 的方法?在 Public-key cryptography 中 % 可是起到了很大的作用。hash function 也可以与一些加密算法的原理结合。

cryptographic 是 hash function 的修饰词,即使用了加密算法的 hash function。

md5 是使用非常广泛也接近过时的一种 cryptographic hash function,可以把任意长度的 data 计算输出为 128 bit 的 hash values。

md5("a") = 0cc175b9c0f1b6a831c399e269772661md5("ab") = 187ef4436122d1cc2f40dc2b92f0eba0

md5 的加密原理步骤很多,是一种不可逆的、单向的 hash function,无法轻易根据 hash values 得到 data。md5 的输入可以是任意大小的,1 GB 的二进制文件也可以hash 为 128 bit 的字符串。

md5 之外,SHA-1 的安全性更高,BLAKE2 的计算速度更快,它们都是典型的 cryptographic hash function。


Serialization

序列化是编程中很常见的一种操作,主要用于把复杂格式的数据转化成易于在不同环境中统一处理的格式,类似于定义一种接口格式,便于网络传输。

把数据转换为统一的过程称为 serialization,从统一格式转换为特殊格式的过程为 deserialization。JSON stringify 的过程也可以认为是一种序列化:

let object = {    field1: "abc",    field2: 123}let str = JSON.stringify(object)    print(str)    // {"field1":"abc","field2":123}

Serialization + CHF

可以明确的是,JSON stringify 的结果是一个字符串,这个时候就可以和之前的 cryptographic hash function 结合起来用了:

md5(str) = d79152b724c5f1e52e6bd4bfaf6e1532

只要定义过数据的 serialization 方法,我们就可以得到任意数据格式的 hash values。

Serialization + CHF + Linked List

Linked list 之间的关联关系常用变量的引用地址表示,但指针不是惟一的方式,数据结构的含义也可以扩展到更大的范围。我们完全可以用节点数据的 hash values 作为关联:

98b 的 hash values,表明值为 a 的节点,下一个节点的 hash values 为 98,也就是值为 b 的节点。

我们也可有使用反向的 linked-list:

a 的 hash values 是 97,表明值为 b 的节点,上一个节点的 hash values 为 97

当然,这里的值可以是更复杂的数据结构,只要定义好 serialization 格式,也可以应用到更复杂的 hash function 上,比如这样正向的 linked-list:

type Node struct {    Value int    Next  string}node1 = Node{ Value: "a" }node1_str = JSON.stringify(node1)   // { "Value": "a" }node1_hash = md5(node1_str)         // 9ad06e8a44d0daf821f110794fb012c7node1.Next = node1_hash

这就构建好了一个节点,以此类推。

另一种也许更好或者更适用于某种特定场景的形式是,将其改为反向的 linked-list:

type Node struct {    Prev string    Value int}node1 = Node{ Value: "a" }node1_str = JSON.stringify(node1)   // { "Value": "a" }node1_hash = md5(node1_str)         // 9ad06e8a44d0daf821f110794fb012c7node2 = Node{ Value: "b" }node2_str = JSON.stringify(node2)   // { "Value": "b" }node2_hash = md5(node2_str)         // 7e332b78dbaac93a818a6ab639f5a71bnode2.Prev = node1_hash

这种反向的 linked-list 就是区块链的基础数据结构。

❌