Skip to content

Bitcoin DSL for scripting layer two contracts and interactions with bitcoin nodes

License

Notifications You must be signed in to change notification settings

pool2win/bitcoin-dsl

Repository files navigation

![CI Badge

Bitcoin DSL - Mission

Make is easy to experiment with bitcoin contracts.

Quick Start

The easiest way to run Bitcoin DSL is using the docker image provided on github.

Install Just and your life will be easier.

Pull the latest docker image
just pull
Run a script from terminal
# Run a script using pulled docker image
just run <path-to-your-script>

# Example, running the ARK single payment example
just run ./lib/contracts/ark/single_payment.rb
Run Jupyter notebook
just lab

Build Docker Image Locally

If you want to buid a docker image locally you can use the just recipes provided. We build bitcoind from source, so it’ll take some time.

  1. Build docker image just dockerize

  2. Run local docker image DSL_IMAGE=dev-notebooks just run <path-to-your-script>.

Example Contracts

Here’s an example showing setting up keys, looking up coinbase transactions from bitcoin node, creating new transactions and confirming them.

Generate keys and find coinbase to spend
# Generate new keys
@alice = key :new
@bob = key :new
@carol = key :new

# Seed alice with some coins
extend_chain to: @alice

# Seed bob with some coins and make coinbase spendable
extend_chain num_blocks: 101, to: @bob

assert_equal get_height, 102, 'The height is not correct'

@coinbase_tx = get_coinbase_at 2
Spend coinbase to multisig
@multisig_tx = transaction inputs: [
     {
       tx: @coinbase_tx,
       vout: 0,
       script_sig: 'sig:wpkh(@bob)'
     }
   ],
   outputs: [
     {
       descriptor: 'wsh(multi(2,@alice,@bob))',
       amount: 49.999.sats
     }
   ]
broadcast @multisig_tx
confirm transaction: @multisig_tx, to: @alice
Spend multisig after signing
@spend_tx = transaction inputs: [
     {
       tx: @multisig_tx,
       vout: 0,
       script_sig: 'sig:multi(@alice,@bob)'
     }
   ],
   outputs: [
     {
       descriptor: 'wpkh(@carol)',
       amount: 49.998.sats
     }
   ]
broadcast @spend_tx
confirm transaction: @spend_tx, to: @alice

Goals

The goals of the DSL are:

Declarative syntax

The DSL will specify what needs to be done, not how it should be done. For example, to build a transaction, we should just say the scriptsig is a sig:wpkh(@bob) instead of making a series of imperative calls to achieve the same goal.

Execute branches in contract execution

Users should be easily able to run the various branches that a contract can be executed on.

High level language for locking and unlocking Script

Support miniscript, descriptors and Script for script sigs and script pubkeys.

Miniscript and descriptors are handy for writing locking conditions in a higher level language, however, we also want to write unlocking scripts in a higher level language.

Interact with bitcoin node

All the JSON-RPC API commands will be directly available from the DSL. This will avoid copy pasting seriaized transactions. The query results from bitcoin node will be available as objects for introspection and manipulation.

Transparent tracking of signing material

The DSL acts as a god system, where the runtime sees all the cryptographic material. For example, to generate a P2WSH signature, the witness program is automatically put in the right place by the DSL.

Features

Currently the DSL allows easily doing the following:

  1. Automatically start/stop a bitcoin node.

  2. Extend chain to generate coinbases or confirm transactions.

  3. Build transactions using a high level DSL

  4. script_pub_key can be specified using miniscript, descriptors or Script.

  5. script_sig can be specified using high level constructs that are extensions for descriptors and Script.

  6. Assert that a transaction will be accepted by mempool.

  7. Submit bitcoin transactions to a node.

  8. Query a bitcoin node to assert a transaction is confirmed.

  9. Query bitcoin node for transactions and blocks - these responses are available as objects for further introspection and manipulation.

Here’s how each of the above is done using the DSL.

Starting a node

This is automagically handled by the DSL. When you run a DSL script, a bitcoin node is setup and when the script finishes, the node is shutdown and all directories are deleted.

There’s no commands required to start/stop a node. The DSL just does it for you.

Here is a simple script to create a coinbase and make it spendable.

@alice = key :new

# Mine 100 blocks, all with coinbase to alice.
extend_chain to: @alice, num_blocks: 101

This is how you run the above script

$ ruby lib/run.rb -s lib/simple.rb
Running script from lib/simple.rb
mkdir -p /tmp/x &&              bitcoind -datadir=/tmp/x -chain=regtest              -rpcuser=test -rpcpassword=test -daemonwait -txindex -debug=1
Bitcoin Core starting
I, [2024-03-01T21:01:13.580365 #73094]  INFO -- : Extending chain by 101 blocks to address bcrt1qy5a0ghjsnmlt4qt0akf7627wkwexljaz6tfame
kill -9 `cat /tmp/x/regtest/bitcoind.pid` && rm -rf /tmp/x

As you see above, the DSL automatically starts a new bitcoin node, runs the script and at the end cleans up by stopping bitcoind and deleting any data directories.

Extend chain

We need to extend chain in a number of situations. When we need to mine some coins to use them later or to confirm a transaction that has been broadcast.

Let’s look at both the cases.

Extend chain to mine some coins

The following generates a new key and mines a block where the coinbase rewards are sent to alice’s WKH.

# Generate new key and call it alice
@alice = key :new

# Extend chain mining coinbases to alice
extend_chain to: @alice

Extend chain to confirm transactions

The following will mine 100 blocks. This will make all previously generated coinbases spendable.

extend_chain num_blocks: 100

In the above, we will generate a throw away key that get the coinbase reward.

Build transactions

I often need to find a spendable coinbase controlled by a key, then create a transaction that spends the coinbase, creating a new UTXO with custom spending conditions.

The following script finds a coinbase spendable by Alice and creates a new transaction to spend the coinbase.

# Find a coinbase that Alice can spend
@alice_coinbase = spendable_coinbase_for @alice

transaction inputs: [
     { tx: @alice_coinbase, vout: 0, script_sig: 'wpkh(@alice)' }
   ],
   outputs: [
     { descriptor: 'wpkh(@bob)', amount: 49.99.sats }
   ]

Note the syntax to generate script_sig and script_pub_keys. In the above transaction:

  1. sig:wpkh(@alice) will sign the transaction knowing it is a p2wpkh output owned by Alice.

  2. wpkh(@bob) will create a p2wpkh output for Bob.

We can even use miniscript policies to generate script_pub_keys and I demonstrate that next.

Use miniscript policy

If we want to generate a multisig transaction we can use miniscript to specify the spending policy. Note how the output is now using the policy keyword instead of the address keyword. The policy in the transaction below is a simple 2 of 2 multisig specified using miniscript.

transaction inputs: [
     { tx: coinbase_tx, vout: 0, script_sig: 'wpkh(@bob)', sighash: :all}
   ],
   outputs: [
     {
       policy: 'thresh(2,pk(@alice),pk(@bob))',
       amount: 49.999.sats
     }
   ]

The sighash: :all directive is optional. By default the DSL uses sighash ALL, but I show this here to point out that we can provide sighash type here.

We can use any other policy and here’s another example with a policy that requires a spending condition with 2 of 2 multisig or an claim after a CSV timelock.

@threshold_tx = transaction inputs: [
     { tx: coinbase_tx, vout: 0, script_sig: 'sig:wpkh(@bob)', sighash: :all }
   ],
   outputs: [
     {
       policy: 'or(99@thresh(2,pk(@alice),pk(@bob)),and(older(10),pk(@bob_timelock)))',
       amount: 49.999.sats
     }
   ]

To spend the transaction, we introduce a csv keyword. The following is an example of a transaction spending from the timelock path of the above transaction.

transaction inputs: [
     { tx: @threshold_tx,
       vout: 0,
       script_sig: 'sig:@bob_timelock sig:@alice',
       csv: 10 }
   ],
   outputs: [
     {
       descriptor: 'wpkh(@alice)',
       amount: 49.998.sats
     }
   ]

Note use of the CSV keyword to setup sequence and locktime values.

We see here how the DSL hides the complications of constructing bitcoin transactions by providing a high level language to build transactions.

Using descriptors

The transaction above using miniscript thresh policy can be written using the multi descriptor instead.

transaction inputs: [
     { tx: coinbase_tx, vout: 0, script_sig: 'wpkh(@bob)', sighash: :all}
   ],
   outputs: [
     { descriptor: 'wsh(multi(2,@alice,@bob))', amount: 49.999.sats }
   ]

Using Script

The same script pubkey can also be written using plain old script. When using script, the DSL wraps the provided script into a wsh descriptor for us, and tracks the witness program for use when we later need to spend from the output.

transaction inputs: [
     { tx: coinbase_tx, vout: 0, script_sig: 'wpkh(@bob)', sighash: :all}
   ],
   outputs: [
     { script: '2 @alice @bob 2 OP_CHECKMULTISIG', amount: 49.999.sats }
   ]

Bitcoin node interactions

All the part about building transactions is fine. However, the sweet part is that we can interact with a bitcoin node to submit the transactions generated and then query the node for the state of the transactions. In fact, the entire range of json-rpc API for bitcoin is directly available in the DSL.

In this post, we only focus on the most often used commands and the abstractions the DSL provides over those.

  1. Broadcast transactions

  2. Verify signatures of a transaction

  3. Assert that the mempool will accept the transaction

  4. Assert that a certain transaction is confirmed at a certain height

Here’s how you do all of the above.

Broadcast transactions

broadcast @alice_bob_multisig_tx

Verify signatures for a transaction

verify_signature for_transaction: @alice_bob_multisig_tx,
                 at_index: 0,
                 with_prevout: [coinbase_tx, 0]

Assert mempool will accept a transaction

assert_mempool_accept @alice_bob_multisig_tx

Assert a transaction is confirmed

To assert that a transaction is confirmed at a given height:

assert_confirmed transaction: @alice_bob_multisig_tx, at_height: 100

Tools Used

I was earlier trying to build an intricate DSL in Lisp, but for the sake of quick iteration decided to build an internal DSL in Ruby. Thankfully, we already have an extensive, well tested and supported library to build bitcoin transactions in Ruby - bitcoinrb - a bitcoin ruby library that provides all the building blocks I need. So my task was made much simpler - build an internal DSL around bitcoinrb.

I want to leverage miniscript to specify pubscripts. For the same, rust-miniscript I provide a CLI wrapper around it and call it from within the Ruby DSL.

Next Steps

Some of the initial goals for the DSL have already been accomplished. Namely, an ability to describe transactions in a high level language and then submit those transactions to a bitcoin node as well as query the bitcoin node.

Some nice features that I am working on include:

  1. Abstractions over taproot so that it is easy to build taproot transactions using an abstract DSL.

  2. Provide highlevel constructs to tweak keys and generate musig and threshold signatures.