由Trust Wallet理解以太坊錢包管理和智慧合約
在前一篇文章中, 已經介紹過Trust
的專案架構、業務流程等了。這篇文章將會解讀一些核心的功能, 包括前一篇文章提到的EtherKeystore
這個業務類, 還有網路層的如何呼叫智慧合約、其它呼叫合約的方式, 以及以太坊交易的結構和流程等。
錢包管理
錢包管理就要提到一個類EtherKeystore
, 應用的核心業務的處理類, 有錢包管理(建立、刪除、匯入、匯出)、助記詞轉化、簽名工作、私鑰管理等功能。
EtherKeystore
中使用了由Trust
開源的了兩個庫: TrustKeystore: 用於管理錢包的通用以太坊金鑰庫。TrustCore: 區塊鏈核心的資料結構和演算法。還有CryptoSwift
錢包建立
在EtherKeystore
類中, 封裝了錢包的建立, 主要使用了TrustKeystore
庫、TrustCore
庫中關於公私鑰和地址的API、以及密碼學的庫CryptoSwift
。我下面所說的整個流程也包括這些庫中的原始碼邏輯, 先建立金鑰對(或者助記詞), 再利用本地生成的隨機密碼對金鑰進行加密儲存, 然後生成錢包, 將錢包、獲取私鑰的密碼以及KeystoreKey
儲存到本地。
Trust
預設的方式是生成助記詞, 這種方式其實是私鑰的一種管理方式, 助記詞是由私鑰通過某種演算法派生出來的, TrustCore
中的Crypto
就是這個功能。而且當你用到私鑰的時候, 你還可以把你的助記詞通過對應的演算法在轉譯成私鑰。所以它只是一種私鑰的儲存方式, 下面文章中以私鑰為例來講述整個流程。
建立公鑰私鑰
建立錢包就相當於生成一對金鑰, 公鑰(PublicKey)和私鑰(PrivateKey)。公鑰其實就相當於你賬戶在區塊鏈中的地址(Address); 私鑰就相當於你錢包的賬號密碼, 它是證明你是錢包主人的唯一證明, 一旦丟失就不可找回。當然, 公鑰並不完全等於地址, 地址是由公鑰經過一系列的演算法生成的, 需要經過SHA3-256
(Keccak)雜湊然後轉化為符合EIP55
規則的字串。
(sk, pk) = generateKeys(keysize)
上面這段虛擬碼中, generateKeys方法把 keysize作為輸入, 來產生一對公鑰和私鑰。私鑰sk被安全儲存,並用來簽名一段訊息;公鑰pk是人人都可以找到的,拿到它,就可以用來驗證你的簽名。下圖是TrustCore
keysize
定義, 私鑰是32位元組, 公鑰地址是20位元組, 所以十六進位制的私鑰長度為64位, 而公鑰地址長度為40位。
具體來說, 建立公鑰和私鑰的功能是由TrustCore
中的PrivateKey
來完成的。而且是通過蘋果官方的Security
庫來建立的公鑰和私鑰, 經過整理金鑰對生成和獲取過程如下:
func getPrivatePublicKey() -> (String, String) {
let privateAttributes: [String: Any] = [
kSecAttrIsExtractable as String: true,
]
let parameters: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: 256,
kSecPrivateKeyAttrs as String: privateAttributes,
]
// PrivateKey To String
guard let privateKey = SecKeyCreateRandomKey(parameters as CFDictionary, nil) else {
fatalError("Failed to generate key pair")
}
guard var priRepresentation = SecKeyCopyExternalRepresentation(privateKey, nil) as Data? else {
fatalError("Failed to extract new private key")
}
defer {
priRepresentation.replaceSubrange(0 ..< priRepresentation.count, with: repeatElement(0, count: priRepresentation.count))
}
let priData = Data(priRepresentation.suffix(32))
var priString = ""
for byte in priData {
priString.append(String(format: "%02x", byte))
}
// PublicKey To String
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
fatalError("Failed to get publickey")
}
guard var pubRepresentation = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
fatalError("Failed to extract new public key")
}
defer {
pubRepresentation.replaceSubrange(0 ..< pubRepresentation.count, with: repeatElement(0, count: pubRepresentation.count))
}
let pubData = Data(pubRepresentation.suffix(32))
var pubString = ""
for byte in pubData {
pubString.append(String(format: "%02x", byte))
}
return (priString, pubString)
}
使用隨機密碼對私鑰加密
在生成了私鑰之後, 將在KeystoreKeyHeader
類中, 這裡使用了CryptoSwift
(安全加密演算法集合的庫)對私鑰進行加密。使用AES-128
演算法進行對稱加密後, 將這些資料以KeystoreKeyHeader
型別儲存在KeystoreKey
中。
建立 Wallet
在前兩個步驟的基礎之上, 就可以建立一個Wallet
了, 並將Wallet
加入到當前的賬戶中。也會計算或者獲取一些引數儲存在Wallet
中, 如公鑰地址Address, Account、KeystoreKey
等。
儲存到本地
KeyStore
會將當前錢包賬戶的KeystoreKey
資料儲存在本地檔案中。檔案以"UTC+時間戳+錢包唯一標識"為名稱儲存在本地, 其中儲存的是上面KeystoreKey
的資料。這些資料使用者每次啟動時, 將會由這些資料再次生成所有的Wallet
資料。
當然, 私鑰當然也是需要儲存的, 前一篇文章中說過了, 這樣的敏感資訊儲存在keychain
中。但keychain
並不是直接儲存這私鑰, 而是將獲取私鑰的密碼儲存在其中了。以錢包的id為key值, 將獲取私鑰的密碼儲存子keychain
之中, 拿出密碼後, 再使用KeystoreKey
進行AES-128
對稱解密, 獲取私鑰, 便可以使用了。所以, KeystoreKey
這個類的主要功能是對私鑰和助記詞的管理以及對私鑰的加解密。
另外, 這樣擁有PrivateKey
的錢包賬戶是不需要儲存在Realm
資料庫中的。只有一種需要儲存到本地的Realm
資料庫中, 那就是匯入地址錢包, 下面將會說明。
錢包的匯入
錢包匯入相對於錢包的建立來說, 只是不需要自己去生成公鑰和私鑰對了, 剩下的流程還是一樣的。當然匯入時會有三種方式, 除了之前提到的私鑰和助記詞的方式, 還有地址的方式。
錢包地址是公開的, 當然你也可以匯入, 也可以檢視這個錢包的任何資料, 但因為你不具備它的私鑰, 所以你不可以進行簽名或者說任何寫入區塊鏈的操作。所以這種方式, 就不需要KeyStore
進行操作了, 只需要EtherKeystore
進行本地操作, 將其放入本地的Realm
資料庫中, 那就是匯入地址錢包。當啟動應用時, 將會以兩者組成的資料為本地錢包列表。
錢包匯出、刪除等
錢包匯出, 當然也會分三種方式, 私鑰和助記詞的方式, 還有地址的方式。在keychain
中將密碼取出, 然後通過KeystoreKey
解密到私鑰或者助記詞, 匯出。地址的方式, 就是直接匯出地址。
如果你把上面的錢包建立條理理清楚了, 你就可以想到刪除只是錢包建立的逆過程, 但沒有那麼複雜。只需要驗證你的私鑰是正確的就可以將你本地KeystoreKey
刪除了。
EtherKeystore 模組結構圖
下圖中畫了 EtherKeystore
在建立或者匯入錢包時的流程, 可一清楚的看到這個模組的結構。綠色的部分是TrustCore
和TrustKeystore
庫中的呼叫, 淺藍色是資料層的一些處理。
智慧合約
在前一篇文章中的網路層中, 對只能合約以及具體網路層業務邏輯沒有做詳細說明。這裡將會討論幾個問題, 網路層具體方案, 以太坊智慧合約的呼叫。
合約呼叫方式
JavaScript API
雖然看起來是兩種 API, 其實後者是通過RPC呼叫與本地節點進行通訊的。也就可以理解為 JavaScript API
是對 JSON RPC API
的封裝, 方便了從JavaScript應用程式內部與ethereum節點
通訊。官方開源的庫web3.js就是做了這個事情。
JSON RPC API
JSON-RPC是一種輕量級的遠端過程呼叫(RPC)協議。該規範主要定義了一些資料結構和處理的規則。它與傳輸無關, 因為這些概念可以通過Socket
、HTTP
, 或者其它的訊息傳遞環境中使用。它使用 JSON(RFC 4627)作為資料格式。
預設的JSON-RPC端點:
RPC的支援:情況
cpp-ethereum | go-ethereum | py-ethereum | parity | |
---|---|---|---|---|
JSON-RPC 1.0 | ✓ | |||
JSON-RPC 2.0 | ✓ | ✓ | ✓ | ✓ |
Batch requests | ✓ | ✓ | ✓ | ✓ |
HTTP | ✓ | ✓ | ✓ | ✓ |
IPC | ✓ | ✓ | ✓ | |
WS | ✓ | ✓ |
合約呼叫
當然, 在Trust
的 iOS端是通過 JSON RPC Over HTTP
的方式進行智慧合約呼叫的。專案中針對合約呼叫的請求, 網路層的設計是 APIKit + JSONRPCKit 的方式。
JSON RPC Over HTTP
在專案中, 以太坊智慧合約呼叫都是JSON RPC Over HTTP
的方式, 而且所使用的以太坊節點前一篇文章網路層中就提到過。
var remoteURL: URL {
let urlString: String = {
switch self {
case .main: return "https://api.trustwalletapp.com"
case .classic: return "https://classic.trustwalletapp.com"
case .callisto: return "https://callisto.trustwalletapp.com"
case .poa: return "https://poa.trustwalletapp.com"
case .gochain: return "https://gochain.trustwalletapp.com"
}
}()
return URL(string: urlString)!
}
網路層結構應該如下圖所示:
當你明白這種網路結構後, 在來看Trust
中, 統一使用xxxRequest
的命名來封裝JSONRPCKit
的應用元件。其中定義了method
、parameters
、response的轉化等, 這裡的method
就是呼叫以太坊智慧合約的介面名稱。專案中, 統一使用xxxProvider
的命名, 按功能對APIKit
的請求元件進行封裝。當然, 沒有這層抽象也是可以的。
下面圖片中, Trust
中涉及到一些 API: eth_estimateGas
、eth_sendRawTransaction
、eth_gasPrice
、eth_blockNumber
、eth_getTransactionByHash
、eth_call
、eth_getBalance
。下面詳細列出了專案中合約呼叫的類和具體使用的以太坊 API, 它們是一一對應的關係。
Web3.swift
Trust
專案中並沒有使用web3
的方式進行合約呼叫, 但是我還是想說一說這種方式。這是因為除了以太坊官方的對 JavaScript API的web3
庫以外, 還有一個純Swift寫的庫Web3.swift。它是可以用於在以太坊網路中籤署交易並與智慧合約進行互動, 而且可以直接使用於你的iOS客戶端。假如你的網路層用Web3.swift
替換APIKit
+ JSONRPCKit
這樣的話, 將會降低網路層結構複雜度, 且程式碼簡潔性也提高了。
網路層其他請求
在Trust
中, 獲取區塊鏈上的資料, 其實分為兩種, 一種是上面提到的直接通過智慧合約獲取的資料。另一種就是Trust
官網已經封裝過的一些介面, 它們是關於多幣種的, 大多需要在區塊鏈中去查詢, 介面不單一且有大工作量的請求, 如transactions, getTokes等。這些介面是直接使用網路庫Moya
進行封裝的, 而沒有呼叫智慧合約。而這些HTTP
請求的伺服器是:
let trustAPI = URL(string: "https://public.trustwalletapp.com")
TrustAPI
類中將這些介面清楚的列舉了出來, 並且將它們集體封裝在TrustNetwork
類中來管理。
到這裡, 就將前一篇文章所遺留的網路層的詳情補充完整了。
交易
交易, 即Transaction
, 我這裡是指轉賬交易。上面簡單介紹過以太坊上的交易, 並瞭解交易的 API是 eth_sendRawTransaction
。下面介紹下在專案中, 轉賬交易的結構, 以及轉賬交易在原生App
和DApp
中分別是怎樣的流程。
交易的結構
在專案的主目錄中, 有一個Transfer
模組, 這個模組主要功能就是處理轉賬交易。在形成一個交易前, 將以定義的Transfer
類為基礎, 封裝出一個Transaction
的結構, 這個結構中包含著傳送地址、接收地址、幣的數量、交易費等等所有交易相關的資料。最後定義TransactionConfigurator
類, 對交易進行最外層的業務管理和校驗。在TransactionConfigurator
中經過校驗、簽名之後的交易才會傳送給以太坊節點, 並在礦工挖到礦並將此交易放入區塊中, 當前Token
的轉賬才算完成。
Transfer
Transfer
中主要包含當前轉賬發起方的Token
相關的一些資料, 如地址、合約等等。而且它有型別之分, 及TransferType
的三種類型, 分別是Coin
、ERC20
、Dapp
, 前兩種是原生App的方式, 後一種是瀏覽器中 DApp的方式。
UnconfirmedTransaction
UnconfirmedTransaction
中, 主要包含當前Token
的一些資訊, 即Transfer
。還有一個轉賬接收方的資訊, 如地址、幣的數量、交易費、Data等等。
TransactionConfigurator
TransactionConfigurator
類, 對交易進行最外層的業務管理和校驗。它其中包含全量的UnconfirmedTransaction
資料, 且還有校驗餘額是否有效、交易費、交易限制等功能, 最終生成一個經過校驗後的完整SignTransaction
。
DappAction
DappAction
只在DApp
進行轉賬交易時, 才能使用到的類。而上面的三個無論是原生App
還是DApp
都需要使用到的交易結構類。DappAction
會將瀏覽器中傳入的訊息進行解析, 得到Method
以及其它資料, 並封裝在DappCommand
裡面。然後以瀏覽器的web標題和URL生成的DAppRequester
等元素生成Transfer
。最終這兩者, 共同生成的DappAction
來決定需要進行哪種操作、需要呼叫合約中的哪種API、還有交易的一些資料等。
交易結構圖
交易的流程
交易流程自然也是分成兩, 一種是原生App
中發起的交易, 一種是DApp
在瀏覽器中發起的交易。之前提及的交易結構會在流程中以資料的形式作為重要的參與部分, 這裡主要說明交易從發起至交易完成的主要流程, 以及需要呼叫哪些以太坊智慧合約的 API。
原生App發起的交易
交易發起。 在原生App
的錢包首頁有著當前賬戶下的Token
列表, 而發起的轉賬交易是在某個具體Token
中操作的。所以當前的Transfer
是已經具備的, 而具體的交易接收地址、幣的數量以及gas費就需要使用者在SendCoordinator
的模組中自行輸入了。
構建交易資料。 交易發起後, 我們就具備了構建UnconfirmedTransaction
和TransactionConfigurator
的所有資料了。它們的具體情況, 前面已經說明過了, 就不贅述了。構建完成TransactionConfigurator
後, 進入流程中的ConfirmCoordinator
模組, 它的功能是讓使用者來確認交易詳情, 以及核實當前Token
的餘額是否足夠等。
智慧合約呼叫。 當用戶確認且餘額足夠支援轉賬的情況下, 就需要SendTransactionCoordinator
來進行核心的轉賬交易業務, 所以它是一個純業務的功能類, 並無頁面。這時候要根據在TransactionConfigurator
經過校驗的 Transaction
, 判斷其noce
是否大於0。如果不大於0, 則需要通過JSON RPC Over HTTP
的方式呼叫以太坊智慧合約的API, 即eth_getTransactionByHash
對nonce
進行更新, 然後重新進行判斷; 如果大於0, 則EtherKeystore
對交易進行簽名, 然後通過JSON RPC Over HTTP
的方式呼叫以太坊智慧合約的API, 即eth_sendRawTransaction
。
交易回撥處理。 交易結果產生後, 要回調至發起的模組, 還要處理後續的業務。如果交易成功, 會將交易儲存到本地的Realm
資料庫等; 如果交易失敗, 提示使用者交易失敗。
到此, 轉賬交易的流程的閉環完成。在後面的圖中也對整個交易流程做了一個梳理。
DApp發起的交易
Trust
具有一個功能齊全的Web3
瀏覽器,可與任何分散式的應用程式(DApp)配合使用。這個情景就是當轉賬交易發生在DApp
中發起的情況。交易整體的流程與原生App
中基本一致, 且交易的核心資料結構一致。它們的區別在於發起方式、回撥處理, 以及DApp
中要多一些解析的過程。
交易發起。 在Web3
瀏覽器中的DApp
中, 發起轉賬交易, 發起方式就是JS
呼叫iOS原生
。通過傳入的資料, 在BrowserCoordinator
模組中, 將資料進行解析。
解析。 通過DAppAction
、DappCommand
、DAppRequester
等類進行解析, 完成後, 封裝入DAppAction
內, 來決定需要進行哪種操作、需要呼叫合約中的哪種API。它有6種響應事件, 分別是:
1.signMessage
2.signPersonalMessage
3.signTypedMessage
4.signTransaction
5.sendTransaction
6.unknown
構建交易資料 和 智慧合約呼叫。 這兩個步驟和原生的之間基本一致, 都是通過資料構建出Transaction
, 用來做交易準備。然後進行校驗, 再呼叫智慧合約。所以就不具體說明了, 請參照原生App
。
交易回撥處理。 交易結果產生後, 也要回調至發起的模組, 來處理後續的業務。這裡與原生App
區別是, 除了需要完成原生App
在成功或失敗下完成的流程外, 還需要將交易結果再通知到Web
, 這樣才能形成完整的閉環。所以, 無論回撥結果如何, 都會通過iOS原生
呼叫JS
的方式通知Web
交易的具體情況。
交易流程圖
Trust
專案到這裡基本就很清晰了, 這兩篇文章雖然只是對Trust wallet
的解讀, 很侷限。但是由它們能延伸到的知識, 如以太坊的智慧合約的知識、錢包和私鑰管理的知識等等, 還有你對區塊鏈的認知, 這些不是狹義的。所以無論你認為區塊鏈是好是壞, 或者有沒有實際的應用和市場的歡迎, 這門技術都帶來了無限創新。