docker部署构建比特币测试网络进行nodejs应用开发

Posted BBinChina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了docker部署构建比特币测试网络进行nodejs应用开发相关的知识,希望对你有一定的参考价值。

学习目标:

掌握比特币应用的基本开发流程

我们将采用Docker容器技术来快速安装喝配置私有节点,用比特币测试网络(bitcoin-testnet)作为开发实验环境,以Node.js程序语言为例子,说明如何调用比特币钱包节点提供的RPC接口服务,实现一些设计比特币区块链的具体应用功能。

RPC(Remote Procedure Call)即远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,比特币网络节点之间的通信协议是比特币特定的协议格式。

学习内容:

1、安装和运行比特币测试网络

实践

一、安装和运行比特币测试网络

1、先下载比特币测试网络的docker镜像

docker pull freewil/bitcoin-testnet-box

2、运行Docker 镜像

 docker run -t -i -p 19001:19001 -p 19011:19011 freewil/bitcoin-testnet-box

19001和19011是配置给节点提供RPC服务的端口

3、进入Docker运行环境后,输入以下命令启动比特币测试网络:

make start


可以看到模拟运行了两个比特币测试钱包节点,组成一个私有范围的比特币测试网络。

4、获取节点的信息

make getinfo

bitcoin-cli -datadir=1  -getinfo
{
  "version": 170100,
  "protocolversion": 70015,
  "walletversion": 169900,
  "balance": 0.00000000,
  "blocks": 0,
  "timeoffset": 0,
  "connections": 1,
  "proxy": "",
  "difficulty": 4.656542373906925e-10,
  "testnet": false,
  "keypoololdest": 1632131032,
  "keypoolsize": 1000,
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": ""
}

bitcoin-cli -datadir=2  -getinfo
{
  "version": 170100,
  "protocolversion": 70015,
  "walletversion": 169900,
  "balance": 0.00000000,
  "blocks": 0,
  "timeoffset": 0,
  "connections": 1,
  "proxy": "",
  "difficulty": 4.656542373906925e-10,
  "testnet": false,
  "keypoololdest": 1632131032,
  "keypoolsize": 1000,
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": ""
}

这是正在运行的分别两个节点的信息,其字段意思如下:
version:客户端节点软件版本
protocolVersion:比特币协议版本
walletversion:钱包数据格式版本
balance:钱包账户余额
blocks:已经产生的区块,因为这是初始化的测试节点,所以为0 ,在实验过程中,我们将确认新的区块
timeoffset:时间的时区偏移量
connection:本节点接入的其他节点数,而这里的私有网络总共两个节点,即除己之外还有另一个节点
proxy:网络代理设置
difficulty:挖矿计算难度
testnet:是否使用外部的测试网络,这里建立的是两个节点的私有测试网络
keypoololdest:预生成的公钥和私钥池的起始时间
keypoolsize:池包含的记录数量,用于生成钱包地址和找零地址,这样钱包备份可以对已有的交易以及未来多笔交易有效
paytxfee:交易手续费,包含额外手续费的交易会更快地被包含在新生成的区块中
relayfee:最少标准手续费
erors:节点运行的错误提示

5、模拟新产生1个区块记录

make generate

bitcoin-cli -datadir=1  generate 1
[
  "5f9f0b5958de37c6b388dde3633a0ba4f1837c5883bcb6dd5bc57e6a854a2171"
]

tester@2e7106ca6250 ~/bitcoin-testnet-box$ make getinfo
bitcoin-cli -datadir=1  -getinfo
{
  "version": 170100,
  "protocolversion": 70015,
  "walletversion": 169900,
  "balance": 0.00000000,
  "blocks": 1,
  "timeoffset": 0,
  "connections": 1,
  "proxy": "",
  "difficulty": 4.656542373906925e-10,
  "testnet": false,
  "keypoololdest": 1632131032,
  "keypoolsize": 999,
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": ""
}
bitcoin-cli -datadir=2  -getinfo
{
  "version": 170100,
  "protocolversion": 70015,
  "walletversion": 169900,
  "balance": 0.00000000,
  "blocks": 1,
  "timeoffset": 0,
  "connections": 1,
  "proxy": "",
  "difficulty": 4.656542373906925e-10,
  "testnet": false,
  "keypoololdest": 1632131032,
  "keypoolsize": 1000,
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": ""
}

