有状态的 UTXO 和 OP_PUSH_TX 的技术原理

一般都认为,UTXO 是“无状态”的。对于生活中常见的支付场景,币转出后就意味着失去了对这些币的“控制权”,收款人可以随意支配收到的 BSV。

那 UTXO 可以做到“有状态”吗?

本文将介绍“有状态” UTXO 的实现原理。请在继续阅读前,先理解 OP_CHECKSIG 操作码的工作方式

UTXO 模型

BSV 的交易会消耗一个或多个输入,同时产生一个或多个输出。交易(除了 Coinbase 交易)的输入,都是之前某笔交易的输出。如果某个交易输出当前是未花费(unspent)的状态,也就是这个交易输出当前还没有被某个交易拿去作为交易输入使用,我们就称之为未花费的交易输出(UTXO,Unspent Transaction Outputs)。这些(前序)交易未被花费的交易输出(UTXO),可以在之后,作为交易输入被花费。我们说,UTXO 是构成交易的基本元素,因为每笔交易(除了 Coinbase 交易)都是在不断地消耗已存在的 UTXO,同时产生新的 UTXO。

交易的输入和输出上都有一个脚本。输出上的脚本被称为锁定脚本(Locking Script,也常用 scriptPubKey 表示),它是一个加密难题,指定了今后花费这个 UTXO 必须要满足的条件,相当于一把锁。输入上的脚本被称为解锁脚本(Unlocking Script,也常用 scriptSig 表示),它解决或满足了之前放置在这个 UTXO 上的加密难题或条件,解锁 UTXO 用于支付,相当于一把钥匙。请注意,当前交易输入上的“钥匙”试图打开的是它前序交易输出上的“锁”。

比特币虚拟机(BVM)在验证钥匙是否能开锁时,会把 [输入的解锁脚本] 和 [该输入引用的输出的锁定脚本] 拼接起来从左到右执行一遍。如果执行过程中没有出现错误并且执行结果为真,则验证通过,意味着钥匙打开了锁,这个 UTXO 可以被花费。

总结起来两句话:只要提供的钥匙能开锁,UTXO 就可以被花费。只有当提供的钥匙能开锁时,UTXO 才可以被花费。

请注意,解锁脚本里不能出现 PUSHDATA 以外的任何操作码,否则会报错

16: mandatory-script-verify-flag-failed (Only non-push operators allowed in signatures)

也就是说,解锁脚本里只能有数据不能出现逻辑操作,否则任何 UTXO 都可以用 OP_RETURN 解锁。

OP_TRUE OP_RETURN

换个角度来看:锁定脚本就像是函数(数据和操作),而解锁脚本是函数的入参(只有数据)

def unlock(解锁脚本) -> bool:
#
# 锁定脚本
#

BVM 验证脚本就像是在调用 unlock 函数,如果返回结果为真则验证通过,否则失败。

我们可以把 P2PKH 的脚本

[签名] [公钥]    OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG

等价成下面的形式:

class P2PKH:

def __init__(self, pkh: bytes):
self.pkh: bytes = pkh

def unlock(self, signature: bytes, public_key: bytes) -> bool:
return hash160(public_key) == self.pkh and checksig(signature, public_key)

注意到,解锁脚本中的签名和公钥,变成了 unlock 方法的参数,锁定脚本中的公钥哈希变成了类的成员变量,锁定脚本中的两个限制写到了 unlock 方法中。此时,创建 UTXO 就等价于新建类实例(instance), 花费 UTXO 就等价于调用对应实例的 unlock 方法。

if __name__ == '__main__':
# 创建
money = P2PKH(alice_pkh)
# 花费
money.unlock(alice_signature, alice_public_key)

这种把脚本抽象成函数的思路十分重要,能帮助你理解 sCrypt 代码的组织方式。

“有状态”的 UTXO

什么是“有状态”的 UTXO?

  1. UTXO 的锁定脚本里记录了状态
  2. UTXO 的状态可以随着交易链条的流转按要求变化,且状态链条不会中断

第一点很容易实现,可以直接在 UTXO 的锁定脚本里记录状态数据,让这个 UTXO 变成一个“有状态”的 UTXO。使用 OP_RETURN 或者 OP_DROP 都可以满足这样的需求。

... OP_RETURN data1 data2 data3 ...
... data1 OP_DROP data2 OP_DROP data3 OP_DROP ...

对于第二点,UTXO 的状态能按需变化很好理解,但状态链条不会中断,就要求锁定脚本中限制该 UTXO 被花费的条件会被传递,而且会被强制传递。也就是说,对某个 UTXO,花费它的条件是:产生的新 UTXO 的锁定脚本逻辑,必须跟该 UTXO 保持一致,否则就无法花费。

只记录状态是不够的,UTXO 的状态还需要能在交易流转过程中保持住。如果不能保证锁定脚本的逻辑强制传递,状态链条就会中断。

这看起来很不可思议。你转出一笔 BSV 后,还能限制这些 BSV 如何被使用

我们要实现一个“计数器” UTXO,把自己被花费的次数记录在链上,具体为:

  1. 有一个 UTXO 被称作“计数器”,状态初始值为 0
  2. 该 UTXO 被花费时,必须生成一个新的“计数器” UTXO,并将之前的状态值加 1 记录到新的 UTXO 中
  3. 规则 [2] 由矿工验证,如果没有生成符合要求的新 UTXO,就无法花费当前这个 UTXO

如上图所示,如果锁定脚本的逻辑不是强制传递的,就可以创建出 TX2’ 这样的交易导致状态中断。概括成一句话:有状态的 UTXO 必须要强制传递锁定脚本的逻辑

