AlbaDsl & albaVm: Haskell based DSL and VM for Bitcoin Cash 2025 contract programming

Yep! For most use cases, contract length reduction is the key metric – between two equivalent contracts (equivalent functionality, safety, audit quality, etc.), compiled length is what impacts real user experiences (TX fees).

Though if you want the compiler to always be able to minimize ultimate TX sizes, you also need an op-cost minimizing “mode” to switch into when the contract’s worst case(s) exceed its budget. E.g. there are cost-prioritized and length-prioritized emulations for various bitwise ops right now, and picking the right one matters a lot for ultimate contract length if a naive length-focused compilation exceeds the op-cost budget. (The compiler can also add a <variable_length_garbage> [...] OP_DROP area to give some spend-time op-cost flexibility if the budget is too close or can’t be proven safe in all cases.)

2 Likes

Loops (OP_BEGIN / OP_UNTIL) have now been added to the 2026 VM and DSL. A few other language constructs were also added, and some optimizations were performed to the VM and examples.

3 Likes

Initial support for OP_DEFINE / OP_INVOKE has been added to the 2026 VM and DSL. Both functions (global namespace) and lambdas were added based on this construct. The OP_EVAL based lambdas were removed but may be added back later (as experimental) for mutable “closure like” support.

3 Likes

Work was continued on the albaDsl secp256k1 EC multiply demo which also lead to some improvements to the albaVm efficiency. Steps were taken to optimize the EC code: VM2026 functions were introduced to compress the code and the algorithm was changed to use Jacobian coordinates. Current code size is ~1300 bytes. Since loops are now used instead of recursion, execution fits within current stack limits. On albaVm multiplying G by n - 1 (where n is the curve order) now takes around 40 ms (20x that of a straightforward native Haskell implementation). The EC algorithm/implementation can be improved further to bring down the execution time and code size.

3 Likes

The VM 2026 function support in albaDsl was revamped somewhat. VM 2026 functions are now managed in the same way as macros and can thus be more easily grouped into Haskell modules and referenced without fuss. During compilation albaDsl will collect all functions into a function table at the beginning of the program (using OP_DEFINE). Slots are assigned to functions based on how frequently they are used. The function breakdown for the EC multiplication demo can be seen below. Program size is now around 900 bytes and execution time on albaVm is ~33ms.

Module                                   Line  Function                  Ops   Slot  Sites
------------------------------------------------------------------------------------------
DslDemo.EllipticCurve.Field              44    primeModulus              1     0     57
DslDemo.EllipticCurve.Field              47    modulo                    20    1     9
DslDemo.EllipticCurve.Jacobian           37    ecMul                     6     2     7
DslDemo.EllipticCurve.JacobianAdd        18    ecDoubleJ                 147   3     2
DslDemo.EllipticCurve.Jacobian           40    ecMulJ                    70    4     1
DslDemo.EllipticCurve.Jacobian           73    toJacobian                58    5     1
DslDemo.EllipticCurve.Jacobian           89    fromJacobian              177   6     1
DslDemo.EllipticCurve.JacobianAdd        65    ecAddJ                    335   7     1
2 Likes

The EC multiply code is now 600 bytes. The function table can be seen below. Note that some functions are quite small but called frequently.

Module                                   Line  Function                  Slot  Ops   Sites
------------------------------------------------------------------------------------------
DslDemo.EllipticCurve.Field              27    feMul                     0     4     36   
DslDemo.EllipticCurve.Field              24    feSub                     1     20    9    
DslDemo.EllipticCurve.Field              33    feCube                    2     6     5    
DslDemo.EllipticCurve.JacobianPoint      44    makeIdentity              3     15    4    
DslDemo.EllipticCurve.JacobianPoint      61    getX                      4     5     4    
DslDemo.EllipticCurve.JacobianPoint      66    getY                      5     5     4    
DslDemo.EllipticCurve.JacobianPoint      71    getZ                      6     4     4    
DslDemo.EllipticCurve.Field              45    primeModulus              7     1     3    
DslDemo.EllipticCurve.JacobianPoint      25    makePoint                 8     18    3    
DslDemo.EllipticCurve.JacobianPoint      58    getTag                    9     4     3    
DslDemo.EllipticCurve.Field              39    feInv                     10    41    2    
DslDemo.EllipticCurve.JacobianAdd        19    ecDoubleJ                 11    75    2    
DslDemo.EllipticCurve.JacobianPoint      76    getField                  12    7     2    
DslDemo.EllipticCurve.Jacobian           38    ecMul                     13    48    1    
DslDemo.EllipticCurve.Jacobian           70    toJacobian                14    29    1    
DslDemo.EllipticCurve.Jacobian           81    fromJacobian              15    57    1    
DslDemo.EllipticCurve.JacobianAdd        40    ecAddJ                    16    183   1    
Functions total: 17
3 Likes