通过make getinfo 可以看到现在已经确认了一个区块,我们也可以产生多个区块

make generate BLOCKS=200

注:现实中的比特币网络中每10分钟才产生1个区块,而由每个节点通过调整挖矿难度来达到这个限制,因这是测试网络,所以可以即时批量地生产

6、向测试钱包地址转账10个BTC

 make sendfrom1 ADDRESS=mkiytxYA6kxUC8iTnzLPgMfCphnz91zRfZ AMOUNT=10

bitcoin-cli -datadir=1  sendtoaddress mkiytxYA6kxUC8iTnzLPgMfCphnz91zRfZ 10
8d64d80df363a62cafa7474d67f58ce321e575f40ab3c60fc78f093de3887449

以上交易记录实际上还存放在UTXO(Unspent Transaction Outputs 未使用的交易),需要由旷工进行区块确认时才会写入到区块链中,达到不可篡改的目的。

接下来在节点再一次运行 make generate BLOCKS=10来产生10个区块,让上面的转账交易得到足够的确认

确认区块后,执行make getinof来查看当前的节点情况

tester@2e7106ca6250 ~/bitcoin-testnet-box$ make getinfo
bitcoin-cli -datadir=1  -getinfo
{
  "version": 170100,
  "protocolversion": 70015,
  "walletversion": 169900,
  "balance": 5539.99996220,
  "blocks": 211,
  "timeoffset": 0,
  "connections": 1,
  "proxy": "",
  "difficulty": 4.656542373906925e-10,
  "testnet": false,
  "keypoololdest": 1632131032,
  "keypoolsize": 999,
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": ""
}

以上信息表示当前节点的钱包信息账户余额为5539.99996220BTC,即当前支出了10个BTC,同时还有手续费等

二、使用Node.js开发应用

基本内容为:导入比特币私钥,发送一个最简单的转账交易

首先需要安装kapitalize,

npm install capitalize

domo

//************************************************//
//   Bitcoin-Testnet RPC sample of node.js        //
//          PPk Public Group ? 2016.              //
//           http://ppkpub.org                    //
//     Released under the MIT License.            //
//************************************************//
//对应比特币测试网络(Bitcoin testnet)的RPC服务接口访问参数
var RPC_USERNAME='admin1'; 
var RPC_PASSWORD='123';
var RPC_HOST="127.0.0.1";
var RPC_PORT=19001;

//测试使用的钱包地址
TEST_ADDRESS='mkiytxYA6kxUC8iTnzLPgMfCphnz91zRfZ'; //测试用的钱包地址,注意与比特币正式地址的区别
TEST_PRIVATE_KEY='cTAUfueRoL1HUXasWdnETANA7uRq33BUp3Sw88vKZpo9Hs8xWP82'; //测试用的钱包私钥
TEST_WALLET_NAME='TestWallet1';  //测试的钱包名称 

MIN_DUST_AMOUNT=10000;  //最小有效交易金额,单位satoshi,即0.00000001 BTC
MIN_TRANSACTION_FEE=10000; //矿工费用的最小金额,单位satoshi

console.log('Hello, Bitcoin-Testnet RPC sample.');
console.log('     PPk Public Group ? 2016      ');

//初始化访问RPC服务接口的对象
var client = require('kapitalize')()

client
    .auth(RPC_USERNAME, RPC_PASSWORD)
    .set('host', RPC_HOST)
    .set({
        port:RPC_PORT
    });

//显示当前连接的比特币测试网络信息
client.getInfo(function(err, info) {
  if (err) return console.log(err);
  console.log('Info:', info);
});

