主页 > imtoken官方版 > 北京大学肖震老师《区块链技术与应用》公开课笔记9——比特币使用的脚本与原理

北京大学肖震老师《区块链技术与应用》公开课笔记9——比特币使用的脚本与原理

imtoken官方版 2023-09-26 05:10:01

图片(第15秒)是比特币的交易示例。 该交易有一个输入和两个输出。 左上角说的是输出,其实就是这笔交易的输入。 对于右边的两个输出,上面的unspent表示没有花费,下面的spent表示花费了。 交易已经收到23次确认,回滚的可能性很小。

以下是本次交易的输入输出脚本。 输入脚本包含两个操作,分别将两个很长的数压入栈中。 比特币使用的脚本语言非常简单,唯一可以访问的内存空间就是栈。 与通用编程语言不同,像C语言C++有全局变量、局部变量、动态分配的内存空间,这里是栈,所以称为基于栈的语言。 这里的输出脚本有两行,分别对应上面两个输出。 每个输出都有自己独立的脚本片段。

如图(第1分40秒)是交易的具体内容。 先来看交易的一些宏观信息。 第一行:交易ID,第二行hash,交易的哈希值。 第三行:使用的比特币协议版本。 第四行:交易规模。 第五行:用于设置交易的生效时间。 这里的0表示立即生效。 大多数情况下,locktime为0,如果为非零值,则交易需要一定的时间才能生效。 例如,它必须等待 10 个区块才能写入区块链。 第六行和第七行的vin和vout是输入输出部分,后面会详细说明。 第八行是这笔交易所在区块的哈希值。 第 9 行:交易有多少确认。 第十行是交易产生的时间,第十一行是区块产生的时间。 (时间和出块时间都是指从很早的时间开始已经过去了多少秒)

如图(3分32秒)是交易的输入结构。 一笔交易可以有多个输入,在这个例子中只有一个输入。 每个输入必须指明输入所花费的货币是前一个交易的输出,因此前两行给出了输出货币的来源。 第一行:上一笔交易的哈希值。 vout 表示本次交易的第一个输出。 所以这里的意思是花费的币来自交易中的第0个输出,其哈希值为c0cb...c57b。 接下来是输入脚本。 输入脚本最简单的形式就是给签名,证明你有花钱的权利。 (后面PPT中的scriptsig写成input script input script)。 如果一笔交易有多个输入,每个输入都必须注明币种来源,并且必须给出签名,这意味着比特币中的一笔交易可能需要多个签名。

如图(第5点),是交易的输出,也是一个数组结构。 本例有两个输出,value为输出金额,即转给对方多少钱,单位为比特币,即0.22684比特币。 另一个单位是聪(one Satoshi),它是比特币中最小的单位。 1 比特币 = 10 的 8 次方中本聪。 n 是序号,表示这是本次交易的第一个输出。

scriptpubkey是输出脚本,后面写成输出脚本。 输出脚本的最简单形式是提供公钥。 下面的asm是输出脚本的内容,里面包含了一系列的操作,后面会详细说明。 require sigs 指示输出需要多少个签名才能生效,在两个示例中只需要一个签名。 type 是输出的类型。 两个例子都是pubkeyhash,就是公钥的hash。 地址是输出的地址。

如图(6分36秒)展示了输入输出脚本是如何执行的。 在区块链的第二个区块中,存在一笔从A→B的转账交易。 B收到转账的钱后,经过两个区块后再次转账给C。 因此,B→C交易的txid和vout指向A→B交易的输出。 为了验证交易的合法性,将B→C的输入脚本与A→B交易的输出脚本拼接在一起。

如图(7分40秒),这里打叉,前面交易的输出脚本放在后面,后面交易的输入脚本放在前面。 在早期的比特币实践中,这两个脚本被拼接在一起,从头到尾执行。 后来为了安全起见,将两个脚本改为分开执行。 先执行输入脚本,如果没有错误,再执行输出脚本。 如果能顺利执行,且栈顶结果为非零值,即为真,则验证通过,交易合法。 如果在执行过程中出现任何错误,则该交易是非法的。 如果一笔交易有多个输入,则每个输入脚本都必须匹配对应交易的输出脚本进行验证。 全部验证通过,交易合法。

