比特币区块链开发指南(三)

阅读次数: 1,917

  • A+
所属分类:建站

本文以比特币测试网络(bitcoin-testnet)作为开发试验环境,解析比特币交易(Transaction)的数据结构,结合Node.js为例来说明如何自行组织特定需要的交易数据并签名后广播,最终被矿工节点确认生效。

比特币区块链开发指南(三)
一、了解比特币的“交易”(Transaction)数据结构

交易(Transaction)是比特币系统的信息载体和最小单元。而块(Block)就是将这若干个这样的“交易”基础单元打包装箱,贴上封条。再按一定的机制和先后顺序将这些块串联起来就构成了区块链(Blockchain)。

对于基于比特币区块链的应用开发,“交易”(Transaction)是最直接用到、最关键的数据结构。除了“交易”外,还需要掌握比特币区块量相关的一些基础术语的含义,包括钱包的私钥、公钥和地址、区块、区块链等,有不少书籍和网上资料都有比较清晰的描述,这里推荐可以看看这本在线书籍《精通比特币》

在这里我们侧重对“交易”(Transaction)的数据结构做一个深入的剖析和了解。

一笔比特币交易是一个含有输入值和输出值的数据结构,该数据结构植入了将一笔资金从初始点(输入值)转移至目标地址(输出值)的代码信息。一笔比特币交易包含一些字段,如下表所示。

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

注:锁定时间字段定义了能被加到区块链里的最早的交易时间。在大多数交易里,它被缺省设置成0,用来表示立即执行。如果锁定时间大于0并且小于5亿,就被视为区块高度,意指在这个指定的区块高度之前,该交易没有被包含在区块链里。如果锁定时间大于5亿,则它被当作是一个Unix纪元时间戳(从1970年1月1日以来的秒数),并且在这个指定时点之前,该交易不会被包含在区块链里。锁定时间的使用相当于将一张纸质支票的生效时间予以后延。

基于区块链技术的应用开发,实际上主要就是在交易的输出数据结构上做文章,来承载具体的业务逻辑,比如像ODIN开源项目就是将标识属性数据按一定格式嵌入到比特币的多重交易输出数据块中。

 

二、交易记录的实例解析

下面是一个比特币交易的原始数据示例(将原始二进制数据按字节以16进制ASCII码形式输出,便于分析):

0100000002eb2121e4e727bdb28525e79d39a90bd711b9e8413c054b29ffc4bb4775e69f82010000006b483045022100df82
cf6c95b4eb64e4e9cee3af88a94c65fa81650e824d515f089192b7e3c09c0220119c1fcfd9354755ea815cf714c181b56784
b8f98f59f33e977c8939cd6f75db0121022e9f31292873eee495ca9744fc410343ff373622cca60d3a4c926e58716114b9ff
ffffffc9f3b07ebfca68fd1a6339d0808fbb013c90c6095fc93901ea77410103489ab7010000008a47304402206b993231ad
ec55e6085e75f7dc5ca6c19e42e744cd60abaff957b1c352b3ef9a022022a22fec37dfa2c646c78d9a0753d56cb4393e8d0b
22dc580ef1aa6cccef208d0141042ff65bd6b3ef04253225405ccc3ab2dd926ff2ee48aac210819698440f35d785ec3cec92
a51330eb0c76cf49e9e474fb9159ab41653a9c1725c031449d31026affffffff031027000000000000475121022e9f312928
73eee495ca9744fc410343ff373622cca60d3a4c926e58716114b9210250504b2d42455441506565722d506565722d6e6574
776f726b207075626c696352aee07b9a3b000000001976a914391ef5239da2a3904cda1fd995fb7c4377487ea988ac000000
00000000000d6a0b436f6465206973204c617700000000

对上述报文按协议规则可按字段分解说明如下:

01000000     // 版本号,UINT32

02        // Tx输入数量,变长INT。0×02=2个输入。

/*** 接下来是第一组Input Tx ***/
eb2121e4e727bdb28525e79d39a90bd711b9e8413c054b29ffc4bb4775e69f82  // Tx交易的Hash值,固定32字节
01000000     // 消费的Tx位于前向交易输出的第2个,UINT32,固定4字节
6b        // 接下来对应签名数据的长度, 0x6b = 107字节 。
// 这107字节长度的签名,含有两个部分:私钥签名 + 公钥。
// 当这里的数值为00时,则表示为尚未经过签名的原始交易
48        // 对应私钥签名的数据长度,0×48 = 72字节

3045022100df82cf6c95b4eb64e4e9cee3af88a94c65fa81650e824d515f089192b7e3c09c022011
9c1fcfd9354755ea815cf714c181b56784b8f98f59f33e977c8939cd6f75db01 //私钥签名内容

