Balzac in a nutshell

BALZaC (for Bitcoin Abstract Language, Analyzer and Compiler) is a domain-specific language to write Bitcoin transactions, based on the paper [AB+18FC]. Balzac features a simple syntax to express Bitcoin transactions. We illustrate it through a series of examples, that you can experiment with in the online editor.

A basic transaction

Bitcoin transactions transfer currency, the bitcoins (BTC). Each transaction has one or more inputs, from where it takes the bitcoins, and one or more outputs, which specify the recipient(s). Balzac also allows for transactions with no inputs: even thought these transactions cannot be appended as is to the actual Bitcoin blockchain, they are useful to refer to transactions which are not known at specification time. An example of transaction with no inputs is the following:

transaction T {
    input = _    // no input
    output = 50 BTC: fun(x) . x == 42
}

The output field of transaction T contains a value, 50 BTC, and an output script, fun(x) . x==42. This means that 50 bitcoins will be transferred to any transaction which provides a witness x such that x == 42.

To append T to the Bitcoin blockchain, the placeholder _ for the input must be replaced with the identifier of an unspent transaction already on the blockchain, which has at least 50 BTC.

You can use the web editor to write Balzac transactions, to check their syntax, and to compile them into actual Bitcoin transactions. The output of the compiler is a serialized transaction for the Bitcoin test network (testnet). To generate transactions for the main network (mainnet), one must specify the network as follows:

network mainnet  // default is testnet

For instance, let us paste transaction T into the editor and then let us add command eval T to it. Now, if we hit the button [Compile], the web editor shows in the output box the transaction T in Bitcoin (testnet) serialization format.

_images/compiling_t.png

The serialized transaction can be sent to the Bitcoin network using the Bitcoin client command bitcoin-cli sendrawtransaction. However, in order to be published, T must redeem a real transaction on the blockchain. In Balzac, it is possible to define a transaction using its hex format, for example:

const realT = tx:0000fa00120... // Hex of a real transaction

transaction T {
       input = realT: ...       // witness to redeem realT
       output = 50 BTC: fun(x) . x == 42
}

Redeeming a transaction

If one needs to use the bitcoin stored within T, she can redeem it with the following transaction:

transaction T1 {
    input = T: 42
    output = 50 BTC: fun(x). x != 0  // any constraint chosen by the user
}

Transaction T1 redeems T by indicating it in the input field, and by providing the number 42 as witness. The value 42 is the actual parameter which replaces the formal parameter x in the output script fun(x) . x == 42, and makes the script evaluate to true. Any other witness would make the script evaluate to false, and would prevent the transaction T1 from being added to the blockchain. A transaction cannot be spent twice: hence, once T1 is on the blockchain, no other transaction having T as input can be appended.

Note that T1 is redeeming exactly the 50 BTC deposited in T: in practice, to be able to append T1 to the blockchain, the value in output of a transaction must be strictly less than the value in input. The difference is retained by Bitcoin miners as a fee for their work. Currently, transactions with zero fee are not likely to be added to the blockchain.

Now, let us insert both T and T1 in the editor. While we write, the editor performs some static checks and signals the errors. For instance, if instead of the value 42 we provide another witness for T (say for instance value 4), the editor will display a warning. If the input field of T1 has a wrong reference (say T3), or if the total amount of outgoing bitcoins is greater than the incoming one, the editor will signal the error.

Signature verification

The output scripts of T and T1 are naive, since anyone can produce the right witnesses. Usually, one wants to transfer bitcoins to a specific user. For instance, the following transaction T2 makes the 50 BTC of T1 redeemable only by user Alice:

// Alice's public key
const pubA = pubkey:03d0272bb640bdbbcaedce10ef69ad6d9d8c7b9c61ff2aa4cf4ed27865d287c224

transaction T2 {
    input = T1: 12
    output = 50 BTC: fun(x) . versig(pubA; x)
}

The constant pubA declares Alice’s public key. Users may generate as many public keys as they want.

The predicate versig(pubA; x) in the output script of T2 is true if x is a valid signature of the transaction which redeems T2, computed with Alice’s private key.

The transaction T2 can be redeemed by a transaction T3 made as follows:

