The easiest way to run Bitcoin DSL is using the docker image provided on github.
Install Just and your life will be easier.
just pull
On ARM machines, you will need to build the image. See Build Docker Image Locally.
# 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
just lab
Here’s an example showing setting up keys, looking up coinbase transactions from bitcoin node, creating new transactions and confirming them.
# 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
@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_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
The goals of the DSL are:
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.
Users should be easily able to run the various branches that a contract can be executed on.
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.
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.
Currently the DSL allows easily doing the following:
-
Automatically start/stop a bitcoin node.
-
Extend chain to generate coinbases or confirm transactions.
-
Build transactions using a high level DSL
-
script_pub_key
can be specified using miniscript, descriptors or Script. -
script_sig
can be specified using high level constructs that are extensions for descriptors and Script. -
Assert that a transaction will be accepted by mempool.
-
Submit bitcoin transactions to a node.
-
Query a bitcoin node to assert a transaction is confirmed.
-
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.
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.
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.
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
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:
-
sig:wpkh(@alice)
will sign the transaction knowing it is a p2wpkh output owned by Alice. -
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.
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.
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 }
]
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 }
]
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.
-
Broadcast transactions
-
Verify signatures of a transaction
-
Assert that the mempool will accept the transaction
-
Assert that a certain transaction is confirmed at a certain height
Here’s how you do all of the above.
verify_signature for_transaction: @alice_bob_multisig_tx,
at_index: 0,
with_prevout: [coinbase_tx, 0]
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.
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:
-
Abstractions over taproot so that it is easy to build taproot transactions using an abstract DSL.
-
Provide highlevel constructs to tweak keys and generate musig and threshold signatures.