//查看当前钱包下属地址账户余额变动情况
client.listaccounts(function(err, account_list) {
  if (err) return console.log(err);
  console.log("Accounts list:\\n", account_list);
});

//检查测试帐号是否已存在于测试节点
client.getaccount(TEST_ADDRESS,function(err, result) {
  if (err || result!=TEST_WALLET_NAME ) { //如不存在,则新导入测试帐号私钥
      console.log('Import the test account[',TEST_WALLET_NAME,']:',TEST_ADDRESS);
      client.importprivkey(TEST_PRIVATE_KEY,TEST_WALLET_NAME,function(err, imported_result) {
          if (err) return console.log(err);
          console.log('Imported OK:', imported_result);
          
          doSample();
      });
  }else{ //如已存在,则直接执行示例
      console.log('The test account[',TEST_WALLET_NAME,'] existed. Address:',TEST_ADDRESS);
      
      doSample();
  }
 
});

// 示例实现功能
function doSample(){
    //获取未使用的交易(UTXO)用于构建新交易的输入数据块
    client.listunspent(6,9999999,[TEST_ADDRESS],function(err, array_unspent) {
      if (err) return console.log('ERROR[listunspent]:',err);
      console.log('Unspent:', array_unspent);

      var array_transaction_in=[];
      
      var sum_amount=0;
      for(var uu=0;uu<array_unspent.length;uu++){
          var unspent_record=array_unspent[uu];
          if(unspent_record.amount>0){
              sum_amount+=unspent_record.amount*100000000; //注意:因为JS语言缺省不支持64位整数,此处示例程序简单采用32位整数,只能处理交易涉及金额数值不大于0xFFFFFFF即4294967295 satoshi = 42.94967295 BTC。 实际应用程序需留意完善能处理64位整数,可以采用字符串方式
              // 将记录追加到数组尾,其index从0开始,所以可以通过length获取最新index
              array_transaction_in[array_transaction_in.length]={"txid":unspent_record.txid,"vout":unspent_record.vout};
              
              if( sum_amount > (MIN_DUST_AMOUNT+MIN_TRANSACTION_FEE) )
                break;
          }
      }
      
      //确保新交易的输入金额满足最小交易条件
      if (sum_amount<MIN_DUST_AMOUNT+MIN_TRANSACTION_FEE) return console.log('Invalid unspent amount');

      console.log('Transaction_in:', array_transaction_in);

      //生成测试新交易的输出数据块,此处示例是给指定目标测试钱包地址转账一小笔测试比特币
      //注意:输入总金额与给目标转账加找零金额间的差额即MIN_TRANSACTION_FEE,就是支付给比特币矿工的交易成本费用,即输入金额包含了手续费
      var obj_transaction_out={
          "mieC38pnPwMqbMAN6sGWwHRQ3msp7nRnNz":MIN_DUST_AMOUNT/100000000,   //目标转账地址和金额
          "mkiytxYA6kxUC8iTnzLPgMfCphnz91zRfZ":(sum_amount-MIN_DUST_AMOUNT-MIN_TRANSACTION_FEE)/100000000  //找零地址和金额,默认用发送者地址
        };
      
      console.log('Transaction_out:', obj_transaction_out);
      
      //生成交易原始数据包
      client.createrawtransaction(array_transaction_in,obj_transaction_out,function(err2, rawtransaction) {
          if (err2) return console.log('ERROR[createrawtransaction]:',err2);
          console.log('Rawtransaction:', rawtransaction);
          
          //签名交易原始数据包
          client.signrawtransaction(rawtransaction,function(err3, signedtransaction) {
              if (err3) return console.log('ERROR[signrawtransaction]:',err3);
              console.log('Signedtransaction:', signedtransaction);
              
              var signedtransaction_hex_str=signedtransaction.hex;
              console.log('signedtransaction_hex_str:', signedtransaction_hex_str);
              
              //广播已签名的交易数据包
              client.sendrawtransaction(signedtransaction_hex_str,false,function(err4, sended) { //注意第二个参数缺省为false,如果设为true则指Allow high fees to force it to spend,会在in与out金额差额大于正常交易成本费用时强制发送作为矿工费用(谨慎!)
                  if (err4) return console.log('ERROR[sendrawtransaction]:',err4);
                  console.log('Sended TX:', sended);
                  
                  client.listaccounts(function(err, account_list) {
                      if (err) return console.log(err);
                      console.log("Accounts list:\\n", account_list); //发送新交易成功后,可以核对下账户余额变动情况
                    });
              });
          });
      });
    });
}