A feature was added to albaVm to export execution logs to HTML. And a feature to albaDsl for sharing information about the function table with albaVm. This way, the function definitions & calls in the HTML can be decorated/annotated. This can be used to illustrate what I think is the key feature of the Functions CHIP: functions provide a means of abstraction.

As an example we will evaluate this albaDsl program which uses a mini-language of functions (‘feMul’, ‘feAdd’, ‘feSub’, and ‘feCube’) to perform modular arithmetic on field elements (integers):

progAbstraction :: FN s (s > TBool)
progAbstraction =
  begin
    # int 3 # int 7 # feMul # int 1 # feAdd # int 12 # feSub # feCube
    # int 1000 # opNumEqual

The functions definitions are not shown here. In this example, the integer arguments are much smaller than the modulus so the results of the function applications are the same as the corresponding arithmetic operators.

Log #1 shows the execution for when functions are not used. I.e. ‘feMul’ etc. are defined as macros. Execution log #2 shows the result when they are defined as functions and the logs have been annotated with the function names.

In log #1 it is hard to see the overall structure of the program and how it ties back to the source code. There is lots of repeated code which one has to continually re-read while trying to grasp what is going on. With functions, the repetition is factored out (log #2). We can read the code at a higher abstraction level and not worry about the underlying details. If we are interested in the details we can expand the function calls by clicking on the respective row. The logging annotation framework hides the OP_INVOKES so that the functions can be read as higher-level opcodes (user defined words in Forth lingo).

I think it is a strength of Bitcoin that the opcode language is readable and not just seen as a compilation target. The functions CHIP greatly improves readability by adding structure and removing repetition. And with functions there can be a one-to-one correspondence between source language constructs and opcode level constructs.

Note how functions in some cases are just a few opcodes long. It is important that the function mechanism is very lightweight for this to be feasible.

3 Likes

I implemented a simple windowed version of Elliptic Curve point multiplication. It improves performance and also shows how OP_DEFINE/OP_INVOKE can be used to implement dynamically computed lookup tables.

My previous changes to reduce the code size using functions came with a performance regression to ~38 ms. But the windowed version of EC multiply brings that down to 26 ms (a ~30% improvement). The code size for table setup and multiply comes to 723 bytes.

The following code shows how to use the windowed EC multiply:

test :: FN s (s > TPoint > TPoint)
test =
  begin
    # gTable # g # setupTable
    # gTable # nat 100_000_000_000 # ecMul
    # gTable # nat 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140 # ecMul
  where
    gTable = nat 100

    g = <generator point>

In this case we will calculate n*G where G is the generator point. But we could have used any other point on the curve. We first initialize a table for G. It starts at function slot 100 and will stretch to slot 115. The table contains entries for n’*G where n’ goes from 0…15. Each entry is 100 bytes long. We then use the precomputed table in two point multiplications. First with a small scalar and then with a large scalar.

The algorithm works by first breaking down ‘n’ into a string of nibbles. To store these a bytestring on the stack is used: each nibble is stored in a byte and then OP_CATed together. I refer to these as vectors. The algorithm then OP_SPLITs off one nibble at a time from the vector, and uses it as an index into the lookup table to get a point value that is then used in the rest of the computation.

The lookup table could have been stored in a vector too, but OP_INVOKE based lookups are more efficient (especially for random access as here). Vectors have the benefit that they don’t consume the limited slot resource. So this is a trade-off. A combination of vectors and OP_DEFINE/OP_INVOKE probably gives best results.

3 Likes

