区块链轻钱包技术分享

一、区块链钱包简述

1. 钱包

钱包,顾名思义,就是装钱的包,实体上的钱包我们每个人都接触过,使用过。但咱们这篇文章所要叙述的并不是实体的钱包。而是网络钱包。说起网络钱包,我们可能瞬间就会想到,微信钱包,支付宝钱包,百度,京东钱包。实际上,咱们要讲的区块链钱包,和这钱包在业务逻辑上并没有太大的区别,只是在实现的差异比较大,我们的区块链钱包实现起来比微信这样的钱包更复杂一些,这些传统的网络钱包都是中心化的钱包,而咱们的区块链钱包,它可以是中心化钱包,也可以是去中心化钱包。

我想提到钱包,我都会考虑如何安全的存放我们的钱,当我们的钱包放到自己的实体钱包里时,你只要保护好它,不要让小偷偷走就行。放好自己的钱包很多人都能做得很好。而当我们使用互联网钱包或者区块链钱包时,是相关公司或者团队帮我们保护钱包。网络上的小偷,可比实际生活中的小偷强得太多了,因为他都是技术高超之人。那么在区块链钱包里面,我们是怎么去实现保护咱们的钱不被盗走的,下面的内容中我们将会详细的讲解。

2. 区块链钱包

我们都知道,现在的区块链项目主要分为币圈和链圈。在币圈,钱包必然是不可少的,因为你的币必须有一个存储的地方。让你能看到它就在那儿。好了,接下来,咱们说一下区块链钱包的实现流程。现在的区块链钱包,用户能看到的,一般都有助记词,私钥,账户余额,转账记录,转账等。正常情况,助记词和私钥都是需要用户自己备份的。助记词可帮助用户找回私钥,而私钥有可生成用户的地址。私钥与地址一一对应,地址是用户的唯一标识,用户的所有钱都是存放在地址里面。这样说来,如果咱们丢失了用户地址,有丢失了私钥,助记词也忘了,那你的钱也就丢了。

3. 区块链钱包分类

  • on-chain:给一个钱包地址发送数字货币,这笔交易在全网广播、被确认、被打包进区块。这是发生在链上的,被称为 on-chain 交易。on-chain 钱包需要自己保管私钥。

  • off-chain:相对于 on-chain 交易是 off-chain 交易。通常,通过交易所进行的交易是 off-chain 的,本人并没有私钥。私钥在交易所,由交易所托管。所以交易所的钱包也是中心化的钱包。

  • 冷钱包:冷即离线、断网,也就是说私钥存储的位置不能被网络所访问。例如纸钱包、脑钱包、硬件钱包等等。

  • 热钱包:热即联网,也就是私钥存储在能被网络访问的位置。例如存放在交易所的、在线钱包网站、手机 App 钱包都属于热钱包。通常而言,冷钱包更加安全,热钱包使用更加方便。

  • 全节点钱包:全节点钱包通俗的来说就是同步了全部的以太坊区块信息的钱包

  • 轻钱包:钱包和节点分离,不需要同步以太坊区块的信息,它不必保存所有区块的数据,只保存跟自己相关的数据。基本可以实现去中心化。

  • 移动端钱包:app 钱包

  • 中心化钱包:在交易所中的钱包,以及类似 OKLink 提供的保险柜服务。

二、钱包重要的概念

区块链钱包中比较重要的名词有:地址、密码、私钥、助记词、keystore。

若以银行账户为类比,这 5 个词分别对应内容如下:

  • 地址=银行卡号

  • 密码=银行卡密码

  • 私钥=银行卡号+密码

  • 助记词=银行卡号+密码

  • Keystore=加密私钥

  • Keystore+密码=私钥

1. 地址

地址=银行卡号

2. 密码

密码=银行卡密码

密码的用途有两个,一是转账时候的支付密码,二是用 keystore 导入钱包时的登录密码。如果对原密码进行修改,有两种方法,一是直接修改密码,这需要输入原密码。如果原密码忘记了,用助记词或私钥导入钱包,同时设置新密码。

3. 私钥

私钥=银行卡号+密码

创建钱包后,输入密码可以导出私钥,这个私钥属于明文私钥,由 64 位字符串组成,一个钱包只有一个私钥且不能修改。在导入钱包中,输入私钥并设置一个密码,就能进入钱包并拥有这个钱包的掌控权,就可以把钱包中的代币转移走。

4 助记词

助记词=银行卡号+密码

