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

相關文章
相關標籤/搜索