学习目标:
掌握比特币应用的基本开发流程
我们将采用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对应的备注脚本空间来嵌入字符串,第二个输出交易是一个找零交易,所以需要设置找零地址即金额。
总结:
首先,以上内容来自于书:<<区块链技术指南>> 由 邹均/张海宁/唐屹/李磊 等老师编著,其中唐屹老师还是我们广州大学的教授,真的超赞的。
在我看来,区块链其实是将已有的技术进行了整合,并非所有场合都适合去中心化,而我认为区块链最大的魅力在于他的不可篡改性,已经在其上建议的新型经济模型,用于优化社会“信任”体系等,未来以来。