最后:
当我们广播完交易数据时,需要到节点make generate 产生区块用于确认交易(在现实中会有旷工进行挖矿)
其流程即可归结为:nodejs应用组织了特定的交易数据,并在签名后广播,最终被矿工节点确认生效。

三、掌握比特币“交易”数据结构

在区块链中,最核心的功能便是产生块,每个块记录链中达成共识的交易数据,这些交易数据被确认后即不能被篡改。那么其交易数据的结构是如何的呢:

字段大小数据类型描述
协议版本4字节uint32_t明确这笔交易参照的规则协议的版本号
输入数量1~9字节var_int被包含的输入交易的数量
输入列表不定tx_in[]一个或多个输入交易构成的数组
输出数量1~9字节var_int被包含的输出交易的数量
输出列表不定tx_out[]一个或多个输出交易构成的数组
锁定时间4字节uint32_t一个UNIX时间戳或区块号

锁定时间需要特殊说明,这个字段定义了能被加到区块链里的最早的交易时间,其缺省值为0,表示立即执行,如果大于0且小于5亿(4字节),就被视为区块高度,当不到指定的高度时,不确认该交易。

基于区块链技术的应用开发,实际上是在输出数据结构上承载具体的业务逻辑。

Demo,在交易的备注数据块中嵌入业务信息

//************************************************//
//  RPC sample based Bitcoin-Testnet of node.js   //
//          PPk Public Group @2016.               //
//           http://ppkpub.org                    //
//     Released under the MIT License.            //
//************************************************//
//对应比特币测试网络(Bitcoin testnet)的RPC服务接口访问参数
var RPC_USERNAME='admin1'; 
var RPC_PASSWORD='123';
var RPC_HOST="127.0.0.1";
var RPC_PORT=19001;

//测试使用的钱包地址
TEST_ADDRESS='mkiytxYA6kxUC8iTnzLPgMfCphnz91zRfZ'; //测试用的钱包地址,注意与比特币正式地址的区别
TEST_PUBKEY_HEX='022e9f31292873eee495ca9744fc410343ff373622cca60d3a4c926e58716114b9';  //16进制表示的钱包公钥,待修改
TEST_HASH160='391ef5239da2a3904cda1fd995fb7c4377487ea9';  //HASH160格式的钱包公钥
TEST_PRIVATE_KEY='cTAUfueRoL1HUXasWdnETANA7uRq33BUp3Sw88vKZpo9Hs8xWP82'; //测试用的钱包私钥
TEST_WALLET_NAME='TestWallet1';  //测试的钱包名称 

MIN_DUST_AMOUNT=10000;  //最小有效交易金额,单位satoshi,即0.00000001 BTC
MIN_TRANSACTION_FEE=10000; //矿工费用的最小金额,单位satoshi

console.log('Hello, Bitcoin-Testnet RPC sample.');
console.log('     PPk Public Group @2016       ');

//初始化访问RPC服务接口的对象
var client = require('kapitalize')()

client
    .auth(RPC_USERNAME, RPC_PASSWORD)
    .set('host', RPC_HOST)
    .set({
        port:RPC_PORT
    });

//显示当前连接的比特币测试网络信息
client.getInfo(function(err, info) {
  if (err) return console.log(err);
  console.log('Info:', info);
});

