NEO從原始碼分析看數字資產

NEOGO發表於2019-01-23

0x00 引言

比特幣是泡沫麼?也許是的。畢竟這東西除了用來炒,幹什麼實事都感覺肉疼。但是有人將比特幣泡沫和鬱金香泡沫相提並論就很氣人了,鬱金香什麼鬼,長那麼一年,開那麼幾天,泡沫還沒破呢,鬱金香已經花開花落幾個春秋了。比特幣就不一樣了,不僅每一個區塊產出的幣都獨一無二,而且每一枚幣還擁有自己的獨一無二的歷史。世界上會有兩千多萬比特幣,但是中本聰創世區塊的那50枚幣什麼都替代不了。話說回來,如果當年鬱金香泡沫時期開放的鬱金香花株能被儲存到現在,價值也絕對槓槓的。 但是問題來了,究竟是什麼限定了加密貨幣的總量,我們擁有的“幣”又究竟是什麼呢?作為NEO原始碼分析希列的第三篇部落格,本文將從原始碼的角度對NEO資產部分的原始碼進行解析。 前兩篇文章連結:

注:

在接下來的文章中,英文縮寫“NEO”指代NEO網路中使用的管理代幣 “NEO Coin", 英文縮寫"GAS"指代NEO網路中的燃料代幣"NEOGas".

0x01 資產總量

在講解NEO網路中具體的資產之前需要講解一下NEO網路中用來註冊新資產的類RegisterTransaction,這個類用於註冊新的資產,這就意味者任何人都可以基於NEO網路來發布新的資產。RegisterTransaction繼承自Transaction,這意味著釋出資產的過程也是一個交易的過程,交易的資訊會被記錄在區塊中來保證資料的不可篡改性。RegisterTransaction中的關鍵欄位如下:

  • AssetType // 資產類別
  • Name // 資產名稱
  • Amount // 代幣總量
  • Precision //代幣精度
  • Owner // 發行者的公鑰
  • Admin // 資產管理員的合約雜湊值
  • Attributes // 交易特性 :用途及其資料 此外,釋出一種新的資產到NEO網路中是非常貴的,需要5000GAS,按照現在的市價,需要人民幣大約150萬。即便是測試網路中,官方施捨給我的GAS也就只有5000個GAS而已。

在NEO網路中存在兩種官方資產,一種是作為管理NEO網路的憑證的管理代幣NEO,另一種是功能和比特幣網路中的BitCoin功能類似的燃料貨幣GAS。因為NEO網路的共識策略採用的是投票機制,持有NEO越多的人,投票權越大,越有機會成為NEO網路中的議員。議員主持NEO網路的日常運轉,生成新的區塊,領取新生成的GAS作為獎勵。除此之外,NEO並沒有別的用處。而GAS則是用來繳納區塊鏈網路中日常交易以及合約執行的手續費。 NEO在NEO網路建立之初總量就確定並寫入區塊鏈中無法再進行更改,建立NEO管理代幣的程式碼在BlockChain.cs檔案中: 原始碼位置:neo/Core/BlockChain.cs

public static readonly RegisterTransaction GoverningToken = new RegisterTransaction
        {
            AssetType = AssetType.GoverningToken,
            Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蟻股\"},{\"lang\":\"en\",\"name\":\"AntShare\"}]",
            Amount = Fixed8.FromDecimal(100000000),  /* NEO管理代幣總量一億份 */
            Precision = 0,   /* 小數點精度為0,意味著NEO最小單位為1, 不可再分 */
            Owner = ECCurve.Secp256r1.Infinity,
            Admin = (new[] { (byte)OpCode.PUSHT }).ToScriptHash(),
            Attributes = new TransactionAttribute[0],
            Inputs = new CoinReference[0],
            Outputs = new TransactionOutput[0],
            Scripts = new Witness[0]
        };
複製程式碼

從程式碼中可以看出,在一開始,NEO的總量就是硬編碼進區塊鏈中的,並不涉及到複雜的計算。 同樣的道理,在註冊NEO資產的程式碼下面,就是註冊GAS資產的程式碼:

        public static readonly RegisterTransaction UtilityToken = new RegisterTransaction
        {
            AssetType = AssetType.UtilityToken,
            Name = "[{\"lang\":\"zh-CN\",\"name\":\"小蟻幣\"},{\"lang\":\"en\",\"name\":\"AntCoin\"}]",
            Amount = Fixed8.FromDecimal(GenerationAmount.Sum(p => p) * DecrementInterval), 
            Precision = 8, //精度為小數點後8位
            Owner = ECCurve.Secp256r1.Infinity,
            Admin = (new[] { (byte)OpCode.PUSHF }).ToScriptHash(),
            Attributes = new TransactionAttribute[0],
            Inputs = new CoinReference[0],
            Outputs = new TransactionOutput[0],
            Scripts = new Witness[0]
        };
複製程式碼

