在這一部分,我們將學習如何使用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伺服器將根據該訂單的訂單號 提取或生成對應的以太坊地址,然後在支付頁面中展示該收款地址。為了 方便使用手機錢包的使用者,可以同時在支付頁面中展示該收款地址的二維碼。
使用者使用自己的以太坊錢包向該收款地址支付以太幣。由於網站的支付處理 程式在週期性地檢查該收款地址的餘額,一旦收到足額款項,支付處理程式 就可以根據收款地址將對應的訂單結束,併為使用者開通對應的服務。