The 2026 Bitwise CHIP has been implemented in albaDsl & albaVm. The VM passes the Libauth success vectors for bitwise.

7 Likes

AlbaVm 2025 & AlbaVm 2026 now pass all the Libauth 2025 & 2026 tests respectively. For the success vectors, VM Limits cost is also verified to be correctly calculated. Left to do is to verify correct VM Limits and failure reasons for the failure vectors. Example test run:

Tests
  Libauth vectors 2025
    bch_2025_standard in standard-mode:        Running all 7855 tests
                                               OK (0.55s)
    bch_2025_standard in non-standard-mode:    Running all 7855 tests
                                               OK (0.52s)
    bch_2025_nonstandard in standard-mode:     Running all 7968 tests
                                               OK (0.54s)
    bch_2025_nonstandard in non-standard-mode: Running all 7968 tests
                                               OK (1.17s)
    bch_2025_invalid in standard-mode:         Running all 12260 tests
                                               OK (0.75s)
    bch_2025_invalid in non-standard-mode:     Running all 12260 tests
                                               OK (0.85s)
  Libauth vectors 2026
    bch_2026_standard in standard-mode:        Running all 14283 tests
                                               OK (1.33s)
    bch_2026_standard in non-standard-mode:    Running all 14283 tests
                                               OK (1.28s)
    bch_2026_nonstandard in standard-mode:     Running all 2289 tests
                                               OK (0.22s)
    bch_2026_nonstandard in non-standard-mode: Running all 2289 tests
                                               OK (0.53s)
    bch_2026_invalid in standard-mode:         Running all 13159 tests
                                               OK (0.97s)
    bch_2026_invalid in non-standard-mode:     Running all 13159 tests
                                               OK (0.97s)

All 12 tests passed (9.68s)
4 Likes

Hey, do you feel like implementing SHA-256 in Script? And make it so that it can continue hashing off an intermediate state. With current hash opcodes we’re limited to 10kB blobs. With stream hashing we could chain transactions together and hash unlimited blobs.

1 Like

Are there compelling use cases for stream hashing in contracts? It could be an interesting project, but I have some other things I’m working on at the moment. If you want to try AlbaDsl you could have a go yourself too. Right now I’m working on a vector library. Perhaps it is even useful for this problem.

I’m dabbling in it now, just thought to ask you since you already got EC implemented.
It can be used to verify TXID preimages greater than 10kB in size, albeit parsing would still be challenging.

I got most of it done albeit struggling with stack juggling (draft, fb_compress, still incomplete and OP_PICK depths are off), but I think I got a good estimate for ballpark compute cost: 1 round needs ~3kB of “filler” bytes :sweat_smile:
So to hash 1 MB (max. consensus TX size) would require ~15k * 3kB “filler” bytes, so hashing 1MB would cost ~0.5 BCH and require a chain of at least 45 x 1MB TXs.
Very impractical, I’m dropping this idea. If we want to be able to analyze our own raw TXs with contracts, maybe dedicated opcodes would be better.

2 Likes

The fillers are to pay for operation costs I assume. In the coming years perhaps the VM per-op-cost can be reduced significantly. But stream hashing, EC scalar point multiplication, and other compute-intense crypto operations may still be very costly in Script. It would be nice if we could introduce generic opcodes (such as vector instructions) to help make such algorithms more practical in Script and reduce the need for custom opcodes. Not sure if we can find such generic solutions for these particular algorithms, but worth investigating.

2 Likes

I worked out the missing details and completed my sha256_compress implementation (fb_compress in the template).

BitAuth IDE link

The test vector matches sha256(0x1234), the state_32b is set up to be sha256 IV, and the block_64b is the message padded to 64-byte block.

The implementation is 632 bytes. To be able to run 1 round it needs to buy extra budget using additional “filler” of 2966 bytes, totaling to 3598 bytes locking script. Practically unusable.

@bitjson was already saying he wants to CHIP reducing the base cost, and I suspect that would make this script executable without any filler, at least for 1 round of compress. If cost would drop so low that 64 bytes could “buy” 1 round of compress, then it would become feasible to use this script to hash some 9.3kB and “carry” the result to next TX.

