NEO从源码分析看UTXO交易

0x00 前言

社区大佬:“交易是操做区块链的惟一方式。”git

0x01 交易类型

在NEO中,几乎除了共识以外的全部的对区块链的操做都是一种“交易”,甚至在“交易”面前,合约都只是一个小弟。交易类型的定义在Core中的TransactionType中:github

源码位置: neo/Core/TransactionType小程序

/// <summary>
        /// 用于分配字节费的特殊交易
        /// </summary>
        [ReflectionCache(typeof(MinerTransaction))]
        MinerTransaction = 0x00,

        /// <summary>
        /// 用于分发资产的特殊交易
        /// </summary>
        [ReflectionCache(typeof(IssueTransaction))]
        IssueTransaction = 0x01,
        
        [ReflectionCache(typeof(ClaimTransaction))]
        ClaimTransaction = 0x02,

        /// <summary>
        /// 用于报名成为记帐候选人的特殊交易
        /// </summary>
        [ReflectionCache(typeof(EnrollmentTransaction))]
        EnrollmentTransaction = 0x20,

        /// <summary>
        /// 用于资产登记的特殊交易
        /// </summary>
        [ReflectionCache(typeof(RegisterTransaction))]
        RegisterTransaction = 0x40,

        /// <summary>
        /// 合约交易,这是最经常使用的一种交易
        /// </summary>
        [ReflectionCache(typeof(ContractTransaction))]
        ContractTransaction = 0x80,

        /// <summary>
        /// 投票合约 //votingDialog
        /// </summary>
        [ReflectionCache(typeof(StateTransaction))]
        StateTransaction = 0x90,
        /// <summary>
        /// Publish scripts to the blockchain for being invoked later.
        /// </summary>
        [ReflectionCache(typeof(PublishTransaction))]
        PublishTransaction = 0xd0,
        /// <summary>
        /// 调用合约   GUI invocatransactiondialog
        /// </summary>
        [ReflectionCache(typeof(InvocationTransaction))]
        InvocationTransaction = 0xd1

这些交易不只名目繁多,并且实际功能和我“觉得”的还有些不一样,为了分别搞清楚每种交易是作什么的,我几乎又把NEO和GUI的源码翻了个遍。数组

  • MinerTransaction: 矿工手续费交易,在新block建立的时候由_议长_添加这笔特殊交易。使用位置:neo/Consensus/ConsensusService.cs/CreateMinerTransaction。
  • IssueTransaction:资产分发交易,用于在新资产建立的时候对新资产进行分发的特殊交易。使用位置:neo/Core/BlockChain.cs/GenesisBlock。
  • ClaimTransaction:NEOGAS提取交易,用于提取GAS。在GUI的主界面里有个“高级”选项卡,点击后下拉列表里有个提取GAS,调用的就是这个交易。使用位置:neo-gui/UI/ClaimForm.cs/button1_Click。
  • EnrollmentTransaction:用于申请成为记帐人,也就是议员的交易。这个交易GUI种并无接入接口,也就是说使用GUI是没办法申请成为记帐人的。悄悄说:想成为记帐人,至少要拥有价值几个亿的NEO才行,因此若是你是记帐人,应该不会吝啬向个人NEO帐户转几个GAS吧。
  • RegisterTransaction:注册新资产的交易,在我以前的博客《从NEO源码分析看UTXO资产》的博客种也提到了这个,NEO和GAS就是经过这个交易在创世区块被创造出来的。可是有意思的是,这个交易只在注册NEO和GAS的时候用到了,GUI里注册新资产使用的方式竟然是进行合约级的系统调用,经过调用“Neo.Asset.Create”来建立新资产。
  • ContractTransaction:我感受最容易搞混的就是这个合约交易了。我一直觉得这个应该是发布应用合约的时候用的,毕竟叫合约交易嘛,然鹅,too young too simple, 这个交易是咱们转帐的时候调用的,没错,每一笔转帐都是一种合约交易,GUI的转帐历史记录里能够看到交易类型,基本上都是这个合约交易。使用位置:neo/Wallets/Wallet.cs/MakeTransaction。
  • StateTransaction:如白皮书所说,NEO网络的_议员_是经过选举产生的,也就是说咱们广大老百姓是能够决定上层建筑结构,参与社会主义现代化建设的,这是NEO给咱们的权力。在GUI中能够右键帐户,而后选择投票来参与选举投票。使用位置:neo-gui/UI/VotingDialog.cs/GetTransaction。
  • PublishTransaction:这个交易才是货真价实的_应用合约_发布交易,也就是说,理论上咱们要发布合约到区块链上,都须要经过这个PublishTransaction类型的交易把咱们的合约广播出去才行。然而呢?现实是残酷的,GUI并无调用这个交易,发布新合约依然是进行合约级的系统调用,经过调用vm的“Neo.Contract.Create”API来建立新合约。部署应用合约的代码位置:neo-gui/UI/DeployContractDialog.cs/GetTransaction。
  • InvocationTransaction:将咱们可怜的RegisterTransaction和PublishTransaction的做用替代的万恶交易类型,合约调用交易。咱们在成功部署合约以后的那笔交易,在GUI的交易历史的类型栏里,赫然写着InvocationTransaction。构造交易的方式更是简单粗暴,就是直接调用EmitSysCall而后传入参数完事。使用位置:neo-gui/UI/AssetRegisterDialog.cs/GetTransaction。

