钱包开发经验分享:BTC篇

文章目录

  • 钱包开发经验分享:BTC篇
    • BTC节点搭建
    • BTC的账户模型——UTXO
    • 计算余额
    • 计算矿工费:
    • 获取未花费列表
    • 离线签名
    • 广播交易
    • 计算矿工费
    • 优化矿工费
    • 生成钱包地址

BTC节点搭建

关于BTC的第一步,自然是搭建节点。由于BTC流行最久最广,网络上关于BTC的节点搭建,或者在同步节点时出现问题的相关文章很多,我这里就不赘述了(主要是没有环境用来搭建节点)。这里推荐一篇文章:区块链-Linux下Bitcoin测试节点搭建。没有搭建节点的可以考虑一下两个网站:blockcypher、blockchain。

BTC的账户模型——UTXO

关于UTXO的含义阐述可以参考理解比特币的 UTXO、地址和交易,这篇文章对UTXO的阐述我觉得挺全面的。在里面提到:在比特币种,一笔交易的每一条输入和输出实际上都是 UTXO,输入 UTXO 就是以前交易剩下的, 更准确的说是以前交易的输出 UTXO。这句阐述得从JSON数据去理解。

每一笔交易包含了大于等于一个输出,如下图:

输出列表包含了输出数量(value)、输入脚本(script)、地址(addresses)和脚本类型(script_type),我们主要关注输入数量。

每一笔交易的JSON都包含了大于等于零个输入(挖矿收益没有输入),如下图:

输入列表包含这笔输入对应的上一笔交易的哈希(prev_hash)、这笔输入对应的上一笔交易输出的下标(output_index),输入脚本(script)、脚本类型(scrip_type)等字段。在输入中最重要的两个字段是上一笔交易的哈希和输出下标,由这两个字段,我们可以轻松找到这笔输入对应上一笔交易的输出,从而从输出中找到这笔输入的数量是多少。

计算余额

由上面的账户模型,我们知道了BTC的账户是由UTXO列表组成,每个账户从创建初期到当前的所有交易就是一系列的输入和输出,这些UTXO通过输入输出的规则串联在一起,形成了链式结构,因此要推算账户余额,我们可以通过计算这一系列的UTXO最终获得余额,但是在实际开发上这样做很消耗性能,因此在开发上我们往往考虑直接从第三方区块链浏览器通过开放API获得计算结果。事实上,基本上结合第三方区块链浏览器开发的API,没有搭建节点我们也可以直接完成很多操作:

参考代码:

    /*** 余额* @param address* @param mainNet* @return*/public static String balance(String address, boolean mainNet){String host = mainNet ? "blockchain.info" : "testnet.blockchain.info";String url = "https://"   host   "/balance?active="   address;OkHttpClient client = new OkHttpClient();String response = null;try {response = client.newCall(new Request.Builder().url(url).build()).execute().body().string();} catch (IOException e) {e.printStackTrace();}Map<String, Map<String, Object>> result = new Gson().fromJson(response, Map.class);Map<String, Object> balanceMap = result.get(address);BigDecimal finalBalance = BigDecimal.valueOf((double) balanceMap.get("final_balance"));BigDecimal balance = finalBalance.divide(new BigDecimal(100000000));return balance.toPlainString();}

测试代码:

 /*** 获取余额* @throws Exception*/@Testpublic void testGetBTCBalance() throws Exception{String address = "17A16QmavnUfCW11DAApiJxp7ARnxN5pGX";String balance = BtcUtil.balance(address, true);logger.warn("balance: {}", balance);}

计算矿工费:

由上面的结论可知,每一笔交易都由零个、一个或多个输入和一个或多个输出组成,每一个输入都指向上一笔交易的输出,这样每一笔交易都由这些输入输出(UTXO)串行而成。一般而言,一笔交易会有一个或多个多个输入,这些输入的数量总和刚好或者大于这次交易的数量,会有一个或多个输出,输出主要有这次交易的收款地址和数量,以及找零地址和找零数量,找零地址通常是原地址,输入的数量总和和输出的数量总和总是不相等的,因为每一笔交易中间包含了矿工费,由此我们可以推断出矿工费的计算方式,即每一笔的输入总和减去输出总和:

参考代码:

    /*** 计算矿工费* @param txid* @param mainNet* @return*/public static String fee(String txid, boolean mainNet){String host = mainNet ? "blockchain.info" : "testnet.blockchain.info";String url = "https://"   host   "/rawtx/"   txid;OkHttpClient client = new OkHttpClient();String response = null;try {response = client.newCall(new Request.Builder().url(url).build()).execute().body().string();} catch (IOException e) {e.printStackTrace();}JSONObject jsonObject = JSONObject.parseObject(response);// 统计输入总和JSONArray inputs = jsonObject.getJSONArray("inputs");BigDecimal totalIn = BigDecimal.ZERO;for (int i = 0; i < inputs.size(); i  ) {JSONObject inputsData = inputs.getJSONObject(0);JSONObject prevOut = inputsData.getJSONObject("prev_out");totalIn = totalIn.add(prevOut.getBigDecimal("value"));}// 统计输出总和JSONArray outs = jsonObject.getJSONArray("out");BigDecimal totalOut = BigDecimal.ZERO;for (int i = 0; i < outs.size(); i  ) {JSONObject outData = outs.getJSONObject(i);totalOut = totalOut.add(outData.getBigDecimal("value"));}return totalIn.subtract(totalOut).divide(new BigDecimal(100000000)).toPlainString();}

测试代码:

 /*** 计算矿工费* https://blockchain.info/rawtx/$tx_hash*/@Testpublic void testGetMinerFee(){String txid = "b8df97b51f54df1c1f831e0e9e5561c03822f6c5a5a59e0118b15836657a4970";logger.warn("Fee: {}", BtcUtil.fee(txid, true));}

通过第三方区块链浏览器开放的API获取的交易数据和自己搭建节点获取的交易数据有些许不同,如果是自己搭建节点,我推荐使用azazar/bitcoin-json-rpc-client或者其他的封装了bitcoinRPC接口的SDK去实现,这样是最简单,最省事的实现方式,他封装了很多对象,不用我们手动从JSONObject对象去获取需要的数据,而且通过这些SDK我们可以真正像调用方法一样调用bitcoin节点的接口。

获取未花费列表

参考代码:

    /**** 获取未消费列表* @param address :地址* @return*/public static List<UTXO> getUnspent(String address, boolean mainNet) {List<UTXO> utxos = Lists.newArrayList();String host = mainNet ? "blockchain.info" : "testnet.blockchain.info";String url = "https://"   host   "/zh-cn/unspent?active="   address;try {OkHttpClient client = new OkHttpClient();String response = client.newCall(new Request.Builder().url(url).build()).execute().body().string();if (StringUtils.equals("No free outputs to spend", response)) {return utxos;}JSONObject jsonObject = JSON.parseObject(response);JSONArray unspentOutputs = jsonObject.getJSONArray("unspent_outputs");List<Map> outputs = JSONObject.parseArray(unspentOutputs.toJSONString(), Map.class);if (outputs == null || outputs.size() == 0) {System.out.println("交易异常,余额不足");}for (int i = 0; i < outputs.size(); i  ) {Map outputsMap = outputs.get(i);String tx_hash = outputsMap.get("tx_hash").toString();String tx_hash_big_endian = outputsMap.get("tx_hash_big_endian").toString();String tx_index = outputsMap.get("tx_index").toString();String tx_output_n = outputsMap.get("tx_output_n").toString();String script = outputsMap.get("script").toString();String value = outputsMap.get("value").toString();String value_hex = outputsMap.get("value_hex").toString();String confirmations = outputsMap.get("confirmations").toString();UTXO utxo = new UTXO(Sha256Hash.wrap(tx_hash_big_endian), Long.valueOf(tx_output_n), Coin.valueOf(Long.valueOf(value)),0, false, new Script(Hex.decode(script)));utxos.add(utxo);}return utxos;} catch (Exception e) {return null;}}

测试代码:

 /*** 获取未花费列表*/@Testpublic void testGetUnSpentUtxo(){String address = "17A16QmavnUfCW11DAApiJxp7ARnxN5pGX";List<UTXO> unspent = BtcUtil.getUnspent(address, true);logger.warn("unspent: {}", unspent);}

离线签名

参考代码:

    /*** 离线签名* @param unSpentBTCList* @param from* @param to* @param privateKey* @param value* @param fee* @param mainNet* @return* @throws Exception*/public static String signBTCTransactionData(List<UTXO> unSpentBTCList, String from, String to, String privateKey, long value, long fee, boolean mainNet) throws Exception {NetworkParameters networkParameters = null;if (!mainNet)networkParameters = MainNetParams.get();elsenetworkParameters = TestNet3Params.get();Transaction transaction = new Transaction(networkParameters);DumpedPrivateKey dumpedPrivateKey = DumpedPrivateKey.fromBase58(networkParameters, privateKey);ECKey ecKey = dumpedPrivateKey.getKey();long totalMoney = 0;List<UTXO> utxos = new ArrayList<>();//遍历未花费列表,组装合适的itemfor (UTXO us : unSpentBTCList) {if (totalMoney >= (value   fee))break;UTXO utxo = new UTXO(us.getHash(), us.getIndex(), us.getValue(), us.getHeight(), us.isCoinbase(), us.getScript());utxos.add(utxo);totalMoney  = us.getValue().value;}transaction.addOutput(Coin.valueOf(value), Address.fromBase58(networkParameters, to));// transaction.//消费列表总金额 - 已经转账的金额 - 手续费 就等于需要返回给自己的金额了long balance = totalMoney - value - fee;//输出-转给自己if (balance > 0) {transaction.addOutput(Coin.valueOf(balance), Address.fromBase58(networkParameters, from));}//输入未消费列表项for (UTXO utxo : utxos) {TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, utxo.getIndex(), utxo.getHash());transaction.addSignedInput(outPoint, utxo.getScript(), ecKey, Transaction.SigHash.ALL, true);}return Hex.toHexString(transaction.bitcoinSerialize());}

签名之后的结果就可以拿去广播了,没有自己搭建节点的可以使用blockcypher/send广播自己的交易。在上面的交易中,找零是自己本身,当然,也可以设置为其他钱包地址。其次,在这个交易中,交易手续费是在前置步骤计算得到的,其计算方式下面会提到。

广播交易

如果是自己搭建了节点,可以直接调用接口广播交易,这里是针对没有搭建节点,但是想要完成整个交易流程的同学们。我们可以在搜索引擎上找到很多可以为我们广播交易的API,我这里使用的是上文提到的blockcypher/send。

参考代码:

    /*** 全网广播交易* @param tx* @param mainNet* @return*/public static String sendTx(String tx, boolean mainNet){String url = "";if(mainNet) {url = "https://api.blockcypher.com/v1/btc/main/txs/push";}else {url = "https://api.blockcypher.com/v1/btc/test3/txs/push";}OkHttpClient client = new OkHttpClient();JSONObject jsonObject = new JSONObject();jsonObject.put("tx", tx);String response = null;try {response = client.newCall(new Request.Builder().url(url).post(RequestBody.create(MediaType.parse("application/json"), jsonObject.toJSONString())).build()).execute().body().string();} catch (IOException e) {e.printStackTrace();}return response;}

计算矿工费

关于矿工费计算公式的解释,可以参考BTC手续费计算,如何设置手续费。通过文章指导,要计算矿工费首先我们需要得到费率,即每字节等于多少聪。

参考代码:

    /*** 获取费率* @param level 3 fastestFee 2 halfHourFee 1 hourFee default fastestFee* @return*/public static String feeRate(int level){OkHttpClient client = new OkHttpClient();String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";String response = null;try {response = client.newCall(new Request.Builder().url(url).build()).execute().body().string();} catch (IOException e) {e.printStackTrace();}JSONObject jsonObject = JSONObject.parseObject(response);switch (level){case 1:return jsonObject.getBigDecimal("hourFee").toPlainString();case 2:return jsonObject.getBigDecimal("halfHourFee").toPlainString();default:return jsonObject.getBigDecimal("fastestFee").toPlainString();}}

测试代码:

 @Testpublic void testGetFeeRate(){logger.warn("feeRate: {}", BtcUtil.feeRate(3));}

获得费率之后就可以计算矿工费了。一般而言,一笔交易包含了若干个输入,这些输入的数量总和刚好能支付这笔交易的数量的时候,输出的体积是最小的,仅一个接收地址的输出,当这些输入的数量总和大于这笔交易的数量时,输出的数量包含了一个接收地址的输出和一个找零的输出,通过上面离线签名的代码也能很容易理解这点。

参考代码:

    /*** 获取矿工费用* @param amount* @param utxos* @return*/public static Long getFee(long amount, List<UTXO> utxos) {Long feeRate = Long.valueOf(feeRate(3));//获取费率Long utxoAmount = 0L;Long fee = 0L;Long utxoSize = 0L;for (UTXO us : utxos) {utxoSize  ;if (utxoAmount >= (amount   fee)) {break;} else {utxoAmount  = us.getValue().value;fee = (utxoSize * 148   34 * 2   10) * feeRate;}}return fee;}

测试代码:

 @Testpublic void testGetFee(){String address = "17A16QmavnUfCW11DAApiJxp7ARnxN5pGX";List<UTXO> unspent = BtcUtil.getUnspent(address, true);Long fee = BtcUtil.getFee(100 * 100000000, unspent);logger.warn("fee: {}", BigDecimal.valueOf(fee / 100000000.0).toPlainString());}

优化矿工费

通过矿工费的计算公式(input*148 34*out 10)*rate,我们很容易想到减少矿工费的手段,主要有两个方面:其一选择较低的矿工费率,这样能明显减低矿工费,因为公式上能明显反映rate和输入输出的体积是倍数关系,所以减小rate是能够最有效减少矿工费的,但是相对的这种方式带来的负面影响也是直接的,它会影响打包的效率。其二是减小输入输出的体积,我们在组装一个能够支付本次交易的列表是,往往是直接遍历未花费列表,累加判断,但是其实我们可以通过一些算法,使得支付当前交易的未花费列表最小化,这个算法计算翻译过来其实是使用尽可能少的列表项,使得交易等式两边成立,根据这个结论,最简单的实现方式就是在使用未花费列表前,先对未花费列表进行倒序排序:

测试代码:

 @Testpublic void testGetFee(){String address = "17A16QmavnUfCW11DAApiJxp7ARnxN5pGX";List<UTXO> unspents = BtcUtil.getUnspent(address, true);Long fee1 = BtcUtil.getFee(100 * 100000000, unspents);Collections.sort(unspents, (o1, o2) -> BigInteger.valueOf(o2.getValue().value).compareTo(BigInteger.valueOf(o1.getValue().value)));Long fee2 = BtcUtil.getFee(100 * 100000000, unspents);logger.warn("排序前矿工费: {}, 排序后矿工费: {}", BigDecimal.valueOf(fee1 / 100000000.0).toPlainString(), BigDecimal.valueOf(fee2 / 100000000.0).toPlainString());}

对比结果:

排序前矿工费: 0.00137968, 排序后矿工费: 0.00001808

根据这个想法,我在测试链发起了两笔交易,交易额都是0.012BTC,对比了两笔交易如下:

优化前:

优化后:

可以看到,优化前有两个输入,优化后只有一个输入,优化后矿工费比优化前少了一些。

生成钱包地址

参考代码:

 public static final Map<String, String> btcGenerateBip39Wallet(String mnemonic, String mnemonicPath) {if (null == mnemonic || "".equals(mnemonic)) {byte[] initialEntropy = new byte[16];SecureRandom secureRandom = new SecureRandom();secureRandom.nextBytes(initialEntropy);mnemonic = generateMnemonic(initialEntropy);}String[] pathArray = mnemonicPath.split("/");List<ChildNumber> pathList = new ArrayList<ChildNumber>();for (int i = 1; i < pathArray.length; i  ) {int number;if (pathArray[i].endsWith("'")) {number = Integer.parseInt(pathArray[i].substring(0, pathArray[i].length() - 1));} else {number = Integer.parseInt(pathArray[i]);}pathList.add(new ChildNumber(number, pathArray[i].endsWith("'")));}DeterministicSeed deterministicSeed = null;try {deterministicSeed = new DeterministicSeed(mnemonic, null, "", 0);} catch (UnreadableWalletException e) {throw new RuntimeException(e.getMessage());}DeterministicKeyChain deterministicKeyChain = DeterministicKeyChain.builder().seed(deterministicSeed).build();BigInteger privKey = deterministicKeyChain.getKeyByPath(pathList, true).getPrivKey();ECKey ecKey = ECKey.fromPrivate(privKey);String publickey = Numeric.toHexStringNoPrefixZeroPadded(new BigInteger(ecKey.getPubKey()), 66);// 正式String mainNetPrivateKey = ecKey.getPrivateKeyEncoded(MainNetParams.get()).toString();Map<String, String> map = Maps.newHashMap();map.put("mnemonic", mnemonic);map.put("mainNetPrivateKey", mainNetPrivateKey);map.put("publickey", publickey);map.put("address", ecKey.toAddress(MainNetParams.get()).toString());return map;}

测试代码:

 @Testpublic void testGenerateBtcWallet(){Map<String, String> map = AddrUtil.btcGenerateBip39Wallet(null, Constants.BTC_MNEMONIC_PATH);String mnemonic = map.get("mnemonic");String privateKey = map.get("mainNetPrivateKey");String publicKey = map.get("publicKey");String address = map.get("address");logger.warn("address: {}, mnemonic: {}, privateKey: {}, publicKey: {}", address, mnemonic, privateKey, publicKey);}

比特币的钱包地址有一个特征可以区分正式网络还是测试网络,一般比特币钱包地址开头是数字1或3是正式网络,开头是m是测试网络,测试网络和正式网络的钱包地址是不互通的。

对我的文章感兴趣的话,请关注我的公众号

钱包开发经验分享:BTC篇相关推荐

  1. 项目看板开发经验分享(番外篇)——集团碳排放看板(万剑归宗)

    集团碳排放看板展示视频 为什么这期算作一个番外篇呢?因为这个看板里用到的所有开发思路都在前三期全部提到了,也有几个可复用的模块是直接照搬了过来,所以我在开发这个看板时只用了很短的工时就开发完了,算是给 ...

  2. 开发经验分享_06_前端开发技巧

    接上一篇:(企业内部)开发经验分享_05_葫芦画瓢 https://gblfy.blog.csdn.net/article/details/103414567 文章目录 一.JS调试技巧 1. 推荐使 ...

  3. 开发经验分享_05_葫芦画瓢

    接上一篇:(企业内部)开发经验分享_04_快速熟悉程序(思路) https://gblfy.blog.csdn.net/article/details/103344458 文章目录 一.页面布局/分割 ...

  4. 开发经验分享_03_解决问题3步走(思路)

    上一篇:(企业内部)开发经验分享_02_解决问题3步走(实战) https://gblfy.blog.csdn.net/article/details/103414259 文章目录 思路 ① 自己动脑 ...

  5. 开发经验分享_02_解决问题3步走(实战)

    接上一篇:开发经验分享_01_遇到问题3步走(思路+实战) https://gblfy.blog.csdn.net/article/details/103413993 文章目录 一.透过现象看本质 1 ...

  6. 开发经验分享_01_遇到问题三步走(思路+实战)

    企业内部开发经验系列,由gblfy根据自己在公司的亲身经历总结而成,会讲述开发的思路和开发技巧,帮助更多开发人员思路明确.高效率的开发!!! 目前总结章节如下: 章节 链接 (企业内部)开发经验分享_ ...

  7. 开发经验分享_04_快速熟悉程序(思路)

    接上一篇:(企业内部)开发经验分享_03_解决问题3步走(思路) https://gblfy.blog.csdn.net/article/details/103414474 文章目录 一.明确组件布局 ...

  8. 区块链钱包开发(Android篇)

    区块链钱包开发(Android篇) 2018年10月27日 23:07:08 a526247082 阅读数 6309 简介 本文主要内容为区块链钱包移动端(Android)开发,介绍比特币钱包和以太坊 ...

  9. 移动端app设计开发经验之设计篇

    分享一下我老师大神的人工智能教程.零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow @我爱静电 在&l ...

最新文章

  1. freertos 创建互斥量_STM32CubeMX+FreeRTOS学习[6] 互斥量(Lu)
  2. 微信小程序 - 支持html空格(提示)
  3. 使用CloudFlare 的 PKI 工具集 cfssl 来生成 Certificate Authority (CA) 证书和秘钥文件
  4. mysql调用短信接口_短信平台接口调用方法参考
  5. apache poi使用例_4.Apache POI使用详解
  6. 14款CSS3图片层叠切换动画
  7. Vue+ElementUI 限制结束时间不能大于开始时间
  8. crm系统是什么很棒ec实力_哪个CRM系统好
  9. matlab形位公差,基于最小区域法的形位公差精确算法.PDF
  10. 把台式计算机硬盘拆下,台式电脑的硬盘可以拆下来吗
  11. c语言---16 关于goto语句
  12. 刚刚!腾讯荣升Linux基金会白金会员
  13. iBooks 书籍存放位置
  14. MATLAB的乘法函数,Matlab乘法函数
  15. sqldeveloper的安装及其使用教程
  16. 点、线、圆、矩形、抛物线的类定义_德语词汇-数学类
  17. 远山启:用数学照亮人性与自由
  18. 分享一个有趣的斯特林发动机
  19. 仿京东商城左侧商品分类导航-JS网页特效
  20. Oracle 数据库巡检模板

热门文章

  1. 快速识记会计中的借贷两方
  2. bzoj3899 弦论
  3. go语言零知识证明gnark框架
  4. html倒计时的原理,JS实现活动精确倒计时 - 轩枫阁
  5. Swift表格Lxr
  6. winform使用CefSharp嵌入浏览器
  7. 教你利用阿里云服务器搭建一个随时随地的Web IDE
  8. 网站信息的采集系列(一)--基本流程
  9. OIer专用-网址导航
  10. JAVA 并发编程之三:CountDownLatch(门闩)、CyclicBarrier(栅栏)和Semaphore(信号量) 三种并发策略