21        // 对应公钥的数据长度,0×21 = 33字节
022e9f31292873eee495ca9744fc410343ff373622cca60d3a4c926e58716114b9  //对应公钥数据
ffffffff     // 序列号,UINT32, 固定4字节。该字段是目前未被使用的交易替换功能,默认都设成0xFFFFFFFF

/*** 第二组Input Tx。与上同理,省略分解 ***/

c9f3b07ebfca68fd1a6339d0808fbb013c90c6095fc93901ea77410103489ab7010000008a473044
02206b993231adec55e6085e75f7dc5ca6c19e42e744cd60abaff957b1c352b3ef9a022022a22fec
37dfa2c646c78d9a0753d56cb4393e8d0b22dc580ef1aa6cccef208d0141042ff65bd6b3ef042532
25405ccc3ab2dd926ff2ee48aac210819698440f35d785ec3cec92a51330eb0c76cf49e9e474fb91
59ab41653a9c1725c031449d31026affffffff

 

03        // Tx输出交易数量,变长INT类型。0×03=3个输入。

/*** 第一组输出 ***/
1027000000000000 //输出的比特币数量,UINT64,8个字节。字节序需翻转得到0×0000000000002710 = 10000 satoshi = 0.0001 BTC
47        //输出描述脚本字节数, 0×47 = 71字节,由一些操作码与数值构成
51        //Ox51代表OP_1(将脚本代码1压入堆栈)
21        //压入堆栈的第一个公钥的数据长度,0×21 = 33字节
022e9f31292873eee495ca9744fc410343ff373622cca60d3a4c926e58716114b9
21        //压入堆栈的第二个公钥的数据长度,0×21 = 33字节
0250504b2d42455441506565722d506565722d6e6574776f726b207075626c6963
52        //Ox52 代表 OP_2 (将脚本代码2压入堆栈)。与前面的0×51合在一起表示1of2多重签名
ae        //Oxae 代表 OP_CHECKMULTISIG (执行多重签名验证)

/*** 第二组输出 ***/
e07b9a3b00000000 //输出的比特币数量,UINT64,8个字节。字节序需翻转,
19        //输出描述脚本字节数, 0×19 = 25字节,由一些操作码与数值构成
76        //脚本起始操作 0×76 代表 OP_DUP(复制栈顶元素)
a9        //地址类型 0xa9 代表 OP_HASH160(栈顶项进行两次HASH,先用SHA-256,再用RIPEMD-160)
14        //地址长度 0×14 = 20字节
391ef5239da2a3904cda1fd995fb7c4377487ea9  // 对应的HASH160值,20字节
88        //0×88 代表 OP_EQUALVERIFY (运行脚本的二进制算术和条件,如结果为0,之后运行OP_VERIFY)
ac        //Oxac 代表 OP_CHECKSIG (交易所用的签名必须是哈希值和公钥的有效签名,如果为真,则返回1)
/*** 第三组输出 ***/
0000000000000000 //输出的比特币数量,UINT64,8个字节。因为是为了加入备注信息,不是普通转账交易,所以输出金额为0
0d  //输出描述脚本字节数, 0x0d = 13字节,由一些操作码与数值构成
6a  //0x6a代表OP_RETURN(标记交易无效),表示该交易只是追加的备注信息,不是普通转账交易
0b  //备注内容长度, 0x0b = 11字节
436f6465206973204c6177  //备注数据内容(将原始二进制数据按16进制ASCII码形式表示)

00000000     // 锁定时间,UINT32,固定4字节

通过上述解析,理解和掌握了对比特币交易记录的常见组织格式后,充分利用其中对应蓝色字体部分的数据内容块来嵌入自定义的一些二进制数据内容,就可以开发实现自己特定的业务逻辑了。

 

三、运行示例程序

下述的示例程序是将一段特定内容的字符串按一定格式嵌入到比特币交易的备注数据块中,这样就可以被存入比特币区块链上。

在运行下述示例程序前,请确认已安装比特币测试网络(bitcoin-testnet)的Docker运行环境。如尚未安装可以参考上一篇《比特币区块链开发由浅入深指南2》里面的说明进行安装( http://www.8btc.com/ppkpub_blockchain_develope_lesson_2 )。

将下述示例代码复制保存到比特币测试网络(bitcoin-testnet)的测试环境下(保存文件名为 OpreturnTestnet.js),在命令行下输入以下命令即可运行并看到运行结果:
node OpreturnTestnet.js

注意:每运行一次测试代码后,都需要到Docker运行环境的命令行下输入”make generate BLOCKS=10″, 模拟产生新的区块记录,让测试代码产生的交易记录得到有效的确认。

示例程序 OpreturnTestnet.js 源码如下:

/********************* 示例代码起始 **********************/

比特币区块链开发指南(三)

/********************* 示例代码结束 **********************/

源码文件如下

//************************************************//
//  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;
}

 

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: