bigbangcore / BigBang

BigBang Core
https://www.bigbangcore.com/
MIT License
27 stars 7 forks source link

高级模板实现调研 #562

Closed AlexiaChen closed 4 years ago

AlexiaChen commented 4 years ago

需要一定的可编程化。先探探路。主要先查阅BTC,以太坊,substrate等智能合约或脚本机制的方案和资料,有一定理解以后再考虑与现有模板机制结合。

AlexiaChen commented 4 years ago

高级模板实现机制调研报告

前言

主要调研BTC的脚本机制实现,ETH的solidity智能合约方案,以及substrate的基于WebAssembly的合约方案,以这三个开源社区的资料调研。暂时不对具体实现代码做分析,也暂时不对编译器,解释器的相关工具技术进行调研,还不到代码实现的阶段。因为这个调研报告与检查点(Check Points)的调研报告不一样,检查点方案很成熟,复杂度不高,目的需求也很明确,直接参考BTC的检查点源码就行了。

BTC的脚本机制

这里的脚本是基于栈的计算语言(也就是stack-based virtual machine/language),语言并不图灵完备(没有循环语句),脚本从左到右解析并计算,BTC把一些基础行为抽象成了几百个脚本指令操作原语(OP CODE),这样是为了安全,设计上也简约,可控性更强。可编程的入口不能提供太多接口功能给用户。这样的方式思想很像大学的时候实现的基于栈的支持四则运算的计算器,但又比这个复杂得多。

下面通过一个例子来学习BTC的脚本,主要是为了直观感受下脚本机制,首先下载编译一个BTC脚本调试器来运行并调试BTC脚本语言,这样可以更好的熟悉BTC的脚本机制,然后我们本地实验下。

btcdeb ['OP_2 OP_1 OP_ADD']

运行后是以下显示:

btcdeb 0.2.19 -- type `btcdeb -h` for start up options
miniscript failed to parse script; miniscript support disabled
valid script
3 op script loaded. type `help` for usage information
script  |  stack
--------+--------
2       |
1       |
OP_ADD  |
#0000 2

看到以上结果,很显然,调试器加载了3个OP CODE,从左往右加载,分别是2,1,OP_ADD。第三个显然是加法指令了,第一,二个显然是数值常数指令。此时类似于x86汇编指令的eip指令寄存器指向2,说明一旦运行,第一次就是执行2这个OP CODE。

这些都是BTC预先定义好的指令,不能乱写,比如你写个OP_20, 这个指令就不存在,最后会执行错误。也就是,BTC的OP CODE根本没有定义20这个数值指令,限制很严格。

好的,接下来再用调试器的step命令来单步跟踪下这三个OP CODE的执行过程和栈上的数据变化。

btcdeb> step
                <> PUSH stack 02
script  |  stack
--------+--------
1       | 02
OP_ADD  |
#0001 1

好了,这次单步运行,执行了OP_2,把2这个数值,压入栈中,然后指令指针指向OP_1,说明下一步就执行OP_1了。

btcdeb> step
                <> PUSH stack 01
script  |  stack
--------+--------
OP_ADD  |      01
        | 02
#0002 OP_ADD

执行了OP_1,把1这个数值又压入栈中,然后指令指针指向OP_ADD,说明下一步该执行加法运算了。

btcdeb> step
                <> POP  stack
                <> POP  stack
                <> PUSH stack 03
script  |  stack
--------+--------
        | 03

OP_ADD做了三件事,首先把栈中的1, 2依次弹出来,做加法运算,结果为3,最后把数值3又压入栈中存储。

记住OP_ADD是个二元运算指令,不支持 btcdeb ['OP_3 OP_2 OP_1 OP_ADD'] 这样的脚本,但是这个脚本是正常的,只是结果不符合直觉,实际上最终的结果是:

btcdeb 0.2.19 -- type `btcdeb -h` for start up options
miniscript failed to parse script; miniscript support disabled
valid script
4 op script loaded. type `help` for usage information
script  |  stack
--------+--------
3       |
2       |
1       |
OP_ADD  |
#0000 3
btcdeb> step
                <> PUSH stack 03
script  |  stack
--------+--------
2       | 03
1       |
OP_ADD  |
#0001 2
btcdeb> step
                <> PUSH stack 02
script  |  stack
--------+--------
1       |      02
OP_ADD  | 03
#0002 1
btcdeb> step
                <> PUSH stack 01
script  |  stack
--------+--------
OP_ADD  |      01
        |      02
        | 03
#0003 OP_ADD
btcdeb> step
                <> POP  stack
                <> POP  stack
                <> PUSH stack 03
script  |  stack
--------+--------
        |      03
        | 03

根据上面的单步运行,其实OP_ADD只支持离栈顶最近的两个数值做运算。多余的数值它不管。所以结果是栈中最后有两个数值3。

BTC还有很多OP CODE,每个OP CODE都对应有自己的二进制编码,需要可以查阅: https://en.bitcoin.it/wiki/Script

最后提一下,这个btcdeb工具还附带了一个btcc的命令行工具,可以把可读的BTC脚本代码翻译成对应的二进制编码(十六进制字符串表示),这些二进制才是可执行的指令编码:

$ btcc OP_DUP OP_HASH160 897c81ac37ae36f7bc5b91356cfb0138bfacb3c1 OP_EQUALVERIFY OP_CHECKSIG
76a914897c81ac37ae36f7bc5b91356cfb0138bfacb3c188ac

以上的脚本逻辑就是一段锁定脚本(scriptPubkey),要与解锁脚本(scriptSig)结合起来才能运行成功。下面我们来结合这个脚本来简单复述下BTC transaction的转账过程。

以Alice给Bob转账为例:

比如Alice的地址上有一个UTXO,如果转账给Bob就需要引用这个UTXO,作为新创建交易的input,这个新交易只有验证了才有可能在BTC的P2P网络中被广播,验证的时候就需要解锁脚本(scriptSig),也就是解锁脚本和交易的input关联,锁定脚本和这个UTXO关联。只有提供正确的解锁脚本,才能正常花费这个UTXO,当然,解锁脚本肯定包含Alice的私钥的签名。换句话说,这个UTXO的钥匙就是解锁脚本。下面开始具体化分析这次正常转账场景的脚本机制:

锁定脚本(scriptPubKey): OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG 
解锁脚本(scriptSig): <signature> <pubkey>

注意,上面解释了,锁定脚本与UTXO关联,解锁脚本与新交易的input关联,所以没有相应的解锁脚本的辅助,光运行锁定脚本就是错误的。好了,解锁脚本到手了,可以组合成一个完整的验证Tx是否合法脚本程序了,最终脚本是这样:

<signature> <pubkey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
stack:
       <pubkey>      <- stack top pointer
       <signature>
stack:
       <pubkey>      <- stack top pointer
       <pubkey>      
       <signature>
stack:
       <pubKeyHashA>      <- stack top pointer
       <pubkey>      
       <signature>

从上面的ripemd160(sha256(\<pubkey>))来看,其实结果就是pubkey所对应的公钥地址,也就是BTC的地址了。

