NEO 构建一笔交易分析

构建一笔交易

通过MakeTransaction(TransferOutput[] outputs, UInt160 from = null)函数构建。

UInt160[] accounts;
if (from is null)
{
    accounts = GetAccounts().Where(p => !p.Lock && !p.WatchOnly).Select(p => p.ScriptHash).ToArray();
}
else
{
    if (!Contains(from))
    throw new ArgumentException($"The address {from.ToString()} was not found in the wallet");
    accounts = new[] { from };
}

如果未指定from账户,则首先调用GetAccount()获取钱包中所有非Lock和非WatchOnly的账户列表,按照脚本哈希排序。
若指定from账户,则判断账户是否包含在钱包中,不包含则抛出异常。
将获取的账户加入accounts。
把outputs中的输出按照(assetId, group, sum)分组,其中assetId表示需要输出的资产ID,group表示输出该类型资产的output集合,sum表示输出资产的总量。

对于每一种资产,分别对每个账户构建脚本,进入虚拟机执行得到资产余额,并记录在balances中。最后将所有余额相加得到资产总余额,如果小于需要输出的总量,则抛出余额不足异常。余额充足则进入下一步。

foreach (TransferOutput output in group)
{
    balances = balances.OrderBy(p => p.Value).ToList();
    var balances_used = FindPayingAccounts(balances, output.Value.Value);
    cosigners.UnionWith(balances_used.Select(p => p.Account));
    foreach (var (account, value) in balances_used)
    {
        sb.EmitAppCall(output.AssetId, "transfer", account, output.ScriptHash, value);
        sb.Emit(OpCode.THROWIFNOT);
    }
}

最主要的便是FindPayingAccounts(balances, output.Value.Value);根据balances和输出的数量选择合适的账户付款。
如果balances刚好与输出相等,则所有账户即为付款账户。
否则,遍历每个账户,如果某个账户的余额等于输出,则该账户为付款账户。如果账户余额大于输出,则该账户为付款账户,数量为需要输出数量,并更新账户余额。
如果所有账户的余额均小于输出,则从余额最多的账户开始向下遍历,依次加入付款账户,直到某账户余额大于需要付款的值,此时从余额最小的账户向上遍历,找到第一个余额大于需要输出的账户,将该账户作为付款账户,并更新余额。

例如有7个账户余额分别为1,2,3,4,5,6,7,输出为15.5。首先会选择7,6,两个账户,此时5>2.5,则从小到大遍历,其中第一个大于2.5的账户为3,所以输出3。最终的付款账户为(7,6,3),值为(7,6,2.5)。

随后记录所有付款账户地址,并按照账户依次构造虚拟机APPCALL转账脚本。计算所有账户Gas余额,并调用
MakeTransaction(snapshot, attributes, script, balances_gas);
该函数首先将脚本进入虚拟机test模式,计算Gas消耗量。如果Gas消耗超过免费额度,则超过部分向上取整作为SystemFee。(对remainder<0不清楚)
接下来计算NetworkFee,

foreach (UInt160 hash in hashes)
                {
                    byte[] witness_script = GetAccount(hash)?.Contract?.Script ?? snapshot.Contracts.TryGet(hash)?.Script;
                    if (witness_script is null) continue;
                    if (witness_script.IsSignatureContract())
                    {
                        size += 66 + witness_script.GetVarSize();
                        tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES64] + ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES33] + InteropService.GetPrice(InteropService.Neo_Crypto_CheckSig, null);
                    }
                    else if (witness_script.IsMultiSigContract(out int m, out int n))
                    {
                        int size_inv = 65 * m;
                        size += IO.Helper.GetVarSize(size_inv) + size_inv + witness_script.GetVarSize();
                        tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES64] * m;
                        using (ScriptBuilder sb = new ScriptBuilder())
                            tx.NetworkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(m).ToArray()[0]];
                        tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES33] * n;
                        using (ScriptBuilder sb = new ScriptBuilder())
                            tx.NetworkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(n).ToArray()[0]];
                        tx.NetworkFee += InteropService.GetPrice(InteropService.Neo_Crypto_CheckSig, null) * n;
                    }
                    else
                    {
                        //We can support more contract types in the future.
                    }
                }
                tx.NetworkFee += size * NativeContract.Policy.GetFeePerByte(snapshot);
                if (value >= tx.SystemFee + tx.NetworkFee) return tx;

NetworkFee分单签和多签的情况。且NetworkFee主要由交易size费用以及操作码费用两部分组成。

对于单签,首先是区块的固定部分size:

int size = Transaction.HeaderSize + attributes.GetVarSize()
+ script.GetVarSize() + IO.Helper.GetVarSize(hashes.Length);

加上脚本的size:

size += 66 + witness_script.GetVarSize();

66代表单签脚本验证时需要添加的PUSHBYTES64+64Bytes的长度(还有1不知道是什么)。

tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES64] + ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES33] + InteropService.GetPrice(InteropService.Neo_Crypto_CheckSig, null);

这部分包括单签验证需要的opcode的费用。
最后加上所有size与每字节的费用:

tx.NetworkFee += size * NativeContract.Policy.GetFeePerByte(snapshot);

就得到了NetworkFee的总量。
类似的,对于多签脚本:

int size_inv = 65 * m;
size += IO.Helper.GetVarSize(size_inv) + size_inv + witness_script.GetVarSize();
tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES64] * m;
using (ScriptBuilder sb = new ScriptBuilder())
    tx.NetworkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(m).ToArray()[0]];
    tx.NetworkFee += ApplicationEngine.OpCodePrices[OpCode.PUSHBYTES33] * n;
using (ScriptBuilder sb = new ScriptBuilder())
    tx.NetworkFee += ApplicationEngine.OpCodePrices[(OpCode)sb.EmitPush(n).ToArray()[0]];
    tx.NetworkFee += InteropService.GetPrice(InteropService.Neo_Crypto_CheckSig, null) * n;

同样是包括总的size费用以及多签验证脚本的操作码对应总费用。

tx.NetworkFee += size * NativeContract.Policy.GetFeePerByte(snapshot);
                if (value >= tx.SystemFee + tx.NetworkFee) return tx;

最后计算SystemFee和NetworkFee的和,如果Gas余额大于之和,则返回Tx,否则抛出异常。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容