bip-199
Version:
BIP-199 helpers for Bitcoin HTLC and atomic swaps
232 lines (197 loc) • 9.45 kB
JavaScript
const bitcoin = require('bitcoinjs-lib')
const crypto = require('crypto');
const { ECPairFactory } = require('ecpair')
const ecc = require('tiny-secp256k1')
const ECPair = ECPairFactory(ecc);
const fs = require('fs')
const HTLC_EXPIRATION = 86400
module.exports = {
getPubKeyHash,
getWitnessScript,
createHTLC,
redeemHTLC,
refundHTLC
}
function getPubKeyHash(address) {
return bitcoin.address.fromBech32(address).data;
}
function getWitnessScript(recipientAddress, refundAddress, contractHash, expiration) {
const recipientPubKeyHash = getPubKeyHash(recipientAddress)
const refundPubKeyHash = getPubKeyHash(refundAddress)
const OPS = bitcoin.script.OPS;
// See https://en.bitcoin.it/wiki/BIP_0199 for full BIP-199 spec
const script = bitcoin.script.compile([
OPS.OP_IF,
OPS.OP_SHA256,
contractHash,
OPS.OP_EQUALVERIFY,
OPS.OP_DUP,
OPS.OP_HASH160,
recipientPubKeyHash,
OPS.OP_ELSE,
bitcoin.script.number.encode(expiration),
OPS.OP_CHECKLOCKTIMEVERIFY,
OPS.OP_DROP,
OPS.OP_DUP,
OPS.OP_HASH160,
refundPubKeyHash,
OPS.OP_ENDIF,
OPS.OP_EQUALVERIFY,
OPS.OP_CHECKSIG,
]);
return script
}
/*
* @param {Object} options
* @param {String} options.recipientAddress: the address with the right to unlock the HTLC with the preimage
* @param {String} options.refundAddress: the address to refund the HTLC to if remains unclaimed after the expiration
* @param {String} options.expiration (optional): defaults to 1 day after the time of the function call. pass in a UNIX timestamp in seconds if you want a custom expiry.
* @param {String} options.network (optional): 'regtest' (default) || 'testnet' || 'bitcoin'. Will add support for litecoin and other non-bitcoin chains later
* @param {String} options.hash (optional): if you're in charge of producing the hash for your swap, leave this blank and we will generate one
* if your counterparty gave you one, pass it in as a hex string here
*
* @returns {Object} swapParams
* @return swapParams.recipientAddress
* @return swapParams.refundAddress
* @return swapParams.preimage
* @return swapParams.contractHash
* @return swapParams.expiration UNIX timestamp
* @return swapParams.network 'regtest' | 'testnet' | 'bitcoin'
* @return swapParams.addressType 'p2wsh'
* @return swapParams.witnessScript you will need this to unlock the HTLC
* @return swapParams.htlcAddress send coins to this address to lock them
*/
function createHTLC(options) {
const NETWORK = options.network || 'regtest'
// Preimage for HTLC. Must be unique every time. If a hash is specified, the counterparty has the preimage and this field can be left null
const preimage = options.hash ? Buffer.alloc(0) : Buffer.from(crypto.getRandomValues(new Uint8Array(32)))
// if a hash is specified use that. otherwise generate one and return the hash + preimage
const hash = options.hash ? Buffer.from(options.hash, 'hex') : bitcoin.crypto.sha256(preimage)
const swapParams = {
recipientAddress: options.recipientAddress,
refundAddress: options.refundAddress,
preimage: preimage.toString('hex'),
contractHash: hash.toString('hex'),
expiration: options.expiration || (Date.now() / 1000 | 0) + HTLC_EXPIRATION,
network: NETWORK,
addressType: 'p2wsh'
}
// Network for transaction: bitcoin, regtest, or testnet
const network = bitcoin.networks[swapParams.network]
const script = getWitnessScript(swapParams.recipientAddress, swapParams.refundAddress, hash, swapParams.expiration)
const p2wsh = bitcoin.payments.p2wsh({
redeem: { output: script, network: network },
network: network
});
swapParams.witnessScript = script.toString('hex')
swapParams.htlcAddress = p2wsh.address
return swapParams
}
/**
* Produces a raw transaction to redeem an existing HTLC
* @param {Object} options
* @param {String} options.preimage Required to unlock HTLC. Must be in HEX format.
* @param {String} options.recipientWIF Private key of recipient address in WIF format
* @param {String} options.witnessScript Witness script for HTLC in hex format. If you used createHTLC, you can grab it from there, or you will have to ask your counterparty for it.
* @param {String} options.txHash Transacion hash with the HTLC output you want to unlock
* @param {Number} options.value The number of sats locked in the HTLC. IMPORTANT: If you send too low a number, the remainder of your sats will be burned. Proceed with caution. Test your code on regtest before using it in production.
* @param {Number} options.feeRate Fee rate in sat/vB. Must be provided manually because there's no RPC connection built into the library.
* @param {Number} options.vout The index number of the UTXO in txHash to use.
* @return {String} Raw redeem transaction to broadcast to network. send it with `bitcoin-cli sendrawtransaction <transaction>`
*/
function redeemHTLC(options) {
const recipientKeypair = ECPair.fromWIF(options.recipientWIF)
const recipientAddress = bitcoin.payments.p2wpkh({
pubkey: recipientKeypair.publicKey,
network: bitcoin.networks[options.network],
}).address;
// This is equivaluent to OP_0 OP_20 WITNESS_SCRIPT_HASH
const witnessScript = Buffer.from(options.witnessScript, 'hex')
const witnessScriptHash = bitcoin.crypto.sha256(witnessScript)
const witnessUtxoScript = Buffer.concat([Buffer.from([0x00, 0x20]), witnessScriptHash])
// Segwit transactions require you to use Psbt to sign (afaik)
const psbt = new bitcoin.Psbt({ network: bitcoin.networks[options.network] })
psbt.addInput({
hash: options.txHash,
index: options.vout,
witnessScript: Buffer.from(options.witnessScript, 'hex'),
witnessUtxo: {
script: witnessUtxoScript,
value: options.value
}
})
const txFee = options.feeRate * 320;
psbt.addOutput({
address: recipientAddress,
value: (options.value - txFee),
})
psbt.signInput(0, recipientKeypair)
const sig = psbt.data.inputs[0].partialSig[0].signature
const witnessStack = [
sig,
recipientKeypair.publicKey,
Buffer.from(options.preimage, 'hex'),
Buffer.from([0x01]), // Segwit OP_TRUE is 0x01
witnessScript
]
// serialize witness
let finalScriptWitness = Buffer.from([0x05]) // 1-byte: Number of items
for (let i=0; i < 5; i++) {
finalScriptWitness = Buffer.concat([ finalScriptWitness, new Uint8Array([witnessStack[i].length]), witnessStack[i] ])
}
psbt.updateInput(0, { finalScriptWitness })
return psbt.extractTransaction().toHex()
}
/**
* Produces a raw transaction to redeem an existing HTLC
* @param {Object} options
* @param {String} options.refundWIF Private key of refund address in WIF format
* @param {String} options.witnessScript Witness script for HTLC in hex format. If you used createHTLC, you can grab it from there, or you will have to ask your counterparty for it.
* @param {String} options.txHash Transacion hash with the HTLC output you want to unlock
* @param {Number} options.value The number of sats locked in the HTLC. IMPORTANT: If you send too low a number, the remainder of your sats will be burned. Proceed with caution. Test your code on regtest before using it in production.
* @param {Number} options.feeRate Fee rate in sat/vB. Must be provided manually because there's no RPC connection built into the library.
* @param {Number} options.vout The index number of the UTXO in txHash to use.
* @return {String} Raw refund transaction to broadcast to network. send it with `bitcoin-cli sendrawtransaction <transaction>`
*/
function refundHTLC(options) {
const refundKeypair = ECPair.fromWIF(options.refundWIF)
const refundAddress = bitcoin.payments.p2wpkh({
pubkey: refundKeypair.publicKey,
network: bitcoin.networks[options.network],
}).address;
// This is equivaluent to OP_0 OP_20 WITNESS_SCRIPT_HASH
const witnessScript = Buffer.from(options.witnessScript, 'hex')
const witnessScriptHash = bitcoin.crypto.sha256(witnessScript)
const witnessUtxoScript = Buffer.concat([Buffer.from([0x00, 0x20]), witnessScriptHash])
// Segwit transactions require you to use Psbt to sign (afaik)
const psbt = new bitcoin.Psbt({ network: bitcoin.networks[options.network] })
psbt.addInput({
hash: options.txHash,
index: options.vout,
witnessScript: Buffer.from(options.witnessScript, 'hex'),
witnessUtxo: {
script: witnessUtxoScript,
value: options.value
}
})
const txFee = options.feeRate * 320;
psbt.addOutput({
address: refundAddress,
value: (options.value - txFee),
})
psbt.signInput(0, refundKeypair)
const sig = psbt.data.inputs[0].partialSig[0].signature
const witnessStack = [
sig,
refundKeypair.publicKey,
Buffer.from([]), // Segwit OP_FALSE is minimal, meaning no data is included
witnessScript
]
// serialize witness
let finalScriptWitness = Buffer.from([0x04]) // 1-byte: Number of items
for (let i=0; i < 4; i++) {
finalScriptWitness = Buffer.concat([ finalScriptWitness, new Uint8Array([witnessStack[i].length]), witnessStack[i] ])
}
psbt.updateInput(0, { finalScriptWitness })
return psbt.extractTransaction().toHex()
}