最近區(qū)塊鏈風(fēng)靡互聯(lián)網(wǎng)行業(yè),個(gè)人也專門了解了相關(guān)技術(shù),也買過數(shù)字貨幣,當(dāng)然也有數(shù)字錢包。一直在思考這種去中心化的錢包,到底安不安全,原理是個(gè)啥:
- 我們的錢包密碼(私鑰)或者其它錢包信息,是否會(huì)在網(wǎng)絡(luò)中傳輸?
- 各大交易平臺(tái)中,我購買的數(shù)字貨幣到底在哪兒?
- 區(qū)塊鏈中到底有沒有用戶的錢包信息?
帶著這些問題,我們要研究一下數(shù)字錢包到底是咋回事?先搞清楚一些概念:
- 加密
- 對稱加密
- 非對稱加密
- 公鑰、私鑰
- 簽名
- 驗(yàn)簽
加密 & 對稱加密 & 非對稱加密
加密學(xué)在區(qū)塊鏈技術(shù)中屬于核心技術(shù)之一,錢包的生成也是由加密算法來完成的,當(dāng)然如果講述加密技術(shù)對我這個(gè)不專業(yè)的人來說不能講述的非常明白,不過我們從對它的功能上來大概的了解一下相關(guān)概念。
通俗的講:
- 加密:把東西(信息)用鑰匙鎖在箱子里面,只有拿到鑰匙的人才能使用這個(gè)東西(查看信息);
- 對稱加密:關(guān)閉箱子的鑰匙和打開箱子的要是一樣;
- 非對稱加密:關(guān)閉箱子的鑰匙和打開箱子的不一樣;
專業(yè)的講:
- 加密:將明文信息改變?yōu)殡y以讀取的密文內(nèi)容,使之不可讀。只有擁有解密方法的對象,經(jīng)由解密過程,才能將密文還原為正常可讀的內(nèi)容;
- 對稱加密:加密和解密時(shí)使用相同的密鑰;
- 非對稱加密:需要兩個(gè)密鑰來進(jìn)行加密和解密,它們分別是公鑰和私鑰,如果用公鑰對數(shù)據(jù)進(jìn)行加密,只能用對應(yīng)的私鑰才能解密;如果用私鑰對數(shù)據(jù)進(jìn)行加密,那么只能用公鑰才能解密;
為什么要用非對稱加密,應(yīng)用場景在哪兒?
其安全性更好:對稱加密的通信雙方使用相同的秘鑰,如果一方的秘鑰遭泄露,那么整個(gè)通信就會(huì)被破解。而非對稱加密使用一對秘鑰,一個(gè)用來加密,一個(gè)用來解密,而且公鑰是公開的,秘鑰是自己保存的,不需要像對稱加密那樣在通信之前要先同步秘鑰。
但是,非對稱加密的缺點(diǎn)是加密和解密花費(fèi)時(shí)間長、速度慢,只適合對少量數(shù)據(jù)進(jìn)行加密。
簽名
為了能讓信息的接收方得知發(fā)送方的身份,數(shù)字簽名技術(shù)運(yùn)用而生。
數(shù)字簽名是一種以電子形式存在于數(shù)據(jù)信息之中的,或作為其附件或邏輯上有聯(lián)系的數(shù)據(jù),可用于辨別數(shù)據(jù)簽署人的身份,并表名簽署人對數(shù)據(jù)信息中包含的信息的認(rèn)可技術(shù)。
- 簽名過程:
- 生成數(shù)據(jù)摘要:將要發(fā)送的數(shù)據(jù)進(jìn)行 Hash
- 生成簽名信息:用發(fā)送者私鑰對數(shù)據(jù)摘要進(jìn)行加密
- 將即將發(fā)送的數(shù)據(jù)+簽名信息 發(fā)送給接收者
- 驗(yàn)簽過程:
- 解密簽名信息:用發(fā)送者公鑰對簽名信息進(jìn)行解密
- 生成數(shù)據(jù)摘要:將接收的數(shù)據(jù)進(jìn)行 Hash
- 對比數(shù)據(jù):如果解密的簽名信息和生成的數(shù)據(jù)摘要相同則能確認(rèn)該數(shù)字簽名是發(fā)送者
加密算法
加密算法有很多種:RSA、RC2、RC4、IDEA、RSA、DSA、ADS、MD5、PKCS、ECC 等等,想了解算法的同學(xué)自己百度一下。
錢包需要什么功能?
錢包的核心功能:
- 錢包初始化
- 創(chuàng)建
- 導(dǎo)入
- 查詢錢包的資產(chǎn)
- 交易
- 轉(zhuǎn)賬
- 調(diào)用合約
相關(guān) Js 庫
- ethereumjs-util:工具類
- bn.js:BigNumber 數(shù)據(jù)類型
- safe-buffer:Buffer 二進(jìn)制數(shù)據(jù)類型
- create-hash: Hash 算法
- secp256k1:橢圓曲線算法
- keccak:SHA-3 算法
- rlp:RLP 編碼
- ethjs-util
- ethereumjs-wallet:錢包創(chuàng)建
- ethereumjs-tx:交易信息處理,簽名、Hash、校驗(yàn)等
- ethereumjs-abi:ABI 處理
- web3:和以太坊之間通訊
錢包的創(chuàng)建(私鑰、公鑰、地址)
創(chuàng)建錢包我們可以采用 ethereumjs-wallet 庫來完成,它是基于 橢圓曲線的ECDSA 算法來創(chuàng)建密鑰對的??丛创a:
- 公鑰生成:(ethereumjs-wallet)generate() -> (ethereumjs-util)privateToPublic -> secp256k1.publicKeyCreate -> publicKey
- 地址生成:(ethereumjs-wallet)generate() -> (ethereumjs-util)privateToAddress -> (ethereumjs-util)sha3 -> (keccakjs)SHA3 -> address
// ethereumjs-wallet 模塊
Wallet.generate = function (icapDirect) {
if (icapDirect) {
while (true) {
var privKey = crypto.randomBytes(32)
if (ethUtil.privateToAddress(privKey)[0] === 0) {
return new Wallet(privKey)
}
}
}
else {
return new Wallet(crypto.randomBytes(32))
}
}
var Wallet = function (priv, pub) {
//...
}
Object.defineProperty(Wallet.prototype, 'pubKey', {
get: function () {
if (!this._pubKey) {
this._pubKey = ethUtil.privateToPublic(this.privKey)
}
return this._pubKey
}
})
// ethereumjs-util 模塊
exports.privateToAddress = function (privateKey) {
return exports.publicToAddress(privateToPublic(privateKey))
}
var privateToPublic = exports.privateToPublic = function (privateKey) {
privateKey = exports.toBuffer(privateKey)
// skip the type flag and use the X, Y points
return secp256k1.publicKeyCreate(privateKey, false).slice(1)
}
exports.pubToAddress = exports.publicToAddress = function (pubKey, sanitize) {
pubKey = exports.toBuffer(pubKey)
if (sanitize && (pubKey.length !== 64)) {
pubKey = secp256k1.publicKeyConvert(pubKey, false).slice(1)
}
assert(pubKey.length === 64)
// Only take the lower 160bits of the hash
return exports.sha3(pubKey).slice(-20)
}
exports.sha3 = function (a, bytes) {
a = exports.toBuffer(a)
if (!bytes) bytes = 256
var h = new SHA3(bytes)
if (a) {
h.update(a)
}
return new Buffer(h.digest('hex'), 'hex')
}
對于使用者非常簡單:
相關(guān)文檔:https://github.com/ethereumjs/ethereumjs-wallet
const ethUtil = require('ethereumjs-util')
const Wallet = require('ethereumjs-wallet');
// 生成錢包
var wallet = Wallet.generate();
var privateKey = wallet.getPrivateKey(); // 返回 Buffer,可以通過 wallet.getPrivateKeyString() 直接得到字符串
var publicKey = wallet.getPublicKey(); // 返回 Buffer,可以通過 wallet.getPublicKeyString() 直接得到字符串
var address = wallet.getAddress(); // 返回 Buffer,可以通過 wallet.getAddressString() 直接得到字符串
// 導(dǎo)入錢包
var privateKey2 = ethUtil.toBuffer('0xe601e598111629240e4dc6ec7a95534e025838bd0f638dabad9ad4152d80443b');
var wallet2 = Wallet.fromPrivateKey(privateKey2);
var publicKey2 = wallet2.getPublicKey();
查詢錢包的資產(chǎn)
查詢錢包資產(chǎn)通過 web3.js 很容易實(shí)現(xiàn):
var balance = web3.eth.getBalance("0x407d73d8a49eeb85d32cf465507dd71d507100c1");
console.log(balance); // instanceof BigNumber
console.log(balance.toString(10)); // '1000000000000'
console.log(balance.toNumber()); // 1000000000000
錢包交易
錢包交易的過程:
- 構(gòu)造交易數(shù)據(jù)
- 以太幣交易數(shù)據(jù)
- 合約交易數(shù)據(jù)
- 交易簽名
- 模擬交易,估算 Gas
- 發(fā)送交易
交易對象
{
nonce: '0x00',
gasPrice: '0x01',
gasLimit: '0x01',
to: '0x633296baebc20f33ac2e1c1b105d7cd1f6a0718b',
value: '0x00',
data: '0xc7ed014952616d6100000000000000000000000000000000000000000000000000000000',
// EIP 155 chainId - mainnet: 1, ropsten: 3
chainId: 3
}
參考以太坊文檔:
- nonce:記錄賬戶已執(zhí)行的交易總數(shù),nonce 的值隨著每個(gè)新交易的執(zhí)行不斷增加
- gasPrice:你愿為該交易支付的每單位 gas 的價(jià)格,gas 價(jià)格目前以 GWei 為單位,其范圍是0.1->100+Gwei
- gasLimit:你愿為該交易支付的最高 gas 總額。該上限能確保在出現(xiàn)交易執(zhí)行問題(比如陷入無限循環(huán))之時(shí),你的賬戶不會(huì)耗盡所有資金。一旦交易執(zhí)行完畢,剩余所有 gas 會(huì)返還至你的賬戶
- to:目標(biāo)地址,如果是轉(zhuǎn)賬交易就是收款地址,如果是合約調(diào)用就是合約地址
- value:即你打算發(fā)送的以太幣總量。如果你要執(zhí)行一個(gè)轉(zhuǎn)賬交易,向另一個(gè)人或合約發(fā)送以太幣,你會(huì)需要設(shè)置 value 值。
- data:不同的交易類型下該字段會(huì)有所不同,在接下來的介紹中會(huì)有該字段的詳細(xì)說明
- chainId:該字段用來標(biāo)明交易數(shù)據(jù)要發(fā)送到哪個(gè)網(wǎng)絡(luò),1為主網(wǎng),3位ropsten網(wǎng)絡(luò)
構(gòu)建 data
如果是合約交易,需要通過合約信息來構(gòu)建 data 字段。這一過程相對復(fù)雜,可以參考Ethereum Contract ABI 分兩大過程:
- 對調(diào)用合約函數(shù)的函數(shù)名進(jìn)行編碼
- 對調(diào)用合約函數(shù)的參數(shù)進(jìn)行編碼
- 細(xì)節(jié)省略一萬個(gè)字...
源代碼:https://github.com/ethereumjs/ethereumjs-abi/blob/master/lib/index.js
對于使用者非常簡單
相關(guān)文檔:https://github.com/ethereumjs/ethereumjs-abi
var abi = require('ethereumjs-abi');
var methodID = abi.methodID('sam', ['bytes', 'bool', 'uint256[]']);
// returns the encoded binary (as a Buffer) data to be sent
var encoded = abi.rawEncode(['bytes', 'bool', 'uint256[]'], ['dave', true, [1, 2, 3]]);
var data = methodID.toString('hex') + rawEncode.toString('hex');
console.log(data);
簽名
交易數(shù)據(jù)構(gòu)造好后,接下來我們將數(shù)據(jù)進(jìn)行簽名,并序列化,最后的數(shù)據(jù)就可以進(jìn)行交易了,繼續(xù)看源代碼:
- Hash:(ethereumjs-tx)hash -> (ethereumjs-util)rlphash -> (rlp)encode -> (keccak)SHA3
- 簽名:(ethereumjs-util)ecsign -> secp256k1.sign
// ethereumjs-tx 模塊
Transaction.prototype.sign = function sign(privateKey) {
var msgHash = this.hash(false);
var sig = ethUtil.ecsign(msgHash, privateKey);
if (this._chainId > 0) {
sig.v += this._chainId * 2 + 8;
}
Object.assign(this, sig);
};
Transaction.prototype.hash = function hash(includeSignature) {
if (includeSignature === undefined) includeSignature = true;
// EIP155 spec:
// when computing the hash of a transaction for purposes of signing or recovering,
// instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
// hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
var items = void 0;
if (includeSignature) {
items = this.raw;
} else {
if (this._chainId > 0) {
var raw = this.raw.slice();
this.v = this._chainId;
this.r = 0;
this.s = 0;
items = this.raw;
this.raw = raw;
} else {
items = this.raw.slice(0, 6);
}
}
// create hash
return ethUtil.rlphash(items);
};
// ethereumjs-util 模塊
exports.ecsign = function (msgHash, privateKey) {
const sig = secp256k1.sign(msgHash, privateKey)
const ret = {}
ret.r = sig.signature.slice(0, 32)
ret.s = sig.signature.slice(32, 64)
ret.v = sig.recovery + 27
return ret
}
同樣對于使用者非常簡單
相關(guān)文檔:https://github.com/ethereumjs/ethereumjs-tx
const EthereumTx = require('ethereumjs-tx');
var privateKey = ...;
var txParams = {
nonce: '0x00',
gasPrice: '0x09184e72a000',
gasLimit: '0x2710',
to: '0x0000000000000000000000000000000000000000',
value: '0x00',
data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
// EIP 155 chainId - mainnet: 1, ropsten: 3
chainId: 3
};
var tx = new EthereumTx(txParams);
tx.sign(privateKey);
var serializedTx = tx.serialize(); // 這是最終交易需要發(fā)送的數(shù)據(jù)
估算 Gas
需要簽名的交易,需要估算 Gas 費(fèi)用,如果給一個(gè)不合理的 Gas 交易不會(huì)發(fā)送成功。我們可以通過 Web3 來估算一個(gè)相對合理的 Gas,讓交易能夠順利的進(jìn)行。
var transactionObject = {
nonce: '',
gasPrice: '',
from: '',
to: '',
value: '',
data: '',
}
web3.eth.estimateGas(transactionObject, function(err, res) {
if (!err)
console.log(res);
});
發(fā)送交易
發(fā)送交易我們使用 web3 的協(xié)議很容易就能搞定了:
- 不需要簽名的交易:web3.eth.sendTransaction
- 需要簽名的交易:web3.eth.sendRawTransaction
var transactionObject = {
nonce: '',
gasPrice: '',
gasLimit: '',
from: '',
to: '',
value: '',
data: '',
}
web3.eth.sendTransaction(transactionObject, function(err, address) {
if (!err)
console.log(address);
});
or
// ...
// 32字節(jié)的16進(jìn)制格式的交易哈希串
web3.eth.sendRawTransaction(serializedTx.toString('hex'), function(err, hash) {
if (!err)
console.log(hash);
});
錢包交易過程

