Skip to content

Understanding Casts

Libs edited this page May 13, 2022 · 4 revisions

A Bitcoin transaction consists of two sides: inputs and outputs.

An output contains a locking script which locks a number of satoshis to a puzzle. An input contains an unlocking script which provides the solution to a previous output's puzzle. These two sides of the puzzles, the locking and unlocking scripts, form a script template.

A Cast is a JavaScript class with an API for defining locking and unlocking scripts. A Cast is therefore a script template. It allows us to lock coins, and later unlock and spend coins, by calling a function and passing a few parameters.

Built-in casts

TxForge ships with the following Casts, supporting some very common transaction templates:

These Casts can be accessed:

import { casts } from 'txforge'

const { P2PKH, P2PK, P2MS, P2RPH, OpReturn } = casts

P2PKH - Pay to Public Key Hash

This is by far the most common transaction type in Bitcoin land. It locks satoshis to the hash of a public key (an address). The satoshis can be unlocked by signing with the corresponding private key.

import { forgeTx, getUTXO, casts } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { P2PKH } = casts

const privkey = nimble.PrivateKey.fromStr(wif)
const address = privkey.toAddress()

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    P2PKH.lock(10000, { address })
  ]
})

const utxo = getUTXO(tx1, 0)

const tx2 = forgeTx({
  inputs: [
    P2PKH.unlock(utxo, { privkey })
  ],
  outputs: [...]
})

P2PK - Pay to Public Key

P2PK scripts are simpler and smaller than their P2PKH cousins, but seen far less frequently. It locks satoshis to a public key. The satoshis can be unlocked by signing with the corresponding private key.

import { forgeTx, getUTXO, casts } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { P2PK } = casts

const privkey = nimble.PrivateKey.fromStr(wif)
const pubkey = privkey.toPublicKey()

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    P2PK.lock(10000, { pubkey })
  ]
})

const utxo = getUTXO(tx1, 0)

const tx2 = forgeTx({
  inputs: [
    P2PK.unlock(utxo, { privkey })
  ],
  outputs: [...]
})

P2MS - Pay to Multi Sig

P2MS scripts allow for satoshis to be locked to a threhold of a number of public keys. The satoshis can be unlocked by signing with the freshold of corresponding private keys. For example, if the coin is locked 3 public keys with a threshold of 2, then it requires two valid signatures to spend the coin.

import { forgeTx, getUTXO, casts } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { P2MS } = casts

const privkeys = [pk1, pk2, pk3]
const pubkeys = privkeys.map(pk => pk.toPublicKey())

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    P2PK.lock(10000, { pubkeys, threshold: 2 })
  ]
})

const utxo = getUTXO(tx1, 0)

const tx2 = forgeTx({
  inputs: [
    P2PK.unlock(utxo, { privkeys.slice(0, 2) })
  ],
  outputs: [...]
})

P2RPH - Pay to R-Puzzle Hash

R Puzzles are a kind of hash puzzle. Unlike a traditional hash puzzle, R Puzzles take advantage of ECDSA mathmetics so that the secret knowledge never needs to be revealed on-chain.

P2RPH scripts involve locking satoshis to the hash of an R value. R is part of an ECDSA signature that is the result of multiplying the ECDSA generator G with a secret value K. The satoshis can therefore be unlocked by signing with any key as long we we use the correct K value.

The benefits of this script are two-fold. Our secret value K is never revealed publicly, and we can in effect sign a valid transaction using any arbitrary key.

TxForge provides an extra/r-puzzle export that contains helpers for generating the values K and R used in R-Puzzles. If a private key isn't provided to unlock the coin, then a random key is used to create the signature.

import { forgeTx, getUTXO, casts } from 'txforge'
import { generateK, calculateR } from 'txforge/extra/r-puzzle'
import nimble from '@runonbitcoin/nimble'

const { P2RPH } = casts

const k = generateK()
const r = calculateR(k)

const tx1 = forgeTx({
  inputs: [...],
  outputs: [
    P2RPH.lock(10000, { r })
  ]
})

const utxo = getUTXO(tx1, 0)
const privkey = nimble.PrivateKey.fromStr(wif)

const tx2 = forgeTx({
  inputs: [
    P2RPH.unlock(utxo, { k, privkey })
  ],
  outputs: [...]
})

OpReturn - Non-spendable data output

Sometimes it is useful to attach arbitrary data to a transaction. This is commonly achieved by creating a non-spendable output that always returns false containing the attached data.

As OpReturn locking scripts are always unspendable, we generally wont want to lock any satoshis to this output. The data parameter can be a string or byte vector, or an array of valid values.

import { forgeTx, casts } from 'txforge'
import nimble from '@runonbitcoin/nimble'

const { OpReturn } = casts

const tx = forgeTx({
  inputs: [...],
  outputs: [
    OpReturn.lock(0, { data: 'hello world' }),
    OpReturn.lock(0, { data: ['hello', 'world'] })
  ]
})

Raw - Raw script pass-through

Sometimes you may want to use TxForge with existing scripts. We got you covered - the Raw Cast is for exactly this use case. Simply pass in your script as a parameter.

import { forgeTx, casts } from 'txforge'

const { Raw } = casts

const script = '76a914a58621fd80c3abcf4f81d343f6a78dc891082d6688ac'

const tx = forgeTx({
  inputs: [...],
  outputs: [
    Raw.lock(10000, { script })
  ]
})