//检查测试帐号是否已存在于测试节点
client.getaccount(TEST_ADDRESS,function(err, result) {
  if (err || result!=TEST_WALLET_NAME ) { //如不存在,则新导入测试帐号私钥
      console.log('Import the test account[',TEST_WALLET_NAME,']:',TEST_ADDRESS);
      client.importprivkey(TEST_PRIVATE_KEY,TEST_WALLET_NAME,function(err, imported_result) {
          if (err) return console.log(err);
          console.log('Imported OK:', imported_result);
          
          doRpcSample();
      });
  }else{ //如已存在,则直接执行示例
      console.log('The test account[',TEST_WALLET_NAME,'] existed. Address:',TEST_ADDRESS);
      
      doRpcSample();
  }
 
});

// 示例实现功能
function doRpcSample(){
    //获取未使用的交易用于生成新交易
    client.listunspent(6,9999999,[TEST_ADDRESS],function(err2, array_unspent) {
      if (err2) return console.log('ERROR[listunspent]:',err2);
      console.log('Unspent:', array_unspent);

      //测试数据定义
      var TEST_DATA='Peer-Peer-network is the future!';
      console.log('TEST_DATA=',TEST_DATA);
      
      //将原始字节字符串转换为用16进制表示
      var str_demo_hex=stringToHex(TEST_DATA);
      console.log('str_demo_hex=',str_demo_hex);
      
      //生成输入交易定义块
      var min_unspent_amount=MIN_DUST_AMOUNT*1+MIN_TRANSACTION_FEE;
      var array_transaction_in=[];
      
      var sum_amount=0;
      for(var uu=0;uu<array_unspent.length;uu++){
          var unspent_record=array_unspent[uu];
          if(unspent_record.amount>0){
              sum_amount+=unspent_record.amount*100000000;
              array_transaction_in[array_transaction_in.length]={"txid":unspent_record.txid,"vout":unspent_record.vout};
              
              if( sum_amount > min_unspent_amount )
                break;
          }
      }

      //确保新交易的输入金额满足最小交易条件
      if (sum_amount<=min_unspent_amount) return console.log('Invalid unspent amount');

      console.log('Transaction_in:', array_transaction_in);
      
      //构建原始交易数据
      var rawtransaction_hex = '01000000';  // Bitcoin协议版本号,UINT32
      rawtransaction_hex += byteToHex(array_transaction_in.length) ; //设置输入交易数量
      for(var kk=0;kk<array_transaction_in.length;kk++){
          rawtransaction_hex += reverseHex(array_transaction_in[kk].txid)+uIntToHex(array_transaction_in[kk].vout); 
          rawtransaction_hex += "00ffffffff";   // 签名数据块的长度和序列号, 00表示尚未签名
      }
      
      rawtransaction_hex += byteToHex(2);  //设置输出交易数量
      
      //使用op_return对应的备注脚本空间来嵌入自定义数据
      rawtransaction_hex += "0000000000000000";  
      rawtransaction_hex += byteToHex(2+str_demo_hex.length/2) + "6a" + byteToHex(str_demo_hex.length/2) +str_demo_hex; 
      
      //最后添加一个找零输出交易 
      var charge_amount = sum_amount - MIN_TRANSACTION_FEE;
      console.log('sum_amount:', sum_amount);
      console.log('min_unspent_amount:', min_unspent_amount);
      console.log('charge_amount:', charge_amount);
      console.log('uIntToHex(',charge_amount,')=', uIntToHex(charge_amount));
      
      rawtransaction_hex += uIntToHex(charge_amount)+"00000000"以上是关于docker部署构建比特币测试网络进行nodejs应用开发的主要内容,如果未能解决你的问题,请参考以下文章

docker搭建比特币私链

比特币系列 - 用docker搭BTC私链

libsecp256k1比特币密码算法开源库

重磅BIP91锁定完成,比特币网络隔离见证部署在即

比特币本地网集群环境部署及omni usdt代币发布

比特币白皮书