Skip to content

Test Code

Foo

bar
`OP_PUSHNUM_5`

`OP_PUSHBYTES_6`

// Public Keys, Descriptors & Miniscript

$alice = xpub661MyMwAqRbcFjVEmr9dDxeGKJznf41v5bEd83wMwu7CJ6PFeqJk3cSECPTh6wzsh32xceVsPvBgJ1q3Cqqie2dvH9nMFdL5865WrtRNhiB;
$bob = xpub661MyMwAqRbcFG1mzmcbw7oZss2Fn9y3d27D1KVjyKQdYGqNsZ8nSvLSexZAtkCNwvhFrAkTWAixvN9wjmnLNR22EsQczTiKccAJoLYW8CK;
$charlie = xpub661MyMwAqRbcEZC9Va3dMVh3uf9Lrob4K47xhfiAyUrDZSpNWi17UiP9TSPYeCVBHTHqaGhqUpSoTtaWqp1LYB4fKyKCHieonMDxngohm8q;

$wpkh = wpkh($alice/5/0/*);
$wsh = wsh(pk($bob/2/*) && pk($alice/2/*));
$tr = tr($alice, [ pk($bob), pk($charlie) && older(30 days) ]);

$htlc_redeem = pk($alice) && sha256(907bde3816465e678dd2d661bf3d84f933e71c5e2ea25543247df7a5858dfa55);
$htlc_refund = pk($bob) && older(2 days);
$htlc_tr = tr(likely@$htlc_redeem || $htlc_refund); // NUMS as the internal key

$3of3_into_2of3 = tr(3 of [ $alice, $bob, $charlie, older(6 months) ]);

// Address generation

$alice_addr = address(wpkh($alice/10));
$htlc_addr = address($htlc_tr, signet);

// Secret keys & Signing

$alice_sk = xprv9s21ZrQH143K3FQmfpccrphXmHAJFbJ4iNK2KfXkPZaDRJ477HzVVp7kM7RV3ihdLh4Wy163wJahwXcdcrpu4R6xSu6CUvKYwftQYCbowYM;
$bob_sk = xprv9s21ZrQH143K2mwJtk5bZyrqKqBmNhFCFoBcCw68QysefUWEL1pXu81xoeva2ZWpCjsJzzmYqph6vw6FjCMjg3q8obNzxYY9bCVgt9bKoHQ;
$wif_sk = 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ;

$signature = schnorr::sign($alice_sk/10, hash::sha256("Hello"));
assert(schnorr::verify($alice/10, hash::sha256("Hello"), $signature));

// Transactions

$tx = tx [
  "version": 2,
  "locktime": 2025-06-02T,
  // all fields are optional

  "inputs": [
    [ "prevout": 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b:3, "sequence": 2 months, "witness": [ 0x010203, 0x040506 ] ],
    [ "prevout": d9cd8155764c3543f10fad8a480d743137466f8d55213c8eaefcd12f06d43a80:2, "script_sig": `0x070809 0x009988` ],
    // or just the prevout's txid:vout
    b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c:0,
  ],

  "outputs": [
    // Works with addresses & descriptors
    tb1qww749rc3svsjh2rlqn7jx345zl8r0agftt3ep2: 1 BTC,
    wpkh($alice/5): 100 bits,
    tr($bob, pk($alice) && older(10)): 150000 sats,
    wpkh(xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa/0/3): 20 uBTC,

    // Works with raw scriptPubKeys
    `OP_ADD 5 OP_EQUAL`: 0.9 BTC, // non-standard

    // Though you probably should scripthash wrap them
    wsh(`OP_ADD 5 OP_EQUAL`): 3 mBTC,
    tr(`OP_ADD 5 OP_EQUAL`): 1500000 sats,

    // Long form
    [ "script_pubkey": tr($bob), "amount": 10 uBTC ],
  ]
];

$tx2 = tx [
  "input": b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c:0,
  "output": tr($bob/5):0.764 BTC
];

$tx2_sighash = tx::sighash($tx2, 0, [ wpkh($alice/9):0.7641 BTC ], SIGHASH_ALL);
$tx2_sig = ecdsa::sign($alice_sk/9, $tx2_sighash) + SIGHASH_ALL;


// PSBT & Transaction Signing

$multisig = 2 of [ $alice/5/*, $bob/5/*, $charlie/5/* ];
$multisig_addr_0 = address(wsh($multisig)/0);

$psbt = psbt [
  "input": [
    "prevout": b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c:0,
    "utxo": wsh($multisig/0):0.5 BTC,
  ],
  "outputs": [
    wpkh($alice/5): 0.1 BTC,
    wsh($multisig/1): 0.399 BTC, // change back to multisig
  ]
];
$psbt_signed = psbt::sign($psbt, [ $alice_sk, $bob_sk ]);
$psbt_tx = psbt::finalize($psbt_signed);

assert::eq(psbt::fee($psbt), 0.001 BTC);


// Script

// Interpolates space-separated expressions enclosed in backticks,
// can optionally wrap data pushes with <>
$script = `<2> $alice $bob $charlie <3> OP_CHECKMULTISIG`;

// Simple opcode composition
OP::2MUL = `OP_DUP OP_ADD`, OP::3MUL = `OP_DUP*2 OP_ADD*2`, OP::4MUL = OP::2MUL*2;
OP::MINIMAL_BOOL = `OP_DUP OP_SIZE OP_EQUALVERIFY`;

// Functions
fn taggedHash($tag) = `$tag OP_SHA256 OP_DUP OP_CAT OP_SWAP OP_CAT OP_SHA256`;
$s_hash = `0x1122334455 taggedHash("TapLeaf") OP_TOALTSTACK`;

// OP_PICK from the altstack (for static $n)
fn pickAlt($n) = `
  OP_FROMALTSTACK*$n
  OP_DUP OP_TOALTSTACK
  {`OP_SWAP OP_TOALTSTACK`*($n-1)}
`;
$s_pick = `"hello" pickAlt(3) OP_CAT`;

// Sum inputs using the Elements introspection opcodes and loop unrolling
fn sumInputs($max_inputs) = `
  le64(0) // 64-bit accumulator for the total input sum
  OP_INSPECTNUMINPUTS // number of inputs, as the for counter
  unrollFor($max_inputs, ` // errors if there are more inputs
    OP_DUP OP_1SUB // get input index (counter-1)
    OP::INPUT_VALUE // get input value
    OP_ROT // bring sum accumulator to top
    OP::ADD64_VERIFY // add current input value to sum
    OP_SWAP // bring current index back to top
  `) // (should check asset id too -- skipped here for brevity)
`;

// Ensure the total inputs sum is at least 2 BTC (for up to 3 inputs)
$minimum_2btc = `sumInputs(3) le64(2 BTC) OP_GREATERTHANOREQUAL64`;

// More script built-ins at https://min.sc/v0.3/#github=src/stdlib/btc.minsc


// Example keys for Simple CTV Vault
$cold_pk = xpub661MyMwAqRbcGEt6aoj3Uf25rMPiRt5Dn7HWgbMD4Fi4moiDMr37K2A7SDoLrmZnoWGPQFyNLDLFxfPh4Yv8Z7ms639K4he2za1FLwam2ia/*;
$hot_pk = xpub661MyMwAqRbcFpRjuYQNK1qKe6eabuHZ9Cpc688A76K2iLbMaMkSKEi3diB1xpBh1fG49ryaTeHBW7KMywKvfYvX1TMhgcVZMdRRAd3AAft/*;
$carol = xpub661MyMwAqRbcFosd2VTmeRe98CyniymZCwefedhVFy1QPmsfkAeZJMDhw6h3zjLC2ofmAZQNDi8tawziRj8rueZtmUcuATWdea8iN41H1Bt/*;
$dave = xpub661MyMwAqRbcGj7qwaMVAr8vwSp4Mo76Q9eju4poV79TG4pHuJptvZhrXw86pbmeFmwA82pKsBTSjf9xCoH8ZYskChPbG2qQqxrThjaqpb5/*;

//
// Simple CTV Vault
//
// The cold key can sign and spend unconditionally using the internal key path.
// The $hot_policy (can be a Policy or a simple PubKey) can initiate a 2-stage withdrawal process with
// an enforced contest delay period of $hot_delay, during which the $cold_pk may claim the funds back.

fn SimpleVault($cold_pk, $hot_policy, $hot_delay, $fee) = fn ($child_num, $txo_amount) =
  tr($cold_pk/$child_num, vault_hot_path($cold_pk/$child_num, $hot_policy/$child_num, $hot_delay, $txo_amount, $fee));

fn vault_hot_path($cold_pk, $hot_policy, $hot_delay, $txo_amount, $fee) =
  `tapscript($hot_policy) ctv::hash[
    "output": tr($cold_pk, $hot_policy && older($hot_delay)): $txo_amount - $fee - DUST_AMOUNT,
    "output": tr($cold_pk, $hot_policy): DUST_AMOUNT, // anchor output for fee bumping
  ] OP_CHECKTEMPLATEVERIFY OP_DROP`;
  // (the Miniscript Policy has to be compiled into raw Script due to the use of CTV)

// Vault setup using simple keys, hot key has to wait 2 weeks
$vault = SimpleVault($cold_pk, $hot_pk, 2 weeks, 500 sats);
$vault_0_addr = address($vault(0, 0.25 BTC));

// A vault where the hot key is a 2-of-2 between the user and a cosigner
$hot2f2_vault = SimpleVault($cold_pk, $hot_pk && $carol, 2 weeks, 500 sats);
$hot2f2_3_tap = $hot2f2_vault(3, 6 BTC);

// Vault with multiple secondary key policies, each with its own delay configuration
fn MultiKeyVault($primary_pk, $fee, $secondary_pks) = fn ($child_num, $txo_amount) =
  tr($primary_pk/$child_num, map($secondary_pks, |$pk_tuple|
    vault_hot_path($primary_pk/$child_num, $pk_tuple.0/$child_num, $pk_tuple.1, $txo_amount, $fee)
  ));

// A vault that provides a recovery mechanism through trusted friends in addition to hot/cold key security
$multi_vault = MultiKeyVault($cold_pk, 500 sats, [
  $hot_pk: 1 week, // the user's own hot key can withdraw with a 1 week delay
  $carol: 1 year, // their friend Carol can help recover with a 1 year delay
  $dave: 6 months, // their best friend Dave can recover sooner
  ($carol && $dave): 2 months, // together they can expedite
]);

$multi_2_tap = $multi_vault(2, 0.9 BTC);
$multi_2_addr = address($multi_2_tap);

// See https://min.sc/v0.3/#github=examples/ctv-vault.minsc for a (WIP) example that includes spending from the vault


//
// CTV Chicken HODL game
//
// Bob and Alice each deposit $amount BTC (+some for fees) into the covenant. If Alice and Bob both HODL
// for $hodl_time, they get their bitcoins back. If they chicken out before that they can still redeem,
// but have to forfeit $penalty BTC to the other party. (based on https://judica.org/blog/hodl-chicken/)

fn HodlChicken($alice_pk, $bob_pk, $hodl_time, $amount, $penalty) {
  // Early redeem tx signed by $loser_pk, $winner_pk wins $penalty
  fn earlyRedeem($loser_pk, $winner_pk) = tx [
    "outputs": [
      tr($loser_pk): $amount - $penalty,
      tr($winner_pk): $amount + $penalty,
    ]
  ];
  // $hodl_time passed, Bob & Alice get their bitcoins back
  $hodlRedeem = tx [
    "input": [ "sequence": $hodl_time ],
    "outputs": [
      tr($alice_pk): $amount,
      tr($bob_pk): $amount,
    ]
  ];

  // Create a Taproot tree with a leaf script for each possible outcome (uses NUMS as the internal key to eliminate key-path spends)
  tr [
    // Alice chickens out
    `$alice_pk OP_CHECKSIG ctv(earlyRedeem($alice_pk, $bob_pk))`,

    // Bob chickens out
    `$bob_pk OP_CHECKSIG ctv(earlyRedeem($bob_pk, $alice_pk))`,

    // Both HODL all the way through & get their bitcoins back (either can sign)
    `$alice_pk OP_CHECKSIG ctv($hodlRedeem)`,
    `$bob_pk OP_CHECKSIG ctv($hodlRedeem)`,

    // Allow other outcomes by mutual agreement (could be an aggregated musig internal key)
    `$alice_pk OP_CHECKSIGVERIFY $bob_pk OP_CHECKSIG`,
  ]
}

$hodl_chicken = HodlChicken($alice/5, $charlie/5, 1 year, 1 BTC, 0.5 BTC);