// Alice's private key
const kA = key:cVdDtCe2Gb6HWeCEzRTpZEitgxYonPtvLfGZrpprWV6BTJ3N37Lw

transaction T3 {
    input = T2: sig(kA)
    output = 50 BTC: fun(x) . versig(pubA; x) // any condition chosen by Alice
}

The witness sig(kA) is the signature of transaction T3 (without considering the witness itself) using the private key kA.

You can use the online form on the sidebar to generate new addresses and keys.

_images/sidebar.png

Multiple inputs and outputs

Transactions can have more than one output, in order to split the money on different recipients. For instance, the amount of bitcoins in T4 is split in two parts:

// Bob's other public key
const pubB = pubkey:0289654c430032f20f8464a84a1f9b3289ceaff8d6cd93c9b654e59a8c5a1cc1b0

transaction T4 {
    input = T3:sig(kA)
    output = [
        40 BTC: fun(x) . versig(pubA; x);
        10 BTC: fun(x) . versig(pubB; x)
    ]
}

In this transaction, the output field has two items, that can be redeemed separately.

Transactions can have more than one input, in case they need to gather money from several sources. For each input, the transaction must provide a suitable witness. In case inputs refer to a transaction with multiple outputs, their outputs are numbered starting from 0. For instance:

// Bob's private key
const kB = key:cVifQzXqqQ86udHggaDMz4Uq66Z7RGXJo5PdVjzRP12H1NDCFsLV

transaction T5 {
    input = [
        T4@0: sig(kA);
        T4@1: sig(kB)
    ]
    output = 50 BTC: fun(x) . versig(pubA; x)
}

which calculates the signature of transaction T5 using the private key k. (see function list ).

Parametric transactions

Transaction definition can be parametric. For instance, in the following example T6 takes one parameter of type pubkey and uses it in the output script.

// parametric transaction
transaction T6(pub) {
    input = _
    output = 1BTC: fun(x). versig(pub;x)
}

To be able to evaluate T6, one must instantiate that one parameter, like:

eval T6(pubA)

One can also use T6 in the definition of its redeeming transaction, as follows:

transaction T7 {
    input = T6(pubA): sig(kA)
    output = 1BTC: fun(x). versig(pubB;x)
}

In case the parameter is a witness, it can be left unspecified as long as it is needed, using the symbol _. For instance, transaction T9 is obtained by T8, without providing a witness :

transaction T8(s:signature, n:int) {
    input = T7: s
    output = 1BTC: fun(x, m). versig(pubA;x) && m == sha256( n )
}
//transaction with empty signature
const T9 = T8(_, 4)

The generation of a signature inside a transaction is done at compilation time, so that all the parameters have been instantiated. Indeed:

transaction T8_bis(n:int) {
    input = T7: sig(kB)
    output = 1BTC: fun(x, m). versig(pubB;x) && m == sha256( n )
}

eval T9_bis(4)  //sig(kA) is calculated now

Participants

Balzac supports the definition of participants.

Participants can be declared as participant Alice {...} and enclose constant and transaction declarations. Consider the following example:

// Global declarations
const pubA = pubkey:0364de313bd23bca4ed8db326dc3c66c32a6aa3687ef6b885445c226b9e2366ebf
const fee = 0.0003 BTC

participant Alice {
    // Local declarations
    const name = "Alice"

    transaction T {
        input = _
        output = 1 BTC - fee :
            fun(x, y) . x == name && versig(pubA; y)
    }
}

eval pubA, Alice.T

The snippet above defines two global declarations pubA and fee, that are globally visible within the same file.

Local declarations can be defined within participants. In order to refers to the participant declarations, the participant name must precede the declaration name, e.g. Alice.T or Alice.name.

Private local declarations are preceded by the keyword private: a private declaration is visible only within the participant in which is declared. In the following example the constant kA is not visible outside the Alice’s participant.

participant Alice {
    private const pubA = pubkey:0364de313bd23bca4ed8db326dc3c66c32a6aa3687ef6b885445c226b9e2366ebf
}

transaction T {
    input = _
    output = 10 BTC : fun(x) .
      versig(Alice.pubA; x)       // Error: couldn't resolve 'Alice.kA'
}