0x00 前言

基于NEO进行dapp开发的过程中,基于UTXO的GAS并不方便作为dapp的代币使用,于是为了便于在DAPP中直接接入GAS,NEL颜值和技术担当李总开发了基于NEP5的SGAS合约,用于与GAS进行一比一兑换,搭建了应用合约与UTXO之间的桥梁。

尽管SGAS开发的初衷是为了NNS域名竞拍系统的功能实现,但是李总在设计的时候便将SGAS定义为一种通用的NEP5合约,因此任何需要在DAPP中使用GAS作为燃料的合约都可以直接使用SGAS。

0x01 获取SGAS

SGAS是NEP5的代币合约,本身并没有发行量,只有当用户向SGAS合约地址转入GAS时,SGAS合约才会发行对应数量的SGAS代币,并把新发行的SGAS转给该用户。

接下来的内容设计应用合约脚本的构建,建议阅读之前先了解下这方面的知识。

前文已经提到获取SGAS需要用户向SGAS合约地址充值指定金额的GAS,但是仅仅转账是没有办法实现SGAS发行的,因为GAS转账的过程是鉴权合约调用的过程,这个过程中SGAS合约无法进行数据的写入,而SGAS发行代币的方式就是通过数据的记录,这也就意味着SGAS没有办法进行发行操作。为了解决这个问题,在转账之后,还需要通过应用合约的调用过程来向用户地址解锁指定数量的SGAS,完成GAS兑换SGAS的过程。

	var sb = new ThinNeo.ScriptBuilder();
	sb.EmitParamJson([]);
    sb.EmitPushString('mintTokens');
    sb.EmitAppCall(DAPP_SGAS);
    return sb.ToArray();

在构造交易的时候,传入构造的脚本,这样交易在完成了转账之后会直接执行交易内的脚本。兑换SGAS的指令是 mintTokens ,SGAS合约在接收到这个指令后,会通过runtime获取交易中GAS的output,在对output进行验证和统计后,向该用户的账户解锁对应数量的SGAS。

合约交易构造过程如下:

	let tran = Transfer.makeTran(target, asset, count);
	tran.type = ThinNeo.TransactionType.InvocationTransaction;
	tran.extdata = new ThinNeo.InvokeTransData();
    //塞入脚本
	(tran.extdata as ThinNeo.InvokeTransData).script = script;
    return await Transfer.signAndSend(tran);

target是SGAS合约地址,makeTran是合约交易中向交易中添加input和output的标准过程。 因为通过GAS兑换SGAS大部分还是UTXO的操作,所以合约构造和操作起来并不是很复杂,但是当通过SGAS兑换GAS的时候,由于NEP5的诸多限制,操作起来就复杂多了。

0x02 兑换GAS

通过SGAS兑换GAS这个过程,如果真正理解了每一步的原理和原因,对于NEO合约的理解应该算是绝对的一大进步,我这么理解。我第一次看的时候懵懵懂懂,代码量也不大,自己为就看懂了,还跑了测试。但是当我用TS自己去写这块的调用代码的时候,才发现举步维艰,其操作之复杂,设计之精巧远超我想象。这也是为什么我想把SGAS的调用单独写成一篇博客来介绍的原因。

首先我们需要明确几件事:

  1. 兑换GAS用的GAS来自SGAS合约账户本身的GAS。
  2. 每个UTXO只能使用一次。
  3. 转账是鉴权合约操作,而鉴权操作只能读取存储区,不能写存储区。
  4. 应用合约无法发起UTXO交易。

以上四条是无法违背的,在兑换GAS的过程中,必须遵循。那现在就需要思考以下几个问题:

  1. SGAS合约如何转账GAS?
  2. SGAS如何控制转账金额?
  3. 如何转GAS给指定账户?

首先我们分析第一个问题,SGAS合约如何转GAS。为了账户的安全,NEO合约无法进行主动的UTXO转账,必须由用户进行鉴权调用,执行转账操作。也就是说,SGAS无法主动退回GAS给用户,必须由用户来从SGAS账户转出GAS。

第二个问题如何控制转账金额。有NEO账户并且进行过NEO或者GAS转账的童鞋应该知道,只要你有转账权限,那么转账的金额完全是由你自己指定的,也就是说,只要你愿意,那你完全可以转出一个账户里的所有资产。但是对于SGAS合约来说,这样就不行,一旦给了用户转账的权限,我们完全无法依靠上帝保佑所有用户只会转出他希望兑换的金额的GAS。

