在这一部分, 我们将学习如何使用 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 服务器将根据该订单的订单号 提取或生成对应的以太坊地址, 然后在支付页面中展示该收款地址. 为了 方便使用手机钱包的用户, 可以同时在支付页面中展示该收款地址的二维码.
用户使用自己的以太坊钱包向该收款地址支付以太币. 由于网站的支付处理 进程在周期性地检查该收款地址的余额, 一旦收到足额款项, 支付处理进程 就可以根据收款地址将对应的订单结束, 并为用户开通对应的服务.
来源: https://www.cnblogs.com/5ishare/p/10745479.html