如图(第8分45秒)是几种形式的输入输出脚本。 最简单的形式之一是 P2PK(支付给公钥)。 输出脚本中直接给出了收款人的公钥,下面的checksig行是校验签名的操作。 在输入脚本中,直接给出签名即可。 这个签名是输入脚本所在的整个交易的签名,带有私钥。 这种形式是最简单的,因为公钥直接在输出脚本中给出。

如图(9分18秒)是脚本的实际执行。 这三行是连接输入脚本和输出脚本的结果。 第一行来自输入脚本,最后两行来自输出脚本。 请注意,在实际代码中出于安全原因,这两个脚本实际上是分开执行的。 第一行:将输入脚本提供的签名压入栈中,第二行将输出中提供的公钥压入栈中,第三行checksig将栈顶的两个元素弹出。 使用公钥检查签名是否正确。 如果正确,则返回true,表示验证通过。 否则执行出错,交易不合法。

股票群老师带领炒比特币_马斯克叫停比特币买车 比特币跳水_比特币老师

如图(第10分24秒)是P2PK的例子。 上面交易的输入脚本是将签名入栈,后面的交易就是上面交易输入的币源。 它的输出有两行,第一行是将公钥入栈,第二行是checksig。 这是第一种形式。

如图(10分52秒)是P2PKH(pay to public key hash)的第二种形式。 与第一种形式不同的是输出脚本中没有直接给出收款人的公钥,而是给出了公钥。 哈希。 公钥在输入脚本中给出。 输入脚本必须同时提供签名和公钥。 输出脚本中还有一些其他的操作,比如DUP、HASH160等,这些操作都是为了验证签名的正确性。 P2PKH 是最常用的形式。

如图(11分37秒)是脚本的执行结果。 这是通过拼接上一页的输入脚本和输出脚本得到的。 前两条语句来自输入脚本,后面的语句来自输出脚本,或者说从上到下Execute。 第一条语句先将签名压入堆栈,第二条语句将公钥压入堆栈。 第三条语句是复制栈顶的元素,所以栈顶又多了一个公钥。 HASH160是弹出栈顶元素,取哈希,然后将得到的哈希值压入栈中。 所以栈顶就变成了公钥的散列。

第五行是将输出脚本中提供的公钥的哈希值压入堆栈。 此时栈顶有两个哈希值。 上面的哈希值在输出脚本中提供。 提供收款人公钥的哈希值。 下面的hash是指你要花钱的时候输入脚本给的公钥,然后前面的操作HASH160取hash后得到。 倒数第二行操作的作用是弹出栈顶的两个元素,比较它们是否相等,也就是比较它们的hash值是否相等。 这样做的目的是防止有人被莫名其妙地替换,用自己的公钥冒充收款人的公钥。 假设两个哈希值相等,则从栈顶消失。 最后一个功能是使用公钥校验从栈顶弹出的元素是否正确。 假设签名正确,整个脚本运行顺利,栈顶为真。 如果在执行过程中的任何一个环节出现错误,例如输入中给出的公钥与输出中给出的哈希值不匹配,或者输入中给出的签名与给出的公钥不匹配,那么本次交易是非法的。

P2PKH是最常用的脚本信息,本例(第14分20秒)就是使用了这个脚本。 输入脚本是将签名入栈,将公钥入栈。 以下输出脚本复制堆栈的顶部元素,然后获取哈希值 hash160。 然后将公钥的哈希值压入栈中,最后比较栈顶的两个哈希值来校验签名。

最后一个,如图(15分25秒),也是最复杂的脚本形式,就是Pay to Script Hash。 这种形式的输出脚本给出的不是收款人公钥的散列,而是收款人提供的脚本的散列。 这个脚本叫做redeemscript,赎回脚本。 以后花这个钱的时候,redeemscript(兑换脚本的具体内容)必须在input script中给出,还必须给出兑换脚本正确运行所需的签名。

