Write in the language you know. Deploy to the chain that scales.
If you've ever looked at raw Bitcoin Script and thought "nobody in their right mind would write this by hand," you're not wrong. Bitcoin Script is a stack-based, Forth-like language with no variables, no functions, and no loops. It was designed to be verified by miners, not written by developers.
And yet — Bitcoin Script is one of the most powerful smart contract execution environments in existence. Every node on the Bitcoin SV network can verify your contract. No virtual machine. No gas fees. No separate "smart contract layer." Just raw, deterministic script evaluation baked into every transaction.
The problem has always been the developer experience. Until now.
Strip away the marketing and a smart contract is just a spending condition attached to money.
In Bitcoin, when someone sends you coins, they don't actually send them to you. They create a transaction output with a locking script — a small program that defines the rules for spending those coins. To spend them, you provide an unlocking script that satisfies those rules.
The simplest example is Pay-to-Public-Key-Hash (P2PKH), the standard Bitcoin payment:
Locking script: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Unlocking script: <signature> <publicKey>
Translation: "Only someone who can prove they own the private key corresponding to this public key hash can spend these coins."
That's a smart contract. A trivial one, but a smart contract nonetheless.
Now imagine the locking script encodes something more complex: "These 2,500 satoshis can only be spent if an oracle cryptographically attests that Player 1 won the blackjack hand, AND the spending transaction sends exactly 2,000 sats to the player's address and 500 sats back to the house."
Same mechanism. Same verification by every node. Just a bigger script with more conditions. That's what Rúnar lets you build.
Rúnar is a compiler that takes smart contracts written in TypeScript, Go, Rust, Solidity-like syntax, or Move-style syntax and compiles them all down to identical Bitcoin Script.
Here's the same P2PKH contract in three formats:
TypeScript:
class P2PKH extends SmartContract {
readonly pubKeyHash: Addr;
constructor(pubKeyHash: Addr) {
super(pubKeyHash);
this.pubKeyHash = pubKeyHash;
}
public unlock(sig: Sig, pubKey: PubKey) {
assert(hash160(pubKey) === this.pubKeyHash);
assert(checkSig(sig, pubKey));
}
}Solidity-like:
pragma runar ^0.1.0;
contract P2PKH is SmartContract {
Addr immutable pubKeyHash;
constructor(Addr _pubKeyHash) {
pubKeyHash = _pubKeyHash;
}
function unlock(Sig sig, PubKey pubKey) public {
require(hash160(pubKey) == pubKeyHash);
require(checkSig(sig, pubKey));
}
}Go:
type P2PKH struct {
runar.SmartContract
PubKeyHash runar.Addr `runar:"readonly"`
}
func (c *P2PKH) Unlock(sig runar.Sig, pubKey runar.PubKey) {
runar.Assert(runar.Hash160(pubKey) == c.PubKeyHash)
runar.Assert(runar.CheckSig(sig, pubKey))
}All three compile to the exact same Bitcoin Script. Byte for byte.
The compiler pipeline is a six-pass nanopass architecture:
- Parse — Source code becomes a Rúnar AST. The parser auto-dispatches based on file extension (
.runar.ts,.runar.sol,.runar.move,.runar.go,.runar.rs). - Validate — Enforces the language subset: no loops, no arbitrary function calls, single contract per file.
- Type-check — Verifies type consistency and rejects calls to non-Rúnar functions.
- ANF Lower — Flattens the AST into Administrative Normal Form, where every intermediate computation gets an explicit name.
- Stack Lower — Translates named variables into stack operations (
OP_PICK,OP_ROLL,OP_SWAP), scheduling which values live where on Bitcoin's operand stack. - Emit — Produces the final hex-encoded Bitcoin Script with method dispatch, constructor parameter slots, and source maps.
Three independent compiler implementations (TypeScript, Go, Rust) all produce byte-identical output. A conformance test suite with golden files ensures they stay in sync.
Standard Bitcoin transactions use ECDSA signatures — a sender proves they own the private key, and the script verifies with OP_CHECKSIG. But what if you need to verify data from an external source? What if the spending condition depends on something that happened off-chain — like the outcome of a game?
Enter Rabin signatures.
A Rabin signature scheme works like this:
signature² mod publicKey == SHA-256(message || padding)
The beauty is that verification is just multiplication and modular arithmetic — operations that Bitcoin Script handles natively with OP_MUL and OP_MOD. No new opcodes needed. No protocol changes. It works on Bitcoin SV today.
This gives us oracles: trusted third parties that sign attestations about real-world events. The oracle never touches the funds. It simply signs a message like "outcome: player wins" with its Rabin private key. The on-chain contract independently verifies that signature against the oracle's known public key.
In Rúnar, using an oracle is as natural as calling a function:
public settle(outcomeType: bigint, rabinSig: RabinSig,
padding: ByteString, playerSig: Sig) {
// Verify the oracle attests to this outcome
const msg = num2bin(outcomeType, 8n);
assert(verifyRabinSig(msg, rabinSig, padding, this.oraclePubKey));
// Verify it's a win
assert(outcomeType == 1n);
// Player authorises the spend
assert(checkSig(playerSig, this.playerPubKey));
}The compiler turns verifyRabinSig into roughly 10 KB of Bitcoin Script arithmetic. It's large, but it's verified by every node — no trust required beyond the oracle's attestation.
Here's where smart contracts on Bitcoin diverge fundamentally from Ethereum's account model.
On Ethereum, a smart contract is a persistent object with an address, storage, and methods. On Bitcoin, a smart contract is a locking script attached to a UTXO — an Unspent Transaction Output. The contract is the UTXO. When you spend it, the UTXO is consumed and new ones are created.
This means a single contract can have multiple unlocking paths. Rúnar compiles each public method into a separate branch of the locking script, using OP_IF/OP_ELSE/OP_ENDIF chains. The spending transaction pushes a method index onto the stack to select which path to execute.
Consider a blackjack bet contract with four possible outcomes:
class BlackjackBet extends StatefulSmartContract {
readonly playerPubKey: PubKey;
readonly housePubKey: PubKey;
readonly oraclePubKey: RabinPubKey;
readonly betAmount: bigint;
// ...
public settleBlackjack(outcomeType: bigint, rabinSig: RabinSig,
padding: ByteString, playerSig: Sig) {
// Natural 21: player takes everything (2.5x bet)
}
public settleWin(outcomeType: bigint, rabinSig: RabinSig,
padding: ByteString, playerSig: Sig) {
// Regular win: player gets 2x, house gets remainder
}
public settleLoss(outcomeType: bigint, rabinSig: RabinSig,
padding: ByteString, houseSig: Sig) {
// Loss: house takes everything
}
public cancel(playerSig: Sig, houseSig: Sig) {
// Push/tie: cooperative refund, each gets their stake back
}
}One UTXO. Four possible futures. Each path enforces exact output amounts — the contract mathematically guarantees that funds go to the right place. No trust. No escrow service. Just script.
For stateful contracts, Rúnar automatically injects preimage verification. The compiler uses the BIP-143 sighash preimage to extract the hash of the spending transaction's outputs, then verifies that the outputs match what the contract logic dictates. This is how the contract enforces "send 2,000 sats to Alice and 500 sats to Bob" — it literally checks the SHA-256 of the serialised outputs against what the spending transaction actually contains.
Theory is nice. Let's look at a real game.
Script 21 is a full end-to-end blackjack application built with Rúnar. It's not a simulation or a demo — it deploys real smart contracts to the Bitcoin SV blockchain and settles real satoshis based on game outcomes.
1. Deck Commitment
Before any cards are dealt, the house shuffles a standard 52-card deck and publishes SHA-256(salt || deckOrder) to the blockchain via an OP_RETURN output. This hash is immutable — it's on-chain before anyone sees a card.
2. Contract Deployment
For each player at the table, a BlackjackBet contract is compiled and deployed. The player contributes 1,000 sats (the bet), the house contributes 1,500 sats (to cover a potential 3:2 blackjack payout). Total locked in the contract: 2,500 sats.
The contract's constructor parameters include both public keys, the oracle's Rabin public key, and a unique threshold value (roundNumber * 1000 + playerIndex) that prevents replay attacks across rounds.
3. Gameplay
Cards are dealt from the committed deck order. Players hit or stand. The dealer follows standard rules (draw to 17). All of this happens client-side — no blockchain interaction needed during play.
4. Settlement
Once outcomes are determined, the oracle signs each result with a Rabin signature. The appropriate settlement method is called on each contract:
- Blackjack?
settleBlackjack— player gets all 2,500 sats - Win?
settleWin— player gets 2,000, house gets 500 - Loss?
settleLoss— house gets all 2,500 - Push?
cancel— player gets 1,000 back, house gets 1,500 back
Each settlement is a Bitcoin transaction that spends the contract UTXO. The locking script verifies the oracle's Rabin signature, checks the outcome type, verifies the appropriate party's ECDSA signature, and enforces exact output amounts.
5. Audit
After all settlements complete, the house publishes the full deck order and salt on-chain via OP_RETURN. Anyone can:
- Fetch the pre-deal commitment hash from the blockchain
- Reconstruct
SHA-256(salt || deckOrder)from the audit data - Verify the hashes match — proving the deck wasn't changed mid-game
- Check that each dealt card matches its position in the committed order
- Verify all settlement transactions on-chain
Every step is independently verifiable. No trust in the house, the oracle, or the platform. Just math and Bitcoin.
The backend is a Go HTTP server that:
- Compiles
BlackjackBet.runar.tsthrough the Go Rúnar compiler - Manages wallets and UTXOs via Bitcoin RPC
- Generates Rabin signatures for oracle attestations
- Builds and broadcasts deployment and settlement transactions
The frontend is a lightweight Alpine.js application with a real-time transaction log, interactive card animations, and a full audit verification modal.
Script 21 is live at script21.masa.gi.
Sit down at a table, place a bet, and play a few hands. Watch the transaction log as contracts deploy and settle in real time. Click "Audit Round" after the dealer plays to walk through the full cryptographic verification — from deck commitment to settlement proofs.
Every hand you play creates real Bitcoin transactions with real smart contracts. The locking scripts enforce the rules. The Rabin signatures prove the outcomes. The deck commitment proves fairness. And it all compiles from a TypeScript file that reads like a normal class.
That's the promise of Rúnar: write smart contracts in the language you already know, and let the compiler handle the Bitcoin Script.
Rúnar is open source. The blackjack contract, the compiler, and the full webapp are available on GitHub. If you can write a class with some assert statements, you can write a Bitcoin smart contract.