比特币的脚本编程是一种基于堆栈的编程语言,用于定义如何花费特定的交易输出。比特币脚本不是图灵完备的,这意味着它没有循环和递归等复杂的编程结构,其设计目的是确保交易的安全性和验证的高效性。比特币的脚本语言是一种基于堆栈的编程语言,主要用于验证交易。脚本语言中的操作码(OP-Codes)是指在脚本中执行特定操作的命令。
一. 基本概念
堆栈:比特币脚本主要通过堆栈操作来完成各种验证。堆栈是一种后进先出(LIFO)的数据结构。
锁定脚本(ScriptPubKey):定义了如何花费一个输出,它被嵌入在交易输出中。
解锁脚本(ScriptSig):提供解锁锁定脚本所需的数据,它被嵌入在交易输入中。
二.常用操作码(OP-Codes)
1.基本操作码
OP_0 (0x00) 💡作用: 将数值0(false)压入堆栈。 💡用途: 通常用于标记或填充空位。
OP_1 (0x51) 到 OP_16 (0x60) 💡作用: 将数值 1 到 16 压入堆栈。 💡用途: 用于条件语句和多重签名验证。
2.堆栈操作
OP_DUP (0x76) 💡作用: 复制堆栈顶部的项。 💡用途: 常用于重复使用堆栈顶部的值。
OP_DROP (0x75) 💡作用: 移除堆栈顶部的项。 💡用途: 删除不再需要的值。
OP_SWAP (0x7c) 💡作用: 交换堆栈顶部的两项。 💡用途: 改变堆栈中项的顺序。
3.算术和逻辑操作
OP_ADD (0x93) 💡作用: 将堆栈顶部的两个数相加并将结果压入堆栈。 💡用途: 执行简单的加法操作。
OP_EQUAL (0x87) 💡作用: 比较堆栈顶部的两个值,如果相等则返回true(压入1),否则返回false(压入0)。 💡用途: 比较两个值是否相等。
OP_EQUALVERIFY (0x88) 💡作用: 运行OP_EQUAL,然后根据结果决定是否继续执行脚本。如果结果为false,则脚本失败。 💡用途: 常用于验证签名等条件。
4.加密和哈希操作
OP_SHA256 (0xa8) 💡作用: 对堆栈顶部的项执行SHA-256哈希运算,并将结果压入堆栈。 💡用途: 生成数据的哈希值。
OP_HASH160 (0xa9) 💡作用: 对堆栈顶部的项先执行SHA-256哈希运算,再执行RIPEMD-160哈希运算,并将结果压入堆栈。 💡用途: 生成比特币地址的哈希值。
OP_CHECKSIG (0xac) 💡作用: 验证堆栈顶部的签名是否有效。 💡用途: 确认交易的有效性。
OP_CHECKMULTISIG (0xae) 💡作用: 验证多个签名是否有效。 💡用途: 多重签名验证。
5.条件操作
OP_IF (0x63) 💡作用: 如果堆栈顶部的值为true,则执行接下来的操作码,直到遇到OP_ELSE或OP_ENDIF。 💡用途: 用于条件执行。
OP_ELSE (0x67) 💡作用: 与OP_IF配合使用,执行OP_IF条件为false时的操作码。 💡用途: 用于条件执行。
OP_ENDIF (0x68) 💡作用: 结束条件执行块。 💡用途: 用于条件执行。
三. p2pkh, p2wpkh, p2sh 和 p2tr 脚本编程
比特币交易中常见的几种脚本类型包括 P2PKH(Pay-to-PubKey-Hash)、P2WPKH(Pay-to-Witness-PubKey-Hash)、P2SH(Pay-to-Script-Hash)和 P2TR(Pay-to-Taproot)。
1.p2pkh 脚本
P2PKH(Pay-to-PubKey-Hash)是比特币交易中最常见的一种脚本类型,它的目的是锁定一个输出,使得只有拥有特定公钥的用户才能解锁并花费该输出。
1.1.锁定和解锁脚本
解锁脚本(ScriptSig):
<signature> <public key>
signature:提供用于验证的签名,推送交易的签名到堆栈。
publicKey:提供用于验证的公钥,推送交易的公钥到堆栈。
锁定脚本(ScriptPubKey):
OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
OP_DUP:复制堆栈顶部的项(公钥)。
OP_HASH160:对堆栈顶部的项执行 HASH160(SHA-256 之后 RIPEMD-160)。
OP_EQUALVERIFY:比较堆栈顶部的两项是否相等,并删除它们,如果不相等则脚本失败。
OP_CHECKSIG:验证签名是否与提供的公钥匹配。
1.2.交易验证流程
解锁脚本和锁定脚本的连接:
<signature> <publicKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
按顺序执行操作码:
OP_DUP 复制堆栈顶部的公钥,堆栈变为 [signature, publicKey, publicKey]。
OP_HASH160 对堆栈顶部的公钥执行 HASH160,堆栈变为 [signature, publicKey, pubKeyHash]。
OP_EQUALVERIFY 比较两个 pubKeyHash 是否相等,如果相等则删除它们,堆栈变为 [signature, publicKey]。
OP_CHECKSIG 验证 signature 是否是 publicKey 对交易的有效签名,如果验证通过,脚本执行成功。
1.3. 伪代码实现
const bitcoin = require('bitcoinjs-lib');
const network = bitcoin.networks.testnet; // 或者使用 bitcoin.networks.bitcoin
// 创建一个新的随机密钥对
const keyPair = bitcoin.ECPair.makeRandom();
const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey, network });
console.log('P2PKH Address:', address);
// 构建交易
const psbt = new bitcoin.Psbt({ network });
// 添加输入(假设有一个有效的 UTXO)
psbt.addInput({
hash: 'your-transaction-id', // UTXO 的交易 ID
index: 0, // UTXO 的输出索引
nonWitnessUtxo: Buffer.from('your-utxo-hex', 'hex'),
});
// 添加输出(接收地址和金额)
psbt.addOutput({
address: 'recipient-address', // 接收地址
value: 90000 // 发送金额(单位为聪)
});
// 签名输入
psbt.signInput(0, keyPair);
// 最终化交易
psbt.finalizeAllInputs();
// 获取交易的原始格式
const transaction = psbt.extractTransaction();
console.log('P2PKH Transaction Hex:', transaction.toHex());
2. p2wpkh 脚本
P2WPKH(Pay-to-Witness-PubKey-Hash)是比特币的一种隔离见证(SegWit)交易类型。P2WPKH交易通过将签名和公钥移出传统的脚本路径,提高了交易的效率和安全性。
2.1.P2WPKH 交易结构
锁定脚本(ScriptPubKey): P2WPKH 的锁定脚本非常简单
0 <pubKeyHash>
0:表示这是一个隔离见证的交易类型(版本号为0), 这一操作码在堆栈上推送一个空值。
pubKeyHash:公钥的哈希值(通过SHA-256和RIPEMD-160计算得出); 将公钥哈希值推送到堆栈上。
解锁脚本(ScriptSig): P2WPKH 的解锁脚本为空(只包含解锁脚本的见证数据部分):
(empty)
见证数据(Witness Data): 见证数据包含用于解锁输出的签名和公钥:
<signature> <publicKey>
在P2WPKH交易中,解锁脚本为空,所有的验证工作都通过见证数据完成:
signature:提供用于验证的签名, 推送交易的签名到见证堆栈。
publicKey:提供用于验证的公钥, 推送交易的公钥到见证堆栈。
2.2.交易验证流程
将见证数据的签名和公钥推送到见证堆栈:
<signature> <publicKey>
通过提供的公钥计算其哈希值,并将其与锁定脚本中的公钥哈希进行比较。如果它们相等,则继续,否则交易无效。
使用公钥和签名验证交易是否有效。
2.3.P2WPKH 的优势
降低交易费用:由于签名和公钥被移出主交易数据,P2WPKH 交易比传统的 P2PKH 交易要小,因此交易费用更低。
提高安全性:隔离见证的引入修复了比特币的“交易延展性”问题,使得多重签名和链下交易等高级功能更加安全。
提高效率:分离见证数据减少了节点验证和传播交易所需的计算量和带宽。
2.4.伪代码实现
const bitcoin = require('bitcoinjs-lib');
const network = bitcoin.networks.testnet; // 或者使用 bitcoin.networks.bitcoin
// 创建一个新的随机密钥对
const keyPair = bitcoin.ECPair.makeRandom({ network });
const { address } = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network });
console.log('P2WPKH Address:', address);
// 构建交易
const psbt = new bitcoin.Psbt({ network });
// 添加输入(假设有一个有效的 UTXO)
psbt.addInput({
hash: 'your-transaction-id', // UTXO 的交易 ID
index: 0, // UTXO 的输出索引
witnessUtxo: {
script: bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey }).output,
value: 100000 // UTXO 的金额(单位为聪)
}
});
// 添加输出(接收地址和金额)
psbt.addOutput({
address: 'recipient-address', // 接收地址
value: 90000 // 发送金额(单位为聪)
});
// 签名输入
psbt.signInput(0, keyPair);
// 最终化交易
psbt.finalizeAllInputs();
// 获取交易的原始格式
const transaction = psbt.extractTransaction();
console.log('P2WPKH Transaction Hex:', transaction.toHex());
3.p2sh 脚本
P2SH(Pay-to-Script-Hash)是比特币的一种交易类型,通过使用脚本哈希,使得复杂的锁定条件变得更加灵活和简单。P2SH 可以用于实现多重签名、时间锁定等复杂的脚本条件。
3.1.P2SH 交易结构
锁定脚本(ScriptPubKey): P2SH 的锁定脚本比较简单,只包含一个哈希值:
OP_HASH160 <scriptHash> OP_EQUAL
OP_HASH160:对堆栈顶部的项(Redeem Script)执行 HASH160(SHA-256 之后 RIPEMD-160),将堆栈顶部的 Redeem Script 哈希,堆栈变为 [scriptHash]。
scriptHash:推送锁定脚本中指定的脚本哈希值到堆栈, 将指定的 scriptHash 推送到堆栈。
OP_EQUAL:比较堆栈顶部的两个项是否相等。如果相等则继续执行,否则脚本失败;比较计算出的 scriptHash 和指定的 scriptHash 是否相等。
解锁脚本(ScriptSig): P2SH 的解锁脚本包含原始脚本(称为 Redeem Script)以及解锁 Redeem Script 所需的数据:
<signature1> <signature2> ... <redeemScript>
redeemScript:提供原始的 Redeem Script,Redeem Script 是一个脚本,包含了实际的锁定条件。
3.2.交易验证流程
将解锁脚本(
<signature1> <signature2> ... <redeemScript> OP_HASH160 <scriptHash> OP_EQUAL
按顺序执行操作码: 💡推送解锁脚本中的签名和 Redeem Script 到堆栈。 💡OP_HASH160 对堆栈顶部的 Redeem Script 执行 HASH160,堆栈变为 [scriptHash]。 💡推送锁定脚本中的 scriptHash 到堆栈,堆栈变为 [scriptHash, scriptHash]。 💡OP_EQUAL 比较两个 scriptHash 是否相等,如果相等则继续执行,否则脚本失败。
重新执行 Redeem Script,验证签名是否有效。
3.3.P2SH 的优势
灵活性:P2SH 允许使用复杂的脚本条件,使得多重签名、时间锁定等高级功能变得容易实现。
简化支付地址:用户只需提供一个 P2SH 地址,而无需暴露复杂的脚本,这样既简化了操作,也提高了隐私性。
增强安全性:通过将复杂的脚本条件放在链下,P2SH 可以有效减少脚本执行的复杂性和风险。
3.4 伪代码实现
const bitcoin = require('bitcoinjs-lib');
const network = bitcoin.networks.testnet; // 或者使用 bitcoin.networks.bitcoin
// 创建两个新的随机密钥对
const keyPair1 = bitcoin.ECPair.makeRandom({ network });
const keyPair2 = bitcoin.ECPair.makeRandom({ network });
// 创建多重签名 Redeem Script(m=2, n=2)
const p2ms = bitcoin.payments.p2ms({ m: 2, pubkeys: [keyPair1.publicKey, keyPair2.publicKey], network });
const p2sh = bitcoin.payments.p2sh({ redeem: p2ms, network });
console.log('P2SH Address:', p2sh.address);
// 构建交易
const psbt = new bitcoin.Psbt({ network });
// 添加输入(假设有一个有效的 UTXO)
psbt.addInput({
hash: 'your-transaction-id', // UTXO 的交易 ID
index: 0, // UTXO 的输出索引
nonWitnessUtxo: Buffer.from('your-utxo-hex', 'hex'),
redeemScript: p2sh.redeem.output,
});
// 添加输出(接收地址和金额)
psbt.addOutput({
address: 'recipient-address', // 接收地址
value: 90000 // 发送金额(单位为聪)
});
// 签名输入
psbt.signInput(0, keyPair1);
psbt.signInput(0, keyPair2);
// 最终化交易
psbt.finalizeAllInputs();
// 获取交易的原始格式
const transaction = psbt.extractTransaction();
console.log('P2SH Transaction Hex:', transaction.toHex());
4.p2tr 脚本
P2TR(Pay-to-Taproot)是比特币的一种交易类型,结合了 Schnorr 签名和 Merkelized Alternative Script Trees(MAST)的优点,使得比特币交易更加高效、隐私性更好和灵活性更强。Taproot 通过将所有可能的支出条件折叠为一个单一的公钥,并且通过 Schnorr 签名使得多重签名交易看起来像单一签名交易。
4.1.P2TR 交易结构
锁定脚本(ScriptPubKey): P2TR 的锁定脚本非常简单,只包含一个公钥:
<x-only-pubKey>
P2TR 锁定脚本只包含一个 x-only 公钥,这个公钥是通过 Schnorr 签名算法生成的。没有复杂的操作码,使得锁定脚本非常简洁。
解锁脚本(ScriptSig): P2TR 的解锁脚本为空,实际解锁工作通过见证数据完成:
(empty)
解锁脚本为空,实际的解锁操作在见证数据中进行。
见证数据(Witness Data): 见证数据包含签名和可能的路径数据:
<control-block> <script-path (if needed)> <signature>
4.2.验证流程
将见证数据中的签名、控制块和可能的脚本路径推送到见证堆栈:
<control-block> <script-path (if needed)> <signature>
根据控制块和脚本路径数据,验证签名是否有效。如果签名有效,则交易成功。
4.3.P2TR 的优势
更好的隐私性:使用 Schnorr 签名和 MAST,所有交易看起来都像是普通的单一签名交易,增强了隐私性。
更高的效率:签名更小,验证更快,降低了交易的带宽和存储需求。
灵活性:支持复杂的支出条件,通过 MAST 实现灵活的脚本结构,而不需要暴露所有的支出路径。
4.4.伪代码实现
const bitcoin = require('bitcoinjs-lib');
const schnorr = require('bip-schnorr');
const bip32 = require('bip32');
const ecc = require('tiny-secp256k1');
bitcoin.initEccLib(ecc);
const network = bitcoin.networks.testnet; // 或者使用 bitcoin.networks.bitcoin
// 创建一个新的随机密钥对
const keyPair = bitcoin.ECPair.makeRandom({ network });
const { publicKey } = keyPair;
const { address } = bitcoin.payments.p2tr({ internalPubkey: publicKey.slice(1, 33), network });
console.log('P2TR Address:', address);
// 构建交易
const psbt = new bitcoin.Psbt({ network });
// 添加输入(假设有一个有效的 UTXO)
psbt.addInput({
hash: 'your-transaction-id', // UTXO 的交易 ID
index: 0, // UTXO 的输出索引
witnessUtxo: {
script: bitcoin.payments.p2tr({ internalPubkey: publicKey.slice(1, 33) }).output,
value: 100000 // UTXO 的金额(单位为聪)
},
tapInternalKey: publicKey.slice(1, 33),
});
// 添加输出(接收地址和金额)
psbt.addOutput({
address: 'recipient-address', // 接收地址
value: 90000 // 发送金额(单位为聪)
});
// 签名输入
psbt.signInput(0, keyPair);
// 最终化交易
psbt.finalizeAllInputs();
// 获取交易的原始格式
const transaction = psbt.extractTransaction();
console.log('P2TR Transaction Hex:', transaction.toHex());
四.小结
比特币脚本编程通过定义复杂的交易条件和验证规则,大大扩展了比特币的应用场景和功能。它不仅提高了交易的安全性和隐私性,还支持去中心化应用和灵活的金融合约。了解和掌握比特币脚本编程是深入研究比特币和区块链技术的关键步骤。