可以看到這個GAS的總量是計算得到的,GenerationAmount陣列中定義的是隨著時間每生成一個區塊獎勵的GAS數量,DecrementInterval則是生成GAS數量的衰減速度:每生成200萬個區塊,新生成的區塊獎勵GAS數按GenerationAmount陣列中的值衰減。我用計算器非常快速的算了一下,這個總量也是一億,和白皮書中定義的一致。

但是問題來了,要釋出一個新的資產,需要消耗5000GAS,但是如果GAS不釋出則NEO網路中不可能有GAS存在。釋出GAS需要GAS,這是個悖論來著。當然,這在我眼中是悖論,在Core開發者眼裡不是,NEO和GAS資產的註冊是直接被硬編碼在了創世區塊裡作為創世區塊交易中的一部分的。而後隨著新組網的節點被同步到整個世界各地。創世區塊中硬編碼寫入的交易如下: 原始碼地址:neo/Core/BlockChain.cs/GenesisBlock

        Transactions = new Transaction[]
            {
                new MinerTransaction // 建立礦工交易
                {
                    Nonce = 2083236893,
                    Attributes = new TransactionAttribute[0],
                    Inputs = new CoinReference[0],
                    Outputs = new TransactionOutput[0],
                    Scripts = new Witness[0]
                },
                GoverningToken,  // 釋出NEO
                UtilityToken,         // 釋出GAS
                new IssueTransaction // 用於分發資產的特殊交易
                {
                    // 程式碼省略
                }
            }
複製程式碼

0x02 資產分發

新的資產型別建立了之後,那些資產去了哪裡呢?有是如何獲得自己建立的資產的呢? 在0x01小節中我將創世區塊生成程式碼中的IssueTransaction交易的詳情略去了,因為這部分需要詳細講解,下面先貼上詳細程式碼:

原始碼地址:neo/Core/BlockChain.cs/GenesisBlock

    new IssueTransaction
                {
                    Attributes = new TransactionAttribute[0],  // 交易屬性
                    Inputs = new CoinReference[0],  
                    Outputs = new[]  //
                    {
                        new TransactionOutput
                        {
                            AssetId = GoverningToken.Hash,
                            Value = GoverningToken.Amount, // 直接分發全部NEO
                            ScriptHash = Contract.CreateMultiSigRedeemScript(StandbyValidators.Length / 2 + 1, StandbyValidators).ToScriptHash() 
                        }
                    },
                    Scripts = new[]
                    {
                       // 程式碼省略
                    }
                }
複製程式碼

IssueTransaction繼承自Transaction,是一種用於分發資產的特殊交易。這種交易最大的特殊性就在於,你需要交一筆系統交易費,這筆費用的定義在protocol.json檔案中:

原始碼位置:neo/protocol.json

       "SystemFee": {
              "EnrollmentTransaction": 1000,
              "IssueTransaction": 500,
              "PublishTransaction": 500,
              "RegisterTransaction": 10000
    }
複製程式碼

在創世區塊中的IssueTransaction交易中,直接將所有的NEO全部分發出去,這意味著什麼呢?意味者,如果你是StandbyValidators之一,那麼你現在已經實現了人生的幾十個小目標。

GAS的分發就相對比較複雜,因為GAS是需要挖掘的,而且還有一個衰減期。挖掘GAS涉及到NEO網路的共識過程,對NEO網路共識演算法感興趣的同學可以看我的另一篇博文《NEO從原始碼分析看共識協議》。在每個檢視週期開始的時候,議長新增礦工交易並將本地快取的交易資訊簽名後廣播給議員,議員進行驗證,在驗證通過的議員數量合法之後,議長建立新的區塊。每個區塊獎勵GAS數的計算在建立礦工交易的時候進行:

原始碼位置:neo/Consensus/ConsensusService.cs/CreateMinerTransaction

Fixed8 amount_netfee = Block.CalculateNetFee(transactions); // 獲取手續費(in-out-sysfee)
TransactionOutput[] outputs = amount_netfee == Fixed8.Zero ? new TransactionOutput[0] : new[] { new TransactionOutput
{
          AssetId = Blockchain.UtilityToken.Hash,
          Value = amount_netfee,
          ScriptHash = wallet.GetChangeAddress()
 } };
複製程式碼

可以看到這裡呼叫了Block的CalculateNetFee方法來計算當前區塊應該獲取的手續費,當前區塊的獎勵也自然歸屬於生當前區塊的賬戶。

0x03 賬戶餘額

前面講了那麼多,但是還是沒有把一個概念講清楚----"{'CH':'幣','EN':'Coin'}" ,幣到底是什麼呢?我們NEO錢包中顯示的餘額究竟是什麼呢? 在NEO網路世界裡,“幣”流通的唯一途徑就是交易,幣的整個生命週期都在交易中度過。註冊一種薪資產的RegisterTransaction方法是交易,資產分發的IssueTransaction 也是一種特殊交易,向礦工支付手續費的MinerTransaction也是交易,甚至每個區塊的獎勵分發ClaimTransaction方法也是一個交易。所以我們就先看看這個所有交易型別之父----交易基類Transaction。 Transaction關鍵欄位如下:

原始碼位置:neo/Core/Transaction.cs

        /// <summary>
        /// 交易型別
        /// </summary>
        public readonly TransactionType Type;
        /// <summary>
        /// 版本
        /// </summary>
        public byte Version;
        /// <summary>
        /// 該交易所具備的額外特性
        /// </summary>
        public TransactionAttribute[] Attributes;
        /// <summary>
        /// 輸入列表
        /// </summary>
        public CoinReference[] Inputs;
        /// <summary>
        /// 輸出列表
        /// </summary>
        public TransactionOutput[] Outputs;
        /// <summary>
        /// 用於驗證該交易的指令碼列表
        /// </summary>
        public Witness[] Scripts { get; set; }
複製程式碼

可以看出,對於每個交易,需要明確指定交易資產的來源Inputs以及交易資產的去向Outputs。每個錢包在組網同步區塊鏈時候,會對區塊鏈上面的每一筆交易進行檢查,如果這筆交易有Outputs指向自己的賬戶,就會新建CoinReference物件來記錄這個轉賬,然後嘗試在本地記錄的資產列表裡查詢,如果這筆轉賬已經被記錄過,則將這筆資產狀態修改為已確認。如果當前轉賬未被記錄過,則將reference物件作為KEY,新建Coin物件作為Value儲存在自己的資產列表中: 原始碼位置:neo/Wallets/WalletIndexer.cs/ProcessBlock

                for (ushort index = 0; index < tx.Outputs.Length; index++)
                {
                    TransactionOutput output = tx.Outputs[index];
                    if (accounts_tracked.ContainsKey(output.ScriptHash))
                    {
                        CoinReference reference = new CoinReference
                        {
                            PrevHash = tx.Hash,
                            PrevIndex = index
                        };
                        if (coins_tracked.TryGetValue(reference, out Coin coin))
                        {
                            coin.State |= CoinState.Confirmed;
                        }
                        else
                        {
                            accounts_tracked[output.ScriptHash].Add(reference);
                            coins_tracked.Add(reference, coin = new Coin
                            {
                                Reference = reference,
                                Output = output,
                                State = CoinState.Confirmed
                            });
                        }
                        batch.Put(SliceBuilder.Begin(DataEntryPrefix.ST_Coin).Add(reference), SliceBuilder.Begin().Add(output).Add((byte)coin.State));
                        accounts_changed.Add(output.ScriptHash);
                    }
複製程式碼

而每筆交易的資產來源也就來自於這個資產列表中記錄的資料。由於每一筆資產的都會記錄prehash,這也就意味著每筆資產都是可以在區塊鏈中進行溯源的,同時,我們也可以知道了另一個問題的答案,就是在NEO網路中,“幣”只是個數字概念,並沒有實體。 資產在使用者之間流通的示意圖如下:

資產在使用者之間流通

可以看到資產在被挖掘出來之後,整個流通的過程隨著交易的過程是個樹狀的結構。但是對於每一份資產來說,它的結構是這樣的:

資產流通結構

從示意圖中可以看出,針對每一份資產,其來源可以一直追隨到其最初被開採出來的那個區塊。

0x04 釋出新資產

NEO網路是支援使用者釋出屬於自己的資產的,前文也已經提到過,NEO和GAS都是在創世區塊中通過特殊交易的形式釋出的資產。那使用者如何釋出自己的資產呢? 這部分程式碼我從neo-gui-nel專案的原始碼中找到的入口: 原始碼位置:neo-gui-nel/neo-gui/UI/AssetRegisterDialog.cs

            using (ScriptBuilder sb = new ScriptBuilder())
            {
                sb.EmitSysCall("Neo.Asset.Create", asset_type, name, amount, precision, owner, admin, issuer);
                return new InvocationTransaction
                {
                    Attributes = new[]
                    {
                        new TransactionAttribute
                        {
                            Usage = TransactionAttributeUsage.Script,
                            Data = Contract.CreateSignatureRedeemScript(owner).ToScriptHash().ToArray()
                        }
                    },
                    Script = sb.ToArray()
                };
            }
複製程式碼

可以看到這裡是進行了系統呼叫"Neo.Asset.Create",這個命令會觸發StateMachine.cs中的Asset_Create方法:

原始碼位置:neo/SmartContract/StateMachie.cs/StateMachine

Register("Neo.Asset.Create", Asset_Create);
複製程式碼

在Asset_Create方法中,根據傳入的新資產的屬性資訊來構造合約。智慧合約部分的講解將在接下來的部落格中進行,此處不再詳細解釋。

最後:

本人正在進行NEO輕錢包微信小程式的開發,主要使用wepy框架,歡迎感興趣的朋友參與進來。NEOThinWallet for Wechat Miniprogram


群交流:795681763

原文:https://my.oschina.net/u/2276921/blog/1622364


相關文章