验证分为两部分(如图中第15分钟和第40秒所示)。 第一步是验证输入脚本中给出的赎回脚本是否与输出脚本中给出的哈希值匹配。 如果不匹配,说明给定的赎回脚本是错误的,就像刚才说的pay to public key hash中给出的public key是错误的。 如果匹配不上,说明给的兑换脚本错误,验证失败。 如果输入中给出的赎回脚本是正确的,那么第二步就是将赎回脚本的内容作为操作指令来执行,看最后能否顺利执行。 如果通过两步验证,则交易是合法的。 听起来有点抽象,我们来看一个具体的例子。

(如第16分47秒所示)使用pay to script hash实现pay to public key的功能。 这里的输入脚本是给签名,然后给序列化赎回脚本。 赎回脚本的内容是给公钥,然后用checksig校验签名。 以下输出脚本用于验证输入脚本中给出的兑换脚本是否正确。

股票群老师带领炒比特币_比特币老师_马斯克叫停比特币买车 比特币跳水

看一下pay to script hash的执行过程如图(17分13秒)。 一开始是输入脚本和输出脚本拼接在一起的。 前两行来自输入脚本比特币老师,后三行来自输出脚本。 先将输入脚本的签名压栈,再将赎回脚本压栈,再进行哈希运算得到赎回脚本的哈希值。 这里的RSH是指redeem script hash,redeem脚本的哈希值。 接下来将输出脚本中给出的哈希值压入栈中,然后栈中有两个哈希值。 最后用equal比较两个hash值是否相等 ,不相等则失败。 假设相等,这两个哈希值从栈顶消失,到这里第一阶段验证结束,接下来进行第二阶段验证。

如图(第18分28秒)第二阶段首先反序列化输入脚本提供的序列化赎回脚本。 这个反序列化操作在PPT上是没有展示的。 节点本身必须完成。 然后执行赎回脚本,首先将公钥压入栈中,然后使用checksig验证输入脚本中给出的签名的正确性。 验证结束后,整个pay to script hash被认为执行完毕。

有人会问:pay to public key就行了,搞这么复杂干嘛? 为什么一定要把这些功能嵌入到兑换脚本中呢? 对于这个简单的例子来说确实复杂,但是支付给脚本 hash 其常见的应用场景是支持多重签名。

比特币系统中的输出可能需要多个签名才能取款。 比如一个公司的账户,可能需要五位合伙人中的任意三位签字才能从公司的账户中提款。 这是私钥。 泄漏提供了一些安全保护。

比如一个合伙人的私钥泄露了,那问题不大,因为提款需要两个人的签名。 这也为私钥的丢失提供了一些冗余。 即使两个人忘记了私钥,剩下的三个人仍然可以把钱取出来,转到一个安全的账户里。

以上功能都是通过check multisig实现的。

如图(第21点),输出脚本中给出了N个公钥,同时指定了一个预置值M。 输入脚本只要提供N个公钥对应的签名中任意M个合法签名即可通过验证。

比如刚才举的例子,N=5,M=3,5个合伙人中任意3个都可以签名,在输入脚本的第一行有一个红色的“×”。 这是什么意思?

比特币老师_马斯克叫停比特币买车 比特币跳水_股票群老师带领炒比特币

在比特币中检查多重签名的实现有一个错误。 当它被执行时,一个额外的元素将从堆栈中弹出。 这是其代码实现中的错误。 现在没有办法修复这个bug,因为这是一个去中心化的系统,通过软件升级来修复这个bug成本会很高,需要硬分叉来修复。 所以实际的解决办法是在输入脚本中多压一个无用的元素入栈,第一行的“×”就是一个无用的额外元素。 另外,需要注意给定的M个签名的相对顺序,必须与它们在N个公钥中的相对顺序一致。

如图(22分48秒)是check multisig的执行流程。 此示例假定给出了三个签名中的两个。 从图中可以看出,这两个签名给出的相对顺序也和它们在公钥中的顺序是一样的。 在公钥中,第一个公钥在第二个公钥之前。 那么在给出这两个签名的时候,第一个签名也排在第二个前面。

第一行的false就是上面提到的冗余元素。 先将冗余元素压栈,然后将两个签名依次压栈,此时执行输入脚本。 在接下来的输出脚本中,将M的值,即前值M,压入栈中。 然后将三个公钥压入栈中,再将N的值压入栈中,最后执行check multisig,看栈中是否包含三个签名中的两个,如果包含则验证通过。

