#transaction #descriptor #input #bitcoin #wallet #key #tappy

app tappy-bitcoin

Developer-friendly Taproot-only descriptor wallet for Bitcoin Core

1 unstable release

0.1.0 May 9, 2023

#46 in #descriptor

CC0 license

48KB
1K SLoC

Tappy

Don't use for real money on the main network! Use on regtest or testnet only!

Developer-friendly Taproot-only descriptor wallet on the command line that works in conjunction with Bitcoin Core.

Create custom key or script spends. Debug and test applications with handmade transactions. Learn about Taproot, descriptors, Miniscript, timelocks, and other transaction internals.

Commands

  • init
    • Create empty state
  • print
    • Print current state
  • key
    • Schnorr key pair
  • img
    • SHA-256 (pre)image pair
  • addr
    • Temporary inbound address for creating UTXOs
  • utxo
    • UTXO (unspent transaction output)
  • in
    • Transaction input
  • out
    • Transaction output
  • locktime
    • Update locktime
  • fee
    • Update transaction fee
  • spend
    • Create transaction witness and print raw transaction hex to send via Bitcoin Core
  • final
    • Finalize transaction and save transaction outputs as UTXOs

Building

cargo build

Bitcoin Core Setup

  1. Install Bitcoin Core (tested with version 24.0.1)
  2. Run bitcoind on regtest or testnet
  3. Make sure you have a wallet with funds

State

tappy keeps track of its state using the file state.json in the current directory. Secrets are stored on the hard drive! Generate the initial state like so:

$ tappy init

You can view the current state like so:

$ tappy print

Key Store

tappy keeps a set of Schnorr key pairs. Generate fresh keys by calling tappy key gen followed by the number of keys.

$ tappy key gen 5

By default, keys are disabled for spending. Enable a key pair by calling tappy key en followed by the xpub.

$ tappy key en 1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f

Disable by tappy key dis plus the xpub.

$ tappy key dis 1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f

Image Store

tappy also keeps a set of SHA-256 (pre)image pairs. Generate a pair by calling tappy img gen followed by the number of pairs.

$ tappy img gen 5

Enable (pre)image pairs for spending by calling tappy img en followed by the image.

$ tappy img en d166f218267103b44f1102a3ef05e87a9911b9f7cc7f0887f91e198e6a7d3fc4

Disable by tappy img dis plus the image.

$ tappy img dis d166f218267103b44f1102a3ef05e87a9911b9f7cc7f0887f91e198e6a7d3fc4

Creating Transactions

In tappy you create a Bitcoin transaction from utxos, inputs and outputs. This is represented in the current state. Inputs and outputs are specified by Taproot descriptors that use keys/images from the key/image store or combinations of them (and, or, thres, multi, ...).

UTXO Set

tappy maintains a set of UTXOs (unspend transaction outputs) that it can spend. Transaction inputs reference some unique UTXO that they want to spend.

Manual UTXO

tappy starts off without any UTXOs, so we must manually add them. Coins must enter the system. We set a temporary inbound address to create a UTXO with a locking script of our choosing. Use tappy addr set followed by a descriptor. Use the Policy to Miniscript compiler to generate valid Miniscript.

$ tappy addr set "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)"
: Fund this address: bcrt1pwkjuv2laefk6wqnhmqqurxnuhsc8jmmyn4xa48l4v26z3q4z6gjs5wymts

Fund this address using bitcoin-cli. In this example we send 1 BTC = 100000000 sat. The RPC returns the transaction id.

$ bitcoin-cli sendtoaddress bcrt1pwkjuv2laefk6wqnhmqqurxnuhsc8jmmyn4xa48l4v26z3q4z6gjs5wymts 1
: 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a

Get the full transaction hex using bitcoin-cli.

$ bitcoin-cli getrawtransaction 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a
: <TX_HEX>

Append 1 for human-readable format.

$ bitcoin-cli getrawtransaction 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a 1

We recommend the use of hal for even better formatting. Among other things, hal displays values in satoshi, which is the required format for tappy utxos.

$ hal tx decode <TX_HEX>

Given this information, convert the inbound address into a UTXO by calling tappy addr utxo followed by the transaction id, the output index (vout) and the value in satoshi.

