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.
$nix develop$cabal run haskledger-examples
Plutus V3 · Conway era · GHC 9.12.2 · Apache-2.0
- 1HASKLEDGER EDSLyour contract, in Haskell
- 2COVENANT ASGa typed graph of builtin calls
- 3C2UPLCcode generation
- 4UPLCwhat the ledger runs
- 5.PLUTUS ENVELOPEguarded-deadline, 407 B; the PlutusTx build of it is the dashed cube, 3,269 B
| 1 | HaskLedger eDSL | Konma | 1.0.0 |
| 2 | Covenant IR | MLabs | 1.3.0 |
| 3 | c2uplc | MLabs | 1.0.0 |
| 4 | plutus-core | IntersectMBO | 1.51.0.0 |
| 5 | .plutus envelope | PlutusV3 | Conway |
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
Redeemer
Valid from
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
- accepted
- 1.unlock with any redeemer · block 4,479,142 ↗
- deadline372 B
- accepted
- refused
- 1.valid from slot 117,652,232 · block 4,479,151 ↗
- 2.valid from slot 103,247,000
- escrow2,478 B
- accepted
- accepted
- refused
- 1.seller claims (r=1) · block 4,479,185 ↗
- 2.buyer refunds (r=0) · block 4,479,188 ↗
- 3.wrong signer claims
- guarded-deadline407 B
- accepted
- refused
- refused
- 1.redeemer=42 + after deadline · block 4,479,161 ↗
- 2.redeemer=99 + after deadline
- 3.redeemer=42 + before deadline
- hash-lock223 B
- accepted
- refused
- 1.Correct preimage · block 4,479,167 ↗
- 2.Wrong preimage
- hash-verify307 B
- accepted
- refused
- 1.Correct preimage · block 4,479,177 ↗
- 2.Wrong preimage
- multisig518 B
- accepted
- refused
- 1.2 of 3 signers · block 4,479,197 ↗
- 2.1 of 3 signers
- one-shot-nft1,279 B
- accepted
- refused
- 1.Mint with seed UTxO · block 4,479,250 ↗
- 2.Mint again (no seed)
- oracle1,217 B
- accepted
- refused
- 1.operator signs · block 4,479,203 ↗
- 2.non-operator signs
- redeemer-match192 B
- accepted
- refused
- 1.Unlock with redeemer 42 · block 4,479,181 ↗
- 2.Unlock with redeemer 99
- token-gate944 B
- accepted
- 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
- accepted
- accepted
- refused
- 1.admin withdraw (r=0) · block 4,479,222 ↗
- 2.deposit (r=1) · block 4,479,224 ↗
- 3.non-admin withdraw
- vesting1,468 B
- accepted
- refused
- 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.
module GuardedDeadline (guardedDeadline) where
import HaskLedger
guardedDeadline :: Validator
guardedDeadline = validator "guarded-deadline" $ do
requireAll
[ ("correct redeemer", asInt theRedeemer .== 42)
, ("past deadline", txValidRange `after` 1769904000000)
]- Note 1:
requireAllruns 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. - Note 2:
asIntunwraps the redeemer’s integer and.==compares integers. Literals such as42work becauseContract Exprhas aNuminstance. - Note 3:
afterreads the lower bound of the validity range, needs it to be finite, and handles open and closed bounds. Times are POSIX milliseconds.
What one import gives you
import HaskLedger
- 01The script context ↗
theRedeemertheDatumtxValidRangetxOutputstxSignatoriestxMintAll sixteen Plutus V3 TxInfo fields, in order. - 02Time ↗
afterbeforeOpen and closed bounds, POSIX milliseconds. - 03Signatures ↗
signedBysignedByAtverifyEd25519verifyEcdsaverifySchnorrRequired signers, and signature checks over arbitrary messages. - 04Payments ↗
paysTopaysAtLeasttotalLovelaceTovaluePreservedinlineDatumEqualsChecks on who gets paid what, in lovelace. - 05Values and minting ↗
valueOflovelaceOfownCurrencySymbolmintedAmountownMintTokenCountToken quantities, and what this policy mints or burns. - 06Branching ↗
caseMaybecaseListcaseDataunpairOnly the taken branch runs. - 07Lists ↗
anyListallListcountListfindListfoldListmapListRight folds over Data; each walks the whole list. - 08Hashing and curves ↗
sha2_256blake2b_256keccak_256bls12_381_G1_addbls12_381_millerLoopThe Plutus hashing builtins and BLS12-381.
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.
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.
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.
c2uplc generates UPLC
c2uplc, also from MLabs, turns the graph into Untyped Plutus Lambda Calculus, the language the Cardano ledger runs.
HaskLedger packages it
Every binding gets a unique name, names become de Bruijn indices, and the term is serialised into a
PlutusScriptV3envelope thatcardano-clireads.
- 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.
requiredelays its failure branch, and thecasefunctions 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×
| Contract | Script bytes · HaskLedger / PlutusTx | CPU steps · HaskLedger / PlutusTx | Memory units · HaskLedger / PlutusTx |
|---|---|---|---|
| always-succeeds | 161 / 2,53315.7× | 976,100 / 25,561,49826.2× | 6,200 / 101,57516.4× |
| redeemer-match | 192 / 2,54513.3× | 1,984,619 / 25,794,57513.0× | 9,662 / 102,60810.6× |
| deadline | 372 / 3,2588.8× | 10,710,190 / 30,778,0582.9× | 32,383 / 132,9414.1× |
| guarded-deadline | 407 / 3,2698.0× | 11,860,171 / 31,118,9722.6× | 36,349 / 134,3753.7× |
| hash-lock | 223 / 2,56111.5× | 3,833,672 / 26,528,9326.9× | 14,082 / 105,0777.5× |
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.
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
- 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.
- Note 2
No short-circuit on booleans.
.&&and.||evaluate both sides. Use thecasefunctions when only one branch should run. - Note 3
Payout guards count lovelace only. Native-token amounts are not part of
paysAtLeastorvaluePreserved. - Note 4
Test helpers live in the repository. They are part of the test suite, not the library.
- Note 5
No CIP-57 blueprints. HaskLedger writes
.plutusenvelopes, notplutus.json. - Note 6
Parameters mean a recompile. Compile-time arguments are applied in Haskell, so each parameter set needs its own compile.
- Note 7
Two script purposes. Spending validators and minting policies. Staking and governance purposes are not exposed yet.
Revisions planned
| A | Datum-parametric versions of the remaining fixed-configuration examples |
| B | Native-token payout floors and richer value checks |
| C | Reporting which check failed, without reading UPLC |
| D | The test helpers as a library module |
| E | CI for contributors |
| F | PlutusTx baselines for escrow, vesting, multisig and treasury |
| G | Validation 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.
$git clone https://github.com/KonmaORG/HaskLedger.git$cd HaskLedger$nix develop$cabal build all$cabal run haskledger-examples
- Getting started ↗Install, build and compile your first contract.
- User guide ↗Datums, redeemers, time, signatures, payments, lists, minting.
- API reference ↗Every exported function, grouped by task.
- Example contracts ↗All thirteen, with the datum and redeemer each expects.
- Security ↗The attacks to guard against, and the guard for each.
- Testing ↗Off-chain checks, and running on the Preview testnet.
HL-1 · Test bench