注意:在此过程中不使用支付给脚本的哈希值。 它是通过比特币脚本中的原生校验多重签名实现的。 这个实现有什么问题吗?

早期的多重签名就是这样实现的。 在实际应用中,有一些不方便的地方。

例如:网上购物。 电商多重签名需要5个合作伙伴中的任意3个签名才能提款,并要求网购用户在支付时产生的转账交易中给出5个合作伙伴的公钥。 还要给出 N 和 M 值。 本例中,N=5,M=3,这些是用户在网上购物时产生转账交易时输出脚本中要给出的信息,给出这5个公钥,给出N和M的值。

那么用户如何知道这些信息呢? 购物网站需要在线发布。 例如,我们可以在互联网上宣布我们使用了多重签名。 我们使用的五个签名中的三个必须给出。 这是五个公钥,然后用户在生成这个转账的时候,填写这个信息。 那么不同电商采用的多重签名规则是不一样的。 一些电子商务公司可能需要五个签名中的任意三个,而其他电子商务公司可能需要四个。 这给用户生成转账交易带来了一些不便,因为这些复杂性暴露给了用户。

那么如何解决呢? 这里使用支付给脚本哈希。

股票群老师带领炒比特币_马斯克叫停比特币买车 比特币跳水_比特币老师

如图(26分39秒)所示,是一个用pay to script hash实现的多重签名。 其本质是将复杂性从输出脚本转移到输入脚本。 现在这个输出脚本变得非常简单,就这三行。 原来的复杂性被移到了 redeemscript 兑换脚本中。 输出脚本只需要给出赎回脚本的哈希值即可。 赎回脚本需要给出N个公钥,以及N和M的值。这个赎回脚本是在输入脚本中提供的,也就是说是由收款人提供的。

和前面网购的例子一样,收款方是一家电商公司。 他只需要在网站上公布赎回脚本的哈希值,然后在用户产生转账交易时,将这个哈希值包含在输出脚本中即可。 至于这个电商的多重签名规则,对用户来说是不可见的,用户也不需要知道。 从用户的角度来看,使用这种支付方式与使用pay to public key hash没有太大区别,只是将公钥的hash值换成了兑换脚本的hash值。 当然,输出脚本的写法有些不同,但不是必须的。 这个输入脚本是电商在消费输出时提供的,里面包含了兑换脚本的序列化版本,也包含了兑换脚本验证通过所需的M个签名。 以后如果电商改变采用的多重签名规则,比如五选三改为三选二,那么只需要改变输入脚本和赎回脚本的内容,然后把新的哈希值发布即可。 对于用户来说,只是支付时要包含的哈希值发生了变化,其他的变化不需要知道。

如图(第29分14秒)是具体的执行流程。 这是输入脚本和输出脚本拼接后的情况。 第一行的FALSE是为了应对check multisig的bug而准备的无用元素。 执行时先入栈,然后是两个A签名入栈,后面是序列化的赎回脚本,目前只作为数据入栈,当脚本被执行时执行脚本进入这里。 下面是输出脚本,取哈希,然后将输出脚本中提供的哈希值压入栈顶。 最后判断两个哈希值是否相等,到这里就完成了第一阶段的验证。

如图(30分18秒),第二阶段验证开始,赎回脚本展开执行。 先将M入栈,再将三个公钥入栈,再将N入栈,最后检查多重签名的正确性,三个中有两个正确。 第二阶段的验证过程与直接使用 check multisig 的情况类似。

如图(30分52秒)是网上使用pay to script hash做多重签名的例子。 上面最后一个输入脚本是序列化的兑换脚本。 反序列化后,结果是一个采用三个中的两个的多重签名脚本。 下面输出脚本的内容和上面说的一样。 目前的多重签名一般采用pay to script hash的形式。

如图(31分25秒),脚本格式比较特殊。 这种格式的输出脚本以返回操作开始,后面可以跟任何内容。 return操作的作用是无条件返回错误,所以包含这个操作的脚本永远无法通过验证。 return语句执行的时候会出错,然后终止执行,后面的内容根本没有机会执行。