stack:
       <pubKeyHash>      <- stack top pointer
       <pubKeyHashA>      
       <pubkey>      
       <signature>
stack:
       <pubkey>         <- stack top pointer
       <signature>
stack:
       true or false        <- stack top pointer

把上面得脚本逻辑代入Alice转账Bob的过程,Alice转账给Bob,那么Bob地址上的UTXO关联的锁定脚本是这样的:

OP_DUP OP_HASH160 <Bob's pubkey Address> OP_EQUALVERIFY OP_CHECKSIG

如果Bob要花费自己地址上的UTXO,也就是给别人转账,那么它必须提供一个解锁脚本,用自己的私钥签名,并提供自己的公钥(pubkey),所以,解锁脚本是Bob提供的,长这样:

<Bob's signatre> <Bob's pubkey>

最终,组合成一个合法的脚本:

<Bob's signature> <Bob's pubkey> OP_DUP OP_HASH160 <Bob's pubkey Address> OP_EQUALVERIFY OP_CHECKSIG

最终查看UTXO是否是转到Bob自己的地址上,并通过这个Bob这个地址所对应的公钥来验证Bob的签名。看Bob是否有花费该地址上的UTXO的权限。这个脚本例子的官方叫法是:pay-to-pubkey-hash。

当然BTC还有很多脚本例子,可以实现一些高级功能。不一一讲解。

总结:对应到我们bigbang中,这段脚本就是实现了VerifyTransaction中destIn.VerifyTxSignature的功能。只是BTC是脚本化了,当然,可以看到,脚本机制与BTC的代码还是有一定耦合的,也不是一整套的隔离沙箱解释器的运行机制,是bigbang模板硬编码的可编程化的自然延申。bigbang要支持高级模板,又要提供一定的解耦的沙箱运行机制有一定挑战,不然UTXO和CTxIn等数据结构就会改变,这个想法还很初步的估计,最终细化可能还是得深入研究下BTC脚本的源码,并且小组讨论以后,才能下结论。不过了解过BTC脚本以后,我发现脚本是最贴合我们的bigbang的,因为bigbang的整体架构就是模仿的BTC的架构,从类,函数的命名都遵循一致的标准,既简单,又可控,防止用户随意编程。

以太坊的合约机制

用solidity语言编写合约,合约最终在ETH上的EVM虚拟机环境执行,猜测应该解耦的比较充分,但是编程自由度太高,为此ETH的智能合约在历史上发生过不少安全事件,可控性比较差,但是灵活很多。而且solidity语言是图灵完备的,也就是支持循环语句(BTC脚本不支持循环语句),常见的控制流语句都支持,也就是理论上它什么都可以写。因为太自由了,为了防止一些无限循环,一些故意耗费EVM计算资源的现象,ETH又在智能合约上加了gas燃油这样的概念,每运行一个solidity语句就会消耗燃油,消耗完毕就停机。让作恶者的作恶成本升高,入不敷出。

也许是因为智能合约太自由,可编程性太高,所以又催生出一个审核(审计)合约代码的一个行业来让专业团队评估一份合约的安全性或正确性, 或者让第三方的合约代码静态分析软件(比如,slither)来自动化分析合约代码的安全。

围绕着solidity语言这套体系生态过于复杂,入门也没BTC的脚本简单,实现上已经实现了一个完整的虚拟机,一套完整的编译,审核流程。

所以,如果要建立完备,开放最大限度的自由的智能合约机制,就需要周边建立完善的生态,不然实际意义不大,用户门槛过高。连BTC那么简单的脚本,都需要有类似第三方btcdeb这样的脚本调试器。

下面来大致看下ETH的合约工作原理,以及部署,调用机制。

编译后,生成了两个文件,bin文件二进制文件,包含EVM虚拟机指令,abi文件,一个描述合约接口的文件,用json描述,调用合约接口会用到abi文件, 这个abi文件就像C++的头文件一样。

合约的部署跟发送一笔交易是一样的操作,调用transaction函数,from为发布者的地址,to为0,data为合约的evm操作码。在矿工打包的时候会生成智能合约地址。智能合约地址的生成是由创建者的账号和发送的交易数作为随机数输入,通过Kecca-256加密算法重新创建一个地址作为账号。也就是说最后合约地址对应合约的代码会保存在区块链数据库。调用者只需要有合约地址和abi文件就可以调用合约的代码。

要调用合约需要合约的地址和合约的方法,智能合约是部署在区块链的代码,区块链本身不能执行代码,代码的执行是在本地的EVM中,实际上,部署在区块链上代码是能够在本地产生原智能合约代码的代码,可以理解区块链为一个数据库,而客户端从数据库中读取了存储的运行代码,并在本地运行后,将结果写入到了区块链这个数据库中,写回到区块链了,所以P2P网络只要同步到保存该结果的块,那么就可以看见,智能合约的最新结果状态了。

> abi=[{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"string","name":"_newgreeting","type":"string"}],"name":"setGreeting","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"greet","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]

> MyContract = eth.contract(abi)   #这时候就体现了abi的重要性,相当于接口的描述文档
> contractAddress = "0xbc7384998a5453a41bc51c1aa0f252034b57b986" #这个地址就是合约地址
> myContract = MyContract.at(contractAddress)
> myContract.greet.call()
"hello world"

总结: 从用户角度看,ETH的智能合约使用就复杂,更别说链上核心实现,以及周边生态工具。个人调研结果,几乎彻底放弃对ETH智能合约的参考。

EOS,Substrate等基于WebAssembly的合约机制

现在主流的合约实现已经开始与以太坊不一样了,以太坊EVM有自己的虚拟机字节码指令,都是自己设计。现在主流智能合约编译出来的中间指令字节码都是WebAssembly这样的标准化字节码了,浏览器可直接运行,复用标准,比如知名的EOS也是基于WASM,以太坊一位核心灵魂开发者Gavin Wood(波卡创始人)自己开创了一个Rust语言写的区块链框架Substrate的合约机制都基于WebAssembly来做了。包括一些区块链创业小公司的合约机制也是基于WASM,甚至直接用Substrate框架来开发自己的公链, 据说该框架是模块化的,很多模块替换很方便,十分钟定制一个自己的极简公链不是问题,开箱即用。

想了解Substrate请看这里 https://zhuanlan.zhihu.com/p/47805322

EOS合约

下面开始讲解下EOS的合约,EOS的合约代码是C++写的,其项目本身也是C++,作为合约开发者来说,C++的门槛还是比ETH的solidity高一些,把C++的合约代码编译成WASM,通过EOS-VM这个专为EOS定制化的WASM解释器来运行合约代码,EOS-VM还附带单步调试C++合约代码的功能。

解释合约WASM的说了,那么开发合约的工具呢?EOS提供了一个EOSIO.CDT的工具包和环境来开发GNU & C++11风格的合约代码。下面说下教程步骤。

wget https://github.com/EOSIO/eos/releases/download/v2.0.0/eosio_2.0.0-1-ubuntu-18.04_amd64.deb
sudo apt install ./eosio_2.0.0-1-ubuntu-18.04_amd64.deb
mkdir contracts && cd contracts && pwd
wget https://github.com/EOSIO/eosio.cdt/releases/download/v1.6.3/eosio.cdt_1.6.3-1-ubuntu-18.04_amd64.deb
sudo apt install ./eosio.cdt_1.6.3-1-ubuntu-18.04_amd64.deb