$ tappy addr utxo 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a 0 100000000

Automatic UTXO

Phew, manually typing all of this stuff was a lot of work. Fortunately, tappy can add UTXOs that result from your transactions almost automatically. See Finalizing below for more.

Transaction Input

Add a new transaction input by calling tappy in followed by the input index, new and the utxo index.

$ tappy in 0 new 0

Call tappy utxo list to list all UTXOs with their index.

$ tappy utxo list

Transaction Output

Add a new transaction output by calling tappy out followed by the output index, descriptor and value in satoshi.

$ tappy out 0 "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)" 99999000

Omit the value to signify that all input funds minus the other outputs minus fees should go to this output. This works for at most one output.

$ tappy out 0 "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)"

Fee

Set the fee to whatever seems reasonable. It should not be zero. During spending the feerate will be displayed, so if Bitcoin Core rejects your transaction you can bump the fee. Call tappy fee followed by the value in satoshi.

$ tappy fee 1000

Locktime

Transaction inputs with absolute timelocks (after(n)) enforce the transaction locktime to be at least n. A transaction is valid if the height of its containing block is strictly greater than its locktime.

Set the locktime by calling tappy locktime followed by the block height. Locktime in unix time is not supported.

$ tappy locktime 785572

The locktime is disabled if all inputs have the default sequence. Timelock opcodes will fail and locktime will be ignored. Change the sequence of any input to a relative locktime (which may be zero) to enable locktime. Other ways to enable locktime are not supported.

$ tappy in 0 seq enable 0

Sequence

While locktime applies to the whole transaction, sequence applies to a single input. Transaction inputs with relative timelocks (older(n)) enforce the sequence of that input to be a relative locktime of at least n. A transaction is valid if the height of its containing block is strictly greater than the height of the utxo block plus n.

Set a relative locktime for an input by calling tappy in followed by the input index, seq enable and the relative block height. Relative locktime in unix time is not supported.

$ tappy in 0 seq enable 10

Disable relative locktime for an input by calling tappy in followed by the input index and seq disable.

$ tappy in 0 seq disable

Spending

With everything set, attempt to create a spending transaction by calling tappy spend. Remember to enable the required keys/images, and pay attention to the inputs' timelocks. Which keys/images are enabled influences the possible spend paths. tappy will return a transaction hex.

$ tappy spend
: <TX_HEX>

Use bitcoin-cli to broadcast this transaction. You will receive a transaction id if it worked.

$ bitcoin-cli sendrawtransaction <TX_HEX>
: <TXID>

Finalizing

Make sure to save the UTXOs that you just created by broadcasting the spending transaction. Call tappy final followed by the transaction id.

$ tappy final 3e59661081cbdbfa69e68a9e679a88f3d9070e209aeb11ff424ea06c806a1e7a

All transaction outputs are automatically converted and added to the UTXO set. The current transaction is cleared and a new transaction is created for the next spend. By default, the first output of the old transaction becomes the first input of the new transaction.

Key Spend

$ tappy in 0 new 0
$ tappy out 0 new "tr(9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800)"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468

Multisig

$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,multi_a(2,816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e,75910f6c72d67cd2530d17ecba4bce9058d003564887925d46201e18513d804a,9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468

Absolute Timelock

$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,and_v(v:pk(+++816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e),after(10)))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468

Relative Timelock

$ tappy in 0 new 0
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f,and_v(v:pk(+++816945ddf16d3a568644d5fe174dca7d68ed2931d3ee4edefbd96d09ae30ec2e),older(10)))"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468

Multiple Inputs, Multiple Outputs

$ tappy in 0 new 0
$ tappy in 1 new 1
$ tappy out 0 new "tr(1ffa25da651d709df36d7563fffb5416a54ff2a9702ac66d8fde4c9d029d4c2f)" 50000000
$ tappy out 1 new "tr(9fb5213dd37f61c98629500a436ae8f390b03d37d3609af2f01d515d4e899800)"
$ tappy fee 1000
$ tappy spend
$ tappy final f4a5f64b1552803ee93db9f35d2faa67be82bd1d508c1e16045112aa6f77d468

Dependencies

~15MB
~192K SLoC