函数返回值在链上没有家:EIP-758 想补的完成通知与当年那场收据之争 图 1
函数返回值在链上没有家:EIP-758 想补的完成通知与当年那场收据之争 · 图 1

一个执行完就蒸发的身影

用合约做一笔操作,最想知道的往往是”结果是什么”:调用返回的地址、算出的数额、判定的状态。但以太坊的执行模型里,函数的返回值只活在那笔交易的执行上下文里,出块即丢弃——它不进状态、不进收据,任何人事后都无法从链上取回它。EIP-758 在 2017 年 11 月为这个缺口写了一份接口层提案,状态 Stagnant。它的思路是:既然返回值在执行当下是存在的,就让节点在交易落块的那一刻,把执行时的返回数据连同通知一起推给当初的调用方。要理解为什么需要这样的绕行设计,得先回到一场更早的收据之争。

函数返回值在链上没有家:EIP-758 想补的完成通知与当年那场收据之争 图 2
函数返回值在链上没有家:EIP-758 想补的完成通知与当年那场收据之争 · 图 2

EIP-658 的选择:收据只配一个布尔位

758 在文中专门交代了这段历史。早期的 EIP-658 曾想把返回数据直接放进交易收据,但被否掉了:收据要为返回数据付出存储与带宽,而返回数据本身不产生持久状态,按当时计费模型等于白送一段可被滥用的空间,容易制造 DoS 与垃圾交易空间。最终落地的 658 版本只给收据加了一个布尔的 status 字段,从拜占庭升级起告诉我们”这笔交易执行成功还是失败”,至于函数本来想返回什么,一概没有。这个取舍定义了此后所有核对手段的地基:成功失败有据可查,具体结果各凭本事——要么解析事件日志,要么对同一状态再模拟一次只读调用。

completedTransaction 订阅的过滤设计

758 的接口挂在订阅系统上:调用方用 eth_subscribe 声明一个 completedTransaction 主题,附带一个可选过滤器,字段共三个——from 与 to 各接受单个地址或地址列表,框定交易的收发双方;hasReturnData 用布尔值筛掉或只留带返回数据的交易。订阅成功返回一个订阅号,之后凡是你提交的交易落块,节点就推一条通知,内含交易哈希与返回数据;若交易被随后的区块重组波及,还会补发一条带 removed 标记的通知。纯 HTTP 的调用方则改用轮询:先创建一个 completedTransaction 过滤器,之后反复领取结果数组。提案也坦承轮询模式下节点无法确认领结果的就是当初发交易的那个客户端,因此不加来源限制。

现实里的主流替代:状态、日志与模拟

这套订阅从未成为标配,今天你能用的核对链条由三块拼成。第一块是收据的 status,配合 gasUsed 与 effectiveGasPrice 能把”成没成、花了多少”钉死(反算方法见 预估和账单对不上:用回执里的 gasUsed 与 effectiveGasPrice 反算实际手续费)。第二块是事件日志:协议都把关键结果写成事件,本质上是社区对 658 那句”结果各凭本事”的集体回答,日志解码的字段规则在 交易的输入数据字段怎么读:四字节选择器、参数槽位与解不出来的情况 的输入数据一文中同源相通。第三块是只读模拟:交易落块后,对同一合约再调一次 view 函数,读取执行后的状态推回你想要的”返回值”。三块拼起来覆盖 758 想覆盖的大部分场景,只是每一块都需要调用方自己组装。

用户侧的现实含义

对普通用户,这段历史解释了一个高频困惑:为什么区块浏览器里看一笔”成功”的交互,有时看不到它具体改变了什么。status 为成功只代表执行没回滚,不等于你预期的效果发生;预期效果要去看该事件对应的日志或余额变化来印证。另一个常见误判是把”页面显示查询不到结果”当成”交易失败”——多数时候只是调用方没能力取回返回值,交易本身早已成功上链。稳妥的动作顺序:先收据后日志,再余额,三步都指向同一结论才收工;浏览器读法见 区块浏览器怎么读:确认数、失败原因和交易状态逐项解释

三点可带走的经验

第一,链上”结果”是两个东西:共识记的是状态变化与收据,函数返回值是执行过程的副产品,工具链的一切变通都源于这个分层。第二,凡遇到声称”能还原任意交易返回值”的服务,先问它用哪条路线——存了全历史数据的私有索引、事后模拟执行、还是干脆胡编——三种答案的可信度天差地别。第三,提案停滞不等于问题消失:758 之后仍有无数次”给收据加返回数据”的尝试在讨论中反复出现,理解 658 当年的 DoS 顾虑,才能评估任何新方案的代价。本文为机制科普,不构成投资建议。