与BigBang类似,EOS也提供类似cli的命令工具去创建钱包,导入密钥对等功能。

# 会返回钱包密码,要记住
cleos wallet create --to-console
cleos wallet open # 打开钱包
cleos wallet list # 列出钱包
cleos wallet unlock # 解锁钱包
cleos wallet create_key # 类似于bigbang的getnewkey

每个新的EOS链都有一个默认的系统账户叫"eosio", 这个账户是通过加载链上治理以及共识的系统内建的合约用来初始化设置这条链,每个新的EOS链都有一个开发密钥,这个密钥大家都是相同的,加载此密钥就是为了代表eosio这个系统账户来签名交易。(千万不要用这个开发密钥来对生产环境上的账户做任何事,因为这个开发密钥是众所周知的,这样就等于生产账户大家都可以访问了。)

后台启动keosd,相当于我们的bigbang -wallet这个钱包,单独的钱包管理程序

keosd &

然后启动节点:

nodeos -e -p eosio \
--plugin eosio::producer_plugin \
--plugin eosio::producer_api_plugin \
--plugin eosio::chain_api_plugin \
--plugin eosio::http_plugin \
--plugin eosio::history_plugin \
--plugin eosio::history_api_plugin \
--filter-on="*" \
--access-control-allow-origin='*' \
--contracts-console \
--http-validate-host=false \
--verbose-http-errors >> nodeos.log 2>&1 &

以上的命令就是加载各个基本插件,打开CORS跨域功能,智能合约调试和输出日志。这样就启动起来了,可以查看日志 nodeos.log看启动状况

查看钱包,可以查看多个钱包:

cleos wallet list

通过http RESTful API查看节点状况,这个功能是chain_api_plugin 这个插件提供的。

curl http://localhost:8888/v1/chain/get_info

一个账号有属于自己的一堆权限。用来识别标记发送者和接收者。账号有灵活的权限系统,可以通过配置相关的授权被个人或组织拥有。账号就是发送和接受交易的地址了。

这里会用到两个用户账号alice和bob,还有个默认的eosio账号作为配置。这几个账号到后面的智能合约会用到。

之前创建过一个public key,在创建开发钱包那一节,把那个public key拿过来通过eosio账号创建两个账号:

cleos create account eosio bob YOUR_PUBLIC_KEY
cleos create account eosio alice YOUR_PUBLIC_KEY

创建的账号会形成两个交易,以交易的形式被广播,最终被P2P网络确认。

executed transaction: 40c605006de...  200 bytes  153 us
#         eosio <= eosio::newaccount            {"creator":"eosio","name":"alice","owner":{"threshold":1,"keys":[{"key":"EOS5rti4LTL53xptjgQBXv9HxyU...
warning: transaction executed locally, but may not be confirmed by the network yet    ]

公钥和账号的关系是什么,公钥和账号都是独立的,公钥只是一组权限的ID,把权限ID与公钥关联上,即使把公钥换了,也不会改变账号的权限。

alice账号的公钥获取:

cleos get account alice

会输出该账号的相关信息,可以看到,会以网络带宽,CPU带宽来限制某个账号

permissions:
     owner     1:    1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
        active     1:    1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
memory:
     quota:       unlimited  used:      3.758 KiB

net bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited

cpu bandwidth:
     used:               unlimited
     available:          unlimited
     limit:              unlimited

从以上信息看,alice其实同时拥有owner和active公钥,EOS有一个独特的授权结构来保证你账号的安全性,在使用active相关联的密钥时,可以将owner密钥保持为冷状态,从而最大限度地减少帐户的曝光。这样,如果您的active密钥被泄露,您就可以用您的owner密钥重新控制您的帐户。

在授权方面,如果您拥有owner权限,则可以更改active私钥的权限,反之不行。

在之前的CONTRACTS_DIR中新建一个hello的目录, 里面新建一个hello.cpp:

cd CONTRACTS_DIR
mkdir hello
cd hello
touch hello.cpp

在hello.cpp里面写下:

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract]] hello : public contract {
  public:
      using contract::contract;
      [[eosio::action]]
      void hi( name user ) {
         print( "Hello, ", user);
      }
};

hi函数就是该合约的一个Action,Action的意思就是,通过交易传递一个参数给EOS网络来执行一个智能合约所暴露的功能接口。

然后把合约C++代码编译成WebAssembly,准备部署合约:

eosio-cpp hello.cpp -o hello.wasm

当一个合约部署后,也就相当于它部署到一个账号上了,账号变成了该合约的接口,为合约创建一个账户hello_account,然后把它部署到链上

cleos create account eosio hello_account YOUR_PUBLIC_KEY -p eosio@active
cleos set contract hello_account CONTRACTS_DIR/hello -p hello@active

通过账户来调用合约action:

cleos push action hello hi '["bob"]' -p bob@active
executed transaction: 4c10c1426c16b1656e802f3302677594731b380b18a44851d38e8b5275072857  244 bytes  1000 cycles
#    hello.code <= hello.code::hi               {"user":"bob"}
>> Hello, bob

官方维护了一些合约代码,可以clone下来

git clone https://github.com/EOSIO/eosio.contracts --branch v1.7.0 --single-branch

下面我们围绕着一个非常重要的合约来分析理解

cd eosio.contracts/contracts/eosio.token

接下来就是创建账号,部署合约了

cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
eosio-cpp -I include -o eosio.token.wasm src/eosio.token.cpp --abigen
cleos set contract eosio.token CONTRACTS_DIR/eosio.contracts/contracts/eosio.token --abi eosio.token.abi -p eosio.token@active

完成后,控制台会看到类似的消息

Reading WASM from ...eosio.contracts/contracts/eosio.token/eosio.token.wasm...
Publishing contract...
executed transaction: a68299112725b9f2233d56e58b5392f3b37d2a4564bdf99172152c21c7dc323f  6984 bytes  6978 us
#         eosio <= eosio::setcode               {"account":"eosio.token","vmtype":0,"vmversion":0,"code":"0061736d0100000001a0011b60000060017e006002...
#         eosio <= eosio::setabi                {"account":"eosio.token","abi":"0e656f73696f3a3a6162692f312e310008076163636f756e7400010762616c616e63...
warning: transaction executed locally, but may not be confirmed by the network yet         ]

再接下来是创建token:

cleos push action eosio.token create '[ "alice", "1000000000.0000 SYS"]' -p eosio.token@active

用了eosio.token账号的create接口创建的token。该接口接收一个参数,该参数由两部分构成,一个是发行人(EOS的一个账号),该发起人将有权调用发行或执行其他操作,如关闭帐户或注销token。一个是一种资产类型,由两个数据组成,一个浮点数设置最大供应量,一个符号用大写字母来表示资产。例如,“1.0000 SYS”。

运行完成后控制台会看到类似消息:

