Skip to content

Latest commit

 

History

History
243 lines (174 loc) · 17.9 KB

File metadata and controls

243 lines (174 loc) · 17.9 KB

UTXO模型

数字货币是区块链技术最为成功也是最重要的一个应用,区块链技术为数字货币的交易模式与发行方式带来了深刻的变革,促进了效率和安全性的提升。在数字货币中,面临的一个很重要的问题就是归属权的问题,比如100个比特币,怎么证明这些比特币在计算机中的归属权。毕竟在计算机中复制一个东西太过容易,所有的价值标的在计算机看来不过是一串二进制数据。

凭借我们的直觉,应该有一个类似银行账户的东西,账户余额的控制权在掌握账号密码人的手中。

但是在去中心化的场景下,这样做稍稍有些问题,比如用账户A给账户B转100人民币,银行会对账户A的余额减100,把账户B的余额加100。但是银行是一个中心化的系统,在之前构建P2P网络的时候,可以保证每个节点的权利平等,既然权利平等,谁能把一个节点的余额加100,谁又能把一个节点的余额减100呢?

如果任意节点均有权操纵其他节点的账户余额,这样就乱套了。

举例来说,A的账户余额有100人民币,A可以给B账户转100人民币,也可以再给账户C转100人民币。虽然这不符合常理,但是确实在数字货币场景中发生了,这就是数字货币所面临的双花问题或者叫双重支付问题。

第一种以区块链为技术基础的数字货币比特币也面临这个问题,不过它的作者中本聪提出一个创造性的解决方案,叫做未花费的输出,也就是大名鼎鼎的UTXO(Unspent Transaction Output)模型。

尽管UTXO模型不太符合我们的直觉,但是确实可以有效的解决双花问题。

UTXO 模型

UTXO全名是Unspent Transaction Outputs,未花费交易输出,它的总体的设计是基于这样一种思路,如果A要花费一笔钱比如100元,这笔钱不会凭空产生,那么必然由B先花费了100元,被A赚到这100元,然后A才能继续花费这一笔钱。这个链条的源头就是先产生这笔钱,在比特币中这被称为铸币(coinbase)。然后会产生这样一个链条,铸币->B->A->? 整个过程从铸币开始,一直可以追溯到当前的状态,当接收到一个UTXO输入的时候,可以基于这个模型判断这笔钱有没有被在别的地方花费过。

UTXO模型:

utxo

假设A挖出了区块1,获得了10个BTC。这个时候在UTXO模型中,A的余额是10,同时A向B和C分别转账5个比特币,这两笔交易被打包到区块2中,这个时候UTXO模型中,B和C的余额分别是5,当查看区块4打包的交易以后,发现这时的UTXO模型中显示,G和C有2.5个BTC,H有5个BTC。

还有一点违反直觉,整个过程是单向的,也就是说如果B只对A花费了50元,A是不会给B找回剩余的50元,但是这显然是不可接收的,这也是UTXO模型的另一个性质,一个交易的输入必须是另一个交易的输出。 但是可以采取一种折中的手段,B向A花费50元,同时B可以向B’花费50元,而B‘就是B控制的另一个地址,这个过程就被称为找零。

B'被称为找零地址

在找零的过程中还可能会发生这样一个问题,如果B忘记了填写找零地址,那么剩余的50元会去哪里呢?当然这些钱不会凭空消失 ,而是被矿工作为这笔交易的手续费收取了。简单来说输入的与输出的差额就会变成矿工的手续费,所以当花费UTXO的时候尤其需要注意。

除了找零,还有凑整的的情况,就是单个地址的输入余额不足以支持花费,所以需要多个地址的余额凑到一起,凑整的过程与找零刚好相反,地址H的输入就是一个凑整的过程。

在上面的交易链条中,每一笔花费都是可以追溯到源头的,显然这是不利于隐私保护的。如果地址没有发生改变,则可以监控这个地址的资金往来,甚至可以根据这个账户资金往来的特征推断出谁拥有这个地址。而这种单向链条可以让B不得不创建一个新地址B’,即能用来接收找零,也可以用来躲避这种追溯。 每一笔交易就会促使一次UTXO地址余额的变换,所有UTXO的集合被称为 UTXO set ,目前有数以百万的UTXO。UTXO集的大小随着新UTXO的增加而增长。UTXO的变换最终会被打包进区块存储起来,整个UTXO模型最终还是被交易所驱动。

