社区大佬:“交易是操做区块链的惟一方式。”git
在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的源码翻了个遍。数组
以上就是NEO中的9种交易类型,也基本上就是除了 议长 建立新区块以外全部的能够影响到区块链生成的操做手段。虽然我将每种交易类型都大概分析了一下,可是仍是有些问题没有搞清楚,好比为何建立新资产和部署合约的时候是进行系统调用而不是发送相应的交易,我从源码里没找到答案。网络
从个人题目就能够看出,我并不许备对每一种交易类型都详细介绍,毕竟究其本质,都只是脚本不一样的合约而已。我这里要详细分析的是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加起来,就是你如今的余额。区块链
在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。这里为了方便,我仍是讲一个小故事。
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); }
以上就是构造一笔新交易的过程。基本上一个交易的结构都有了,从哪里来,到哪里去,转多少,都已经构造完成,接下来就须要对这笔交易进行签名。
与传统的面对面交易不一样,在网络中发布的交易如何对用户身份进行确认呢?只要知道对方的地址,就能够获取到对方的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数字签名验证时很是重要的参数。
仅仅计算出签名是不够的,咱们还须要将签名添加到交易中,这个过程就是添加见证人:
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