Anyway, it was a fun challenge to implement this, I’ll re-evaluate once we start talking about base cost reduction, maybe it will be usable for some future VM benchmark.

PS this is actually useful as-is for my idea of header+coinbase oracle. Even though I can’t parse a >10kB coinbase TX in Script - I can at least use the sha256_compress to prove that it is >10kB without actually having to reveal anything other than last 64-byte block of the TX. This way, the oracle can be convinced to just skip that block.

3 Likes

AlbaDsl: Vector, Tuple, and Maybe libraries

In CashVm stack entries are 10K bytes large. This means that we can store collections of smaller sized values in a single stack entry. That entry can then be conveniently moved around on the stack as a unit and be passed as an argument to functions. Two, possibly heterogeneous data types with varying sizes, can be grouped into a Tuple. Tuples can be recursively nested to form larger structures. Data types that have a fixed-size representation (or can be packed/unpacked into one) can be stored in Vectors with O(1) lookups. Optionally present values may be encapsulated in Maybe types.

I have implemented Vector, Tuple, and Maybe types and their corresponding libraries (work in progress) of accessor functions in AlbaDsl. In addition I have added Int8, Int64, and Bytes128 as examples of types that can be packed/unpacked into a fixed size representation and used as elements in Vectors. Tuples of such types can also be held in Vectors (using TupleFs).

The APIs for Vector, Maybe, and Tuple are modeled after the corresponding Haskell APIs and currently offer the following operations:

Vector: length, null, lookup, head, last, init, tail, take, drop, splitAt, uncons, unsnoc, empty, singleton, replicate, generate, iterateN, cons, snoc, append, reverse, map, zip, zipWith, unzip, filter, foldl.

Maybe: just, nothing, isJust, isNothing, fromMaybe, fromMaybe’, ifJust, maybe, map.

Tuple: tuple, untuple, fst, snd.

Of the operations above, the trivial ones are implemented as macros. All other operations are implemented as CashVM functions with macro wrappers. In the Vector library many of the functions need to know the max size of the types they work on and their corresponding pack/unpack operations. The macro wrappers for these functions use Haskell’s Typeclass mechanism to automatically inject the correct type information into the CashVM function so that the user of the library does not need to manually handle it.

The below example uses the Vector library to sum the integers 1…10:

import Alba.Dsl.V1.Bch2026 
import Alba.Dsl.V1.Bch2026.Contract.Int8 (TInt8)
import Alba.Dsl.V1.Bch2026.Contract.Vector (foldl, generate)

f :: FN s (s > TInt)
f =
  begin
    # lambda2 add
    # int 0
    # (nat 10 # lambda1 (op1Add # cast) # generate)
    # foldl

add :: FN (s > TInt > TInt8) (s > TInt)
add = cast # opAdd

The example uses ‘generate’ to build a vector of ten byte-sized integers (TInt8). Then uses ‘foldl’ to collapse it into a sum stored in a standard size integer (TInt). Lambdas are used to inject behavior into ‘generate’ and ‘foldl’ which are higher-order functions. No explicit loops are needed. If the integers to add were larger (such as Satoshi amounts) one could instead use TInt64s to hold them. That is achieved by replacing all the '8’s in the code above with '64’s. No other changes are needed.

The example compiles into 232 bytes and results in this function table:

Module                                   Line  Col   Function                  Slot  Ops   Sites
------------------------------------------------------------------------------------------------
Alba.Dsl.V1.Bch2026.Contract.Int8        39    3     int8PackFs                0     5     2    
Alba.Dsl.V1.Bch2026.Contract.Maybe       47    8     just                      1     3     1    
Alba.Dsl.V1.Bch2026.Contract.PackFs      74    3     mkPackFs                  2     11    1    
Alba.Dsl.V1.Bch2026.Contract.PackFs      86    11    getSize                   3     4     1    
Alba.Dsl.V1.Bch2026.Contract.PackFs      90    3     getPack                   4     7     1    
Alba.Dsl.V1.Bch2026.Contract.PackFs      98    3     getUnpack                 5     4     1    
Alba.Dsl.V1.Bch2026.Contract.Tuple       43    9     tuple                     6     8     1    
Alba.Dsl.V1.Bch2026.Contract.Tuple       60    3     untuple                   7     5     1    
Alba.Dsl.V1.Bch2026.Contract.Vector      258   3     splitAtUnsafeF            8     9     1    
Alba.Dsl.V1.Bch2026.Contract.Vector      278   3     unconsF                   9     29    1    
Alba.Dsl.V1.Bch2026.Contract.Vector      359   3     generateF                 10    38    1    
Alba.Dsl.V1.Bch2026.Contract.Vector      433   9     snocF                     11    5     1    
Alba.Dsl.V1.Bch2026.Contract.Vector      684   3     foldlF                    12    33    1    
Alba.Dsl.V1.Bch2026.Contract.Int8        42    11    <lambda>                  13    2     1    
Alba.Dsl.V1.Bch2026.Contract.Int8        43    11    <lambda>                  14    1     1    
Demo                                     141   11    <lambda>                  15    1     1    
Demo                                     143   21    <lambda>                  16    1     1    
Functions total: 17

