1. 概述
在本学习笔记中,我们将深入探讨如何使用
bitcoinjs-lib
库构建和签名一个 P2PKH(Pay-to-PubKey-Hash) 比特币交易。P2PKH 是比特币网络中最常见和最基本的交易类型之一,理解其工作原理是掌握比特币交易构建的关键。
想要详细了解p2pkh工作原理可以查看该网址:p2pkh(Pay to PublicKey Hash)
1.1 目标
- 主要目标:学习如何使用
bitcoinjs-lib
构建、签名并广播一个 P2PKH 交易。 - 具体任务:
- 生成一个 P2PKH 地址。
- 构建一个包含输入和输出的交易。
- 将交易广播到regtest网络中。
1.2 P2PKH 的工作原理
- 锁定资金:
发送方创建一个交易输出,将比特币锁定到接收方的公钥哈希(即地址)。
- 解锁资金:
- 接收方在花费比特币时,需要提供:
- 公钥。
- 使用私钥生成的签名。
- 输入脚本(
scriptSig
)格式:<Signature> <PublicKey>
。
- 验证过程:
- 比特币网络会验证签名是否与公钥匹配,以及公钥哈希是否与输出脚本中的哈希一致。
在比特币脚本中,OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
是 P2PKH(Pay-to-PubKey-Hash) 交易的 锁定脚本(scriptPubKey) 的核心部分。它定义了如何锁定比特币以及如何解锁比特币的条件。
以下是对每个操作码的详细解释:
1. OP_DUP
- 作用:复制栈顶元素。
- 操作:
- 将栈顶的元素复制一份,并将副本压入栈中。
- 示例:
- 如果栈顶是
A
,执行OP_DUP
后,栈变为[A, A]
。
- 如果栈顶是
2. OP_HASH160
- 作用:对栈顶元素进行哈希运算。
- 操作:
- 弹出栈顶元素,先进行 SHA-256 哈希,再进行 RIPEMD-160 哈希,最后将结果压入栈中。
- 示例:
- 如果栈顶是
A
,执行OP_HASH160
后,栈变为[Hash160(A)]
。
- 如果栈顶是
3. <PubKeyHash>
- 作用:将接收方的公钥哈希压入栈中。
- 操作:
- 这是一个数据推送操作,将接收方的公钥哈希(20 字节)压入栈中。
- 示例:
- 如果接收方的公钥哈希是
B
,执行后栈变为[Hash160(A), B]
。
- 如果接收方的公钥哈希是
4. OP_EQUALVERIFY
- 作用:验证栈顶两个元素是否相等,并移除它们。
- 操作:
- 弹出栈顶两个元素,比较它们是否相等。
- 如果相等,继续执行;如果不相等,脚本执行失败。
- 示例:
- 如果栈顶是
[Hash160(A), B]
,执行OP_EQUALVERIFY
后:- 如果
Hash160(A) == B
,栈为空,继续执行。 - 如果
Hash160(A) != B
,脚本失败。
- 如果
- 如果栈顶是
5. OP_CHECKSIG
- 作用:验证签名和公钥是否匹配。
- 操作:
- 弹出栈顶两个元素:签名和公钥。
- 使用公钥验证签名是否对当前交易有效。
- 如果验证成功,将
true
压入栈中;否则,将false
压入栈中。
- 示例:
- 如果栈顶是
[Signature, PublicKey]
,执行OP_CHECKSIG
后:- 如果签名有效,栈变为
[true]
。 - 如果签名无效,栈变为
[false]
。
- 如果签名有效,栈变为
- 如果栈顶是
6. 整体脚本的执行流程
假设解锁脚本(scriptSig
)提供了签名和公钥:<Signature> <PublicKey>
,锁定脚本(scriptPubKey
)为 OP_DUP OP_HASH160 <PubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
。
执行步骤:
-
初始栈:
- 解锁脚本将签名和公钥压入栈中:
[Signature, PublicKey]
- 解锁脚本将签名和公钥压入栈中:
-
执行
OP_DUP
:- 复制公钥:
[Signature, PublicKey, PublicKey]
- 复制公钥:
-
执行
OP_HASH160
:- 对公钥进行哈希:
[Signature, PublicKey, Hash160(PublicKey)]
- 对公钥进行哈希:
-
压入
<PubKeyHash>
:- 将接收方的公钥哈希压入栈中:
[Signature, PublicKey, Hash160(PublicKey), PubKeyHash]
- 将接收方的公钥哈希压入栈中:
-
执行
OP_EQUALVERIFY
:- 比较
Hash160(PublicKey)
和PubKeyHash
:- 如果相等,移除这两个元素:
[Signature, PublicKey]
- 如果不相等,脚本失败。
- 如果相等,移除这两个元素:
- 比较
-
执行
OP_CHECKSIG
:- 使用公钥验证签名:
- 如果签名有效,栈变为:
[true]
- 如果签名无效,栈变为:
[false]
- 如果签名有效,栈变为:
- 使用公钥验证签名:
-
脚本结果:
- 如果栈顶为
true
,交易有效,资金可以解锁。 - 如果栈顶为
false
,交易无效,资金无法解锁。
- 如果栈顶为
1.3 工具和技术栈
1.3.1 bitcoinjs-lib
-
简介:
bitcoinjs-lib
是一个用于处理比特币交易的 JavaScript 库。它支持构建、签名和解析比特币交易,适用于 Node.js 和浏览器环境。 -
安装:
# 选择一个目录作为项目目录,打开vscode,打开命令行窗口
npm init -y
# 安装依赖
npm i bitcoinjs-lib ecpair tiny-secp256k1
1.3.2 比特币测试网
- 简介:比特币测试网(Testnet)是一个用于开发和测试的比特币网络,其功能与主网相同,但比特币没有实际价值。(新手建议还是使用regtest,testnet测试网需要申请领取测试币,每次领取金额都较少,一般为10000聪)
- 用途:
- 测试交易构建和签名。
- 避免在主网上浪费真实比特币。
- 获取测试网比特币:
- 使用测试网水龙头(如 testnet-faucet.com)获取免费的测试网比特币。
1.3.3 比特币测试网浏览器
- 用途:查看交易状态和区块链数据。
- 推荐工具:
- blockstream.info/testnet
- mempool.space/testnet
1.4 p2pkh交易demo
1.4.1准备工作
交易:Alice向Bob转账2个比特币
先使用bitcoin-core向Alice的地址转2个比特币,再用这笔未花费交易给Bob发送
在交易记录中右击刚刚的交易,复制原始交易,再根据上一节预备知识中提到的解析交易
或者使用控制台(命令行窗口)
sendtoaddress mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r 2
getrawtransaction txid
decoderawtransaction hex
记录交易的txid、vout中是alice的输出的次序n,金额value
以下是Alice给Bob发送比特币的代码(使用p2pkh)
const bitcoin = require('bitcoinjs-lib');
const network = bitcoin.networks.regtest;
ECPairFactory = require('ecpair').default;
ecc = require('tiny-secp256k1');
const base58check = require('base58check'); //npm i --save base58check
ECPair = ECPairFactory(ecc);
const hashtype = bitcoin.Transaction.SIGHASH_ALL;
// Alice给Bob发送2个比特币
const ONE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex',);
const keyPairAlice = ECPair.fromPrivateKey(ONE, { network });
const TWO = Buffer.from('0000000000000000000000000000000000000000000000000000000000000002', 'hex',);
const keyPairBob = ECPair.fromPrivateKey(TWO, { network });
// alice_address: mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r
// bob_address: mg8Jz5776UdyiYcBb9Z873NTozEiADRW5H
// alice上一笔未花费的交易的txid,
const txid = '4a3cc600d4973479aafb62b5789590dd7738c3f9f1956a513a361e20ef56f9bb'
const vout = 1; // 上一笔交易中的输出
const amount = BigInt(200000000);// 转账金额
// 收款人bob的地址
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPairBob.publicKey })
var PayeeBobAddr = base58check.decode(address, 'hex')
console.log('Bob_hashPubkey:', Buffer.from(PayeeBobAddr.data, 'Hex').toString('hex'));
const tx = new bitcoin.Transaction(network);
// 添加输入
tx.addInput(Buffer.from(txid, 'hex').reverse(), vout);
// 添加输出
const outputScript = bitcoin.payments.p2pkh({ pubkey: keyPairBob.publicKey }).output;
tx.addOutput(outputScript, amount - BigInt(10000));
// 添加签名
// 上一笔交易的输出脚本
const preoutputScript = bitcoin.payments.p2pkh({ pubkey: keyPairAlice.publicKey }).output;
// 生成比特币交易中某个输入的签名哈希值(sighash)。hashtype=1表示对交易的所有输入都进行哈希处理
const signhash = tx.hashForSignature(0, preoutputScript, hashtype);
// 构造p2pkh的解锁脚本: [签名 公钥]
const scriptSig = bitcoin.script.signature.encode(keyPairAlice.sign(signhash), hashtype);
tx.setInputScript(0,
bitcoin.script.compile([scriptSig, keyPairAlice.publicKey])
)
// 输出交易原始数据
console.log('Raw Transaction:', tx.toHex());
// 广播交易
//sendrawtransaction <raw_transaction_hex>
// 获取txHex: getrawtransaction txid
// 解析交易: decoderawtransaction txHex