General assembly · Rev. 1.0.0

Haskell in.
Plutus out.

HaskLedger is a Haskell eDSL for Cardano smart contracts. You write validators in plain Haskell, and HaskLedger compiles them through MLabs' Covenant into UPLC you can deploy.

Shell
$nix develop
$cabal run haskledger-examples

Plutus V3 · Conway era · GHC 9.12.2 · Apache-2.0

4UPLCwhat the ledger runs3C2UPLCcode generation2COVENANT ASGa typed graph of builtin calls1HASKLEDGER EDSLyour contract, in Haskell5 · .PLUTUS ENVELOPEguarded-deadline · 407 BPLUTUSTX BUILD (PHANTOM)same contract · 3,269 BPARTS LIST · GHC 9.12.2ITEMPARTFROMVER.1HaskLedger eDSLKonma1.0.02Covenant IRMLabs1.3.03c2uplcMLabs1.0.04plutus-coreIntersectMBO1.51.0.05.plutus envelopePlutusV3Conway
  1. 1HASKLEDGER EDSLyour contract, in Haskell
  2. 2COVENANT ASGa typed graph of builtin calls
  3. 3C2UPLCcode generation
  4. 4UPLCwhat the ledger runs
  5. 5.PLUTUS ENVELOPEguarded-deadline, 407 B; the PlutusTx build of it is the dashed cube, 3,269 B
Parts list · GHC 9.12.2
1HaskLedger eDSLKonma1.0.0
2Covenant IRMLabs1.3.0
3c2uplcMLabs1.0.0
4plutus-coreIntersectMBO1.51.0.0
5.plutus envelopePlutusV3Conway

Sheet 2 · Test bench

Try to
break it.

The example contracts were run on the Cardano Preview testnet with inputs that should pass and inputs that should be refused, and the logs were kept. Set the switches, press run, and replay what was recorded.

0 of 8 recorded bench cases replayed

  • redeemer 42, after the deadlinenot replayed yet
  • redeemer 99, after the deadlinenot replayed yet
  • redeemer 42, before the deadlinenot replayed yet
  • redeemer 99, before the deadlineNOT IN RUNtry it anyway
HL-1 · Test bench
GUARDED-DEADLINE
READY
REDEEMER 42 · VALID FROM AFTER

Redeemer

Valid from

Script407 B
Checks—
Block—

The deadline is fixed in the script. The bench replays results recorded on the Preview testnet. No wallet, nothing is submitted.

All thirteen, as recorded

28 recorded cases on Preview. A refused case means the script failed evaluation, so cardano-cli could not build the transaction. One case was skipped in the recorded run and is marked as such.

  • always-succeeds161 B
    1. accepted
  • deadline372 B
    1. accepted
    2. refused
  • escrow2,478 B
    1. accepted
    2. accepted
    3. refused
  • guarded-deadline407 B
    1. accepted
    2. refused
    3. refused
    • 1.redeemer=42 + after deadline · block 4,479,161 ↗
    • 2.redeemer=99 + after deadline
    • 3.redeemer=42 + before deadline
  • hash-lock223 B
    1. accepted
    2. refused
  • hash-verify307 B
    1. accepted
    2. refused
  • multisig518 B
    1. accepted
    2. refused
  • one-shot-nft1,279 B
    1. accepted
    2. refused
  • oracle1,217 B
    1. accepted
    2. refused
  • redeemer-match192 B
    1. accepted
    2. refused
  • token-gate944 B
    1. accepted
    2. skipped
    • 1.Unlock while holding ACCESS token · block 4,479,208 ↗
    • 2.Unlock without ACCESS token · can't isolate a pure-ADA UTxO
  • treasury1,434 B
    1. accepted
    2. accepted
    3. refused
  • vesting1,468 B
    1. accepted
    2. refused
    3. refused
    • 1.beneficiary + after deadline · block 4,479,236 ↗
    • 2.non-beneficiary + after
    • 3.beneficiary + before deadline

Sheet 3 · Write it

Write the rule.

Each check is a line of Haskell, built with do-notation, operators and integer literals. The combinators do the Plutus Data work underneath: a single after walks more than ten levels of constructor encoding.

GuardedDeadline.hsv1.0.0 source ↗
module GuardedDeadline (guardedDeadline) where

import HaskLedger

guardedDeadline :: Validator
guardedDeadline = validator "guarded-deadline" $ do
  requireAll
    [ ("correct redeemer", asInt theRedeemer .== 42)
    , ("past deadline",    txValidRange `after` 1769904000000)
    ]
  1. Note 1: requireAll runs each check in order and stops at the first one that fails. The labels are for you and your tests; they are not stored on-chain.
  2. Note 2: asInt unwraps the redeemer’s integer and .== compares integers. Literals such as 42 work because Contract Expr has a Num instance.
  3. Note 3: after reads the lower bound of the validity range, needs it to be finite, and handles open and closed bounds. Times are POSIX milliseconds.

Sheet 4 · How it compiles

Four stages,
one graph.