executed transaction: 10cfe1f7e522ed743dec39d83285963333f19d15c5d7f0c120b7db652689a997  120 bytes  1864 us
#   eosio.token <= eosio.token::create          {"issuer":"alice","maximum_supply":"1000000000.0000 SYS"}
warning: transaction executed locally, but may not be confirmed by the network yet         ]

创建完Token后就是发行token了,也就是发币。

cleos push action eosio.token issue '[ "alice", "100.0000 SYS", "memo" ]' -p alice@active
executed transaction: d1466bb28eb63a9328d92ddddc660461a16c405dffc500ce4a75a10aa173347a  128 bytes  205 us
#   eosio.token <= eosio.token::issue           {"to":"alice","quantity":"100.0000 SYS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet         ]

这次输出包含几个操作:一个发行操作和三个转账操作。虽然签名的唯一操作是发行,但是发行操作执行了内部的转账,并且内联转账通知了发送方和接收方帐户。输出指示调用的所有操作处理程序、调用它们的顺序以及是否由该操作生成任何输出。

从技术上讲EOS代币合约本可以跳过内部转账,直接选择修改余额。然而,在这种情况下EOS代币合约遵循一种象征性的约定,即要求所有账户余额都可以通过引用它们的转账操作的总和来派生。它还要求通知资金的发送者和接收者,以便他们能够自动处理存款和取款。

转账Token,这个是最后一步了

cleos push action eosio.token transfer '[ "alice", "bob", "25.0000 SYS", "m" ]' -p alice@active
executed transaction: 800835f28659d405748f4ac0ec9e327335eae579a0d8e8ef6330e78c9ee1b67c  128 bytes  1073 us
#   eosio.token <= eosio.token::transfer        {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"}
#         alice <= eosio.token::transfer        {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"}
#           bob <= eosio.token::transfer        {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"}
warning: transaction executed locally, but may not be confirmed by the network yet         ]

Alice转给了Bob 25 SYS。

Bob可以查到这笔收款:

cleos get currency balance eosio.token bob SYS

Alice也可以查到自己的余额,总共发行了100,扣除25,还剩75:

cleos get currency balance eosio.token alice SYS

总结: EOS的合约跟账户,授权各种权限系统关联耦合,合约系统并不独立,虽然是C++实现,但是实现也非常复杂,在实现上做减法也是件很困难的事情。

Substrate合约

Substrate是Rust语言编写的模块化区块链开发框架。很多模块是可以替换的,并且有内建的可选模块,比如共识有GrandPa和Aura,合约有 FRAME Contracts pallet 和 FRAME EVM pallet 模块,可以添加到Runtime里获得模块提供的功能支持,后者的EVM模块是为了兼容以太坊合约字节码,前者是Substrate自己的合约模块。FRAME Contracts pallet的合约语言是ink!, 基于Rust语言的合约代码,其实也就是写Rust语言了,开发区块链和开发合约都相当于写Rust代码了。

总结: Substrate现在发展不错,很多小微创业型的区块链企业是用这个框架快速开发定制自己的链,但是周边生态还不是很繁荣,Rust语言受众就比较小,更别说其中的一个区块链开发框架了,也就是遇到问题,也不好定位和解决。团队适合小而精的Rust团队。我们的高级模板也不好参考这个框架。门槛,社区,语言都完全不合适。

基于Lua语言从零开始实现合约系统

这个要说起段BigBang的发展历史,在BBC还在酝酿阶段,LomoCoin发展的早期,其主要架构师就开了一个分支想基于Lua实现一份LMC的智能合约系统,后来据说是写了一半,就没有后续消息了,BigBang的自主化设计的架构思想主要是类似BTC,LomoCoin改自的PPCoin也来自BTC。所以两个的架构一些原理非常相似,原架构师的对智能合约是有一定前瞻性的思考的,从这里出发来看,大体是基于BTC架构的前提下,又要提供比BTC脚本机制更加灵活完备的合约机制,可能基于Lua是不错的选择,开发上的减法不用做太多,比改以太坊,EOS的代码复杂度已经大量减少,很可能大部分是在Lua解释器上做修改,对接BBC链本身的API,然后再考虑与现有模板机制结合。

另外,调研中,也发现已经有一个叫Aergo的项目用Lua语言+SQL语言来写合约了,口号是:It's not a blockchain. It's the Blockchain for Business.

感觉是为了对接企业级业务而存在的合约平台,当然,它底层也是一个公链,只是是高TPS的企业级公链,采用BFT+DPoS混合共识,可以多查看下它们的应用场景以及客户类型。这里是它们的主网区块链浏览器,出块速度非常快。在韩国,其风头很大,跟三星等有战略合作,以下是Aergo的相关新闻:

  1. 商业白皮书:https://paper.aergo.io/AERGO_Whitepaper_v5.2.pdf
  2. 技术白皮书: https://www.aergo.io/downloads/AERGO_Chain_Technical_Whitepaper_V1.0.pdf

这些新闻标题,以及官网介绍,大体上是符合现阶段高级模板与企业对接落地的原始需求的。

下面来关注其技术部分,技术与业务是怎么结合的,以及一些场景

How does it work?

Aergo uses a hybrid architecture so you can balance the need to maximize performance and control of your most important data while leveraging a highly secure and trusted distributed public blockchain network.

It also offers an easy-to-deploy architecture that can run on a serverless cloud, so you can focus on building your business applications, services, and ecosystems. No need to build complex IT infrastructure any longer.

Easy-to-use development and deployment frameworks allow developers of all levels to program and build solutions with minimal effort and complexity.

Aergo的五大落地方案:

Aergo技术概览:

  1. 混合区块链

结合了公链和私链的特点,企业业务服务可以托管在私有的链上,然后私链可以通过主网的主链来进行安全的结算。这样的机制与BBC的树状无限可分叉的链很像,子链一般对应一个企业的业务,跨链的交易结算需要通过安全主链来保证。虽然实现细节不一样,但是主要的思想趋同。

Aergo链也是高性能,高可扩展的,BBC的理念也是如此。Aergo的主网主链的共识是BFT+DPoS共识,23个DPoS节点,安全主链作为独立私链和应用程序开发的中心枢纽,它提供了在自己的侧链上集成不同应用程序和服务的功能,使开发人员能够利用现实世界中复杂的网络拓扑来实现复杂的业务模型。私链也可以有自己的共识机制。

无论从哪个角度看,Aergo的诞生设计之初与BBC的诞生设计理念都高度吻合,无非就是实现细节完全不同。

  1. 智能合约

Aergo Lua,基于Lua的合约开发环境,并支持SQL。可以编写业务逻辑,构建Dapp。当然Aergo一门ASCL的全新的合约语言,完全类似PLSQL的语法,从这里可以看出,Lua + SQL的模式只是发展初期的一种简化方案。BBC高级模板初期也应该这样,尽量基于Lua等简单的语言构建合约环境。

  1. 平台生态

主要就是Aergo核心技术与云计算,云存储相关结合的上层生态服务,类似于BaaS(Blockchain as a service)整个生态。其中用到了弹性伸缩,k8s,容器,负载均衡等等。从这里可以看出,BBC的上层产品的路线,也应该类似这样。