创建钱包后,会出现一个备份助记词功能,选择备份助记词,输入密码,会出现 12 个单词,每个单词之间有一个空格,这个就是助记词,一个钱包只有一个助记词且不能修改。助记词是私钥的另一种表现形式,具有和私钥同样的功能,在导入钱包中,输入助记词并设置一个密码,就能进入钱包并拥有这个钱包的掌控权,就可以把钱包中的代币转移走。助记词只能备份一次,备份后,在钱包中再也不会显示,因此在备份时一定要抄写下来。

5. keystore

keystore=加密私钥,keystore+密码=私钥

钱包里有一个备份 keystore 功能,选择备份 keystore,输入密码,会出现一大段字符,这个就是 keystore。在导入钱包中,选择官方钱包,输入 keystore 和密码,就能进入钱包了。需要说明的是,这个密码是本手机原来设置的本钱包密码, 这一点和用私钥或助记词导入钱包不一样,用私钥或助记词导入钱包,不需要知道原密码,直接重置密码。keystore 属于加密私钥,和钱包密码有很大关联,钱包密码修改后,keystore 也就相应变化,原来备份的 keystore 也就失去了作用,因此,若是修改了钱包密码,就需要重新备份 keystore。

三、区块链钱包助记词和 keystore

1. 生成助记词

使用 bip39 库,关于 bip 系列库,请大家关注我的 github;里面会有详细的文章讲解。

引入 bip39 库

    const bip39 = require('bip39');

钱包的助记词一般为 12 个单词,下面代码生成的助记词就是 12 个

    let words = bip39.generateMnemonic();

接下来,再根据助记词和密码生成随机数种植

    let password = '123456';
    let seedAsHex = bip39.mnemonicToSeedHex(words, password);
    console.log("random deed is " = seedAsHex);

这里生成的随机数种子可以当作私钥字符串拿去生成keystore

2. 生成 keystore,并导出私钥

这里需要用到 keythereum 库,Keythereum 是一个生成、导入和导出以太坊 keys 的一个 javascript 工具。这个库提供了一中在本地或者 web 钱包中使用相同的账户。它可以用于可验证的冷库钱包。

Keythereum 使用相同的密钥派生函数(PBKDF2-SHA256 或 scrypt),对称密码(AES-128-CTR 或 AES-128-CBC)和消息验证代码作为 geth。您可以将生成的密钥导出到文件,将其复制到数据目录的密钥库中,并立即在您的本地 Ethereum 客户端中使用它。

注意:从版本 0.5.0 开始,Keythereum 的加密和解密函数都会返回缓冲区而不是字符串。 对于任何直接使用这些功能的人来说,这是一个突破性改变!

2.1 安装

    npm install keythereum

当然运行上面的命令,你需要安装 nodejs,关于 nodejs 的安装,你可以选择自己喜欢的方式去进行安装

2.2 使用

2.2.1 生成 keystore

在 nodejs 中使用 Keythereum,你需要引入它,使用关键字 require

    var keythereum = require("keythereum");

压缩文件 dist/keythereum.min.js 在浏览器中使用时。 包只需将 Keythereum 对象附加到窗口即可:

    <script src="dist/keythereum.min.js" type="text/javascript"></script>

生成 keystore,如果你需要使用上面生成的随机数种子,那么我建议,把随机种子处理成合乎规范的私钥就可以使用了。

      var params = { keyBytes: 32, ivBytes: 16 };
      var dk = keythereum.create(params);
      var options = {
        kdf: "pbkdf2",
        cipher: "aes-128-ctr",
        kdfparams: {
          c: 262144,
          dklen: 32,
          prf: "hmac-sha256"
        }
      };
      var keyObject = keythereum.dump(password, dk.privateKey, dk.salt, dk.iv, options);
      keythereum.exportToFile(keyObject);

dk.privateKey 是明文的私钥,实际上只要将其导出即可。如果你是用的是助记词的方式,那么你把 dk.privateKey 改成随机数种子,将随机数种子处理为 64 位的 0-9、a-z 的字符串即可。

下面是生成的 keystore 格式的数据

    {
        "address":"db36dd3503bce9ad5528661a5ffa0e37e9fe8000",
        "crypto":
        {
            "cipher":"aes-128-ctr",             "ciphertext":"4b8c4aa4d72962de227f8fd9cc2f85c78c60ed3e8fcb1e97f5dddcb160131f37",
            "cipherparams":
            {
                "iv":"ccb1c35bb961b11ba4b3f91bd9bc8c77"
            },
            "mac":"b7a456c48ae0f11e56dc5981ff3b538109ffca5056425586db70fb68c1b4363b",
            "kdf":"pbkdf2",
            "kdfparams":
            {
                "c":262144,
                "dklen":32,
                "prf":"hmac-sha256",
                "salt":"edcf78706161fd859abc2b54b9eb811ea9f03dd1097c021ac17011885c7319dd"
            }
        },
        "id":"b3e14cd2-9ec0-48ea-b8cf-94bd935520fe",
        "version":3
    }