HaskLedger does not generate Plutus itself. It builds Covenant IR and hands it to MLabs' code generator, so improvements to that back end can reach HaskLedger contracts without changes to HaskLedger.

  1. Your contract builds a graph

    Running a validator’s body evaluates nothing on-chain. Every combinator adds nodes to a Covenant ASG: function nodes, builtin calls, literals and arguments.

  2. Covenant checks it

    Covenant, the intermediate representation built by MLabs, type-checks each node as it is added, so a badly formed contract fails at compile time.

  3. c2uplc generates UPLC

    c2uplc, also from MLabs, turns the graph into Untyped Plutus Lambda Calculus, the language the Cardano ledger runs.

  4. HaskLedger packages it

    Every binding gets a unique name, names become de Bruijn indices, and the term is serialised into a PlutusScriptV3 envelope that cardano-cli reads.

Sharing
Covenant hash-conses the graph. A value your contract reads in five places is one node, bound once with a let.
Laziness
Plutus evaluates strictly. require delays its failure branch, and the case functions run only the branch that is taken.
Nothing extra
No runtime library and no decoding step. The script holds the builtin calls your contract needs, and traces only if you add traceMsg.

What a contract compiles to ↗Why builtins, not pattern matching ↗Covenant on GitHub ↗

Sheet 5 · Measured

Measured,
with the method.

Five contracts, written the usual PlutusTx way and in HaskLedger, run on identical inputs with the chain’s own cost model (plutus-core 1.51.0.0). Ratios are PlutusTx over HaskLedger.

Script bytes, PlutusTx over HaskLedger
8.0–15.7×
CPU steps, PlutusTx over HaskLedger
2.6–26.2×
Memory units, PlutusTx over HaskLedger
3.7–16.4×
  • always-succeeds

    Script bytes

    161 / 2,53315.7×

    CPU steps

    976,100 / 25,561,49826.2×

    Memory units

    6,200 / 101,57516.4×

  • redeemer-match

    Script bytes

    192 / 2,54513.3×

    CPU steps

    1,984,619 / 25,794,57513.0×

    Memory units

    9,662 / 102,60810.6×

  • deadline

    Script bytes

    372 / 3,2588.8×

    CPU steps

    10,710,190 / 30,778,0582.9×

    Memory units

    32,383 / 132,9414.1×

  • guarded-deadline

    Script bytes

    407 / 3,2698.0×

    CPU steps

    11,860,171 / 31,118,9722.6×

    Memory units

    36,349 / 134,3753.7×

  • hash-lock

    Script bytes

    223 / 2,56111.5×

    CPU steps

    3,833,672 / 26,528,9326.9×

    Memory units

    14,082 / 105,0777.5×

HaskLedger PlutusTx, same contract

Scope

  • Five contracts from the lowest complexity tiers. The other examples are measured on the HaskLedger side only.
  • Cost is the execution budget of one validation. It is not a claim about protocol throughput.
  • Most of the difference comes from reading only the fields a contract uses, where idiomatic PlutusTx decodes the whole script context first.
  • HaskLedger has not been benchmarked against Aiken or Plutarch.

Run it yourself

Shell
$cabal run haskledger-bench

Method ↗Full results at v1.0.0 ↗

Sheet 6 · Notes

Limits, stated
up front.

HaskLedger is young. These are the things to know before you pick it for a project, and the work that comes next.

Notes · current limits

  1. Note 1

    No typed datums yet. Fields are read by index and converted by hand. A wrong index is a run-time failure, not a compile error.

  2. Note 2

    No short-circuit on booleans. .&& and .|| evaluate both sides. Use the case functions when only one branch should run.

  3. Note 3

    Payout guards count lovelace only. Native-token amounts are not part of paysAtLeast or valuePreserved.

  4. Note 4

    Test helpers live in the repository. They are part of the test suite, not the library.

  5. Note 5

    No CIP-57 blueprints. HaskLedger writes .plutus envelopes, not plutus.json.

  6. Note 6

    Parameters mean a recompile. Compile-time arguments are applied in Haskell, so each parameter set needs its own compile.

  7. Note 7

    Two script purposes. Spending validators and minting policies. Staking and governance purposes are not exposed yet.

Revisions planned

ADatum-parametric versions of the remaining fixed-configuration examples
BNative-token payout floors and richer value checks
CReporting which check failed, without reading UPLC
DThe test helpers as a library module
ECI for contributors
FPlutusTx baselines for escrow, vesting, multisig and treasury
GValidation on physical RISC-V hardware

Open issues on GitHub ↗ · HaskLedger compared with Aiken, Plutarch and PlutusTx ↗

Sheet 7 · Start

Start here.

You need Nix with flakes. The dev shell brings GHC 9.12.2 and everything else, on Linux and macOS; Windows works through WSL2.

Shell
$git clone https://github.com/KonmaORG/HaskLedger.git
$cd HaskLedger
$nix develop
$cabal build all
$cabal run haskledger-examples
HaskLedger

Project

HaskLedger

Release

v1.0.0

Source

b7857cb

Licence

Apache-2.0

Built by

Konma

Site

haskledger.com

Figures, receipts and code on this page are generated from KonmaORG/HaskLedger at v1.0.0. HaskLedger compiles through Covenant and c2uplc by MLabs.