How to Write Custom Bitcoin Scripts in Bitauth IDE
Bitauth IDE is an integrated development environment for bitcoin authentication. This guide explains features of the IDE and language.
Bitcoin Script is the language used by Bitcoin to validate transactions. Bitcoin “addresses” are actually locking scripts defined in this language, and in order to spend funds, every transaction must have a matching unlocking script, which provides the signatures and other data required to “unlock” the funds.
To learn more about Bitcoin Script, check out Bitcoin Script for Beginners.
Bitauth IDE is an integrated development environment for bitcoin authentication. This guide will explain features of the IDE and templating language.
When you work in Bitauth IDE, you’re working on a Bitauth Template. It’s a JSON file which fully describes the authentication scheme for a bitcoin wallet. Compatible wallet software can import your template and generate a fully-functional wallet, even for complex, multi-party schemes. The IDE lets you write, test, and export Bitauth templates.
Bitauth templates include two primary concepts:
- Entities — the individuals and/or devices participating in the wallet.
- Scripts — the code used by wallet software to create addresses and transactions.
A Bitauth template defines a set of entities which will use the template. Each entity can be assigned one or more variables for which they are responsible. There are currently 6 variable types: Key, HDKey, WalletData, AddressData, CurrentBlockHeight, and CurrentBlockTime.
When a wallet is created, each entity shares the public elements of their variables. Values are validated to prevent man-in-the-middle attacks, and then wallet addresses are generated.
Bitauth templates define a set of scripts used by the entities. There are 4 types of scripts:
- Locking Scripts — scripts from which wallet addresses are generated.
- Unlocking Scripts — scripts which enable wallet software to spend from the wallet.
- Isolated Scripts — scripts used as macros or bytecode templates.
- Isolated Script Tests — a pair of scripts (Setup and Check) used to verify the functionality of an isolated script.
Bitauth Templating Language (BTL)
Bitauth template scripts are written in Bitauth Templating Language (BTL). The language is very low-level — any bitcoin virtual machine bytecode can be represented in BTL.
Opcode identifiers in BTL are prefixed with
OP_. During compilation, opcode identifiers are replaced with their bytecode equivalents. E.g
OP_0 OP_1 OP_ADD will compile to the bytecode
All opcodes are also autocompleted within the IDE. To read a description of a given opcode, hover over it in the editor. You can also find resources describing bitcoin opcodes online.
Literal Data Types
BTL supports 3 literal data types:
- Hex literals — hex-encoded data, prefixed with
- UTF8 literals — UTF8-encoded data, surrounded by single quotes (
') or double quotes (
'this is a string'or
- BigInt literals — integers, e.g.
1234. (Bitauth IDE supports arbitrary integer sizes, but numbers which overflow 64 bits are considered non-standard and may not be supported in the future.)
Push statements are surrounded by
>, and generate the opcode to push their compiled contents to the stack.
<"abc"> will generate the bytecode to push
616263) to the stack:
OP_PUSHBYTES_3 0x616263). Pushes are automatically minimized: e.g.
<1> compiles to
<OP_0> (equivalent to
<0x00>) compiles to
Any valid BTL can be contained in a push statement (including further push statements), so code like
<<<<1>>>> is valid. (Result:
Including Variables & Scripts
Every Script and variable has a unique ID within the template. Both can be included by referencing the unique ID. E.g. an
AddressData variable with an ID of
nonce can be pushed to the stack with
When referenced, variables and scripts are included directly as bytecode. This makes it possible to provide segments of bytecode in variables and to use isolated scripts as macros. E.g.
<my_number> pad_value might push the variable
my_number and then insert the
pad_value script, which might be defined as
<8> OP_NUM2BIN, padding
my_number to 8 bytes.
Each variable has a type which specifies its role in a template. There are currently 6 variable types:
AddressData– Address Data is the most low-level variable type. It must be collected and stored each time a script is generated (usually, a locking script). Address Data can include any type of data, and can be used in any way. For more persistent data, use
CurrentBlockHeight– The Current Block Height type provides the current block height as a Script Number at the time of compilation. This is useful when computing a height for
OP_CHECKSEQUENCEVERIFYwhich is relative to the height at the moment a script is created (usually, a locking script).
CurrentBlockTime– The Current Block Time type provides the current block time (at the time of compilation) as a Script Number. This is useful when computing a time for
OP_CHECKSEQUENCEVERIFYwhich is relative to the current time at the moment a script is created (usually, a locking script).
HDKey– The HD Key (Hierarchical-Deterministic Key) type automatically manages key generation and mapping in a standard way. For greater control, use a Key. (NOTE: HDKey is not yet supported by Bitauth IDE.)
Key– The Key type provides fine-grained control over key generation and mapping. Most templates should instead use
WalletData– The Wallet Data type provides a static piece of data – collected once and stored at the time of wallet creation. Wallet Data is persisted for the life of the wallet, rather than changing from locking script to locking script. For address-specific data, use
Some variable types provide operations which are accessed with a period (
.), e.g. the public key of the
owner Key can be pushed to the stack with
Several operations are available to
public_key– include the public key.
signature.[signing_serialization_type]– create an ECDSA signature using the key and the selected signing serialization algorithm.
schnorr_signature.[signing_serialization_type]– create a schnorr signature using the key and the selected signing serialization algorithm.
data_signature.[SCRIPT_ID]– create a data signature using the key by signing the compiled output of SCRIPT_ID.
schnorr_data_signature.[SCRIPT_ID]– create a schnorr data signature using the key by signing the compiled output of SCRIPT_ID.
schnorr_signature) are generated by serializing elements of the signed transaction in a standard way, hashing the serialization, and signing the message hash.
There are 6 signing serialization algorithms:
all_outputs– the recommended (and most commonly used) signing serialization algorithm. This signs each element of the transaction using the private key, preventing an attacker from being able to reuse the signature on a modified transaction. (A.K.A. "SIGHASH_ALL")
all_outputs_single_input– a modification to the "all_outputs" signing serialization algorithm which does not cover inputs other than the one being spent. (A.K.A. "SIGHASH_ALL" with "ANYONE_CAN_PAY")
corresponding_output– a signing serialization algorithm which only covers the output with the same index value as the input being spent. Warning: this can cause vulnerabilities by allowing the transaction to be modified in certain ways after being signed. (A.K.A. "SIGHASH_SINGLE")
corresponding_output_single_input– a modification to the "corresponding_output" signing serialization algorithm which does not cover inputs other than the one being spent. (A.K.A. "SIGHASH_SINGLE" with "ANYONE_CAN_PAY")
no_outputs– a signing serialization algorithm which only covers other inputs. Warning: this allows anyone to modify the outputs after being signed. (A.K.A. "SIGHASH_NONE")
no_outputs_single_input– a modification to the "no_outputs" signing serialization algorithm which does not cover inputs other than the one being spent. (A.K.A. "SIGHASH_NONE" with "ANYONE_CAN_PAY")
Most authentication schemes should use the
all_outputs setting, e.g.
<owner.signature.all_outputs>. This prevents an attacker from being able to reuse the signature on a different transaction (which the key holder did not intend to authorize).
For unique circumstances, the other algorithms can also be specified — you can find resources online which describe some of these scenarios and their security implications.
To display debugging information, Bitauth IDE transparently integrates scripts into a simple transaction and evaluates it in the bitcoin-ts virtual machine implementation.
For data signatures (
schnorr_data_signature), the message to hash and sign is provided as a script, e.g.
<owner.data_signature.message> will hash the compiled bytecode representation of the
message isolated script, signing the hash using the
Evaluations are segments of code surrounded by
) which use the bitcoin virtual machine itself to assist in generating bytecode. The contents of an evaluation are compiled and evaluated, and the top element on the resulting stack is then inserted as bytecode. E.g.
$(<1> <2> OP_ADD) "abc" produces the bytecode
This is surprisingly useful — often the procedure to create a desired bytecode sequence is similar to the procedure later used to validate it. For example, a P2SH locking script is generated using this BTL:
OP_HASH160 <$(<redeem_script> OP_HASH160)> OP_EQUAL
First, in the evaluation, the compiled bytecode of
redeem_script is pushed to the stack and hashed. Then the final locking script can be generated by inserting the bytecode for OP_HASH160, followed by a push of the generated redeem script hash, followed by the bytecode for OP_EQUAL.
The easiest way to get started working with Bitauth IDE is to review the example templates. You’ll find examples of both common wallet types and of complex, multi-entity authentication schemes.
Thanks for reading!
This guide is still under development. If you have questions or ideas for improvement, please open an issue on GitHub or message me on twitter.