This example is clearly an inefficient way of calculating a simple sum (both in terms of code size and op cost). Vectors are not required here. But the example illustrates the expressiveness of the functional programming style and gives a taste of the features of the Vector library. It also shows how far the 2026 CHIPs take us. And how dependency injection via lambdas allow us to write generic functions like foldl that can be reused across many different value types and use cases.

4 Likes

AlbaDsl now supports constants. Constants are assigned to CashVM function slots so that they can be conveniently referenced throughout the program without being computed more than once. When invoked, the function just returns the already computed value. A constant is initialized to its value via its initialization code; a snippet of AlbaDsl code that takes no arguments and leaves a single value on the stack.

Two types of constants are supported:

Runtime constants: These are computed during function table initialization which happens before the body of the contract is executed. The constant initialization code has access to the full opcode set, to functions, and to other constants. Metaprogramming is used to assign the computed constant to a function slot.

Compile time constants. These are computed during program compilation by evaluating the constant initialization code in AlbaVm. The computed value is then made part of the contract and assigned to a function slot during function table initialization. Compile time constants have access to the full opcode set except introspection ops. They can’t invoke functions/lambdas or reference other constants, but they are allowed to create lambdas.

Below is an example compile time constant from the implementation of TInt8. The constant initialization code assembles a record containing size information for the TInt8 and its pack/unpack lambdas. ‘mkPackFsM’ is a macro that packs the three fields into a record which becomes the value of the constant.

int8PackFs :: FN s (s > TPackFs TInt8)
int8PackFs =
  constant
    ( begin
        # size @TInt8
        # lambda1 (pack @TInt8)
        # lambda1 (unpack @TInt8)
        # mkPackFsM
    )

The constant above can be turned into a runtime constant by replacing ‘constant’ with ‘runtimeConstant’.

1 Like

Proof of Concept: external libraries in read-only inputs

When compiling CashVM 2026 code with AlbaDsl the bytecode blob that the compiler outputs will consist of two parts: the function table setup code followed by the contract code that then relies on the defined functions and constants. The Function Identifiers that AlbaDsl generates are now in the 0-2 byte size range which it considers to be the local function identifier space for the contract. AlbaDsl now also has a feature that allows a prefix to be provided to the compiler for prefixing generated Function Identifiers. This can, for example, be used to produce 3-byte Function Identifiers, which are useful if we want to support External Libraries using e.g. read-only inputs. In case of 3-byte Function Identifiers, two of the bytes can be used to identity the library (allowing for ~65 thousand libraries) and one byte to identify the function (allowing for 256 functions per library).

The compileLibrary function takes a Function Identifier prefix and a “library showcase” program and outputs the function table setup code for all functions that the program references and their recursive dependencies. If this function table setup code later is run in the context of an arbitrary contract it will initialize the library functions at predefined locations in the contract’s function table. This allows the contract to then reference these functions using their known Function Identifiers. The library can have self-references (e.g. 3-byte refs) to its own functions. Such self references are not unusual in libraries.

Representing a library by its function setup code is simpler than for example having a contract extracting functions one at a time (using OP_SPLIT) from some library blob. If a library contains unneeded functions and it is too costly to bring unused functions in, one can create a bespoke library with the required function subset rather than trying to cherry pick functions.