2.2.2 通过key和密码导出私钥
    keythereum.importFromFile("db36dd3503bce9ad5528661a5ffa0e37e9fe8000", ./keystore, function (keyObject) {
          keythereum.recover("123456", keyObject, function (privateKey) {
              console.log("private key is " + privateKey)
        });
    });
2.2.3 导入私钥生成账户
     var params = { keyBytes: 32, ivBytes: 16 };
      var dk = keythereum.create(params);
      var options = {
        kdf: "pbkdf2",
        cipher: "aes-128-ctr",
        kdfparams: {
          c: 262144,
          dklen: 32,
          prf: "hmac-sha256"
        }
      };
      var keyObject = keythereum.dump(password, dk.privateKey, dk.salt, dk.iv, options);
      keythereum.exportToFile(keyObject);

将 dk.privateKey 改成你导入的私钥就行。到此为止,钱包的账户体系就形成了。你就可以通过地址进行获余额,发起转账了。

四、区块链钱包获取账户余额;

1. 使用 web3 和以太坊交互

假设咱们开发的是一个以太坊轻钱包,那么咱们得和以太坊的钱包节点进行交互,因此我需要使用到 web3js。

引入 web3js

    var Web3 = require("web3");

    if (typeof web3 !== 'undefined')
    {
        web3 = new Web3(web3.currentProvider);
    }
    else
    {
        web3 = new Web3(new Web3.providers.HttpProvider("http://10.23.1.209:8545"));
    }

2. 获取账户余额(使用 web3)

    web3.eth.getBalance("0x68db18a9cd87403c39e84467b332195b43fc33b5", function(err, result)
    {
        if (err == null)
        {
            console.log('~balance Ether:' +web3.fromWei(result, "ether"));
        }
        else
        {
            console.log('~balance Ether:' + web3.fromWei(result, "ether"));
        }
    });

3. 获取账户余额(自己发起 http 请求)

    var body = {
            "jsonrpc": "2.0",
            "method": "eth_getBalance",
            "params": ["0x68db18a9cd87403c39e84467b332195b43fc33b5", "latest"],
            "id":83
          };
          var bodyString = JSON.stringify(body);
          var headers = {
            'Content-Type': 'application/json',
            'Content-Length': bodyString.length
          };
          var options = {
            host: 'localhost',
            port: 8545,
            path: '',
            method: 'POST',
            headers: headers
          };
          var req = http.request(options, function (res) {
            res.setEncoding('utf-8');
            var responseString = '';
            res.on('data', function (data) {
              responseString += data;
              console.log("get balance from wallet node success and back data is " + data);
              var balance = JSON.parse(data);
              var tenBalance = parseInt(balance.result,16);
              console.log("balance from wallet is " + tenBalance);
              console.log(web3.fromWei(tenBalance, "ether"))
            });
            res.on('end', function (res) {
              console.log("response end");
            });
            req.on('error', function (e) {
              console.log('error occur,error is', e);
            });
          });
          req.write(bodyString);
          req.end();

五、区块链钱包签名交易,发起转账

1. 获取交易的 nonce(web3方式)

    web3.eth.getTransactionCount("0x68db18a9cd87403c39e84467b332195b43fc33b5", function (err, result)
    {
        if (err == null)
        {
            console.log('nonce:' + result);
        }
        else
        {
            console.log('nonce:' + result);
        }
    });

2. 签名交易,发起转账(web3 方式)

转账需要使用到 ethereumjs-tx 库,对交易进行签名,关于 ethereumjs-tx 库,请参阅咱们相关的文章

    var Tx = require('ethereumjs-tx');
    var privateKey = new Buffer.from('0f8e7b1b99f49d1d94ac42084216a95fe5967caec6bba35c62c911f6c4eafa95', 'hex')

    var rawTx = {
        subId:'0x0000000000000000000000000000000000',
        nonce: '0x1',
        gasPrice: '0x1a13b8600',
        gas: '0xf4240',
        to: '0x82aeb528664bb153d2114ae7ca4ef118ef1e7a98',
        value: '0x8ac7230489e80000',
        data:'0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675',
        //chainId:"10"
    };

    var tx = new Tx(rawTx);
    tx.sign(privateKey);

    var serializedTx = tx.serialize();

    if (tx.verifySignature())
    {
        console.log('Signature Checks out!')
    }

    web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), function(err, hash) {
        if (!err)
        {
            console.log("hash:" + hash); // "0x7f9fade1c0d57a7af66ab4ead79fade1c0d57a7af66ab4ead7c2c2eb7b11a91385"
        }
        else
        {
            console.log(err);
        }
    });