以上就是NEO中的9种交易类型,也基本上就是除了 议长 建立新区块以外全部的能够影响到区块链生成的操做手段。虽然我将每种交易类型都大概分析了一下,可是仍是有些问题没有搞清楚,好比为何建立新资产和部署合约的时候是进行系统调用而不是发送相应的交易,我从源码里没找到答案。网络

0x02 余额

从个人题目就能够看出,我并不许备对每一种交易类型都详细介绍,毕竟究其本质,都只是脚本不一样的合约而已。我这里要详细分析的是UTXO交易,就是合约交易,也是咱们平常在NEO网络上进行的转帐操做。 在社区里常常听到有人在问:“NEO帐户到底是什么?“NEO和GAS余额是怎么来的?”“交易究竟转的是什么?”。我感受这些问题我以前都有过,就是首先对 代币 这种概念不是很清晰,其次对虚拟的_帐户_更是没法理解。其实在最初开始看源码,也是但愿能在这个过程当中解决本身对于区块链和智能合约认知上的不足。幸运的是,随着一个个模块看下来,对于NEO总体的系统架构和运行原理已经基本能够说是了然于胸。可见Linux之父那句:”Talk is cheap,show me your code.“(别逼逼,看代码),仍是颇有道理的。 对不起,我逼逼的有点多。架构

要理解余额的计算原理,首先仍是要理解UTXO交易模型,这个东西社区大佬李总在群里发布了一系列的讲解视频,感兴趣的能够去膜一下。《Mastering BitCoin》的第六章 Transaction 也对此进行了详细的讲解,想看的同窗能够在文末找到下载链接。 计算余额的代码在wallet类中。dom

源码位置:neo/Wallets/Wallet源码分析

public Fixed8 GetBalance(UInt256 asset_id)
{
    return GetCoins(GetAccounts().Select(p => p.ScriptHash))
              .Where(p => !p.State.HasFlag(CoinState.Spent)  //未花费状态
                                  && p.Output.AssetId.Equals(asset_id)) //资产类型
              .Sum(p => p.Output.Value);
}

其实从这里就能够很清晰的看出来这个余额的计算过程了,就是将与地址对应的Output进行遍历,将与指定资产类型相同且状态不是spent(Spent表明已转出)的值相加。更加直白通俗的解释就是,加入每个指向你帐户的Output至关于给你的一笔钱,全部未被花费的Output加起来,就是你如今的余额。区块链

0x03 新交易

在NEO中,转帐的时候须要构造一个Transaction对象,这个对象中须要指定资产类型、转帐数额、资产源(Output),目标地址,见证人(Witness)。因为GUI中的转帐这块是能够同时从当前钱包的多个地址中构造一个交易的,比较复杂,我这里用我小程序的代码来作讲解,基本原理是同样的。ui

轻钱包关于交易处理的这块的代码是改自NEL的TS轻钱包,不过去除了TS本来代码中关于界面的代码,而后从新封装为js模块,至关于GUI转帐功能魔鬼瘦身版本以后又进行的地狱级瘦身,代码量极大减小。 小程序转帐的入口在send.wpy界面中:

源码位置:neowalletforwechat/src/pages/send.wpy/OnSend

//交易金额
var count = NEL.neo.Fixed8.parse(that.amount + '');
//资产种类
let coin = that.checked === 'GAS' ? CoinTool.id_GAS : CoinTool.id_NEO;
wepy.showLoading({ title: '交易生成中' });
//构造交易对象
var tran = CoinTool.makeTran(UTXO.assets, that.targetAddr, coin, count);
// console.log(tran);
//生成交易id 没错是随机数
let randomStr = await Random.getSecureRandom(256);
//添加资产源、资产输出、见证人、签名
const txid = await TransactionTool.setTran(
  tran,
  prikey,
  pubkey,
  randomStr
);

