0x00 前言
目前已写好的NEO多签分为两种,一种是基于NEL-GUI的GUI多签工具,仅支持对鉴权合约的多签工作;另一种是基于NEL-ThinSDK-cs的请钱包多签工具,终端操作,支持鉴权合约和应用合约的多签工作。本文将分为两大部分,第一部分介绍两个工具的使用方法,第二部分介绍技术实现细节。
0x01 NEL-GUI多签
这是第一部分的第一小结,介绍通过NEL-GUI进行鉴权合约的多签,这个工具的目的主要是方便对多签合约账户的转账操作。
首先下载NEL-GUI,也就是 God of NEO 李总基于NEO-GUI写的GUI工具。地址:
git clone https://github.com/NewEconoLab/neo-gui-nel.git
下载完之用VS打开,项目选择neo-gui:
然后启动项目,项目启动成功后会出现与熟悉的NEO-GUI很像的界面,不同的是NEL-GUI在选项卡里多了Plugin选项:
这个插件选项里面就是NEL开发的各种小工具了,我写的多签工具也放在这里面。
在Plugin下拉列表种选择多签工具,就可以打开多签工具的界面:
如果要对多签合约操作,只需要在合约地址列表种选择多签合约:
然后在下面的WIF框里添加需要进行签名的私钥就可以了:
最后填好目标账户地址和金额就可以了。
0x02 ThinSDK 多签
这里是第一部分的第二小节,主要介绍ThinSDK 多签的终端操作。
首先下载 ThinSDK-cs:
git clone https://github.com/NewEconoLab/neo-thinsdk-cs.git
然后用vs打开,选择smartContractDemo项目,编译运行。
输入multisign进入多签功能。
多签功能里的 <2> 是进行应用合约多签的。为了方便测试,我在测试网发布了一个多签的应用合约,合约代码:
public class Multi : SmartContract
{
public readonly static byte[] pubkey0 = { 2, 201, 40, 35, 13, 133, 217, 231, 75, 94, 76, 243, 237, 8, 84, 124, 118, 197, 253, 56, 208, 101, 194, 157, 78, 192, 203, 94, 102, 154, 16, 143, 55 };
public readonly static byte[] pubkey1 = { 3, 177, 14, 33, 182, 106, 2, 10, 89, 150, 79, 237, 3, 70, 190, 12, 174, 176, 227, 235, 111, 113, 254, 3, 207, 183, 188, 189, 16, 191, 31, 225, 223, };
public static bool Main(string method, object[] args)
{
if(Runtime.CheckWitness(Multi.pubkey0) && Runtime.CheckWitness(Multi.pubkey1))
{
return true;
}
return false;
}
}
其中pubkey0的私钥对应:L2ME3NL8XgWLa6XVVzCJyccPw3C7bnqHzWhtfdPaeZzzdX8MJSkj pubkey1的私钥对应:KwuezVnxhfUGiex7HM4ttrKBF4pTQRREkVmL1PW91gBZTRtsrLm9
合约地址为:0x4c0f57b61d997297560190b1e397fe6d58fce94a
合约信息:
在调用合约时,首先输入需要验证的私钥的数量,我这里输入2,然后分别输出私钥:
等15秒左右之后通过neodebug工具来查看合于执行结果:
交易id:0x500f037992dadcfb16fd55882109bd4c1629be8a19665f02e33d7dee9cc77632
可以看到这里是执行成功的。为了确定这里是不是真的有效,我们使用错误的私钥再调用一下:
错误的私钥如下:
- L4ZntdDCocMJi4ozpTw4uTtxtAFNNCP2mX6m3P9CMJN66Dt2YJqP
- KwEre52ubrRLr1zcTSnEbxM1eTzwqbUn7SVowknJsT9ypu8oTEku
重新调用多签合约:
交易id 0x096997ca95011f5a4856b8947fe4a57780aeb8643b0a2515ed0568190962cfc5
经以上测试,多签工具功能正常。
0x03 多签分析
首先分析多签合约账户。
多签合约账户的构造代码如下:
using (ScriptBuilder sb = new ScriptBuilder())
{
sb.EmitPush(m);
foreach (ECPoint publicKey in publicKeys.OrderBy(p => p))
{
sb.EmitPush(publicKey.EncodePoint(true));
}
sb.EmitPush(publicKeys.Length);
sb.Emit(OpCode.CHECKMULTISIG);
return sb.ToArray();
}
可以分析出多签合约脚本结构:
脚本执行的时候通过多签码进入多签合约验证逻辑,然后读取公钥数量,从脚本中加载公钥。
在对交易进行多签的时候,数据结构如下:
{
"type": "Neo.Core.ContractTransaction",
"hex": "8000000165046db244a897aeb4b04abdc154847083204f3eb64bbe4edb8736e3a45b6e50000001e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c6000e1f505000000007264db854ba951aa2244150df4345585c310444a",
"items": {
"0xc627d59f76070a2239d3ccd1fa86e44916be580e": {
"script": "52210387419467127d60bef1e11126cb1311609319e7a50c04781950498355cb8e1ad62102976eca673c5ccb15ca293f219734189b2738875b0e799856101e3347e2d7114a52ae",
"parameters": [{
"type": "Signature"
}, {
"type": "Signature"
}],
"signatures": {
"0387419467127d60bef1e11126cb1311609319e7a50c04781950498355cb8e1ad6": "f02796e55c82657e3f363ac9a3bf1a74e64a45f2051e9437a856d279a6febde38efaad0e038f37cccd7796acce9441852916cf056945ff0fd640497d7d3baf24"
}
}
}
}
可以看到根据最小签名的多少,会有相应数量的Signature,我们对这个signature进行分析,发现这个signatures相互之间没有关联,完全是私钥对hex字段的内容进行签名得到的。
于是遍历输入的Wif私钥,轮流对交易添加签名:
for (int j = 0; j < wifsList.Items.Count; j++)
{
var prikey = Wallet.GetPrivateKeyFromWIF(wifsList.Items[j].ToString());
KeyPair key = new KeyPair(prikey);
WalletAccount account = plugin_multisign.api.CurrentWallet.GetAccount(context.ScriptHashes[0]);
byte[] signature = context.Verifiable.Sign(key);
context.AddSignature(account.Contract, key.PublicKey, signature);
}
于是就可以实现多签了。
针对应用合约的多签于此有点不同,在调用合约的时候,首先会对合约中input的来源数量与签名数量进行验证:
if (hashes.Length != verifiable.Scripts.Length) return false;
这样代码位置在neo/Core/Helper.cs中,用在合约验证的时候。
hashes是通过input获取的地址哈希列表:
/// <summary>
/// 获取需要校验的脚本散列值
/// </summary>
/// <returns>返回需要校验的脚本散列值</returns>
public virtual UInt160[] GetScriptHashesForVerifying()
{
if (References == null) throw new InvalidOperationException();
HashSet<UInt160> hashes = new HashSet<UInt160>(Inputs.Select(p => References[p].ScriptHash));
hashes.UnionWith(Attributes.Where(p => p.Usage == TransactionAttributeUsage.Script).Select(p => new UInt160(p.Data)));
foreach (var group in Outputs.GroupBy(p => p.AssetId))
{
AssetState asset = Blockchain.Default.GetAssetState(group.Key);
if (asset == null) throw new InvalidOperationException();
if (asset.AssetType.HasFlag(AssetType.DutyFlag))
{
hashes.UnionWith(group.Select(p => p.ScriptHash));
}
}
return hashes.OrderBy(p => p).ToArray();
}
verifiable.Scripts则是添加的签名数量,也就是说如果想参与多签,那么就需要添加相应账户的input,这也就意味着,如果这个账户里没有资产,或者是一个新的账户,那么就无法参与多签。 因此在构造合约多签的时候,我通过遍历wif,对每个wif账户都添加了input:
List<TransactionInput> list_inputs = new List<TransactionInput>();
List<TransactionOutput> list_outputs = new List<TransactionOutput>();
foreach (var prikey in prikeys)
{
byte[] pubkey = ThinNeo.Helper.GetPublicKeyFromPrivateKey(prikey);
string address = ThinNeo.Helper.GetAddressFromPublicKey(pubkey);
Dictionary<string, List<Utxo>> dir = await Helper.GetBalanceByAddress(Config.api, address);
if (dir.ContainsKey(Nep55_1.id_GAS) == false)
{
Console.WriteLine("no gas");
return null;
}
TransactionInput input = new TransactionInput();
input.hash = dir[Nep55_1.id_GAS][0].txid;
input.index = (ushort)dir[Nep55_1.id_GAS][0].n;
list_inputs.Add(input);
TransactionOutput outputchange = new TransactionOutput();
outputchange.toAddress = ThinNeo.Helper.GetPublicKeyHashFromAddress(address);
outputchange.value = dir[Nep55_1.id_GAS][0].value;
outputchange.assetId = new Hash256(Nep55_1.id_GAS);
list_outputs.Add(outputchange);
}