3. 签名交易,通过 http 发起转账

3.1 签名交易
    function getSignTransaction(privateKey, nonce, toAddress, sendToBalance, sendFee, sendContent) {
      if(!privateKey || !nonce || !toAddress || !sendToBalance || !sendFee) {
        console.log("one of fromAddress, toAddress, sendToBalance, sendFee is null, please give a valid param");
      } else {
        console.log("param is valid, start sign transaction");
        var numBalance = parseInt(sendToBalance);
        var balancetoWei = web3.toWei(numBalance, "ether");
        console.log("balancetoWei is " + balancetoWei);
        var oxNumBalance = parseInt(balancetoWei).toString(16);
        console.log("16 oxNumBalance is " + oxNumBalance);
        var privateKeyBuffer = privateKey;
        var rawTx = {
          nonce: nonce,
          gasPrice: '0x1a13b8600',
          gas: '0xf4240',
          to: '0x' + toAddress,
          value:'0x' + oxNumBalance,
          data:'0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675',
        };
        var tx = new transaction(rawTx);
        tx.sign(privateKeyBuffer);
        var serializedTx = tx.serialize();
        if(serializedTx == null) {
          console.log("Serialized transaction fail")
        }else {
          console.log("Serialized transaction success and the result is " + serializedTx.toString('hex'));
          console.log("The sender address is " + tx.getSenderAddress().toString('hex'));
          if (tx.verifySignature()) {
            console.log('Signature Checks out!')
          }else {
            console.log("Signature checks fail")
          }
        }
      }
      return '0x' + serializedTx.toString('hex');
    }
3.2 发起转账
      keythereum.recover(sendInfo.password, keyObj, function (privateKey) {

            var body = {
                  "jsonrpc":"2.0",
                  "method":"eth_getTransactionCount",
                  "params":['0x68db18a9cd87403c39e84467b332195b43fc33b5', "latest"],
                  "id":1
                };
                var bodyString = JSON.stringify(body);
                var headers = {
                  'Content-Type':'application/json',
                  'Content-Length':bodyString.length
                };
                var options = {
                  host:'localhost',
                  port:8545,
                  path:'',
                  method:'POST',
                  headers:headers
                };

                var req = http.request(options, function (res) {
                  res.setEncoding('utf-8');
                  var responseString = '';
                  res.on('data', function (data) {
                    responseString += data;
                    console.log("get nonce from wallet node json is " + data);
                    var nonceStr = JSON.parse(data);
                    console.log("nonce from wallet is " + nonceStr.result);
                    var signTx = send.getSignTransaction(privateKey, nonceStr.result, sendInfo.toAddress, sendInfo.sendToBalance, sendInfo.sendFee, sendInfo.sendContent);
                    if( signTx == null ) {
                      requestBack({
                        success:false,
                        error:"sign transaction result is null",
                      })
                    }
                    console.log("The sign of the transaction is " + signTx);
                    var body = {
                      "jsonrpc":"2.0",
                      "method":"eth_sendRawTransaction",
                      "params":[signTx],
                      "id":1
                    }
                    var bodyString = JSON.stringify(body);
                    var headers = {
                      'Content-Type':'application/json',
                      'Content-Length':bodyString.length
                    };
                    var options = {
                      host:'localhost',
                      port:8545,
                      path:'',
                      method:'POST',
                      headers:headers
                    };
                    var req = http.request(options, function (res) {
                      res.setEncoding('utf-8');
                      var responseString = '';
                      res.on('data', function (data) {
                        responseString += data;
                        console.log("send information wallet back data is= " + data)
                        var sendInformation = JSON.parse(data);
                        if(sendInformation.result != null ) {
                          console.log("send data from wallet parse is " + sendInformation.result);
                        } 
                      });
                      res.on('end', function (res) {
                        console.log("response end");
                      });
                      req.on('error', function (e) {
                        console.log('error occur,error is', e)
                      });
                    });
                    req.write(bodyString);
                    req.end();
                  });

                  res.on('end', function (res) {
                    console.log("response end");
                  });
                  req.on('error', function (e) {
                    console.log('error occur,error is', e)
                  });
                });
                req.write(bodyString);
                req.end();
       });

上面这段代码大家可以仔细品读

六、区块链钱包确认转账成功

方式一:

以太坊一般的 12个 区块确认一笔交易,扫块确认交易,在转账前先查看当前的区块数,转账完成后开始扫块,一旦有 12 个块生成,说明交易已经被确认,转账成功。

方式二:查看交易的 nonce 是否发生变化

每一笔交易都有一个 nonce,如果 nonce 发生变化,说明转账成功。


0
726