在构造交易对象的时候调用CoinTool的makeTran方法,须要传入四个参数,一个是OutPuts,一个是目标帐户,第三个是资产类型,最后一个是资产数量。这个方法对应于neo core中的neo/Wallets/Wallet.cs/MakeTransaction<T>。二者功能基本是一致的。makeTran中的对交易对象的初始化代码以下:

//新建交易对象
        var tran = new NEL.thinneo.TransAction.Transaction();
        //交易类型为合约交易
        tran.type = NEL.thinneo.TransAction.TransactionType.ContractTransaction;
        tran.version = 0;//0 or 1
        tran.extdata = null;
        tran.attributes = [];
        tran.inputs = [];

在UTXO交易模型中,每笔交易会有一个或者多个资金来源,也就是那些指向当前地址的Output,将这些OutPut做为新交易的输入:

//交易输入
        for (var i = 0; i < us.length; i++) {
            //构造新的input
            var input = new NEL.thinneo.TransAction.TransactionInput();
            //新交易的prehash指向output
            input.hash = NEL.helper.UintHelper.hexToBytes(us[i].txid).reverse();
            input.index = us[i].n;
            input["_addr"] = us[i].addr;
            //向交易资产来源中添加新的input
            tran.inputs.push(input);
            //计算已添加的资产总量
            count = count.add(us[i].count);
            scraddr = us[i].addr;
            //若是已添加的资产数量大于或等于须要的数量,则再也不添加新的
            if (count.compareTo(sendcount) > 0) {
                break;
            }
        }

在一笔交易中,本身帐户的output变成新交易的input,而后新交易会指定新的output。一般,一笔交易中除了有一个指向目的帐户的output以外,还会有一个用于找零的Output。这里为了方便,我仍是讲一个小故事。

  • 第一幕:小明写博客发给社区,每写一篇博客,社区会给小明5个GAS做为奖励,每次社区给小明奖励,小明的帐户里就会多一个5GAS的Output。如今小明写了三篇博客,社区也奖励了小明三次。因此小明有三个分别为5GAS的Output。
  • 第二幕:小明但愿用写博客挣的GAS给女友买化妆品,已知支持GAS支付的化妆品售价为6.8个GAS。
  • 第三幕:一个Output只有5个GAS,明显是不够支付化妆品的,因而小明不得不拿出两个Output来支付。
  • 第四幕:因为每一个Output都是不可分割的,就像100块钱同样,你不能够把一张一百的按比例撕开来支付,只能是你给人家100,人家给你找零。Output也是这个道理,你拿出两个Output来支付,那么交易不可能从已有的Output扣出1.8。只能是同时将两个Output都彻底设置为Spent,而后给商家一个6.8的OutPut,再给小明一个3.2的Output做为找零。如今小明就只有一个5GAS的output和一个3.2GAS的output了。
  • 第五幕:小明靠本身的努力为女友买到了化妆品,女友很开心,因而为小明买了新的搓衣板。

UTXO交易其实就是这样的,output是不能分割的,只要被转出,就一块儿转出,而后再转入一个新的output做为找零。output构造的代码以下:

//输出
if (sendcount.compareTo(NEL.neo.Fixed8.Zero) > 0) {
    var output = new NEL.thinneo.TransAction.TransactionOutput();
    //资产类型
    output.assetId = NEL.helper.UintHelper.hexToBytes(assetid).reverse();
    //交易金额
    output.value = sendcount;
    //目的帐户
    output.toAddress = NEL.helper.Helper.GetPublicKeyScriptHash_FromAddress(targetaddr);
    //添加转帐交易
    tran.outputs.push(output);
}

//找零
var change = count.subtract(sendcount); //计算找零的额度
if (change.compareTo(NEL.neo.Fixed8.Zero) > 0) {
    var outputchange = new NEL.thinneo.TransAction.TransactionOutput();
    //找零地址设置为本身
    outputchange.toAddress = NEL.helper.Helper.GetPublicKeyScriptHash_FromAddress(scraddr);
    //设置找零额度
    outputchange.value = change;
    //找零资产类型
    outputchange.assetId = NEL.helper.UintHelper.hexToBytes(assetid).reverse();
    //添加找零交易
    tran.outputs.push(outputchange);
}

以上就是构造一笔新交易的过程。基本上一个交易的结构都有了,从哪里来,到哪里去,转多少,都已经构造完成,接下来就须要对这笔交易进行签名。

0x04 签名

与传统的面对面交易不一样,在网络中发布的交易如何对用户身份进行确认呢?只要知道对方的地址,就能够获取到对方的Output,若是仅仅靠一个转帐对象就可以成功转出对方帐户的资金,那么这不全乱套了么。因此对于一笔交易,除了须要构造交易必须的元素以外,还须要对交易进行签名,向区块链证实,是帐户的全部者在进转出交易。 在NEO Core中的签名方法在neo/Cryptography/ECC/ECDsa.cs文件中定义,因为这部分属于密码学范畴,不属于我要分析的部分,这里就大概提一下:

源码位置:thinsdk-ts/thinneo/Helper.cs/Sign

//计算公钥
 var PublicKey = ECPoint.multiply(ECCurve.secp256r1.G, privateKey);
 var pubkey = PublicKey.encodePoint(false).subarray(1, 64);
 //获取CryptoKey
 var key = new CryptoKey.ECDsaCryptoKey(PublicKey, privateKey);
 var ecdsa = new ECDsa(key);
 {
     //签名
     return new Uint8Array(ecdsa.sign(message,randomStr));
 }

真正签名的部分其实就是标准的ECDsa数字签名,返回值是一个长度为64的Uint8数组,前32字节是R,后32字节是S:

let arr = new Uint8Array(64);
Arrayhelper.copy(r.toUint8Array(false, 32), 0, arr, 0, 32);
Arrayhelper.copy(s.toUint8Array(false, 32), 0, arr, 32, 32);
return arr.buffer;

参数S和R都是ECDsa数字签名验证时很是重要的参数。

0x05 Witness

仅仅计算出签名是不够的,咱们还须要将签名添加到交易中,这个过程就是添加见证人:

tran.AddWitness(signdata, pubkey, WalletHelper.wallet.address);

添加见证人的过程其实就是将上一步的签名信息和经过公钥获取到的验证信息push到见证人脚本中,删除了复杂验证过程的见证人添加过程以下:

源码位置:thinsdk-ts/thinneo/Transaction.cs

//增长我的帐户见证人(就是用这我的的私钥对交易签个名,signdata传进来)
 public AddWitness(signdata: Uint8Array, pubkey: Uint8Array, addrs: string): void {
    var vscript = Helper.GetAddressCheckScriptFromPublicKey(pubkey);
    //iscript 对我的帐户见证人他是一条pushbytes 指令
    var sb = new ScriptBuilder();
    sb.EmitPushBytes(signdata);
    var iscript = sb.ToArray();
    this.AddWitnessScript(vscript, iscript);
}

//增长智能合约见证人
public AddWitnessScript(vscript: Uint8Array, iscript: Uint8Array): void {
    var newwit = new Witness();
    newwit.VerificationScript = vscript;
    newwit.InvocationScript = iscript;
    //添加新见证人
    this.witnesses.push(newwit);

}

这里我仍是有问题的,就是这个交易加入涉及到一个钱包下的多个帐户,是否是应该有多个签名呢?理论上确定是的,毕竟每一个帐户都拥有本身独立的私钥,因此我又去看了下GUI的转帐代码. GUI获取交易并签名的入口是位于MainForm文件中的转帐方法,调用的是Helper中的SignAndShowInformation,在这个SignAndShowInformation中,调用的是Wallet文件中的Sign方法,传入的是交易的上下文:

源码位置:neo/Wallets/Wallet.cs

public bool Sign(ContractParametersContext context)
{
    bool fSuccess = false;
    foreach(UInt160 scriptHash in context.ScriptHashes)
    {
        WalletAccount account = GetAccount(scriptHash);
        if (account ?.HasKey != true) continue;
        KeyPair key = account.GetKey();
        byte[] signature = context.Verifiable.Sign(key);
        fSuccess |= context.AddSignature(account.Contract, key.PublicKey, signature);
    }
    return fSuccess;
}

从源码中能够看出,NEO在对交易进行签名的时候会分别使用每个参与交易的帐户的私钥来进行签名。在签名完成后,会调用GetScripts方法来获取脚本,就是在这个方法中,交易添加了多个见证人:

public Witness[] GetScripts()
{
    if (!Completed) throw new InvalidOperationException();
    // 脚本哈希数量 == 见证人数量
    Witness[] scripts = new Witness[ScriptHashes.Count];
    for (int i = 0; i < ScriptHashes.Count; i++)
    {
        ContextItem item = ContextItems[ScriptHashes[i]];
        using(ScriptBuilder sb = new ScriptBuilder())
        {
            foreach(ContractParameter parameter in item.Parameters.Reverse())
            {
                sb.EmitPush(parameter);
            }
            //添加新的见证人
            scripts[i] = new Witness
            {
                InvocationScript = sb.ToArray(),
                    VerificationScript = item.Script ?? new byte[0]
             };
        }
    }
    return scripts;
}

因此若是你的GUI钱包往外转帐金额大于单独一个帐户的金额时,实际上是多个帐户共同完成一笔交易的。

下载链接:《Mastering BitCoin》:https://github.com/Liaojinghui/BlockChainBooks

相关文章
相关标签/搜索