在这一部分,我们将学习如何使用C#管理以太坊账户,这包括:
- 了解私钥、公钥和账户的关系
- 离线创建以太坊账户
- 导入其他账户私钥
- 创建和使用钱包
- 创建和使用账户凭证
以太坊作为一个去中心化的系统,必然不会采用中心化的账户管理 方案 —— 没有一个中心数据库来保存以太坊平台上的所有账户信息。 事实上,以太坊使用非对称密钥技术来进行身份识别,一个以太坊 账户对应着一对密钥:
在这一部分的内容里,我们将使用Nethereum.Signer命名空间 中的类来管理密钥、账户和钱包。
私钥、公钥与地址
以太坊使用非对称密钥对来进行身份识别,每一个账户都有 对应的私钥和公钥 —— 私钥用来签名、公钥则用来验证签名 —— 从而 在非可信的去中心化环境中实现身份验证。
事实上,在以太坊上账户仅仅是对应于特定非对称密钥对中公钥的20字节 哈希
从私钥可以得到公钥,然后进一步得到账户地址,而反之则无效。 显然,以太坊不需要一个中心化的账户管理系统,我们可以根据以太坊约定 的算法自由地生成账户。
在C#中,可以使用EthECKey类来生成密钥对和账户地址。一个EthECKey 实例封装一个私钥,同时也提供了访问公钥和地址的方法:
例如,下面的代码首先使用EthECKey的静态方法GenerateKey()创建一个 随机私钥并返回EthECKey实例,然后通过相应的实例方法读取私钥、公钥 和账户地址:
EthECKey keyPair = EthECKey.GenerateKey(); string privateKey = keyPair.GetPrivateKey(); byte[] publicKey = keyPair.GetPubKey(); string address = keyPair.GetPublicAddress(); Console.WriteLine("Private Key => " + privateKey); Console.WriteLine("Public Key => " + publicKey.ToHex(true)); Console.WriteLine("Address => " + address); Console.ReadLine();
GetPubKey()方法返回的是一个byte[]类型的字节数组,因此我们使用 静态类HexByteConvertorExtensions的静态方法ToHex()将其转换为16进制 字符串,参数true表示附加0x前缀。 ToHex()的原型如下:
注意HexByteConvertorExtensions是静态类而且ToHex()的第一个参数为 byte[]类型,因此byte[]类型的对象可以直接调用ToHex()方法。
namespace KeyAndAddressDemo { class KeyAndAddress { public void Run() { EthECKey keyPair = EthECKey.GenerateKey(); string privateKey = keyPair.GetPrivateKey(); byte[] publicKey = keyPair.GetPubKey(); string address = keyPair.GetPublicAddress(); Console.WriteLine("Private Key => " + privateKey); Console.WriteLine("Public Key => " + publicKey.ToHex(true)); Console.WriteLine("Address => " + address); Console.ReadLine(); } } }
class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); Console.WriteLine("Key and Address"); KeyAndAddress demo = new KeyAndAddress(); demo.Run(); Console.ReadLine(); } }
导入私钥
我们已经知道,只有私钥是最关键的,公钥和账户都可以从私钥一步步 推导出来。
假如你之前已经通过其他方式有了一个账户,例如使用Metamask创建的钱包,那么可以把该账户导入C#应用,重新生成公钥和账户地址:
using Nethereum.Signer; using System; namespace ImportKeyDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); EthECKey keyPair = EthECKey.GenerateKey(); string privateKey = keyPair.GetPrivateKey(); string address = keyPair.GetPublicAddress(); Console.WriteLine("Original Address => " + address); //import EthECKey recovered = new EthECKey(privateKey); Console.WriteLine("Recoverd Address => " + recovered.GetPublicAddress()); Console.ReadLine(); } } }
keystore钱包
鉴于私钥的重要性,我们需要以一种安全地方式保存和迁移,而不是简单地 以明文保存到一个文件里。
keystore允许你用加密的方式存储密钥。这是安全性(一个攻击者需要 keystore 文件和你的钱包口令才能盗取你的资金)和可用性(你只需要keystore 文件和钱包口令就能用你的钱了)两者之间完美的权衡。
下图是一个keystore文件的内容:
从图中可以看出,keystore的生成使用了两重算法:首先使用你指定的钱包口令 采用kpf参数约定的算法生成一个用于AES算法的密钥,然后使用该密钥 结合ASE算法参数iv对要保护的私钥进行加密。
由于采用对称加密算法,当我们需要从keystore中恢复私钥时,只需要 使用生成该钱包的密码,并结合keystore文件中的算法参数,即可进行 解密出你的私钥。
KeyStoreService
KeyStoreService类提供了两个方法,用于私钥和keystore格式的json之间的转换:
下面的代码创建一个新的私钥,然后使用口令7878生成keystore格式 的json对象并存入keystore目录:
EthECKey keyPair = EthECKey.GenerateKey(); string privateKey = keyPair.GetPrivateKey(); Console.WriteLine("Original Key => " + privateKey); KeyStoreService ksService = new KeyStoreService(); string password = "7878"; string json = ksService.EncryptAndGenerateDefaultKeyStoreAsJson(password, keyPair.GetPrivateKeyAsBytes(), keyPair.GetPublicAddress()); EnsureDirectory("keystore"); string fn = string.Format("keystore/{0}.json", ksService.GenerateUTCFileName(keyPair.GetPublicAddress())); File.WriteAllText(fn, json); Console.WriteLine("Keystore Saved => " + fn);
尽管可以从私钥推导出账户地址,但EncryptAndGenerateDefaultStoreAsJson()方法 还是要求我们同时传入账户地址,因此其三个参数依次是:私钥口令、私钥、对应的地址。
GenerateUTCFileName()方法用来生成UTC格式的keystore文件名,其构成如下:
解码keystore
在另一个方向,使用DecryptKeyStoreFromJson()方法,可以从keystore 来恢复出私钥。例如,下面的代码使用同一口令从钱包文件恢复出私钥并重建密钥对:
byte[] recoveredPrivateKey = ksService.DecryptKeyStoreFromJson(password, json); Console.WriteLine("Recovered Key => " + recoveredPrivateKey.ToHex(true));
using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.KeyStore; using Nethereum.Signer; using System; using System.IO; namespace KeystoreDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); EthECKey keyPair = EthECKey.GenerateKey(); string privateKey = keyPair.GetPrivateKey(); Console.WriteLine("Original Key => " + privateKey); KeyStoreService ksService = new KeyStoreService(); string password = "7878"; string json = ksService.EncryptAndGenerateDefaultKeyStoreAsJson(password, keyPair.GetPrivateKeyAsBytes(), keyPair.GetPublicAddress()); EnsureDirectory("keystore"); string fn = string.Format("keystore/{0}.json", ksService.GenerateUTCFileName(keyPair.GetPublicAddress())); File.WriteAllText(fn, json); Console.WriteLine("Keystore Saved => " + fn); byte[] recoveredPrivateKey = ksService.DecryptKeyStoreFromJson(password, json); Console.WriteLine("Recovered Key => " + recoveredPrivateKey.ToHex(true)); Console.ReadLine(); } private static void EnsureDirectory(string path) { if (Directory.Exists(path)) return; Directory.CreateDirectory(path); } } }
{ "crypto": { "cipher": "aes-128-ctr", "ciphertext": "38a0299356d70c3cd54eda1c5f8f58d3b84d0a7c377295b4c6a630f81dbf610a", "cipherparams": { "iv": "2aefcf10a52376f9456992e470ec3234" }, "kdf": "scrypt", "mac": "feba237a6258625be86b46fc44d09f4fc3e4e7ea4cc6ce7db4bce47508ab627f", "kdfparams": { "n": 262144, "r": 1, "p": 8, "dklen": 32, "salt": "7e6ff7ae6ae7e83c1f5d8f229458a7e102e55023f567e1f03cd88780bdc18272" } }, "id": "cb7b8d03-c87a-446a-b41e-cede3d936b59", "address": "0x78E4a47804743Cc673Ba79DaF2EB03368e4be145", "version": 3 }
离线账户与节点管理的账户
在以太坊中,通常我们会接触到两种类型的账户:离线账户和节点管理的账户。
在前面的课程中,我们使用EthECKey创建的账户就是离线账户 —— 不需要 连接到一个以太坊节点,就可以自由地创建这些账户 —— 因此被称为离线账户。 离线账户的私钥由我们(应用)来管理和控制。
另一种类型就是由节点创建或管理的账户,例如ganache自动随机生成的账户, 或者在geth这样的节点软件中创建的账户。这些账户的私钥由节点管理,通常 我们只需要保管好账户的口令,在需要交易的时候用口令解锁账户即可。ganache仿真器的账户不需要口令即自动解锁。因此当使用ganache作为节点 时,在需要传入账户解锁口令的地方,传入空字符串即可。
对于这两种不同的账户类型,Nethereum提供了不同的类来封装,这两种 不同的类将影响后续的交易操作:
离线账户:Account
Account类对应于离线账户,因此在实例化时需要传入私钥:
BigInteger chainId = new BigInteger(1234); Account account = new Account(privateKey, chainId);
参数chainId用来声明所连接的的是哪一个链,例如公链对应于1,Ropsten 测试链对应于4,RinkeBy测试链对应于5...对于ganache,我们可以随意指定 一个数值。
另一种实例化Account类的方法是使用keystore文件。例如下面的代码 从指定的文件载入keystore,然后调用Account类的静态方法
string privateKey = "0x197b09426db81c7ebaefbcea4ab09c9379c23628c73e20c5475b0f13e7eacaba"; BigInteger chainId = new BigInteger(1234); Account account = new Account(privateKey, chainId);
节点管理账户:ManagedAccount
节点管理账户对应的封装类为ManagedAccount,实例化一个节点管理账户 只需要指定账户地址和账户口令:
Web3 web3 = new Web3("http://localhost:7545"); string[] accounts = await web3.Eth.Accounts.SendRequestAsync(); ManagedAccount account = new ManagedAccount(accounts[0], "");
Nethereum提供这两种不同账户封装类的目的,是为了在交易中可以使用 一个抽象的IAccount接口,来屏蔽交易执行方式的不同。
using Nethereum.Web3; using Nethereum.Web3.Accounts; using Nethereum.Web3.Accounts.Managed; using System; using System.IO; using System.Numerics; using System.Threading.Tasks; namespace AccountDemo { class Program { static void Main(string[] args) { Console.WriteLine("cuiyw-test"); CreateAccountFromKey(); CreateAccountFromKeyStore(); CreateManagedAccount().Wait(); Console.ReadLine(); } public static void CreateAccountFromKey() { Console.WriteLine("create offline account from private key..."); string privateKey = "0x197b09426db81c7ebaefbcea4ab09c9379c23628c73e20c5475b0f13e7eacaba"; BigInteger chainId = new BigInteger(1234); Account account = new Account(privateKey, chainId); Console.WriteLine(" Address => " + account.Address); Console.WriteLine(" TransactionManager => " + account.TransactionManager); } public static void CreateAccountFromKeyStore() { Console.WriteLine("create offline account from keystore..."); string fn = "keystore/UTC--2019-04-21T08-15-35.6963027Z--78E4a47804743Cc673Ba79DaF2EB03368e4be145.json"; string json = File.ReadAllText(fn); string password = "7878"; BigInteger chainId = new BigInteger(1234); Account account = Account.LoadFromKeyStore(json, password, chainId); Console.WriteLine(" Address => " + account.Address); Console.WriteLine(" TransactionManager => " + account.TransactionManager); } public static async Task CreateManagedAccount() { Console.WriteLine("create online account ..."); Web3 web3 = new Web3("http://localhost:7545"); string[] accounts = await web3.Eth.Accounts.SendRequestAsync(); ManagedAccount account = new ManagedAccount(accounts[0], ""); Console.WriteLine(" Address => " + account.Address); Console.WriteLine(" TransactionManager => " + account.TransactionManager); } } }
为网站增加以太币支付功能
在应用中生成密钥对和账户有很多用处,例如,用户可以用以太币 在我们的网站上购买商品或服务 —— 为每一笔订单生成一个新的以太坊 地址,让用户支付到该地址,然后我们检查该地址余额即可了解订单 的支付情况,进而执行后续的流程。
为什么不让用户直接支付到我们的主账户?
稍微思考一下你就明白,创建一个新地址的目的是为了将支付与订单 关联起来。如果让用户支付到主账户,那么除非用户支付时在交易数据 里留下对应的订单号,否则你无法简单的确定收到的交易与订单之间的 关系,而不是所有的钱包软件—— 例如coinbase —— 都支持将留言包含 在交易里发送到链上。
解决方案如下图所示:
当用户选择使用以太币支付一个订单时,web服务器将根据该订单的订单号 提取或生成对应的以太坊地址,然后在支付页面中展示该收款地址。为了 方便使用手机钱包的用户,可以同时在支付页面中展示该收款地址的二维码。
用户使用自己的以太坊钱包向该收款地址支付以太币。由于网站的支付处理 进程在周期性地检查该收款地址的余额,一旦收到足额款项,支付处理进程 就可以根据收款地址将对应的订单结束,并为用户开通对应的服务。