數(shù)字錢包初探

最近區(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);
});

錢包交易過程

Flowchart

回顧一下錢包的核心功能:

  • 錢包初始化:
    • 創(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è)核心方法,你就可以完成數(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)

Wallet

問題:

我們了解了一下錢包的大致原理后,來看看最早提出來的問題:

  • 我們的錢包密碼(私鑰)或者其它錢包信息,是否會(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)持的一俢〗

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容