总结:从三大方向的技术概览来看,BBC是可以参考Aergo的,特别是合约相关,因为这里着重合约调研,但是合约又应该本身结合BBC的特性,来发展。也就是对比ETH,BTC,EOS,Substrate下来,从大方向上看,BBC与Aergo是非常契合的,但是Aergo主体是Go语言写的,可能各种细节的复杂度,要移植到C++开发的BBC上,工程量还是复杂的。

结论(这里待团队讨论分析)

所以合约系统经过个人分析,高级模板的实现优先偏好考虑如下顺序:

  1. 基于Lua从零自研 = 复用Aergo的一些合约设施(这个不清楚能不能,核心代码是Go语言,但貌似不是合约部分,代码细节没看,也没尝试使用,没法预估工作量)
  2. 修改EOS合约代码(工作量无法估计)
  3. BTC脚本机制(看当前的需求反馈,这样的脚本暂时不满足需求,可以不可以在其基础上完善,这又是另一个问题)
  4. 以太坊合约代码 或者Substrate合约机制(全是Go或Rust,与BBC完全不契合,几乎不考虑)

参考资料:

AlexiaChen commented 4 years ago

经过小组简要讨论,高级模板还是需要与 @woondroo 来核对需求,互相交流,开发才方便选型深入。这周先暂时把Aergo的lua合约细化一下。

AlexiaChen commented 4 years ago

小组讨论结果,暂时不考虑EOS,ETH等完备的语言及虚拟机的智能合约机制,所以考虑BTC的脚本机制,并需要代入初级的需求场景去验证满足不满足。

AlexiaChen commented 4 years ago

比特币合约系统

介绍

这个合约非智能的,也就是英文只说了contracts,而不是以太坊,EOS那样的智能合约(smart contract)。

比特币合约是使用分布式的比特币系统执行金融协议的交易。比特币合约通常可以被精心设计,以尽量减少对外部代理的依赖,例如法院系统,这大大降低了在金融交易中与未知实体打交道的风险。

以下各小节将描述已在使用的各种比特币合约。因为合约涉及的是真实的人,而不仅仅是交易,所以它们是以故事的形式被框起来的。也就是业务场景例子了。

除下文所述的合约类型外,还提出了许多其他合同类型。其中一些是在比特币维基的合同页面上收集的。

以下是wiki上的BTC合约描述:

分布式的合约是使用比特币通过区块链与人们达成协议的一种方法。合约不会使以前不可能的事情成为可能,相反,它们允许你以最小化信任的方式解决常见问题。最小信任通常使事情变得更方便,因为它允许人的判断脱离循环,从而实现完全自动化。

理论

BTC合约的底层依赖的理论依据。

比特币中的每一笔交易都有一个或多个input和output。每个input/output都有一个小的纯函数(不会改变状态的函数)与之关联,称为脚本。脚本可以包含交易本身简化形式的签名。

每个交易都可以有一个与之关联的锁定时间。这使得交易可以挂起并可替换,直到约定的未来时间为止,指定为块索引或时间戳(这两个字段都使用相同的字段,但小于5亿的值被解释为块索引)。如果一个交易的锁定时间已经到达,我们称它为final。

每个交易的input都有一个序列号。在的普通转账的交易中,序列号都是UINT_MAX,锁定时间为零。如果尚未达到锁定时间,但所有序列号都是UINT_MAX,则该交易也被视为final。序列号可用于发布交易的新版本,而不会使其他input签名失效,例如,在交易的的每个input来自不同的发送方的情况下,每个input可以从序列号0开始,并且这些数字可以独立地递增。

SIGHASH标记

这个标记很重要,是BTC的合约实现的底层依赖机制。

签名的校验是灵活的,因为签名的交易形式可以通过使用SIGHASH标志来控制,SIGHASH标志被粘贴在签名的末尾。通过这种方式,可以构建合约,其中每一方只签署其中的一部分,允许在没有他们参与的情况下更改其他部分。SIGHASH标志有两个部分,一个mode和ANYONECANPAY修饰符:

SIGHASH_ANYONECANPAY修饰符可以与上述三种模式组合使用。设置后,只有该input有符号,其他inut可以是任何内容。

脚本可以包含CHECKMULTISIG操作码。此操作码提供n-of-m检查:提供多个公钥,并指定必须存在的有效签名的数量。签名的数目可以小于公钥的数目。通过将output设置为以下值,可以需要使用两个签名:

2 <pubkey1> <pubkey2> 2 CHECKMULTISIGVERIFY

安全创建合约有两种通用模式:

这样做的目的是确保人们总是知道他们同意什么,这些特性使我们能够在区块链上构建有趣的新的金融业务工具。

托管和仲裁的场景例子

客户Charlie想从商人Bob那里购买产品,但是他们都不信任对方,所以他们用合约来帮助确保Charlie得到商品,Bob得到付款。

一个简单的合约可以说,Charlie为了付款需要把token花在一个unspent的output上,但只有当Charlie和Bob都签署了支出token的input时,这个output才能被使用(双方多签)。这意味着除非Charlie拿到他的商品,否则Bob不会得到报酬,但Charlie不可以拿到商品的同时并保留他的付款。

如果双方有争议,这个简单的合约没有多大用处,所以Bob和Charlie请求仲裁员Alice帮助创建一个托管合约。Charlie把他的token花在一个output上,只有三个人中的两个人共同签署了input才能使用这个output。现在,如果一切正常,Charlie可以给Bob付钱,如果有问题,Bob可以退还Charlie的钱,或者Alice可以仲裁决定,如果有争议,谁应该得到token。

为了创建多重签名(multisig)的output,它们各自给其他人一个公钥。然后Bob创建以下P2SH multisig赎回脚本:

OP_2 [Alice's pubkey] [Bob's pubkey] [Charlie's pubkey] OP_3 OP_CHECKMULTISIG

OP_2和OP_3被读取到就会将实际的数字2和3压入到栈上。OP_2指定签名需要2个签名;OP_3指定提供了3个公钥(未隐藏)。这是一个2/3的multisig pubkey脚本,更一般地称为m-of-n pubkey脚本(其中m是所需的最小匹配签名,n是提供的公钥数量中的n)。一般多签都是这样的模式。

Bob将这个多签的赎回脚本交给Charlie,Charlie检查以确保他的公钥和Alice的公钥都包含在内。然后,他对赎回脚本进行Hash,以创建P2SH赎回脚本,拿到脚本的ID,相当于BBC的模板地址,并向脚本ID地址转账付款,形成一个交易。Bob看到付款的交易被添加打包到区块链中,就可以发货了。

不幸的是,货物在运输途中受到轻微损坏。Charlie想要全额退款,但Bob认为10%的退款就足够了。他们求助于Alice来仲裁解决这个问题。Alice向Charlie索要货物损坏的照片证据,以及Charlie检查过了的Bob创建的赎回脚本的副本。

在看过证据后,Alice认为40%的退款就足够了,所以她创建并签名了一个新的有两个output的交易(但是BBC一个交易没有多个output,UTXO的模型与BTC有一定的差别),一个output将总token的60%用于Bob的公钥,另一个将剩余的40%用于Charlie的公钥。也就是60%的output只有Bob可以花费,40%的output只有Charlie可以花费,也就是Charlie拿到了40%的退款。

在签名脚本中,Alice将她的签名和Bob创建的未Hash的赎回脚本源码的副本放在一起。Alice把未完成交易的副本给了Bob和Charlie。Bob和Charlie买卖双方中的任何一个都可以通过添加他自己的签名来创建以下签名脚本来完成此操作:

OP_0 [A's signature] [B's or C's signature] [serialized redeem script]

其实也就是两个脚本合并了,变成了以下一个脚本来完成验证签名:

OP_0 [Alice's signature] [Bob's or Charlie's signature] OP_2 [Alice's pubkey] [Bob's pubkey] [Charlie's pubkey] OP_3 OP_CHECKMULTISIG

这段脚本才真正完成了多重签名的验证。

当交易被广播到P2P网络时,买卖双方根据Charlie先前支付的P2SH的交易的output以检查签名脚本,确保赎回脚本与先前提供的赎回脚本哈希匹配。然后计算赎回脚本,将这两个签名用作输入数据。假设赎回脚本有效,这交易的两个output在Bob和Charlie的钱包中显示为有可花费的余额。

但是,如果Alice创建并签署了一个他们都不同意的交易,比如把所有的token都转到自己身上,Bob和Charlie可以找到一个新的仲裁员,并将token花费到另一个2-of-3 multisig赎回脚本ID地址中,这一个赎回脚本包含来自第二个仲裁员的公钥。这意味着鲍勃和查理不必担心他们的仲裁员会偷他们的钱。

总结: 这个场景BBC也照搬不了BTC,Tx的结构和UTXO的结构要改动,增加解锁与锁定脚本的字段。因为BTC的脚本就是结合交易的UTXO来做的。这里也没见到if和else等语句判断,高级业务实现太复杂,下面就会说到这些脚本是怎样与BTC的交易字段结合的,与BBC完全不同

BTC的脚本与交易

普通交易

通过BTC的命令行,getrawtransaction和decodeawtransaction命令就可以查到交易的细节,与BBC完全不同:

{
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid":"7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
      "vout": 0,
      "scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
      "sequence": 4294967295
    }
 ],
  "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",
    }
  ]
}

可以看到,之前的调研报告就说明过,锁定脚本(scriptPubKey)与解锁脚本(scriptSig)分别于output和input关联。但是,大体的UTXO模型于BBC是一样的。但是这个输出,一个交易却有多个输出,这个是BBC没有的,并且还多了脚本字段,是为了验证这个UTXO是否可以被输出所指定公钥地址花费。这段脚本就是实现了VerifyTransaction中destIn.VerifyTxSignature的功能。只是BTC是脚本化了。

所以BTC的脚本是围绕着UTXO和input来展开的, 当一笔比特币交易被验证时,每一个输入值中的解锁脚本与其对应的锁定脚本同时 (互不干扰地)执行,以确定这笔交易是否满足支付条件。

脚本是一种非常简单的语言,被设计为在执行范围上有限制,可在一些硬件上执行,可能与嵌入式装置一样简单。 它仅需要做最少的处理,许多现代编程语言可以做的花哨的事情它都不能做。 但用于验证可编程货币,这是一个经深思熟虑的安全特性。

上面的例子就是一种叫P2PKH(Pay-toPublic-Key-Hash)的脚本,当然还有其他的类型,这种语言允许表达几乎无限的各种条件。这也是比特币作为一种“可编程的货币”所拥有的力量。

比特币脚本语言包含许多操作码,但都故意限定为一种重要的模式——除了有条件的流控制以外,没有循环或复杂流控制能力。这样就保证了脚本语言的图灵非完备性,这意味着脚本有限的复杂性和可预见的执行次数。脚本并不是一种通用语言,这些限制确保该语言不被用于创造无限循环或其它类型的逻辑炸弹,这样的炸弹可以植入在一笔交易中,引起针对比特币网络的“拒绝服务”攻击。记住,每一笔交易都会被网络中的全节点验证,受限制的语言能防止交易验证机制被作为一个漏洞而加以利用。

BTC的交易验证引擎就依赖锁定脚本和解锁脚本,两个组合运行的结果决定input合不合法,可以不可以花费这笔UTXO。

一个简单例子,也可以组合出一个简单的花费条件:

加锁脚本(scriptPubKey): 3 OP_ADD 5 OP_EQUAL 解锁脚本(scriptSig): 2

组合起来: 2 3 OP_ADD 5 OP_EQUAL 那么结果就是OP_TRUE,条件为真,就可以花费这笔UTXO,可以通过脚本组合很多种逻辑。

解锁和锁定脚本其实就是构造一个条件触发器,条件为真,才可以触发input可以花费UTXO,验证Tx才能通过。

总结: 从P2PKH脚本中暂时还看不到可以满足BBC的分红需求的场景,非常简单

高级交易

下面需要介绍,高级的交易是需要什么样的机制来配合脚本实现简单的功能的。

高级的脚本需要依赖一个叫P2SH的脚本类型(Pay-to-Script-Hash),之前说过,翻译过来就是,向脚本的Hash ID转账,它旨在使复杂脚本的运用能与直接向比特币普通地址支付一样简单。在P2SH 支付中,复杂的锁定脚本被Hash ID所取代。所以它类似BBC中的模板ID地址。然后这个脚本可以自己编写,不是硬编码,所以可以实现高级的功能对比P2PKH类型的脚本。从这里初步可以看出,这个脚本类型很类似BBC将要开发的高级模板机制。

当一笔交易试图花费UTXO时,要解锁支付脚本,解锁它必须含有与哈希相匹配的脚本。P2SH的含义是,向与该哈希匹配的脚本支付,当output被花费时,该解锁脚本将在后续的Tx的input中呈现。

在P2SH交易中,锁定脚本由哈希运算后的20字节的散列值取代,被称为赎回脚本。因为它在系统中是在赎回时出现而不是以锁定脚本模式出现。

Redeem Script: 2 <Pubkey1> <Pubkey2> <Pubkey3> <Pubkey4> <Pubkey5> 5 CHECKMULTISIG
Lock Script: OP_HASH160 <20 bytes hash ID of redeem script> OP_EQUAL
UnLock Script: <Sig1> <Sig2> <redeem script>

在本小节中的BTC几乎所有高级脚本交易都可以用P2SH类型的脚本实现。

P2SH的另一重要特征是它能将脚本哈希编译为一个地址(其定义请见BIP0013 /BIP-13)。P2SH地址是基于Base58编码的一 个含有20个字节哈希的脚本,就像比特币地址是基于Base58编码的一个含有20个字节的公钥。由于P2SH地址采用5作为前缀,这导致基于Base58编码的地址以“3”开头。例如,Mohammed的脚本,基于Base58编码下的P2SH地址变 为“39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw”。

此时,Mohammed可以将该地址发送给他的客户,这些客户可以 采用任何的比特币钱包实现简单支付,就像这是一个比特币地址一样。以“3”为前缀给予客户这是一种特殊类型的地址的暗示,该地址与一个脚本相对应而非与一个公钥相对应,但是它的效果与比特币地址支付别无二致。 P2SH地址隐藏了所有的复杂性,因此,运用其进行支付的人将不会看到脚本。