也就是说我们现在我们必须给用户转账的权限,但是又需要限制用户转账的金额。这怎么办?

同时,由于GAS本身是艺UTXO的形式存在,而UTXO本身又是无法分割,一次使用的,我们无法保证对于每一个用户希望兑换的GAS额度都刚好有对应金额的GAS的UTXO存在,那我们如何分割UTXO来构造出用户需要的UTXO呢?

当然,这些问题都是我在看到李总的解决方案之后,又通过几乎一行代码一行代码向印炜大神请教之后才整理出的。

接下来就开始分析李总对于这一系列几乎使得SGAS方案陷于不可行问题的解决方案。

第一步: 获取可用UTXO。

由于转账操作无论如何都需要用户主动发起,所以用户必须首先获取SGAS合约地址的GAS的UTXO。

第二步:查询UTXO可用性。

多出这一步的原因是由于兑换GAS操作的复杂性,在一个共识周期内无法完成所有操作,所以本轮共识很可能会有之前共识周期中已经被标记的UTXO存在,我们需要在使用之前检测以下这个UTXO是否被标记过。

	var sb = new ThinNeo.ScriptBuilder();
	sb.EmitParamJson([output_hash]);
    sb.EmitPushString('getRefundTarget');
    sb.EmitAppCall(DAPP_SGAS);
    return sb.ToArray();

第三步:拆分UTXO。

由于UTXO只能使用一次,每次被使用过后就会报废,因此我们只能通过构造才能获得我们需要数额的UTXO,通过SGAS合约给SGAS合约自己的地址转账构造出用户需要的UTXO。同时用户需要记录下这个UTXO。

该步骤主体就是普通的转账操作,直接构造ContractTransaction就可以,需要注意的地方是,这里用的input和指向的output地址都需要是SGAS合约地址。此外,在添加见证人的时候,除了需要添加用户签名之外,还需要添加sgas合约脚本为见证脚本。

	let sb = new ThinNeo.ScriptBuilder();
    sb.EmitPushString("whatever")
    sb.EmitPushNumber(new
	Neo.BigInteger(250));
    tran.AddWitnessScript(SGASScript,
	sb.ToArray());

第四步:标记UTXO。

由于转账GAS是个鉴权操作,所以我们还需要在转账之后进行一个合约调用来标记用户生成的对应UTXO,将这个UTXO标记为只能该用户领取。

标记UTXO本身是GSAS合约的工作,但是这部分功能还需要用户进行触发才能执行,在用户拆分UTXO的时候,需要构造一个脚本来进行SGAS合约的调用:

	var sb = new ThinNeo.ScriptBuilder();
	sb.EmitParamJson([address_hash]);
    sb.EmitPushString('refund');
    sb.EmitAppCall(DAPP_SGAS);
    return sb.ToArray();

第五步:领取GAS。

用户在经过一个共识周期确认UTXO已经拆分完成并且标记完成之后就可以通过记录的UTXO来进行SGAS转账,由于在SGAS应用合约执行期间用户地址就已经和该UTXO进行了绑定因此,在转账的时候,只有该用户可以转走这个UTXO。

至此,SGAS兑换GAS完成。

光是看描述原理的篇幅都知道这个SGAS兑换GAS是多么复杂的一个过程,当然这也是因为目前NEO的一些限制导致的,如果未来NEO对DAPP开放更多的接口和权限,这个过程肯定会大大简化。

不知道看到这里的人有没有对李总的清奇脑洞震撼到,这鉴权调用和应用调用翻来倒去,对此稍有一点不清楚都会云里雾里不知所云。尤其是当所有的代码都写在同一个文件里的时候,感觉都是两种人格在战斗。

参考资料:

SGAS:https://github.com/NewEconoLab/neo-ns/blob/master/dapp_sgas/sgas.cs

ThinSDK-cs:https://github.com/NewEconoLab/neo-thinsdk-cs/blob/master/smartContractDemo/tests/nns/sgas.cs

nel-wallet-vue: https://github.com/NewEconoLab/nel-wallet-vue

04-06 07:02