为什么要设计这样的输出脚本? 这样的输出岂不是永远都用不完? 不管输入脚本里写什么,执行return语句的时候都会报错,这里的钱永远花不完。 事实上,这个脚本是一种销毁比特币的方法。

为什么要烧掉比特币? 这一般有两种应用场景:

比特币老师_股票群老师带领炒比特币_马斯克叫停比特币买车 比特币跳水

① 部分小币种需要销毁一定数量的比特币才能获得该币种。 有时这种小货币被称为 AltCoin(另类硬币)。 比特币以外的其他小型加密货币可被视为替代币。 比如有些小币种要求销毁一个比特币可以得到1000个小币,也就是说必须通过上述的方法来证明已经付出了一定的代价才能得到这个小币。

② 将一些内容写入区块链。 区块链是一本不可篡改的账本。 有些人利用这个特性来添加一些需要永久保存的内容,比如第一课提到的数字承诺。 为了证明在某个时间,某些事情是已知的。 比如涉及知识产权保护,将某项知识产权的内容进行哈希处理后,将哈希值放在return语句后面。 它后面的内容无论如何都不会被执行,你在里面写什么也无所谓。 而且这里放的是一个哈希值,不会占用太多空间,不会泄露你知识产权的具体内容。 如果以后发生纠纷,比如一些知识产权的专利诉讼比特币老师,哈希值的具体输入内容会被公布出来,以证明你在某个时间点已经知道某个知识。

这个应用场景类似于coinbase领域。 coinbase交易中有个coinbase字段,没人关心这个字段写什么,那为什么不用这里的coinbase方法呢? Coinbase 可以直接写入它而不会破坏比特币。

coinbase方法只能由获得记账权的节点使用。 如果是全节点,可以在挖完出块后,往coinbase交易中的coinbase字段写入一些内容。

而我们上面提到的方法是所有节点都可以使用的,甚至不是一个节点,它可能是比特币上的普通用户,任何人都可以使用这种方法来写一些内容。 发布交易不需要记账权,但是发布区块需要记账权。 任何用户都可以通过这种方式销毁少量的比特币,比如0.0000001个比特币,以换取将一些内容写入区块链的机会。 事实上,有些交易根本不会销毁比特币,而只是支付交易费用。

让我们看两个例子

如图(37分44秒)是一次coinbase交易。 该交易有两个输出。 第一次输出的脚本是正常支付给公钥哈希,输出金额是获得的区块奖励加上交易手续费。 第二次输出金额为0,输出脚本就是刚才说的格式:开头是return,后面是一些乱七八糟的内容。 第二个输出的目的是将一些东西写入区块链。

如图(38分20秒)这是一个普通的转账交易,输出脚本也是以return开头。 本次交易输入0.05比特币,输出金额为0,表示输入金额全部用于支付交易手续费。 这笔交易实际上并没有销毁任何比特币,它只是将输入中的比特币作为交易费用转移给挖矿的矿工。

这种脚本形式的一个好处是矿工看到这个脚本就知道里面的输出永远不会兑现,所以不需要保存在UTXO中,对全节点比较友好。 还有一点要说明一下:为简单起见,本PPT中涉及比特币脚本的操作,不加OP前缀。 比如CHECKSIG其实应该写成OP_CHECKSIG,CHECKMULTISIG和DUP也是一样的。

比特币系统使用的脚本语言非常简单,甚至没有专门的名字。 它被称为比特币脚本语言(bitcoin scripting language)。 稍后您会看到,以太坊中使用的智能合约语言比这复杂得多。 比如比特币的脚本语言不支持循环,所以有很多功能是这种语言无法实现的。 这个设计有它自己的目的。 如果不支持循环,就会死循环,不用担心宕机。 以太坊中智能合约的语言表达能力很强,所以需要依赖gas费机制来防止程序陷入死循环。

另一方面,虽然这种语言在某些方面的功能有限,但在其他方面却非常强大,比如与密码学相关的功能。 比如checkmultisig,一条语句就可以完成多重签名校验,比很多通用编程语言方便多了。 因此,虽然比特币的脚本语言看起来很简单,但实际上针对比特币的应用场景做了很好的优化。