Tapscript
A basic library for working with Taproot, Schnorr Signatures, and Bitcoin transactions.
Note: For nodejs users, please upgrade to version 19+ for globalThis support.
Introduction
Tapscript uses the latest feature upgrade to Bitcoin called Taproot. If you are new to Bitcoin or the Taproot upgrade, please continue reading for a brief overview of how it works. This library will be easier to follow if you know what taproot is doing under the hood.
If you already have a good understanding of Bitcoin and Taproot, feel free to skip ahead by clicking here.
What is Taproot?
Bitcoin uses a simple scripting language (called Bitcoin Script) 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.
Taproot 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.
Taproot 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).
Read more about the Taproot upgrade in 2019 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 value. We do this by hashing values together in pairs of two, repeatedly, until we are naturally left with one value (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 lead us to the root hash of the tree.
About Key Tweaking
Another clever trick that Taproot uses, is something called "key tweaking".
In order to create a pair of keys used for signatures, we start with a secret number, or "key". We then multiply this key by a very large prime number, called a "generator" (G). This process is done in a way that is computationally impossible to reverse without knowing the secret. The resulting number then becomes our public key.
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 = tweaked_seckey
pubkey + (tweak * G) = tweaked_pubkey
Later, we can choose to reveal the original public key and tweak, as 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!
Taproot uses key tweaking in order to lock coins to our pubkey + root of our tree. This provides us with two paths for spending coins:
- Using the tweaked pubkey (without revealing anything).
- Using the interal pubkey + script + proof.
Note: the "proof" is the path of hashes we described earlier, which is needed to recompute the root hash.
If you want to eliminate the key-spending path (so that a script must be used to redeem funds), you can replace the pubkey with a random number. However, it is best to use a number that everyone can verify has an unknown secret key. One example of such a number is the following:
'0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'
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
decode : (address : string, network ?: Networks) => Buff
hash : (input : Bytes | ScriptData) => Buff
encode : (input : Bytes, network ?: Networks) => string
scriptPubKey : (input : 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
}
}
Signer 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,
encodeScript : (script: ScriptData, version?: number) => string,
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 = {
create : (data : Partial<TxData>) => TxData,
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,
getTxSize : (txdata : TxData | Bytes) => TxSizeData,
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[]
}
interface TxSizeData {
size : number
bsize : number
vsize : number
weight : number
}
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 partial examples to help demonstrate using the library. Check out the test/example/taproot directory to see a full implementation of each example.
Please feel free to contribute more!
Basic Pay-to-Pubkey Spending
Full example: keyspend.test.ts
const secret = 'ccd54b99acec77d0537b01431579baef998efac6b08e9564bc3047b20ec1bb4c'
const seckey = new SecretKey(secret, { type: 'taproot' })
const pubkey = seckey.pub
const [ tseckey ] = Tap.getSecKey(seckey)
const [ tpubkey ] = Tap.getPubKey(pubkey)
const _tpubkey_example = new SecretKey(tseckey).pub.x.hex
const address = Address.p2tr.fromPubKey(tpubkey, 'regtest')
const txdata = Tx.create({
vin : [{
txid: '1ec5b5403bbc7f26a5d3a3ee30d69166a19fa81b49928f010af38fa96986d472',
vout: 1,
prevout: {
value: 100_000,
scriptPubKey: [ 'OP_1', tpubkey ]
},
}],
vout : [{
value: 99_000,
scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
}]
})
const sig = Signer.taproot.sign(tseckey, txdata, 0)
txdata.vin[0].witness = [ sig ]
const isValid = await Signer.taproot.verify(txdata, 0)
Basic Pay-to-TapScript
Full example: tapscript.test.ts
const secret = '0a7d01d1c2e1592a02ea7671bb79ecd31d8d5e660b008f4b10e67787f4f24712'
const seckey = new SecretKey(secret, { type: 'taproot' })
const pubkey = seckey.pub
const script = [ pubkey, 'OP_CHECKSIG' ]
const sbytes = Script.encode(script)
const tapleaf = Tap.tree.getLeaf(sbytes)
const _tapleaf = Tap.encodeScript(script)
const [ tpubkey, cblock ] = Tap.getPubKey(pubkey, { target: tapleaf })
const address = Address.p2tr.fromPubKey(tpubkey, 'regtest')
const txdata = Tx.create({
vin : [{
txid: '181508e3be1107372f1ffcbd52de87b2c3e7c8b2495f1bc25f8cf42c0ae167c2',
vout: 0,
prevout: {
value: 100_000,
scriptPubKey: [ 'OP_1', tpubkey ]
},
}],
vout : [{
value: 99_000,
scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
}]
})
const sig = Signer.taproot.sign(seckey, txdata, 0, { extension: tapleaf })
txdata.vin[0].witness = [ sig.hex, script, cblock ]
const isValid = await Signer.taproot.verify(txdata, 0, { pubkey })
Create / Spend from a Tree of Scripts
Full example: taptree.test.ts
const secret = '0a7d01d1c2e1592a02ea7671bb79ecd31d8d5e660b008f4b10e67787f4f24712'
const seckey = new SecretKey(secret, { type: 'taproot' })
const pubkey = seckey.pub
const scripts = [
[ 1, 7, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 2, 6, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 3, 5, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 4, 4, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 5, 3, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 6, 2, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ],
[ 7, 1, 'OP_ADD', 8, 'OP_EQUALVERIFY', pubkey, 'OP_CHECKSIG' ]
]
const tree = scripts.map(s => Tap.encodeScript(s))
const index = Math.floor(Math.random() * 10) % 7
const script = scripts[index]
const target = Tap.encodeScript(script)
const [ tpubkey, cblock ] = Tap.getPubKey(pubkey, { tree, target })
const address = Address.p2tr.fromPubKey(tpubkey, 'regtest')
const txdata = Tx.create({
vin : [{
txid: 'e0b1b0aea95095bf7e113c37562a51cb8c3f50f5145c17952e766f7a84fcc5d7',
vout: 0,
prevout: {
value: 100_000,
scriptPubKey: [ 'OP_1', tpubkey ]
},
}],
vout : [{
value: 99_000,
scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
}]
})
const sig = Signer.taproot.sign(seckey, txdata, 0, { extension: target })
txdata.vin[0].witness = [ sig.hex, script, cblock ]
const isValid = await Signer.taproot.verify(txdata, 0, { pubkey })
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).
Full example: inscribe.test.ts
const marker = Buff.encode('ord')
const mimetype = Buff.encode('image/png')
const secret = '0a7d01d1c2e1592a02ea7671bb79ecd31d8d5e660b008f4b10e67787f4f24712'
const seckey = new SecretKey(secret, { type: 'taproot' })
const pubkey = seckey.pub
const script = [ pubkey, 'OP_CHECKSIG', 'OP_0', 'OP_IF', marker, '01', mimetype, 'OP_0', imgdata, 'OP_ENDIF' ]
const tapleaf = Tap.encodeScript(script)
const [ tpubkey, cblock ] = Tap.getPubKey(pubkey, { target: tapleaf })
const address = Address.p2tr.fromPubKey(tpubkey, 'regtest')
const txdata = Tx.create({
vin : [{
txid: 'b8ed81aca92cd85458966de90bc0ab03409a321758c09e46090988b783459a4d',
vout: 0,
prevout: {
value: 100_000,
scriptPubKey: [ 'OP_1', tpubkey ]
},
}],
vout : [{
value: 99_000,
scriptPubKey: Address.toScriptPubKey('bcrt1q6zpf4gefu4ckuud3pjch563nm7x27u4ruahz3y')
}]
})
const sig = Signer.taproot.sign(seckey, txdata, 0, { extension: tapleaf })
txdata.vin[0].witness = [ sig, script, cblock ]
const isValid = await Signer.taproot.verify(txdata, 0, { pubkey, throws: true })
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