然后它由自己的优点:

时间锁

时间锁是只允许在一段时间后才允许支出的交易。比特币从一开始就有一个交易级的时间锁定功能。它由交易中的nLocktime字段实现。在2015年底和2016年中期推出了两个新的时间锁定功能,提供UTXO级别的时间锁定功能。这些是CHECKLOCKTIMEVERIFY和CHECKSEQUENCEVERIFY。

时间锁对于后期交易和将资金锁定到将来的日期很有用。更重要的是,时间锁将比特币脚本扩展到时间的维度,为复杂的多级智能合同打开了大门。

检查锁定时间验证Check Lock Time Verify (CLTV)

2015年12月,引入了一种新形式的时间锁进行比特币软分叉升级。根据BIP-65中的规范,脚本语言添加了一个名为CHECKLOCKTIMEVERIFY(CLTV)的新脚本操作符。 CLTV是每个输出的时间锁定,而不是每个交易的时间锁定,与nLocktime的情况一样。这允许在应用时间锁的方式上具有更大的灵活性。 简单来说,通过在输出的赎回脚本中添加CLTV操作码来限制输出,从而只能在指定的时间过后使用。

注释 当nLocktime是交易级时间锁定时,CLTV是基于输出的时间锁。

CLTV不会取代nLocktime,而是限制特定的UTXO,并通过将nLocktim设置为更大或相等的值,从而达到在未来才能花费这笔钱的目的。

CLTV操作码采用一个参数作为输入,表示为与nLocktime(块高度或Unix纪元时间)相同格式的数字 。如VERIFY后缀所示,CLTV如果结果为FALSE,则停止执行脚本的操作码类型。如果结果为TRUE,则继续执行。

为了使用CLTV锁定输出,将其插入到创建输出的交易中的输出的赎回脚本中。例如,如果Alice支付Bob的地址,输出通常会包含一个这样的P2PKH脚本:

DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

要锁定一段时间,比如说3个月以后,交易将是一个P2SH交易,其中包含一个赎回脚本:

<now + 3 months> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

其中是从交易开始被挖矿时间起计3个月的块高度或时间值:当前块高度+12,960(块)或当前Unix纪元时间+7,760,000(秒)。现在,不要担心CHECKLOCKTIMEVERIFY之后的DROP操作码,下面很快就会解释。

当Bob尝试花费这个UTXO时,他构建了一个引用UTXO作为输入的交易。他使用他的签名和公钥在该输入的解锁脚本,并将交易nLocktime设置为等于或更大于Alice设置的CHECKLOCKTIMEVERIFY 时间锁。然后,Bob在比特币网络上广播交易。

Bob的交易评估如下。如果Alice设置的CHECKLOCKTIMEVERIFY参数小于或等于支出交易的nLocktime,脚本执行将继续(就好像执行“无操作”或NOP操作码一样)。否则,脚本执行停止,并且该交易被视为无效。 更确切地说,CHECKLOCKTIMEVERIFY失败并停止执行,标记交易无效

具有控制流的脚本(条件子句 (Conditional Clauses))

比特币脚本的一个更强大的功能是流量控制,也称为条件条款。您可能熟悉使用构造IF ... THEN ... ELSE的各种编程语言中的流控制。比特币条件条款看起来有点不同,但是基本上是相同的结构。

在基本层面上,比特币条件操作码允许我们构建一个具有两种解锁方式的赎回脚本,这取决于评估逻辑条件的TRUE / FALSE结果。例如,如果x为TRUE,则赎回脚本为A,ELSE赎回脚本为B. 此外,比特币条件表达式可以无限期地“嵌套”,这意味着这个条件语句可以包含其中的另外一个条件,另外一个条件其中包含别的条件等等 。Bitcoin脚本流控制可用于构造非常复杂的脚本,具有数百甚至数千个可能的执行路径。嵌套没有限制,但协商一致的规则对脚本的最大大小(以字节为单位)施加限制。

比特币使用IF,ELSE,ENDIF和NOTIF操作码实现流程控制。此外,条件表达式可以包含布尔运算符,如BOOLAND,BOOLOR和NOT。

乍看之下,您可能会发现比特币的流量控制脚本令人困惑。那是因为比特币脚本是一种堆栈语言。同样的方式,当1+1看起来“向后”当表示为1 1 ADD时,比特币中的流控制条款也看起来“向后”(backward)。 在大多数传统(程序)编程语言中,流控制如下所示: 大多数编程语言中的流控制伪代码

if (condition):
  code to run when condition is true
else:
  code to run when condition is false
code to run in either case

在基于堆栈的语言中,比如比特币脚本,逻辑条件出现在IF之前,这使得它看起来像“向后”,如下所示: Bitcoin脚本流控制:

condition
IF
  code to run when condition is true
ELSE
  code to run when condition is false
ENDIF
code to run in either case

带有VERIFY操作码的条件子句

比特币脚本中的另一种条件是任何以VERIFY结尾的操作码。 VERIFY后缀表示如果评估的条件不为TRUE,脚本的执行将立即终止,并且该交易被视为无效。 与提供替代执行路径的IF子句不同,VERIFY后缀充当保护子句,只有在满足前提条件的情况下才会继续。

例如,以下脚本需要Bob的签名和产生特定哈希的前图像(秘密地)。

解锁时必须满足这两个条件:

1)具有EQUALVERIFY保护子句的赎回脚本。

HASH160 <expected hash> EQUALVERIFY <Bob's Pubkey> CHECKSIG

为了兑现这一点,Bob必须构建一个解锁脚本,提供有效的前图像和签名:

2)一个解锁脚本以满足上述赎回脚本。

<Bob's Sig> <hash pre-image>

没有前图像,Bob无法访问检查其签名的脚本部分。

该脚本可以用IF编写: 具有IF保护条款的赎回脚本

HASH160 <expected hash> EQUAL
IF
   <Bob's Pubkey> CHECKSIG
ENDIF

Bob的解锁脚本是一样的: 解锁脚本以满足上述赎回脚本

<Bob's Sig> <hash pre-image>

使用IF的脚本与使用具有VERIFY后缀的操作码相同; 他们都作为保护条款。 然而,VERIFY的构造更有效率,使用较少的操作码。

那么,我们什么时候使用VERIFY,什么时候使用IF? 如果我们想要做的是附加一个前提条件(保护条款),那么验证是更好的。 然而,如果我们想要有多个执行路径(流控制),那么我们需要一个IF ... ELSE流控制子句。

提示 诸如EQUAL之类的操作码会将结果(TRUE / FALSE)推送到堆栈上,留下它用于后续操作码的评估。 相比之下,操作码EQUALVERIFY后缀不会在堆栈上留下任何东西。 在VERIFY中结束的操作码不会将结果留在堆栈上。

在脚本中使用控制流

比特币脚本中控制流的一个非常常见的用途是构建一个提供多个执行路径的赎回脚本,每个脚本都有一种不同的赎回UTXO的方式。

我们来看一个简单的例子,我们有两个签名人,Alice和Bob,两人中任何一个都可以赎回。 使用多重签名,这将被表示为1-of-2 多重签名脚本。 为了示范,我们将使用IF子句做同样的事情:

IF
 <Alice's Pubkey> CHECKSIG
ELSE
 <Bob's Pubkey> CHECKSIG
ENDIF

看这个赎回脚本,你可能会想:“条件在哪里?”IF子句之前没有什么!“ 条件不是赎回脚本的一部分。

相反,该解锁脚本将提供该条件,允许Alice和Bob“选择”他们想要的执行路径。

Alice用解锁脚本兑换了这个:

<Alice's Sig> 1

最后的1作为条件(TRUE),将使IF子句执行Alice具有签名的第一个兑换路径。

为了兑换这个Bob,他必须通过给IF子句赋一个FALSE值来选择第二个执行路径:

<Bob's Sig> 0

Bob的解锁脚本在堆栈中放置一个0,导致IF子句执行第二个(ELSE)脚本,这需要Bob的签名。

由于可以嵌套IF子句,所以我们可以创建一个“迷宫”的执行路径。 解锁脚本可以提供一个选择执行路径实际执行的“地图”:

IF
    script A
ELSE
   IF
script B
  ELSE
script C
  ENDIF
ENDIF

在这种情况下,有三个执行路径(脚本A,脚本B和脚本C)。 解锁脚本以TRUE或FALSE值的形式提供路径。

要选择路径脚本B,例如,解锁脚本必须以1 0(TRUE,FALSE)结束。

这些值将被推送到堆栈,以便第二个值(FALSE)结束于堆栈的顶部。 外部IF子句弹出FALSE值并执行第一个ELSE子句。 然后,TRUE值移动到堆栈的顶部,并通过内部(嵌套)IF来评估,选择B执行路径。

使用这个结构,我们可以用数十或数百个执行路径构建赎回脚本,每个脚本提供了一种不同的方式来兑换UTXO。 要花费,我们构建一个解锁脚本,通过在每个控制流的条件点的堆栈上放置相应的TRUE和FALSE值来导航执行路径。

复杂的脚本示例

在本节中,我们将本章中的许多概念合并成一个例子。 我们的例子使用了迪拜公司所有者Mohammed的故事,他们正在经营进出口业务。

在这个例子中,Mohammed希望用灵活的规则建立公司资本账户。他创建的方案需要不同级别的授权,具体取决于时间锁定。

多重签名的计划的参与者是Mohammed,他的两个合作伙伴Saeed和Zaira,以及他们的公司律师Abdul。三个合作伙伴根据多数规则作出决定,因此三者中的两个必须同意。然而,如果他们的钥匙有问题,他们希望他们的律师能够用三个合作伙伴签名之一收回资金。最后,如果所有的合作伙伴一段时间都不可用或无行为能力,他们希望律师能够直接管理该帐户。

这是Mohammed设计的脚本: 具有时间锁定(Timelock)变量的多重签名

IF
  IF
    2
  ELSE
    <30 days> CHECKSEQUENCEVERIFY DROP
    <Abdul the Lawyer's Pubkey> CHECKSIGVERIFY
    1
  ENDIF
  <Mohammed's Pubkey> <Saeed's Pubkey> <Zaira's Pubkey> 3 CHECKMULTISIG
ELSE
  <90 days> CHECKSEQUENCEVERIFY DROP
  <Abdul the Lawyer's Pubkey> CHECKSIG
ENDIF

Mohammed的脚本使用嵌套的IF ... ELSE流控制子句来实现三个执行路径。

在第一个执行路径中,该脚本作为三个合作伙伴的简单的2-of-3 multisig操作。

该执行路径由第3行和第9行组成。第3行将multisig的定额设置为2(2 - 3)。

该执行路径可以通过在解锁脚本的末尾设置TRUE TRUE来选择: 解锁第一个执行路径的脚本(2-of-3 multisig)

0 <Mohammed's Sig> <Zaira's Sig> TRUE TRUE

提示 此解锁脚本开头的0是因为CHECKMULTISIG中的错误从堆栈中弹出一个额外的值。 额外的值被CHECKMULTISIG忽略,否则脚本签名将失败。 推送0(通常)是解决bug的方法,如CHECKMULTISIG执行中的错误章节所述。

第二个执行路径只能在UTXO创建30天后才能使用。 那时候,它需要签署Abdul(律师)和三个合作伙伴之一(三分之一)。

这是通过第7行实现的,该行将多选的法定人数设置为1。要选择此执行路径,解锁脚本将以FALSE TRUE结束: 解锁第二个执行路径的脚本(Lawyer + 1-of-3)

0 <Saeed's Sig> <Abdul's Sig> FALSE TRUE

提示 为什么先FALSE后TRUE? 反了吗?这是因为这两个值被推到堆栈,所以先push FALSE,然后push TRUE。 因此,第一个IF操作码首先弹出的是TRUE。

最后,第三个执行路径允许律师单独花费资金,但只能在90天之后。 要选择此执行路径,解锁脚本必须以FALSE结束: 解锁第三个执行路径的脚本(仅适用于律师)

<Abdul's Sig> FALSE

在纸上运行脚本来查看它在堆栈(stack)上的行为。

高级脚本与高级交易详情请看 http://v1.8btc.com/books/834/masterbitcoin2cn/_book/ch07.html

参考资料

shangqd commented 4 years ago
AlexiaChen commented 4 years ago

根据需求,部门经理Alice首先创建P2SH类型的脚本(Redeem Script),脚本Hash ID是脚本源码Hash出来的,间接可以得到脚本地址。

假设一个具体场景:

设这个月30天,共有销售人员A,B, C。有一个销售领导Bob。第15天的时候销售C离职了,第17天销售D入职,直到本月30天后,开始对A,B,C,D相应的业绩发工资。

根据之前我对P2SH脚本的机制理解,一个UTXO对应一个花费条件(加锁脚本表示一个花费条件的ID),由input附带的解锁脚本(签名+赎回脚本)形成一个满足花费条件的脚本,这时候需要把具体业务分解成这样的一个底层形式。

Redeem Script: 花费条件全部写在赎回脚本中
Lock Script:  OP_HASH160 <20 bytes hash ID of redeem script> OP_EQUAL
UnLock Script: <signature1> <signature2> <Redeem Script source code>

最后看最终组合在一起的脚本是不是值为OP_TRUE, 如果是TRUE就可以花费这个UTXO。这个是底层最原子的方式,需要以这样的方式组合,达到分红,业务绩效,同时也有人员流动这个业务场景。从编码的角度看,有很大门槛,脚本可读性,表达性没有高级语言好。

对应上原理了,主要是设计Redeem Script这里,也就是赎回脚本的源码需要部门经理Alice怎样写,才能满足以上的场景业务。

vchData的结构简单用json表示业务数据:

”user_id“: "员工id",
“user_name“: "销售员A",
"money":  123812.34 // 销售员销售额

根据不同的部门或公司的业务,需要自己定义适合自己的json结构和字段,这里只是原型,不会细化。

AlexiaChen commented 4 years ago

之前讨论有个稍微细化的结果,所以暂时不做继续调研了。