无论是交易的输入还是输出,其中包含的比特币必须等于聪(satoshis)倍数的值作为。正如人民币可以分为小数点后两位数字一样,比特币可以被分为小数点后八位,作为聪(satoshis)。

交易的输入和输出却并不简单只有数值,还包含一段脚本,而这段脚本就是用来验证谁有权来花费这些余额。

输入

交易输入标识(通过引用)将使用哪个UTXO并通过解锁脚本提供所有权证明。

为了建立交易,客户端从其控制的UTXO中选择具有足够价值的UTXO进行所请求的付款。有时候一个UTXO就足够了,有时候需要多个UTXO。对于将用于进行此项付款的每个UTXO,客户端将创建一个指向UTXO的输入,并使用解锁脚本将其解锁。

让我们更详细地看看输入的组成部分。输入的第一部分是指向UTXO的指针,引用交易的哈希值和输出索引,该索引标识该交易中特定的UTXO。第二部分是一个解锁脚本,为了满足UTXO中设置的花费条件。大多数情况下,解锁脚本是证明比特币所有权的数字签名和公钥。但是,并非所有解锁脚本都包含签名。第三部分是序列号。

交易的输入是 vin 数组:

The transaction inputs in Alice’s transaction

"vin": [
  {
    "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
    "vout": 0,
    "scriptSig" : "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
    "sequence": 4294967295
  }
]

如你所见,列表中只有一个输入(因为这个UTXO包含足够的值来完成此次付款)。输入包含四个元素:

  • 交易ID,引用包含正在使用的UTXO的交易;
  • 输出索引(vout),被用来标识使用来自该交易的哪个UTXO(第一个从0开始),假设当前交易引用了一个具有100BTC的UTXO,但是本次支付只需要向地址A转移50BTC,向地址B转移20BTC,然后将剩余的30BTC作为找零发送回自己的另一个地址,由于UTXO不可拆分的特性,必须一次全部花费完,这时就需要用到这个值将交易和所花费的UTXO对应起来。
  • scriptSig,满足UTXO上的条件的脚本,用于解锁并花费;
  • 一个序列号,是发送者定义的交易版本;

通过交易ID和输出索引可以唯一的标识一笔交易。

输出

在比特币中,输出是位于vout的数组中的。

"vout": [
  {
    "value": 0.01500000,
    "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY
    OP_CHECKSIG"
  },
  {
    "value": 0.08450000,
    "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
  }
]

该交易包含两个输出。每个输出由一个值和一段指令组成。Value值以比特币单位,但在交易本身中,它被记录为以聪为单位的整数。每个输出的第二部分是设置花费验证的脚本。 scriptPubKey并展示了该脚本的人类可读的表示。

通过上面的介绍可以总结归纳一下UTXO模型的特点;

  • 除了BTC产生的交易外每一笔交易的输出都是另一笔交易输入;
  • 如果丢失私钥丧失账户的控制权后,UTXO会一直保存这个账户的余额,因为没有输出;
  • 随着比特币的碎片化和账户私钥的丢失,UTXO模型会越来越膨胀;
  • 验证一笔交易的余额是否足够需要向上追溯;
  • 通过UTXO模型可以一定程度上避免双花攻击;
  • UTXO通过找零设置新地址增加了一定的隐私性,因为除了你本人是不知道哪个地址是找零地址,哪个是收款地址。

比特币脚本

每一笔交易除了铸币交易(coinbase)外,每一笔交易都拥有至少一个输入(TxIn)和至少一个输出(TxOut),和我们直觉上理解的交易的TxIn和TxOut应该是数字不太一样,在比特币中是以脚本的形式存在。

比特币脚本是一种基于栈的,在有限的范围内设计,可在多种硬件上执行并且足够的简单脚本语言。它只能进行有限的操作还不能完成许多现代编程语言能够做的事情。足够的简单意味着很难从中挖掘出漏洞,对于数字货币来说,这是一个深思熟虑的安全特性。

比特币脚本由许多的操作码构成,按照功能不同大体可以分为四类,分别是栈操作,算术操作,密码学操作,有限制的流操作。下面是一些常见的操作码:

栈操作:

OP_DUP: 复制栈顶元素,压入栈中。
OP_PUSHDATA:将数据压入栈顶。

算术操作:

OP_ADD: 弹出2个元素相加后压入栈中。
OP_EQUALVERIFY:弹出2个元素,比较是否相等。若不相等,则标记交易为无效

密码学操作:

OP_HASH160: 弹出栈顶元素,进行RipeMD160哈希计算,将结果压入栈顶。
OP_CHECKSIG:出两个元素(一般是签名和公钥),验证签名,如成功,则压入TRUE;否则压入FALSE。

有条件的流控制:

OP_IF: 如果栈顶元素不为0,则执行语句。

在比特币中没有账户的概念,谁拥有这笔交易的输出谁就可以花费这笔交易中的比特币,为了证明自己拥有这笔交易的输出就需要提供密钥接受验证,验证通过就可以花费这笔交易的输出。

其基本的设计思路是,每个人都能从UTXO集合中看到每笔交易的输出,而谁能提供密钥让这个脚本运行通过,谁就能花费这个UTXO。

执行的脚本由输入和输出拼接而成,如下图所示;

script

在最初版本的比特币客户端中,输入脚本和输出脚本按顺序拼接起来执行。出于安全因素考虑,在2010年发生了改变,因为存在一个漏洞,允许格式错误的解锁脚本将数据推送到堆栈并损坏锁定脚本。而在当前的方案中,脚本是单独执行的,在两次执行之间传输堆栈,

比特币提供了三种输入输入脚本的形式,分别是Pay to Publish KeyPay to Publish Key HashPay to Script Hash

公钥支付

公钥支付(Pay to Publish Key)即输出脚本直接给出了收款人的公钥,输入脚本提供了用私钥对整个交易的签名,最后通过OP_CHECKSIG验证。在签名算法中私钥签名公钥验证,如果通过验证,则证明这个行为确实是私钥拥有者所为。在这个例子中,花费交易用私钥对这笔交易进行签名,而上一笔输出交易用公钥对这笔花费交易的私钥进行验证,验证通过后,就可以证明这个交易确实是私钥拥有者做出的,并非冒牌。

在比特币脚本中是这样表示的;

input script:
    OP_PUSHDATA(Sig)
output script:
    OP_PUSHDATA(PubKey)
    OP_CHECKSIG

首先 OP_PUSHDATA(Sig)OP_PUSHDATA(PubKey)将Sig和PubKey压入栈中

接着 OP_CHECKSIG 弹出栈顶两个元素验证签名

栈中结果是True,证明私钥拥有者同时也拥有花费这笔交易Out的权利。

公钥哈希支付(Pay to Public Key Hash)

Pay to Publish Key中,输出脚本中直接暴露了下一笔交易花费者的公钥,显然是不太合理的,于是又有了第二种输出脚本的类型Pay to Public Key Hash

Pay to Public Key Hash类型的输出脚本直接给出了收款人公钥的哈希,输入脚本提供了用私钥对整个交易的签名,同时也提供了自己的公钥用作验证,整个过程大同小异。

input script:
    OP_PUSHDATA(Sig)  //压入签名
    OP_PUSHDATA(PublicKey)  //压入公钥
output script:
    OP_DUP  //复制栈顶元素,再压入栈
    OP_HASH160  //弹出栈顶元素,取哈希在压入栈
    OP_PUSHDATA(PubKeyHash)  //压入输出脚本提供的公钥哈希
    OP_EQUALVERIFY   //弹出栈顶元素,比较是否相等
    OP_CHECKSIG   //公钥检查签名是否正确

脚本哈希支付

脚本哈希支付(Pay to Script Hash)这种形式的输出脚本是收款人提供脚本(redeemScript)的哈希,到时候收款人要花费这笔交易的时候需要输入脚本的内容和签名,验证的时候分两步;

  • 验证输入脚本的哈希是否与输出脚本中的哈希值匹配;
  • 反序列化并执行redeemScript,验证输入脚本给出的签名是否正确;

采用BIP16的方案

input script:
    ...
    OP_PUSHDATA(Sig)          
    ...
    OP_PUSHDATA(serialized redeemScript)  
output script:
    OP_HASH160                   
    OP_PUSHDATA(redeemScriptHash)  
    OP_EQUAL        

