深度解析量子链账户抽象层(Qtum AAL):为Dapp提供了新的基础平台
Qtum账户抽象层(AAL)实现简析
Qtum设计上以比特币UTXO为基础账户模型,并实现了支持EVM规范的智能合约,这是通过账户抽象层(Account Abstract Layer, AAL)来完成的。AAL对UTXO账户和EVM合约账户之间进行了适配,这样通过AAL可以使用UTXO交易输出实现在链上创建智能合约,发送交易到合约账户用于触发合约的执行,完成执行后AAL最终对执行结果进行处理并适配至UTXO。由于采用了AAL,合约开发者不需关心对合约操作相关的UTXO转换细节,即可使用EVM的特性进行开发而且兼容现有以太坊的智能合约。本文通过对从UTXO交易到智能合约执行的实现代码进行解读,初略分析了AAL的工作过程。
1.UTXO交易新增的脚本操作码
Qtum 针对UTXO交易脚本新增了三个操作码OP_CREATE,OP_CALL和OP_SPEND,目的是用于为UTXO和EVM账户模型之间的转换提供操作支持。这些操作码定义在opcodetype枚举类型中:
enum opcodetype{ …… OP_CREATE = 0xc1, OP_CALL = 0xc2, OP_SPEND= 0xc3, …… }
这个三个操作码分别有以下作用 :
为了在区块生成过程中,识别并处理由这几个操作码控制的交易,在用于UTXO模型交易的类CTransaction中增加了HasCreateOrCall()和HasOpSpend()函数,用于新区块中对mempool中的交易处理,并在脚本操作码解析的EvalScript()函数中增加了相应的处理。
2.UTXO交易到EVM模型交易的转换
产生新的区块时,除了对UTXO交易进行常规的参数合法性、共识规则、DDOS攻击检查等之外,还需要使用操作码检查函数HasCreateOrCall()判断交易输出是否包含OP_CREATE或OP_CALL,分别对应着EVM需要执行合约创建或合约调用。这部分有以下的处理过程:
2.1 进行EVM模型的账号参数提取
合约在EVM的执行用到了data、gasPrice、gasLimit、VM version这几个参数,这些参数是通过RPC调用sendtocontract 发送的,sendtocontract会生成一个UTXO交易,并在交易输出中使用了OP_CALL操作码,之后交易会广播到区块链网络上。AAL中从UTXO到EVM的适配是通过QtumTxConverter类实现的,在这一步中该类的成员函数extractionQtumTransactions()和parseEthTXParams()完成对所有此类UTXO交易输出的参数提取。代码片段如下:
dev::Address receiveAddress; valtype vecAddr;
if (opcode == OP_CALL) { vecAddr = stack.back(); stack.pop_back(); receiveAddress = dev::Address(vecAddr); } valtype code(stack.back()); stack.pop_back(); uint64_t gasPrice = CScriptNum::vch_to_uint64(stack.back()); stack.pop_back(); uint64_t gasLimit = CScriptNum::vch_to_uint64(stack.back()); stack.pop_back(); VersionVM version(CScriptNum::vch_to_uint64(stack.back())); stack.pop_back();
return EthTransactionParams{version, dev::u256(gasLimit), dev::u256(gasPrice), code, receiveAddress }
以上代码首先判断如果opcode 为OP_CALL,则说明地址为vecAddr的合约已经创建,因此直接转换成EVM格式的地址receiveAddress,否则为OP_CREATE,对应合约的创建,没有该字段,所以不做提取。接下来依次完成了data、gasPrice、gasLimit、VM version的提取,这些都是EVM执行bytecode时必不可少的参数。
2.2 进行EVM账户模型的交易转换
交易转换是通过QtumTxConverter类的函数 createEthTX()完成,使用前面一步提取的参数和UTXO的交易输出vout创建了QtumTransaction类型的交易。由于QtumTransaction派生自EVM中的dev::eth::Transaction类,因此和EVM执行相关的操作QtumTransaction类都支持。
QtumTransaction txEth;
if ( etp.receiveAddress == dev::Address() ) { txEth = QtumTransaction(txBit.vout[nOut].nValue, etp.gasPrice, (etp.gasLimit * etp.gasPrice), etp.code, dev::u256(0)); }
else{ txEth = QtumTransaction(txBit.vout[nOut].nValue, etp.gasPrice, (etp.gasLimit * etp.gasPrice), etp.receiveAddress, etp.code, dev::u256(0)); } dev::Address sender(GetSenderAddress(txBit, view)); txEth.forceSender(sender); txEth.setHashWith(uintToh256(txBit.GetHash())); txEth.setNVout(nOut);
首先代码etp.receiveAddress == dev::Address()判断该合约是EVM状态中没有而需要新创建的还是EVM状态已经包含的合约,差别只在于合约地址。然后,QtumTransaction()构造函数完成了部分的交易参数构造,接下来的语句提取交易的发送者(sender),之后设置交易HASH。一个UTXO交易支持多个输入和输出,Qtum的AAL设计考虑到了这种情况,因此AAL支持一个交易输出包含UTXO账号和合约账号,通过最后设置的nOut指示该交易的nOut输出是发送到智能合约的,所以该输出将触发合约执行。这样就按照EVM的账号模型完成了交易的转换。
3.合约执行及执行结果的UTXO转换
合约的执行会改变状态(由QtumState类的实例化对象globalState统一管理),对于合约的状态,Qtum沿用了EVM定义,所以能兼容所有的符合EVM规范的智能合约。但是账户金额的转移(transfer),Qtum做了UTXO的转换,这意味着智能合约和普通的UTXO模型账号之间能完成交互,这是AAL实现UTXO支持智能合约的重要的一环。下面简要介绍一下合约执行和状态结果的转换过程。
3.1 合约执行环境构建及合约执行
合约的执行是对合约处理中很关键的一步,直接对合约的状态产生影响。通过ByteCodeExec类实现了EVM对合约bytecode的执行,主要函数是performByteCode()。这一步的主要流程是使用上面提取的交易参数,来进行虚拟机执行环境的构建,之后完成合约的执行,其代码如下:
for(QtumTransaction& tx : txs){ dev::eth::EnvInfo envInfo(BuildEVMEnvironment()); std::unique_ptr se(dev::eth::ChainParams(dev::eth::genesisInfo(dev::eth::Network::HomesteadTest)). createSealEngine());
if(!tx.isCreation() && !globalState->addressInUse(tx.receiveAddress())){ dev::eth::ExecutionResult execRes; execRes.excepted = dev::eth::TransactionException::Unknown; result.push_back(ResultExecute{execRes, dev::eth::TransactionReceipt(dev::h256(), dev::u256(), dev::eth::LogEntries()), CTransaction()});
continue; } result.push_back(globalState->execute(envInfo, *se.get(), tx, type, OnOpFunc())); }
首先是构建合约执行环境,由BuildEVMEnvironment()完成。可以看到这个执行环境是针对每个独立交易进行的,这样就最大限度的把不同交易的合约执行过程隔离开,避免合约执行过程中的交叉影响。然后构建一个新的sealEngine类,该类是EVM执行引擎,由createSealEngine()函数具体完成。中间对出现的可能状态异常进行检查,之后globalState->execute()完成合约的执行,这里使用到了构建的执行环境envInfo和EVM执行引擎se。
3.2 合约执行结果的UTXO转换
合约执行完成后的结果保存在vector result,vector向量理记录了每个合约执行产生的EVM账户间transfer关系,AAL通过把这些transfer转换成UTXO交易,完成了从EVM账户模型到UTXO模型交易的转换。这一处理是通过processingResults()函数实现的,以下是代码片段。
ByteCodeExecResult resultBCE;
for(size_t i = 0; i < result.size(); i++){
if(result[i].execRes.excepted != dev::eth::TransactionException::None){
if(txs[i].value() > 0){ CMutableTransaction tx; tx.vin.push_back(CTxIn(h256Touint(txs[i].getHashWith()), txs[i].getNVout(), CScript() << OP_SPEND)); CScript script(CScript() << OP_DUP << OP_HASH160 << txs[i].sender().asBytes() << OP_EQUALVERIFY << OP_CHECKSIG); tx.vout.push_back(CTxOut(CAmount(txs[i].value()), script)); resultBCE.valueTransfers.push_back(CTransaction(tx)); } } else { resultBCE.usedFee += CAmount(result[i].execRes.gasUsed); CAmount ref((txs[i].gas() - result[i].execRes.gasUsed) * txs[i].gasPrice());
if(ref > 0){ CScript script(CScript() << OP_DUP << OP_HASH160 << txs[i].sender().asBytes() << OP_EQUALVERIFY << OP_CHECKSIG); resultBCE.refundOutputs.push_back(CTxOut(ref, script)); resultBCE.refundSender += ref; } } if(result[i].tx != CTransaction()){ resultBCE.valueTransfers.push_back(result[i].tx); }}
首先定义了ByteCodeExecResult类型的resultBCE变量,用于保存转换的结果。使用操作码OP_SPEND,用于实现交易的花费,这是因为比特币的UTXO通过私钥签名在交易输入解锁后来实现余额花费的,而EVM执行涉及不同账户之间的transfer,所以需要通过OP_SPEND实现这些transfer到UTXO模型交易的转换。如果execRes.excepted不为None,即合约执行异常,则将余额返还给合约调用者。否则,如果没有异常,则将扣除消耗的gas之后的剩余gas返还给合约的调用者。对于合约执行中出现的transfer其UTXO交易保存在result[i].tx中。因此,经过这一步处理合约执行产生的不同UTXO账户之间的交易就保存在valueTransfers向量中了,最终这些交易会包含进新的区块中。至此AAL模块就完成了从EVM交易到UTXO的转换。
4.总结
AAL通过新增的UTXO脚本操作码,协助完成合约的创建、执行和花费。在合约创建和执行前,需要进行UTXO交易到EVM模型交易的转换,之后使用构建的EVM执行环境和引擎,完成合约的执行。AAL最终对合约的执行结果进行处理并从EVM适配至UTXO,这样就实现了基于UTXO的智能合约。 AAL使得Qtum兼容符合EVM规范的智能合约,为Dapp提供一个新的基础平台,同时UTXO的优点使得诸如并行处理、隐私性等优点能得以保留。