目录
- 基础知识
- 开发准备
- 实战开发
- 总结
- 参考文献
基础知识
1、什么是比特币?
- 比特币是一种创新的支付网络和一种新型的货币。
- 比特币本质上就是一个分布式的P2P网络系统。它由一系列电脑(或其它计算设备)相互连通构成一个P2P网络。每个电脑上都装有一系列的软件,这些软件就构成一个分布式计算系统,用于协调这些电脑完成相互链接、相互传递消息和通信、协调各自的任务和分工。最终这些电脑彼此交互要实现一个共同的目标——维护一套数据库的完整和更新。
- 从技术层面来看,比特币是最早和最成功的区块链应用,它可以被看作一个由加密算法,共识机制,p2p网络等技术组合而成的系统。
2、比特币钱包又是什么?
- 比特币钱包相当于与比特币交易的实体钱包。不同平台有不同的钱包。要开始使用比特币(比特币),首先你需要一个比特币钱包。它允许您进行交易,即买卖加密货币。比特币钱包的主要任务是存储访问比特币地址所需的密钥,以及相应的资金。
- 从技术角度来看,比特币本身不存储在任何地方,只存储秘密数字密钥,这可以访问公共比特币地址和“签署”交易的能力。这是为了这个信息,需要一个比特币钱包。钱包是不同的,取决于它们所针对的设备,您甚至可以不使用计算机并将密钥写在纸上。当然,钱包有备份副本并且受到保护以防止未经授权的访问是非常重要的。
3、比特币交易又是什么?
-
比特币的交易(Transation,缩写Tx),并不是通常意义的交易,例如一手交钱一手交货,而是转账。交易由N个输入和M个输出两部分组成。交易的每个输入便是前向交易的某个输出,那么追踪到源头,必然出现一个没有输入的交易,此类交易称为CoinBase Tx。CoinBase类交易是奖励挖矿者而产生的交易,该交易总是位于Block块的第一笔。
注:此图来自比特币白皮书
开发准备
1、bitcoinj基础:
bitcoinj是用于处理比特币协议的库。它可以维护一个钱包,发送/接收交易,而无需本地的Bitcoin Core副本,并具有许多其他高级功能。它用Java实现。
2、bitcoinj特征:
- 高度优化的轻量级简化付款验证(SPV)模式。在这种模式下,仅下载了一小部分区块链,从而使比特币适合在受约束的设备(如智能手机或廉价的虚拟专用服务器)上使用。
- 实验性完整验证模式,其验证工作与Bitcoin Core相同。在这种模式下,将计算未使用的事务输出集(UTXO集),并且由于有PostgreSQL存储,因此可以将其索引到数据库中,从而可以按地址快速查找余额。
- 带有加密,费用计算,多重签名,确定性密钥派生,可插入硬币选择/硬币控制,扩展支持和事件监听器的钱包类,可让您随时了解余额的变化。
- 支持小额支付通道,使您可以在客户端和服务器之间建立多签名合同,然后在该通道上进行协商,从而实现快速的小额支付,从而避免了矿工费用。
- 为网络IO 提供异步和每连接线程数,从而允许您在可伸缩性和仅阻止功能(例如SOCKS代理)之间进行选择。
- 轻松实现使用比特币合约功能的应用程序。
- 一个简单的GUI钱包应用程序,您可以将其用作自己的应用程序的基础。观看或阅读有关如何对其进行自定义并构建不需要Java的本机安装程序的教程。
- 用于处理钱包和链式文件,支付协议,网络等的命令行工具。
3、我们怎么使用bitcoinj:
- 在Github下载源码,自己编译。
gradle clean build
gradle clean assemble
- 使用Maven或Gradle依赖。
compile 'org.bitcoinj:bitcoinj-core:0.15.6'
for Android
implementation 'org.bitcoinj:bitcoinj-core:0.15.6'
实战开发
生成账号:
1、随机生成账号
String mnemonic = ChainUtil.genMnemonic(Words.TWELVE);
ECKey ecKey = ChainUtil.genECKey(mnemonic, "m/44'/0'/0'/0/0");
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("mnemonic:" + mnemonic);
System.out.println("privateKey:" +ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
mnemonic:across slow mechanic portion hospital perfect zone best capable champion pond exhaust
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU
2、助记词导入
String mnemonic = "across slow mechanic portion hospital perfect zone best capable champion pond exhaust";
ECKey ecKey = ChainUtil.genECKey(mnemonic, "m/44'/0'/0'/0/0");
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("mnemonic:" + mnemonic);
System.out.println("privateKey:" + ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
mnemonic:across slow mechanic portion hospital perfect zone best capable champion pond exhaust
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU
3、私钥导入
String privateKey = "L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj";
ECKey ecKey = DumpedPrivateKey.fromBase58(MainNetParams.get(), privateKey).getKey();
Address address = Address.fromKey(MainNetParams.get(), ecKey, Script.ScriptType.P2PKH);
System.out.println("privateKey:" +ecKey.getPrivateKeyAsWiF(MainNetParams.get()));
System.out.println("publicKey:" + ecKey.getPublicKeyAsHex());
System.out.println("address:" + address.toString());
输出:
privateKey:L1QVhXZ4bBnQGwAWYDRCNQrkyeSLCiBjKhp5ewLKdXwDpxYVoifj
publicKey:020afa4282079e6898fe04da1ba2c0eb6eec6e0c04d691c281869672ea212f1fe1
address:17TBeyxjCpTcZb3TUg8b1GRR6DCJ5mi1UU
4、获取余额:
https://blockchain.info/balance?active=$address
curl --location --request GET 'https://blockchain.info/balance?active=39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M'
结果:
{
"39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M": {
"final_balance": 0,
"n_tx": 2,
"total_received": 712880
}
}
地址:39rgKZGLDfNcQ9nLrNEujpm53c6SAqfM8M
解析final_balance字段就可以拿到余额了
5、获取未花费的交易输出(Unspent outputs)
https://blockchain.info/unspent?active=$address
curl --location --request GET 'https://blockchain.info/unspent?&active=1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY'
结果:
{
"notice": "",
"unspent_outputs": [
{
"tx_hash": "9b14b25798b431d2d0f749e958fd8cec0bbe109e16d890754671aec3a0602628",
"tx_hash_big_endian": "282660a0c3ae71467590d8169e10be0bec8cfd58e949f7d0d231b49857b2149b",
"tx_output_n": 0,
"script": "76a914c825a1ecf2a6830c4401620c3a16f1995057c2ab88ac",
"value": 1274264286,
"value_hex": "4bf3bade",
"confirmations": 138,
"tx_index": 0
}
]
}
说明:unspent_outputs就是你未花费的列表。
6、费用估算API
curl --location --request GET 'https://bitcoinfees.earn.com/api/v1/fees/recommended' \
--header 'Cookie: __cfduid=d402b8e2522ca17f6b6cbb1392842166b1572919642'
结果:
{
"fastestFee": 8,
"halfHourFee": 4,
"hourFee": 2
}
7、手续费计算。
- size 的计算公式
size = 148 x inputNum + 34 x outputNum + 10
算出字节数后,再乘以rate(Satoshi/byte),rate通过费用估算API获取
提示:所以为了转账少花手续费,最好把utxo列表根据余额从大到小做个排序
- 手续费计算:
unSpentBTCList
value
rate sta/byte
-1发送的value超出了你的余额
public static long getFee(@NonNull List unSpentBTCList, long value, int rate) {
long fee = 0L;
int inputNum = 0;
long totalMoney = 0;
for (UnSpentBTC us : unSpentBTCList) {
inputNum++;
totalMoney += us.getSatoshis();
if (totalMoney > value) {
fee = (148 * inputNum + 34 * 1 + 10) * rate;
if (totalMoney == (value + fee))
return fee;
else if (totalMoney > (value + fee)) {
fee = (148 * inputNum + 34 * 2 + 10) * rate;
if (totalMoney >= (value + fee))
return fee;
}
}
}
return -1;
}
有的朋友可能还会需要算出,最大能够发送的钱数,我这里也给一下代码,仅做参考:
public static long getMaxSendValue(List unSpentBTCList, long totalMoney, int rate) {
while (true) {
long fee = getFee(unSpentBTCList, totalMoney, rate);
if (fee == -1)
totalMoney -= 100;
else
break;
}
/* long fee = getFee(unSpentBTCList, totalMoney, rate);
if (fee == -1) {
totalMoney -= 100;
getMaxSendValue(unSpentBTCList, totalMoney, rate);
}*/
return totalMoney;
}
8、签名交易
NetworkParameters params = MainNetParams.get();
String formAddr = "你的地址";
String privateKey = "你的私钥";
String recevieAddr = "接收地址";
long amount = 1000; // "你要转多少"
List unUtxos = getUnspentOutputs(formAddr);
int sat = 10;//手续费比列
if (null != unUtxos && !unUtxos.isEmpty()) {
Collections.sort(unUtxos, (o1, o2) -> o2.getValue() - o1.getValue());
List utxos = new ArrayList<>();
DumpedPrivateKey dumpedPrivateKey = null;
ECKey key = null;
if (!TextUtils.isEmpty(privateKey)) {
dumpedPrivateKey = DumpedPrivateKey.fromBase58(params, privateKey);
key = dumpedPrivateKey.getKey();
}
// 接收地址
Address receiveAddress = Address.fromString(params, recevieAddr);
// 构建交易
Transaction tx = new Transaction(params);
tx.addOutput(Coin.valueOf(amount), receiveAddress); // 转出
// 如果需要找零 消费列表总金额 - 已经转账的金额 - 手续费
long value = 0;
int fee = getFee(unUtxos, amount, sat);
int size = fee / sat;
for (UnspentOutputsBean.UnspentOutputsBean unUtxo : unUtxos) {
value += unUtxo.getValue();
utxos.add(new UTXO(Sha256Hash.wrap(unUtxo.getTx_hash_big_endian()),
unUtxo.getTx_output_n(),
Coin.valueOf(unUtxo.getValue()),
unUtxo.getConfirmations(),
false,
new Script(HexUtils.toBytes(unUtxo.getScript()))));
if (value > fee + amount) {
break;
}
}
Address toAddress = Address.fromString(params, formAddr);
tx.addOutput(Coin.valueOf(value - amount - fee), toAddress); //找零地址
for (UTXO utxo : utxos) {
TransactionOutPoint outPoint = new TransactionOutPoint(params, utxo.getIndex(), utxo.getHash());
// YOU HAVE TO CHANGE THIS
if (key != null) {
tx.addSignedInput(outPoint, utxo.getScript(), key, Transaction.SigHash.ALL, true);
}
}
org.bitcoinj.core.Context context = new org.bitcoinj.core.Context(params);
tx.getConfidence().setSource(TransactionConfidence.Source.NETWORK);
tx.setPurpose(Transaction.Purpose.USER_PAYMENT);
System.out.println("size: " + size);
System.out.println("sat: " + sat);
System.out.println("fee: " + fee);
System.out.println("hash: " + tx.getTxId().toString());
System.out.println("serializeHex: " + Hex.toHexString(tx.bitcoinSerialize()));
}
9、广播
在上一步拿到了序列化的16进制交易,我们拿到了后直接广播就可以了。
curl --location --request POST 'https://blockchain.info/pushtx' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'tx=你的16进制交易数据'
返回状态码200就广播成功了,接下来就等着被确认了。
10、获取交易详情。
在第7步的时候已经拿到了交易hash,并在8步交易成功后,就可以查询交易详情就,具体操作如下。
https://blockchain.info/rawtx/$tx_hash
curl --location --request GET 'https://blockchain.info/rawtx/e56b59f60f45842e43e4f3cf418d0e090b358b3c7433bde6cecb26f04f3b1b3a'
结果:
{
"ver": 2,
"inputs": [
{
"sequence": 4294967295,
"witness": "0000000000000000000000000000000000000000000000000000000000000000",
"script": "032c800904eb7e785e45552f48756f42692ffabe6d6d177b70cf4446e4298c1c2d14e8eec6d95846065d0e6fc12cd1a9b0726bdf16400800000090fed2180358dcf2a44b140000000000"
}
],
"weight": 1048,
"block_height": 622636,
"relayed_by": "0.0.0.0",
"out": [
{
"spent": true,
"spending_outpoints": [
{
"tx_index": 0,
"n": 13
}
],
"tx_index": 0,
"type": 0,
"addr": "1MvYASoHjqynMaMnP7SBmenyEWiLsTqoU6",
"value": 1261606981,
"n": 0,
"script": "76a914e582933875bedfdc448473c00b474f8f053a467588ac"
},
{
"spent": false,
"tx_index": 0,
"type": 0,
"value": 0,
"n": 1,
"script": "6a24aa21a9edf8ce97a293f7d1ba0a8e5634d3a12080bbfdf22a3392005c52a2ca9ad6ea65ca"
},
{
"spent": false,
"tx_index": 0,
"type": 0,
"value": 0,
"n": 2,
"script": "6a24b9e11b6d8d12ceae709815f42c2aedf406aa65d9e25b4c6075a7ce5f8505c0ff18c946ef"
}
],
"lock_time": 0,
"size": 289,
"block_index": 0,
"time": 1584955126,
"tx_index": 0,
"vin_sz": 1,
"hash": "e56b59f60f45842e43e4f3cf418d0e090b358b3c7433bde6cecb26f04f3b1b3a",
"vout_sz": 3
}
总结
1、生成比特币账号相对简单点,都提供了相关的API,调用即可。
2、交易就相对麻烦一点,要知道比特币的交易模型,添加输入输出,计算手续费,并构建16进制的交易数据,广播即可。
3、在创建交易的时候一定要添加找零地址,不然的话后果就是,你没用完的比特币会全部交给矿工,切记 切记 切记。
参考文献
[1] 比特币白皮书:https://bitcoin.org/files/bitcoin-paper/bitcoin_zh_cn.pdf
[2] BitcoinJ Github:https://github.com/bitcoinj/bitcoinj
[3] 比特币官网:https://bitcoin.org/zh_CN/
[4]感谢获取手续费文章:https://blog.csdn.net/wypeng2010/article/details/81357684
[5]blockchain:https://www.blockchain.com/zh-cn/api/blockchain_api