BitcoinJS 学习笔记 2 - P2SH
1.概述
- 1.1 P2SH 是什么?
- 1.1.1 定义:Pay to Script Hash
- P2SH 允许你将比特币发送到一个“脚本的哈希值”上,而不是像P2PKH直接发送到一个“公钥的哈希值”上
- 即P2SH的地址的脚本的哈希值,而P2PKH的地址是公钥的哈希值。
- 1.1.1 定义:Pay to Script Hash
- 1.2 P2SH 地址
- 1.2.1 生成方式:对赎回脚本进行哈希运算,并进行 Base58Check 编码
- 赎回脚本 (
redeemScript
) 是 P2SH 交易中用于花费比特币的脚本。 - 生成 P2SH 地址时,首先需要对赎回脚本进行哈希运算 HASH160(通常是 SHA256 + RIPEMD160),然后将哈希值进行 Base58Check 编码。
- 赎回脚本 (
- 1.2.2 与 P2PKH 地址的区别
- 锁定脚本不同: P2PKH 使用公钥哈希锁定比特币,而 P2SH 使用脚本哈希锁定比特币。
- 功能不同: P2PKH 只能实现简单的支付功能,而 P2SH 可以实现更复杂的支付逻辑,例如多重签名、时间锁等。
- 1.2.1 生成方式:对赎回脚本进行哈希运算,并进行 Base58Check 编码
- 1.3 P2SH锁定脚本与解锁脚本
scriptSig
(解锁脚本):包含Signatures
签名 和redeemScript
赎回脚本。scriptPubKey
(锁定脚本): 图中的scrtipHash是**赎回脚本**经过
HASH160`处理得到的哈希值
想要详细了解p2sh工作原理可以查看该网址:p2pkh(Pay to Script Hash)
(如果使用校园网可能打不开该网址,搭梯子可能也打不开,可以手机开热点给电脑用)
2.bitcoinjs实现P2SH过程
2.1 创建 P2SH 地址
- 构建赎回脚本 (redeemScript)**
多重签名脚本
- 多重签名脚本需要指定多个公钥和所需的签名数量。
- 创建一个 2-of-3 的多重签名脚本,可以借助p2ms(多重签名协议)来生成 2-of-3 的多重签名脚本:
const pubkeys = [ '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', '02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5', '02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9' ]; const p2ms = bitcoin.payments.p2ms({ m: 2, pubkeys, network }); const redeemScript = p2ms.output; console.log("redeemScript(p2ms):", Buffer.from(p2ms.output).toString('hex'));
控制台打印
redeemScript
: 52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817982102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee52102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f953ae
在p2sh网站中有解析脚本16进制字符串的工具以及HASH160工具,点击即可展开使用
2-of-3多重签名脚本为:OP_2 PUBKEY1 PUBKEY2 PUBKEY3 OP_CHECKMULTISIG
OP_PUSHBYTES_33表示将33字节压入栈中
15fc0754e73eb85d1cbce08786fadb7320ecb8dc
是redeemScript
经过HASH160处理后得到的哈希值,等会生成p2sh地址后,使用bitcoin-core的控制台发送2个比特币后,可以查看该交易中对应的输出,查看锁定脚本中的赎回脚本哈希是否与我们现在得到的一致。
其他复杂脚本
- 除了多重签名脚本,P2SH 还可以支持其他复杂脚本,例如时间锁脚本、哈希锁脚本等。
- 可以使用 BitcoinJS 提供的
script
模块构建各种复杂的赎回脚本。
- 构建赎回脚本 (redeemScript)**
2.2 生产p2sh地址
使用
bitcoin.payment
来生成各种协议的地址const p2sh = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network }, network }) console.log('p2sh.address: ', p2sh.address);
2.3 构建 P2SH 交易
- 2.3.1 输入部分
- 引用之前的交易输出
- 需要指定要花费的 UTXO 的交易 ID 和输出索引。
- 提供解锁脚本 (scriptSig)
- 解锁脚本用于证明你有权花费该 UTXO。
- 对于 P2SH 交易,解锁脚本需要包含签名和赎回脚本,对于2.2提及的2-of-3多重签名脚本(作为赎回脚本)需要提供2个有效的签名,只要三个公钥中有两个公钥可以验证这两个签名即有效。
- 引用之前的交易输出
- 2.3.2 输出部分
- 指定新的 P2SH 地址
- 将比特币锁定到新的 P2SH 地址。
- 设置转账金额
- 指定要转账的比特币数量。
- 指定新的 P2SH 地址
- 2.3.1 输入部分
2.4 签名和广播交易
- 2.4.1 使用私钥对交易进行签名
- 使用 BitcoinJS 提供的
ECPair
模块创建密钥对,并使用私钥对交易进行签名。
- 使用 BitcoinJS 提供的
- 2.4.2 将交易广播到比特币网络
- 使用 bitcoin-core的控制台将交易广播到比特币网络。
- 2.4.1 使用私钥对交易进行签名
3.代码示例
查看以下代码前,可以再看看该网站 https://learnmeabitcoin.com/technical/script/p2sh/ 学习
3.1 示例1—2-0f-3多重签名**
使用2-0f-3多重签名作为赎回脚本
解锁脚本需提供两个签名,以及赎回脚本,要求3个公钥中有两个公钥可以验证这两个签名,满足即可解锁资金
- 生成使用2-0f-3多重签名作为赎回脚本的p2sh地址
const bitcoin = require('bitcoinjs-lib');
const network = bitcoin.networks.regtest;
ECPairFactory = require('ecpair').default;
ecc = require('tiny-secp256k1');
ECPair = ECPairFactory(ecc);
const hashtype = bitcoin.Transaction.SIGHASH_ALL;
const ONE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex',);
const keyPairAlice = ECPair.fromPrivateKey(ONE, { network });
const TWO = Buffer.from('0000000000000000000000000000000000000000000000000000000000000002', 'hex',);
const keyPairBob = ECPair.fromPrivateKey(TWO, { network });
const THTEE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000003', 'hex',);
const keyPairCandy = ECPair.fromPrivateKey(THTEE, { network });
// alice_address: mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r
// bob_address: mg8Jz5776UdyiYcBb9Z873NTozEiADRW5H
// 1.赎回脚本 2-of-3 多重签名
// 1.1生成公钥
const pubkeys = [
Buffer.from(keyPairAlice.publicKey, 'hex'),
Buffer.from(keyPairBob.publicKey, 'hex'),
Buffer.from(keyPairCandy.publicKey, 'hex'),
];
// console.log("pubkeys:", pubkeys[0], pubkeys[1], pubkeys[2]);
// pubkeys: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
// 1.2使用p2ms来生成赎回脚本
const p2ms = bitcoin.payments.p2ms({ m: 2, pubkeys, network });
console.log("redeemScript(p2ms):", Buffer.from(p2ms.output).toString('hex'));
const redeemScript = p2ms.output;
/**
* redeemScript(p2ms) hex: 52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817982102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee52102f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f953ae
* 将hex转成asm的工具地址:https://learnmeabitcoin.com/technical/script/p2sh/
* redeemScript(p2ms) asm: OP_2 OP_PUSHBYTES_33 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 OP_PUSHBYTES_33 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 OP_PUSHBYTES_33 02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 OP_3 OP_CHECKMULTISIG
*/
// 2.生成p2sh地址
const p2sh = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network }, network })
console.log('p2sh.address: ', p2sh.address);
// p2sh.address: 2MuFU6ZyBLtDNadMA6RnwJdXGWUSUaoKLeS
// 使用bitcoin-core向p2sh地址转账一个比特币
使用bitcoin-core查看p2sh的地址:2MuFU6ZyBLtDNadMA6RnwJdXGWUSUaoKLeS
向该地址发送一个比特币,查看锁定脚本中scrtipHash
是否与2.1中赎回脚本使用HASH160工具生成的哈希值(15fc0754e73eb85d1cbce08786fadb7320ecb8dc
)一致,经检验一致。
至此,我们得到一笔使用 2-of-3多重签名作为赎回脚本的交易,接下来我们将使用这笔交易。
- 构建 P2SH 交易
// 3.构造交易
const previousTxHex0 = '02000000000101351878f0e784388627348ddaf93ee9647038fa78900e00fc24314b08ca083f2e0000000000fdffffff02d4241a1e0100000017a914dee8a50fd4fd2c59509a420a13f5bd0b13ae9b4d8700c2eb0b0000000017a91415fc0754e73eb85d1cbce08786fadb7320ecb8dc87024730440220300f13f139dfec5bd0c3a38aeed97b77a0f1565af5d936345d6b11dc98cc59900220406193d6c37900d8ad221f4a565aec9ffdaa5ff2a8f29f4a03da69f1e19bb1780121036fd98cc721091b19f3347383d1beadc8ae5a17351e0db0f491476725d0122d8400000000';
const utxo0 = bitcoin.Transaction.fromHex(previousTxHex0);
const txid = utxo0.getId(); // 上一笔交易的txid
const vout = 1; // 上一笔交易中的输出
console.log("txid: ", Buffer.from(txid, 'hex').toString('hex'));
const tx = new bitcoin.Transaction(network);
// 3.1添加输入
tx.addInput(Buffer.from(txid, 'hex').reverse(), vout); //txid vout
// 3.4添加输出 输出地址为Bob的p2pkh地址
const outputScript = bitcoin.address.toOutputScript('mg8Jz5776UdyiYcBb9Z873NTozEiADRW5H', network);
tx.addOutput(outputScript, BigInt(utxo0.outs[vout].value - BigInt(10000)));
// 3.2构造 one & three 的签名 (one, two, therr其中两个即可)
const signhash = tx.hashForSignature(0, p2shOutputScript, hashtype); // 生成待签名消息
const sigAlice = keyPairAlice.sign(signhash);
console.log("sigAlice:", Buffer.from(sigAlice).toString('hex'));
const sigBob = keyPairBob.sign(signhash);
const sigCandy = keyPairCandy.sign(signhash);
// 6.确保签名符合 DER 规范
const canonicalSignatureAlice = bitcoin.script.signature.encode(sigAlice, hashtype);
const canonicalSignatureBob = bitcoin.script.signature.encode(sigBob, hashtype);
const canonicalSignatureCandy = bitcoin.script.signature.encode(sigCandy, hashtype);
// 3.3构造p2sh的解锁脚本 [sigAlice sigBob redeemScript]
const scriptSig = bitcoin.script.compile([
canonicalSignatureAlice,
canonicalSignatureBob,
redeemScript
]);
console.log("scriptSig:", Buffer.from(scriptSig).toString('hex'));
tx.setInputScript(0, scriptSig);
// 3.5输出交易原始数据
console.log('decoderawtransaction ' + '"' + tx.toHex() + '"');
console.log('sendrawtransaction ' + '"' + tx.toHex() + '"');
记录问题:使用bitcoin-core控制台进行广播,出现错误:
mandatory-script-verify-flag-failed (Operation not valid with the current stack size) (code -26)
仍未解决
3.2 示例2(数学问题作为赎回脚本)
前言:6+7数学问题作为赎回脚本可以看看这个网页 p2sh-6+7,开始学习p2sh也是看的这个例子
const bitcoin = require('bitcoinjs-lib')
ECPairFactory = require('ecpair').default;
ecc = require('tiny-secp256k1');
ECPair = ECPairFactory(ecc);
const network = bitcoin.networks.regtest
// 1.设置赎回脚本 两个数相加等于13
const redeemScript = bitcoin.script.compile([
bitcoin.opcodes.OP_ADD,
bitcoin.opcodes.OP_13,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_1
]);
console.log('redeem script: ' + Buffer.from(redeemScript).toString('hex'));
// 2.生成p2sh地址
const p2sh = bitcoin.payments.p2sh({ redeem: { output: redeemScript, network }, network })
console.log('p2sh.address: ', p2sh.address);
// 2NCibkYCCdQrMcQPoPdbTAtUGtcsFtYPpnh 使用bitcoin-core向p2sh地址转账2个比特币
const outputScript = bitcoin.address.toOutputScript(p2sh.address, network);
console.log('Output Script: ' + Buffer.from(outputScript).toString('hex'));
const asmCode = bitcoin.script.toASM(outputScript);
console.log('Output Script ASM:', asmCode);
// 3.构造交易
const previousTxHex0 = '02000000000101c90a36e787b617b67cf58dbddec91c3dede6371c3d106ae481df0c4abed9c2400000000000fdffffff02d4241a1e0100000017a914465d18f6b11821907549d27bd3685b1d679ab9718700c2eb0b0000000017a914d597c9a73051aa37cebefb2fae06b16a860cc801870247304402206c359b88506ae84b53dec33b8d1826ae08fd5d55006efe91b645236f928e2e3a02204326e1e1775407322e6a50a47c1ba2591267112c879c5d9fffef50f4ad6bcd6f0121036fd98cc721091b19f3347383d1beadc8ae5a17351e0db0f491476725d0122d8400000000';
const utxo0 = bitcoin.Transaction.fromHex(previousTxHex0);
const txid = utxo0.getId();
const voutidx = 1; // 上一笔交易中的输出的顺序
// console.log(utxo0.outs[1].value);
// console.log("txid: ", Buffer.from("txid", 'hex').toString('hex'));
const tx = new bitcoin.Transaction(network);
// 3.1 添加输入
tx.addInput(Buffer.from(txid, 'hex').reverse(), voutidx); //txid vout
// 3.2 创建解锁脚本
const scriptSig = bitcoin.script.compile([
bitcoin.opcodes.OP_6, // 代表数字6
bitcoin.opcodes.OP_7, // 代表数字7
redeemScript
]);
tx.setInputScript(0, scriptSig);
// 3.3添加输出 输出地址为Bob的p2pkh地址
const outputScript_bob = bitcoin.address.toOutputScript('mg8Jz5776UdyiYcBb9Z873NTozEiADRW5H', network);
tx.addOutput(outputScript_bob, BigInt(utxo0.outs[voutidx].value - BigInt(10000)));
// 3.4输出交易原始数据
console.log('decoderawtransaction ' + '"' + tx.toHex() + '"');
console.log('\n');
console.log('sendrawtransaction ' + '"' + tx.toHex() + '"');
四、 总结
4.1 P2SH 的意义和应用场景
- P2SH 是比特币脚本系统的重要组成部分,它为比特币带来了更强大的功能和灵活性。
- P2SH 广泛应用于多重签名钱包、时间锁合约、哈希锁合约等场景。
4.2 目的:实现更复杂的交易脚本,增强比特币脚本的灵活性
- 缩短交易脚本长度,节省区块空间: 将复杂脚本移到交易输入中,交易输出只需存储脚本哈希值,大大减少了交易体积。
- 增强隐私性,隐藏脚本细节: 交易输出只显示脚本哈希值,无法直接看出脚本的具体内容,提高了交易的隐私性。
- 支持多重签名等复杂脚本: P2SH 为多重签名、时间锁等复杂脚本的实现提供了便利,极大地扩展了比特币脚本的应用场景。