Tapscript Tools
A basic library for working with Tapscript, Schnorr Signatures, and Bitcoin transactions.
Introduction
Tapscript uses some pretty cutting-edge stuff. If you are new to tapscript, please continue reading for a brief overview of how tapscript works. The library will make more sense if you have a general idea about what is happening under the hood.
If you already have a good understanding of tapscript, feel free to skip ahead by clicking here).
What is Tapscript?
Bitcoin uses a simple scripting language that allows you to lock up coins into a contract. These contracts are published to the blockchain and enforced by all nodes in the network.
In order to settle a contract (and claim its coins), you are required to publish the entire contract, including parts that are not relevant to the settlement. This is expensive and wasteful, plus it leaks information that could have otherwise been kept private.
Tapscript is a new way to publish these contracts to the blockchain that fixes the above concerns. It allows you to settle contracts by publishing only the portion of the contract that is relevant. This means smaller transactions, cheaper fees, and better privacy guarantees for the contract as a whole.
Tapscript also comes with many other benefits, including:
- It drastically simplifies the flow and logic of writing a contract.
- You can create large, complex contracts that only need a small transaction to settle.
- Commitments to data and other arbitrary things can be thrown into your contract for free.
- The new schnorr-based signature format lets you do some crazy cool stuff (BIP340).
These new features came with the Taproot upgrade in 2019. Read more about it here.
How does Taproot work?
Taproot uses a simple trick involving something called a "merkle tree".
hash(ab, cd) <- Final hash (the root)
/ \
hash(a, b) hash(c, d) <- Combined hash (the branches)
/ \ / \
hash(a) hash(b) hash(c) hash(d) <- Initial hash (the leaves)
[ script(a), script(b), script(c), script(d) ]
A merkle tree is simply a list of data that is reduced down into a single hash. We do this by hashing items together in pairs of two, repeatedly, until we are naturally left with one item (the root).
The great thing about merkle trees is that you can use the root hash to prove that a piece of data (such as a script) was included somewhere in the tree, without having to reveal the entire tree.
For example, to prove that script(a) exists in the tree, we simply provide hash(b) and hash(c, d). This is all the information we need to recreate the root hash(ab, cd). We do not reveal any of the other scripts.
This allows us to break up a contract into many scripts, then lock coins to the root hash of our combined scripts. To redeem coins, we simply need to provide one of the scripts, plus a 'path' of hashes that let us to recompute the root of the tree.
About Key Tweaking
Another clever trick that tapscript uses, is something called "key tweaking".
Typically, we create a pair of signing keys by multiplying a secret number with a prime number called a "generator" (G). This process is done in a way that is computationally impossible to reverse without knowing the secret.
seckey * G => pubkey
We use a special set of numbers when making key-pairs, so that some arithmetic still works between the keys, without breaking their secret relationship with G. This is how we produce signatures and proofs.
seckey + randomkey + msg = signature <= Does not reveal seckey.
pubkey + (randomkey * G) + (msg * G) = signature * G <= Proves that seckey was used.
Key tweaking is just an extention of this. We use a piece of data to "tweak" both keys in our key-pair, then use the modified keys to sign and verify transactions.
seckey + tweak = tweakedkey = tweakedsec
pubkey + (tweak * G) = (tweakedkey * G) = tweakedpub
Later, we can choose to reveal the original public key and tweak, with a proof that both were used to construct the modified key. Or we can simply choose to sign using the modified key, and not reveal anything!
Tapscript uses key tweaking in order to lock coins to the root hash of our script tree. This provides us with two paths for spending coins:
- Using the tweaked key (without revealing anything).
- Using the interal key + script + proof.
You can also create tweaked keys using an internal pubkey that has a provably unknown secret key. This is useful for locking coins so that they cannot ever be spent with a tweaked key, and must be redeemed using a script!
Tool Index
This library provides a suite of tools for working with scripts, taproot, key tweaking, signatures and transactions. Use the links below to jump to the documentation for a certain tool.
Address Tool
Encode, decode, check, and convert various address types.
Script Tool
Encode scripts into hex, or decode into a script array.
Signer Tool
Produce signatures and validate signed transactions.
Tap Tool
Build, tweak, and validate trees of data / scripts.
Tx Tool
Encode transactions into hex, or decode into a JSON object.
About Buff
This library makes heavy use of the Buff tool for converting between data types. Buff is an extention of the Uint8Array type, so all Buff objects can naturally be treated as Uint8Array objects. Buff objects however incude an extensive API for converting into different types (for ex: buff.hex for hex strings). Please check the above link for more information on how to use Buff.
Import
Example import into a browser-based project:
<script src="https://unpkg.com/@cmdcode/tapscript"></script>
<script> const { Address, Script, Signer, Tap, Tx } = window.tapscript </script>
Example import into a commonjs project:
const { Address, Script, Signer, Tap, Tx } = require('@cmdcode/tapscript')
Example import into an ES module project:
import { Address, Script, Signer, Tap, Tx } from '@cmdcode/tapscript'
Address Tool
This tool allows you to encode, decode, check, an convert various address types.
Address = {
p2pkh : => AddressTool,
p2sh : => AddressTool,
p2wpkh : => AddressTool,
p2wsh : => AddressTool,
p2tr : => AddressTool,
decode : (address : string) => AddressData,
toScriptPubKey : (address : string) => Buff
}
interface AddressTool {
check : (address : string, network ?: Networks) => boolean
encode : (key : Bytes, network ?: Networks) => string
decode : (address : string, network ?: Networks) => Buff
scriptPubKey : (key : string) => string[]
fromPubKey (pubkey : Bytes, network ?: Networks) : => string
fromScript (script : ScriptData, network ?: Networks) : => string
}
interface AddressData {
data : Buff
network : Networks
prefix : string
script : string[]
type : keyof AddressTools
}
type Networks = 'main' | 'testnet' | 'signet' | 'regtest'
Examples
Example of using the main Address API.
const address = 'bcrt1q738hdjlatdx9xmg3679kwq9cwd7fa2c84my9zk'
const decoded = Address.decode(address)
{
prefix : 'bcrt1q',
type : 'p2w',
network : 'regtest',
data : 'f44f76cbfd5b4c536d11d78b6700b8737c9eab07',
script : [ 'OP_0', 'f44f76cbfd5b4c536d11d78b6700b8737c9eab07' ]
}
const bytes = Address.toScriptPubKey(address)
const address = Address.fromScriptPubKey(scriptPubKey)
Example of using the AddressTool API for a given address type.
const pubkey = '03d5af2a3e89cb72ff9ca1b36091ca46e4d4399abc5574b13d3e56bca6c0784679'
const address = Address.p2w.fromPubKey(pubkey, 'regtest')
const address = Address.p2w.encode(keyhash, 'regtest')
const bytes = Address.p2w.decode(address)
const script = Address.p2w.scriptPubKey(bytes)
Script Tool
This tool helps with parsing / serializing scripts.
Script = {
encode : (script : ScriptData, varint = true) => string,
decode : (script : string, varint = false) => ScriptData
fmt : {
toAsm() => string[] (asm format).
toBytes() => Buff
toParam() => Buff
}
}
Signature Tool.
This tool helps with signatures and validation.
Signer.taproot = {
hash : (
txdata : TxData | Bytes,
index : number,
config : HashConfig = {}
) => Uint8Array,
sign : (
seckey : Bytes,
txdata : TxData | Bytes,
index : number,
config : HashConfig = {}
) => Uint8Array,
verify : (
txdata : TxData | Bytes,
index : number,
config : HashConfig = {}
) => boolean
}
interface HashConfig {
extension ?: Bytes
pubkey ?: Bytes
script ?: Bytes
sigflag ?: number
separator_pos ?: number
extflag ?: number
key_version ?: number
throws ?: boolean
}
Example
Example of a basic pay-to-taproot key spend (similar to pay-to-pubkey):
const seckey = '730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6'
const pubkey = '0307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba3'
const [ tseckey ] = Tap.getSecKey(seckey)
const [ tpubkey ] = Tap.getPubKey(pubkey)
const address = Address.p2tr.encode(tpubkey, 'regtest')
const txdata = Tx.create({
vin : [{
txid: 'fbde7872cc1aca4bc93ac9a923f14c3355b4216cac3f43b91663ede7a929471b',
vout: 0,
prevout: {
value: 100000,
scriptPubKey: [ 'OP_1', tpubkey ]
},
}],
vout : [{
value: 99000,
scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
}]
})
const sig = Signer.taproot.sign(tseckey, txdata, 0)
txdata.vin[0].witness = [ sig ]
await Signer.taproot.verify(txdata, 0, { throws: true })
console.log('Your address:', address)
console.log('Your txhex:', Tx.encode(txdata).hex)
You can find more examples in the main Examples section further down.
Note: There is also an identical Signer.segwit tool for signing and validating segwit (BIP0143) transactions. The segwit signer currently does not support the use of OP_CODESEAPRATOR. Any scripts containing this opcode will throw an exception by default.
Tap Tool
Tap = {
getPubKey : (pubkey : Bytes, config ?: TapConfig) => TapKey,
getSecKey : (seckey : Bytes, config ?: TapConfig) => TapKey,
checkPath : (tapkey : Bytes, target : Bytes, cblock : Bytes, config ?: TapConfig) => boolean,
tree : TreeTool,
tweak : TweakTool,
util : UtilTool
}
interface TapConfig {
isPrivate ?: boolean
target ?: Bytes
tree ?: TapTree
throws ?: boolean
version ?: number
}
type TapKey = [
tapkey : string,
cblock : string
]
Examples
Example of tapping a key with no scripts (key-spend).
const [ tapkey ] = Tap.getPubKey(pubkey)
Example of tapping a key with a single script and returning a proof.
const bytes = Script.encode([ 'script' ])
const target = Tap.tree.getLeaf(bytes)
const [ tapkey, cblock ] = Tap.getPubKey(pubkey, { target })
Example of tapping a key with many scripts.
const scripts = [
[ 'scripta' ],
[ 'scriptb' ],
[ 'scriptc' ]
]
const tree = scripts
.map(e => Script.encode(e))
.map(e => Tap.tree.getLeaf(e))
const bytes = encodeData('some data')
const leaf = Tap.tree.getLeaf(bytes)
tree.push(leaf)
const target = tree[0]
const [ tapkey, cblock ] = Tap.getPubKey(pubkey, { tree, target })
Tree Tool
This tool helps with creating a tree of scripts / data, plus the proofs to validate items in the tree.
Tap.tree = {
getTag : (tag : string) => Buff,
getLeaf : (data : Bytes, version ?: number) => string,
getBranch : (leafA : string, leafB : string) => string,
getRoot : (leaves : TapTree) => Buff,
}
type TapTree = Array<string | string[]>
Tweak Tool
This tool helps with tweaking public / secret (private) keys.
Tap.tweak = {
getSeckey : (seckey: Bytes, data ?: Bytes | undefined) => Buff,
getPubkey : (pubkey: Bytes, data ?: Bytes | undefined) => Buff,
getTweak : (key : Bytes, data ?: Bytes, isPrivate ?: boolean) => Buff,
tweakSeckey : (seckey: Bytes, tweak: Bytes) => Buff,
tweakPubkey : (seckey: Bytes, tweak: Bytes) => Buff
}
Util Tool
This tool provides helper methods for reading and parsing data related to taproot.
Tap.util = {
readCtrlBlock : (cblock : Bytes) => CtrlBlock,
readParityBit : (parity ?: string | number) => number
}
interface CtrlBlock {
version : number
parity : number
intkey : Buff
paths : string[]
}
Example
const cblock = 'c1187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27'
const { intkey, parity, paths, version } = Tap.util.readCtrlBlock(cblock)
{
intkey: '187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27',
parity: 3,
paths: [],
version: 192
}
Tx Tool
This tool helps with parsing / serializing transaction data.
Tx = {
encode : (
txdata : TxData,
omitWitness ?: boolean
) => string,
decode : (bytes : string | Uint8Array) => TxData,
fmt : {
toJson : (txdata ?: TxData | Bytes) => TxData,
toBytes : (txdata ?: TxData | Bytes) => Buff
},
util : {
getTxid : (txdata : TxData | Bytes) => Buff,
readScriptPubKey : (script : ScriptData) => ScriptPubKeyData,
readWitness : (witness : ScriptData[]) => WitnessData
}
}
interface TxData {
version ?: number
vin : InputData[]
vout : OutputData[]
locktime ?: LockData
}
interface InputData {
txid : string
vout : number
prevout ?: OutputData
scriptSig ?: ScriptData
sequence ?: SequenceData
witness ?: ScriptData[]
}
interface OutputData {
value : number | bigint
scriptPubKey : ScriptData
}
export interface ScriptPubKeyData {
type : OutputType
data : Buff
}
interface WitnessData {
annex : Buff | null
cblock : Buff | null
script : Buff | null
params : Bytes[]
}
type SequenceData = string | number
type LockData = number
type ScriptData = Bytes | Word[]
type Word = string | number | Uint8Array
type Bytes = string | Uint8Array
Transaction Object
This is an example transaction in JSON format.
const txdata = {
version: 2
vin: [
{
txid: '1351f611fa0ae6124d0f55c625ae5c929ca09ae93f9e88656a4a82d160d99052',
vout: 0,
prevout: {
value: 10000,
scriptPubkey: '512005a18fccd1909f3317e4dd7f11257e4428884902b1c7466c34e7f490e0e627da'
},
sequence: 0xfffffffd,
witness: []
}
],
vout: [
{
value: 9000,
address: 'bcrt1pqksclnx3jz0nx9lym4l3zft7gs5gsjgzk8r5vmp5ul6fpc8xyldqaxu8ys'
}
],
locktime: 0
}
Example Transactions
Here are a few examples to help demonstrate using the library. Please feel free to contribute more!
Create / Publish an Inscription
Creating an inscription is a three-step process:
- We create a script for publishing the inscription, and convert it into a bitcoin address.
- Send funds to the bitcoin address.
- Create a redeem transaction, which claims the previous funds (and publishes the data).
import { Address, Script, Signer, Tap, Tx } from '@cmdcode/tapscript'
const seckey = 'your secret key (in bytes)'
const pubkey = 'your x-only public key (in bytes)'
const marker = ec.encode('ord')
const mimetype = ec.encode('image/png')
const imgdata = getFile('image.png')
const script = [
pubkey, 'OP_CHECKSIG', 'OP_0', 'OP_IF', marker, '01', mimetype, 'OP_0', imgdata, 'OP_ENDIF'
]
const bytes = Script.encode(script)
const target = Tap.tree.getLeaf(sbytes)
const [ tapkey, cblock ] = Tap.getPubKey(pubkey, { target })
const address = Address.p2tr.encode(tapkey)
console.log('Your taproot address:', address)
const txdata = {
version : 2
vin: [
{
txid : 'replace with the txid of your previous transaction.',
vout : 'replace with the vout index spending to the previous address.',
prevout : {
value: 'replace with the amount sent to this address from the previous transaction',
scriptPubKey: Address.toScript(address)
},
sequence : 0xfffffffd,
witness : []
}
],
vout : [
{
value: 'replace with the amount sent from the previous transaction, minus fees (for the miners)',
scriptPubKey: Address.toScript('replace with an address of your choice!')
}
],
locktime: 0
}
const sig = Signer.taproot.sign(seckey, txdata, 0, { extension: target })
txdata[0].witness = [ sig, script, cblock ]
Signer.taproot.verify(txdata, 0, { pubkey })
const txhex = Tx.encode(txdata).hex
console.log('Your transaction:', txdata)
console.log('Your raw transaction hex:', txhex)
More examples to come!
Development / Testing
This library uses yarn for package management, tape for writing tests, and rollup for bundling cross-platform compatible code. Here are a few scripts that are useful for development.
yarn build
yarn start contrib/example.ts
yarn test
yarn release
Bugs / Issues
If you run into any bugs or have any questions, please submit an issue ticket.
Contribution
Feel free to fork and make contributions. Suggestions are welcome!
Future Roadmap
- Add signature and validation for ecdsa (segwit and earlier).
- Refactor and stress-test tree compilation with many (many) leaves.
- Allow arbitrary ordering of tree elements.
- Write more unit and vector tests (cover all the things).
Dependencies
This library contains minimal dependencies.
Buff-Utils
The swiss-army-knife of byte manipulation.
https://github.com/cmdruid/buff-utils
Crypto-Utils
User-friendly cryptography tools.
https://github.com/cmdruid/crypto-utils
Resources
BIP340 Wiki Page
This BIP covers schnorr signatures and verification.
https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
BIP341 Wiki Page
This BIP covers the construction of trees, signature hashes, and proofs.
https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki
BIP342 Wiki Page
This BIP covers changes to opcodes and signature verification.
https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki
Tapscript example using Tap
This is a guide on how to use a command-line tool called btcdeb and Tap.
This tool will help you create a taproot transaction from scratch, which
is great for learning (and to debug any issues with this library :-)).
https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example-with-tap.md
License
Use this library however you want!
Contact
You can find me on twitter at @btctechsupport or on nostr at npub1gg5uy8cpqx4u8wj9yvlpwm5ht757vudmrzn8y27lwunt5f2ytlusklulq3