其实可以用Pay to Script Hash实现Pay to Public Key

redeemScript:
    OP_PUSHDATA(PubKey)
    OP_CHECKSIG   
input script
    OP_PUSHDATA(Sig)
    OP_PUSHDATA(serialized redeemScript)
output script:
    OP_HASH160
    OP_PUSDHDATA(redeemScriptHash)
    OP_EQUAL

Pay to Script Hash在比特币最初版本是没有的,后期软分叉的方式的方式加入,其中最重要的一点是对多重签名的支持。

多重签名中,只要提超过供指定数量即可,容忍了一定程度的私钥丢失

原来的多重签名需要外部用户提供一一对应的公钥,一共有多少公钥,几个公钥通过验证才可以完成交易,对用户比较繁琐。现在使用Pay to Script Hash将整个过程打包到脚本中,对外部用户来说降低了多重签名的复杂性,将复杂性转移到了系统中。

剪枝输出脚本

除了利用比特币脚本完成支付相关的操作外还可以存储少量的数据,许多开发人员认为这是对比特币系统的滥用,会增加系统的负担并且希望可以阻止这样的使用方式,而另一部分人则认为这是体现区块链技术强大的一个示例。

这种脚本的形式如下所示:

output script
    RETURN
    ...

假如有一个交易的input指向这个output,不论input里的input script如何设计,执行到RETURN这个命令之后都会直接返回false,RETURN后面的其他指令也就不会执行了,所以这个output无法再被花费出去,永远不会花费的交易永远不会从UTXO集中移除,最终导致UTXO数据库的大小永远增加或“膨胀”。

为了解决这个问题,在Bitcoin Core客户端的0.9版本中,通过引入 RETURN 运算符达成了一个折衷方案。 RETURN 允许开发人员将80个字节的非付款数据添加到交易输出中。但是与使用“假”UTXO不同,RETURN 运算符会创建一个显式的可验证不可消费的输出,该输出不需要存储在UTXO集合中。 RETURN输出记录在区块链中,因此它们消耗磁盘空间并会导致区块链大小的增加,但它们不存储在UTXO集中,因此不会使UTXO内存池膨胀,完整节点也不用承担昂贵的内存负担。

可以通过一笔具体的交易在区块链浏览器上观察到具体的输出脚本,1a2e22a717d626fc5db363582007c46924ae6b28319f07cb1b907776bd8293fc就是一个例子,它的输出脚本是

NULL_DATA
OP_RETURN 215477656e74792062797465206469676573742e

这种形式的比特币脚本通常在如下场景使用;

  • 永久存储一些信息,比如在某个时间证明存在某些事情,比如在2020年1月1日把知识产权的哈希放到链上,当以后产生纠纷的时候,你把知识产权公布出来,知识产权的哈希在特定时间已经上链,就可以证明你在特定时间已经知道了这个知识产权。国外甚至有专门的网站来做这件事。 Proof of Existence(http://proofofexistence.com)。
  • 代币转换,比如你把一些比特币转换成其他数字货币,你需要通过这种方式来证明你付出了一些代价。
  • 销毁比特币。

无状态验证

比特币交易脚本语言是无状态的,在执行脚本之前没有状态,在执行脚本之后也不保存状态。因此,执行脚本所需的所有信息都包含在脚本中。脚本在任何系统上都能可预测地执行。如果你的系统验证了脚本,你可以确定比特币网络中的其他每个系统都会验证该脚本,这意味着有效的交易对每个人都有效,每个人都知道这一点。结果的可预测性是比特币系统的一个重要好处。

图灵不完备

比特币交易脚本语言包含许多操作符,但是故意在一个重要方面进行了限制 - 除了条件控制外,没有循环或复杂的流程控制功能。这确保语言不是 图灵完备 Turing Complete 的,这意味着脚本具有有限的复杂性和可预测的执行时间。脚本不是通用语言。这些限制确保了该语言不能用于创建无限循环或其他形式的“逻辑炸弹”,这种“逻辑炸弹”可能嵌入交易中,导致对比特币网络的拒绝服务攻击。请记住,每笔交易都由比特币网络上的每个完整节点验证。有限制的语言会阻止交易验证机制被当作漏洞

可以说比特币已经具有了下一代区块链平台以太坊中智能合约的雏形。