回顧一下錢包的核心功能:
- 錢包初始化:
- 創(chuàng)建:ethereumjs-wallet.generate()
- 導(dǎo)入:ethereumjs-wallet.fromPrivateKey(privateKey)
- 查詢錢包的資產(chǎn):web3.eth.getBalance(addressHexString [, defaultBlock] [, callback])
- 交易:
- 構(gòu)造交易數(shù)據(jù):
- 交易對象:{ from: '', to: '', ...}
- data:ethereumjs-abi.methodID() + ethereumjs-abi.rawEncode()
- 交易簽名:ethereumjs-tx.sign(privateKey) -> ethereumjs-tx.serialize()
- 發(fā)送交易:
- 轉(zhuǎn)賬:web3.eth.sendTransaction(transactionObject [, callback])
- 合約(已經(jīng)簽名的交易):web3.eth.sendRawTransaction(signedTransactionData [, callback])
- 構(gòu)造交易數(shù)據(jù):
有了以上幾個(gè)核心方法,你就可以完成數(shù)字錢包應(yīng)用了。
錢包核心 SDK 的封裝
為了簡化以上的操作,并且讓錢包具有更好的擴(kuò)展性(支持以太幣、比特幣等),我們將上面的整過過程進(jìn)行一次封裝,讓開發(fā)人員更好的使用,我們做成了 trip-wallet。
Install
yarn add trip-wallet
Or
npm install trip-wallet
Usage
import Wallet from 'trip-wallet';
let wallet = Wallet('eth');
wallet.generate();
wallet.setProvider('http://host:port');
// async/await
let balance = await wallet.getBalance(wallet.address);
// Promise
wallet.getBalance(wallet.address).then(res => {
balance = res;
}, err => {
});
Object & Attributes
- walletObject
- privateKey: String (hex string)
- publicKey: String (hex string)
- address: String (hex string)
- currency: String
- transactionObject
- contract: Object
- methodName: String
- arguments: Array[]
- privateKey: String (hex string)
- from: String (hex string)
- to: String (hex string)
- value: Number | String | BigNumber
- gasLimit: Number | String | BigNumber
- gasPrice: Number | String | BigNumber
- data: String
- none: Number
Methods
- generate([currency]): Object
- import(key [, type] [, currency]): Object
- type: 'privateKey', 'keystore', 'mnemonicPhrase', 'readonly'
- key: String
- currency: String
- setProvider(host)
- getBalance(addressHexString): Promise
- sendTransaction(transactionObject): Promise
- getTransaction(transactionHash): Promise
- contract(abi, address): Object
- estimateGas(transactionObject): Promise
- gasPrice(): Promise
eth-util
- toWei(num, unit)
- fromWei(num, unit)
- toBigNumber
- toBuffer
- toHex
- verifyPrivateKey
- decodeAbi
- encodeAbi
- signTransaction
錢包 App 的整體架構(gòu)