To support deploying libraries as read-only inputs, AlbaDsl now has function libraryToTx that will take a library blob and produce a transaction to deploy the library. The library is split across a bunch of UTXOs (where each code chunk is sandwiched in some wrapper code). There is also a Bitcoin Script function importLibrary that will concatenate a bunch of such UTXOs into a library blob, verify its SHA256 checksum, and then invoke the library setup code to bring all functions and constants into the contract’s function space.

To demo this External Library feature, I made a small contract that fits in a single 201 byte P2S UTXO. It pulls in two libraries:

  • “VC” (size: 1448 bytes, 49 functions): This library contains part of the AlbaDsl Vector library and also includes the Merge Sort function.

  • “DC” (size: 207 bytes): This library contains an implementation of the LZSS decompressor.

The contract’s withdraw entry point looks like this:

withdraw :: CFN (Base > TBytes > TBytes)
withdraw =
  begin
    # (opBin2Num # int 0 # opNumEqualVerify)
    # loadDecompressionLib 0
    # loadVectorLib dcNumUtxos
    # (Dc.decompress # b2v # Vc.sort # target # opEqual)
  where
    dcNumUtxos = fromIntegral Dc.numUtxos

    target :: FN s (s > TVector TInt8)
    target = bytes "     Saabeeeeeeehhhhllllorsssssssty" # b2v

    b2v :: FN (s > TBytes) (s > TVector TInt8)
    b2v = cast

(https://github.com/albaDsl/alba-dsl/blob/0aa48815e8a3d5f0e77a53125e57414548c26306/apps/contracts/permutationChallenge/Contract.hs#L48)

The entry point pulls in both the DC and VC libraries from their UTXOs. It expects an LZSS compressed English sentence as input argument (and some filler data). It uses LZSS to decompress this input argument and then sorts the result to verify that it matches a specific letter frequency. Further checks have not yet been implemented so it is very easy to fool the contract. Here are the Chipnet transactions for the demo:

  • TX that deploys the libraries and the contract:

    • 9d4e2a4c0bc0a958352b397869a1c3ea6c5214a5ab515b9908f68637fe51cd21
  • TX that spends the funds in the contract:

    • dd2896bfcbdab85422ffc388aa6fd6dfe4a8c1c771e25f090c4753cc5b291d8b

The deploy transaction deploys the two libraries and the contract in a single transaction. They could also have been deployed separately.

The spend transaction contains 7500 bytes of filler data. This is needed as the execution cost for decompress & sort is quite high. According to AlbaVm the execution cost for the contract as a whole is 5,745,589. If the calls to ‘decompress’ and ‘sort’ are removed then what’s left is basically the library import, and that execution cost comes to 78,332 for comparison. For this demo to make sense from a byte cost perspective VM op cost limits would have to be lower.

Since the library UTXOs are not actually read-only they do get consumed by the spend transaction. With real read-only inputs, these library UTXOs could remain in the UTXO set and be reused the next time the same contract was executed.

The purpose of the LZSS implementation was originally to compress the VC library. The VC library compresses to about 65% of its original size. The demo contract can first import the decompressor and then use it to decompress VC before running VC’s initialization code. This reduces the number of inputs required for the VC library and thus reduces transaction cost for the spend transaction above. This works, but only after tweaking the AlbaVm costBudgetPerInputByte parameter to increase the cost budget by 75x. So this idea is not practical with current Chipnet cost limits.

All in all this demonstrates that with read-only inputs it is possible to deploy external libraries that contracts can reference via normal function calls. This technique can potentially be used to keep transaction sizes and costs down for frequently used contracts. Unique, yet short, function references allow libraries to self reference their functions and to co-exist with other libraries in a contract’s function space without relocation. Security is ensured by taking a hash across the library as a whole. Future updates to reduce VM op cost and increase max lock script sizes make this setup more feasible.

3 Likes

By switching to a “bitstream” version of LZSS it is possible to reduce the size of the decompressor function to 111 100 bytes. It also becomes ~30% more cost efficient. Its compute cost is still too high for it to be practical for decompressing contract code, but in a future when the cost budget is increased, compression might become a useful technique for large contracts to trade some cost budget for a reduction in contract size.

1 Like