我们可以将锁定脚本分成两个部分:逻辑(代码)和状态(数据)。

前者用于控制状态转换,并在交易流转过程中保持不变。

对 P2PKH 和 P2PK 这类“支付型” UTXO,它们当然是“无状态”的,因为锁定脚本中既没有保存状态,也没有强制传递逻辑限制(不是必须创建新的同类型 UTXO 才可以花费当前 UTXO)。

交易的上下文

为了实现“有状态”的 UTXO,在花费时(绿色),至少需要知道以下信息(黄色):

  1. 这个 UTXO 在前序交易中的锁定脚本。用于获取逻辑限制和当前状态
  2. 当前交易交易输出的锁定脚本。用于验证花费这个 UTXO 的交易所产生的新 UTXO 的锁定脚本,其逻辑是否与当前 UTXO 保持一致,其状态变化是否符合要求

很眼熟是不是?这些信息,都能在当前交易交易摘要中找到:

  • 第 [5] 项是该 UTXO 锁定脚本的原文,我们可以从中得到逻辑 L 和状态 S
  • 第 [8] 项 hashOutputs 是当前交易交易输出的双哈希。这没有什么问题,我们只需要对逻辑 L 和正确的新状态 S’ 拼接后的内容做双哈希,保证结果与交易摘要中的 hashOutputs 一致即可。

至此,我们可以得出以下结论:“有状态”的 UTXO 需要在解锁时知道当前交易的上下文,可以通过在解锁脚本中放入当前交易的交易摘要来实现。

但这样会带来新的问题:放入解锁脚本中的交易摘要,如何保证不可伪造。换句话说,如何保证放入解锁脚本中的交易摘要,真的是当前交易的交易摘要

OP_PUSH_TX 的技术原理

操作码 OP_CHECKSIG 的工作方式十分有趣,它并不在意签名是怎么来的。它总是从栈上获取公钥和签名,然后根据当前交易为每个输入计算交易摘要并验签。至于这个签名是解锁脚本里直接提供的还是通过其它方式计算出来后再上栈的,并不重要。

注意到没,OP_CHECKSIG 从栈上拿到签名和公钥后,会把当前交易的交易摘要当做被签名的消息来验签

我们假想有这样一个操作码 OP_SIGN,它完成两个动作:

  1. 从栈上弹出私钥和消息,执行 ECDSA 签名
  2. 将计算出的签名数据重新压回栈中

如果实现了这样的“链上签名”逻辑,就可以“借助” OP_CHECKSIG 来保证放入解锁脚本中的交易摘要,确实是当前交易的交易摘要。

任意选取一对公私钥 $a$ 和 $A = aG$,对下面这段脚本:

... [交易摘要] [a] OP_SIGN [A] OP_CHECKSIG ...

当 BVM 执行到假想的操作码 OP_SIGN 时,会先从栈上弹出我们手动放入的交易摘要和私钥 $a$ 进行 ECDSA 签名,然后把签名结果重新压回栈中。

... [链上计算出的签名] [A] OP_CHECKSIG ...

当 BVM 继续执行到操作码 OP_CHECKSIG 时,会先从栈上弹出公钥 $A$ 和链上计算出的签名,然后计算当前交易的交易摘要,把它当做被签名的消息来验签。如果验签通过,那么只有一种可能,就是我们手动放入脚本的交易摘要,与当前交易的交易摘要完全一致,因为 OP_CHECKSIG 只有在被签名的消息是当前交易的交易摘要时才会验签通过

请注意,直接把私钥 $a$ 放到脚本中公开这没有任何问题,因为它不控制任何 BSV 而只是为了完成 ECDSA 签名。这里的私钥 $a$ 可以重复使用,当然也可以每次都随机选取。

我们给这段脚本起一个名字:OP_PUSH_TX。虽然它也以 OP 开头,但跟 OP_SIGN 一样,是一个假想出来的操作码。使用 OP_PUSH_TX 的目的非常直观:保证解锁脚本中交易摘要的真实性。

那么,这么牛逼的特性该如何上手呢?

sCrypt

sCrypt 已经把这些技术都封装好了。

创世纪升级后,你可以在 BSV 中使用几乎全部的脚本操作码(还剩 5 个操作码计划在 Chronicle 升级中重新启用)。直接使用它们在脚本里编程是十分可怕的,不紧难度大、可读性差,还很容易让别人原地爆炸。如果把脚本操作码看成是汇编指令,把脚本看成是汇编代码,那 sCrypt 就是一门高级编程语言。

不论是编写逻辑复杂的脚本,还是管理“有状态”的 UTXO,sCrypt 都可以胜任。只有正确理解并熟练使用它,才能在 BSV 上进行智能合约的开发。

你可以参考 sCrypt 的官方文档,了解更多技术细节。

总结

本文围绕 UTXO 的状态延续问题进行了一系列介绍,主要内容包括:

  • UTXO 被锁定脚本锁定,除非提供正确的解锁脚本,否则无法花费
  • 对“有状态”的 UTXO,除了要在锁定脚本中记录状态外,还需要保证状态能在交易链条中保持。只有强制传递锁定脚本的逻辑才能保证状态链条不会中断
  • 为了实现 UTXO 的状态延续,需要在解锁该 UTXO 时能获取到当前交易的上下文
  • 把当前交易的交易摘要放到解锁脚本中解决了交易上下文的问题,但还需要利用“链上签名”技术和 OP_CHECKSIG 来保证传入交易摘要的真实性
  • sCrypt 是 BSV 脚本的高级编程语言,可以用来编写复杂的脚本,它还封装了 OP_PUSH_TX 技术可以管理“有状态”的 UTXO,是你开发 BSV 智能合约的必备工具

参考