問題:
我們了解了一下錢包的大致原理后,來看看最早提出來的問題:
- 我們的錢包密碼(私鑰)或者其它錢包信息,是否會(huì)在網(wǎng)絡(luò)中傳輸?
- 區(qū)塊鏈中沒有存儲(chǔ)任何賬戶私鑰信息,至少在交易過程中,我們沒有將私鑰信息傳遞出去;
- 在生成或者導(dǎo)入錢包,交易信息的構(gòu)建過程,都是在本地完成,我們不會(huì)泄露用戶私鑰;
- 除非軟件本身代碼上做手腳;
- 所以錢包應(yīng)用需要開源;
- 各大交易平臺(tái)中,我購買的數(shù)字貨幣到底在哪兒?
- 區(qū)塊鏈中到底有沒有用戶的錢包信息?
- 當(dāng)錢包地址產(chǎn)生了交易,只會(huì)存儲(chǔ)錢包的地址信息
- 如果錢包地址沒有產(chǎn)生任何交易,沒有存儲(chǔ)該錢包的相關(guān)信息
所以,中本聰通過這一獨(dú)特的思維,將用戶錢包信息(賬戶體系)全部由用戶自己本地來管理;賬本或者交易(公開信息)中除了錢包地址沒有存儲(chǔ)任何其它帳戶信息。這樣一來區(qū)塊鏈看起來是公開透明又是安全可靠的。
〖堅(jiān)持的一俢〗