当前位置:首页 » 《休闲阅读》 » 正文

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

26 人参与  2022年01月04日 14:33  分类 : 《休闲阅读》  评论

点击全文阅读


学习目标:

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

我们将采用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";  //找零金额,UINT64
      rawtransaction_hex += "1976a914" + TEST_HASH160 +"88ac";  //找零地址为发送者的钱包地址
      
      rawtransaction_hex += "00000000"; //锁定时间,缺省设置成0,表示立即执行,是整个交易数据块的结束字段

      console.log('Rawtransaction:', rawtransaction_hex);
      
      //签名交易原始数据包
      client.signrawtransaction(rawtransaction_hex,function(err3, signedtransaction) {
          if (err3) return console.log('ERROR[signrawtransaction]:',err3);
          console.log('Signedtransaction:', signedtransaction);
          
          if (!signedtransaction.complete) return console.log('signrawtransaction failed');
          
          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);
          });
      });
    });
}

//1字节整数转换成16进制字符串
function byteToHex(val){
    var resultStr='';
    var tmpstr=parseInt(val%256).toString(16); 
    resultStr += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    
    return resultStr;
}

//将HEX字符串反序输出
function reverseHex(old){
    var array_splited=old.match(/.{2}|.+$/g);
    var reversed='';
    for(var kk=array_splited.length-1;kk>=0;kk--){
        reversed += array_splited[kk];
    }
    return reversed;
}
//32位无符号整数变成16进制,并按翻转字节序
function uIntToHex(val){
    var resultStr='';
    var tmpstr=parseInt(val%256).toString(16); 
    resultStr += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    
    tmpstr=parseInt((val%65536)/256).toString(16); 
    resultStr += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    
    tmpstr=parseInt(parseInt(val/65536)%256).toString(16); 
    resultStr += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    
    tmpstr=parseInt(parseInt(val/65536)/256).toString(16); 
    resultStr += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    
    return resultStr;
}

//Ascii/Unicode字符串转换成16进制表示
function stringToHex(str){
    var val="";
    for(var i = 0; i < str.length; i++){
        var tmpstr=str.charCodeAt(i).toString(16);  //Unicode
        val += tmpstr.length==1? '0'+tmpstr : tmpstr;  
    }
    return val;
}

//十六进制表示的字符串转换为Ascii字符串
function hexToString(str){
    var val="";
    var arr = str.split(",");
    for(var i = 0; i < arr.length; i++){
        val += arr[i].fromCharCode(i);
    }
    return val;
}

在调用signrawtransaction之前,我们构建了一个字符串 rawtransaction_hex,该字符串以上部分介绍的交易数据结构格式进行构建,数据格式中,我们构建了2个输出交易数据,第一个输出交易采用op_return对应的备注脚本空间来嵌入字符串,第二个输出交易是一个找零交易,所以需要设置找零地址即金额。

总结:

首先,以上内容来自于书:<<区块链技术指南>> 由 邹均/张海宁/唐屹/李磊 等老师编著,其中唐屹老师还是我们广州大学的教授,真的超赞的。
在我看来,区块链其实是将已有的技术进行了整合,并非所有场合都适合去中心化,而我认为区块链最大的魅力在于他的不可篡改性,已经在其上建议的新型经济模型,用于优化社会“信任”体系等,未来以来。


点击全文阅读


本文链接:http://zhangshiyu.com/post/32676.html

交易  区块  节点  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1