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网络中,“币”只是个数字概念,并没有实体。 资产在用户之间流通的示意图如下:

NEO从源码分析看数字资产-LMLPHP

可以看到资产在被挖掘出来之后,整个流通的过程随着交易的过程是个树状的结构。但是对于每一份资产来说,它的结构是这样的:

NEO从源码分析看数字资产-LMLPHP

从示意图中可以看出,针对每一份资产,其来源可以一直追随到其最初被开采出来的那个区块。

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

04-06 06:10