Skip to content

Commit 8f11c27

Browse files
committed
docs: Add PAIRCOMMIT merkle tree and one-liner examples
1 parent b7dd39f commit 8f11c27

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
Minsc is a high-level, domain-specific, embeddable language for Bitcoin scripting that simplifies the creation and fulfillment of complex spending conditions using an expressive pseudo-code-like syntax.
1212

13-
It features built-in support for Descriptors, Miniscript, Script, Transactions, PSBT, Taproot, Xpubs/Xprvs, CTV and more.
13+
It features built-in support for Descriptors, Miniscript, Script, Transactions, PSBT, Taproot, BIP32, CTV and more.
1414

1515
The language is dynamically typed, functional and immutable.
1616

@@ -28,6 +28,9 @@ The language is dynamically typed, functional and immutable.
2828
> - [Recovery after a delay period](https://min.sc/v0.3/#github=examples/recovery-after-delay.minsc) (simple CSV-based, delay period since the coins last moved)
2929
> - [Inheritance after a contest period](https://min.sc/v0.3/#github=examples/inheritance-after-contest-presigned.minsc) (2-stage using pre-signed txs, contest delay period following the 'trigger')
3030
>
31+
> #### One Liners
32+
> - [Simple one/few-liners](https://gist.github.com/shesek/fe1ca13232720d10a6ea3c9ea313cb15) for inpsecting and manipulating bitcoin data types
33+
>
3134
> #### Manual Scripting
3235
> ##### *Without* Descriptors, Miniscript or PSBT
3336
> - [Manual Signing](https://min.sc/v0.3/#github=examples/manual-signing-p2wpkh.minsc) (P2WPKH)
@@ -41,6 +44,7 @@ The language is dynamically typed, functional and immutable.
4144
> - [Payment Pool](https://min.sc/v0.3/#github=examples/payment-pool.minsc) (shared UTXO ownership with pre-signed unilateral exit)
4245
> - [Fair Coin-Flip Bet](https://min.sc/v0.3/#github=examples/script-coin-flip.minsc) (commit-reveal scheme with a security deposit)
4346
> - [Lookup Tables](https://min.sc/v0.3/#github=examples/script-lookup.minsc) (one-time & reusable tables, 4-bit OP_MUL)
47+
> - [PAIRCOMMIT Merkle Trees](https://min.sc/v0.3/#github=examples/paircommit-merkle-tree.minsc)
4448
> - More scripting examples are available in [the playground's default code](https://min.sc/v0.3/)
4549
>
4650
> #### Elements/Liquid Introspection
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
fn PairCommit($a, $b) = hash::sha256(PAIRCOMMIT_TAG + bytes(len($a)) + $a + bytes(len($b)) + $b);
2+
PAIRCOMMIT_TAG = {$h=hash::sha256("PairCommit"); $h+$h};
3+
4+
// CAT-based PAIRCOMMIT, so scripts can be tested on https://ide.scriptwiz.app/
5+
//OP_PAIRCOMMIT = `
6+
// OP_SIZE OP_SWAP OP_CAT // <a> <blen+b>
7+
// OP_SWAP OP_SIZE OP_SWAP OP_CAT // <blen+b> <alen+a>
8+
// OP_SWAP OP_CAT // <alen+a+blen+a>
9+
// PAIRCOMMIT_TAG OP_SWAP OP_CAT OP_SHA256
10+
//`;
11+
12+
// Note this is currently re-encoded as Elements' OP_PUSHCURRENTINPUTINDEX, which uses the same opcode byte
13+
OP_PAIRCOMMIT = script(0xcd);
14+
15+
// Example tx: https://mutinynet.com/tx/4833dee2a5fc6c01884510f1fa69af3bd80811b29d0ddea838919cfceb2b0bf7
16+
17+
18+
// Make a Merkle tree
19+
fn merklize($elements) {
20+
if len($elements) == 1 {
21+
$leaf_hash = hash::sha256(bytes($elements.0));
22+
[ "hash": $leaf_hash, "depth": 0, "value": $elements.0 ]
23+
} else {
24+
$left_size = len($elements) / 2;
25+
$left = merklize(slice($elements, 0, $left_size));
26+
$right = merklize(slice($elements, $left_size));
27+
$depth = max($left->depth, $right->depth)+1;
28+
$branch_hash = PairCommit($left->hash, $right->hash);
29+
[ "hash": $branch_hash, "depth": $depth, "left": $left, "right": $right ]
30+
}
31+
}
32+
33+
// Left/right encoding in proofs
34+
LEFT = 0, RIGHT = 1;
35+
36+
// Make a merkle path inclusion proof
37+
// e.g. [ [ hash0, side0 ], [ hash1, side1 ] ]
38+
fn merkleProof($root, $value) {
39+
$paths = merkleValuePaths($root); // xxx could reuse the map
40+
$path = $paths->{$value}; // an array of left/right
41+
fold($path, [ $root, [] ], |[ $node, $proof ], $side| {
42+
$other_side = if $side == "left" then "right" else "left";
43+
$this_side_int = if $side == "left" then LEFT else RIGHT;
44+
$merkle_step = [ $node->{$other_side}->hash, $this_side_int ];
45+
[ $node->{$side}, $proof+[ $merkle_step ] ]
46+
}).1
47+
}
48+
49+
// Get a flat map of each leaf value and the left/right path leading to it
50+
fn merkleValuePaths($node, $curr_path=[]) {
51+
if $node->value? {
52+
[ $node->value: $curr_path ]
53+
} else {
54+
$left_flat = merkleValuePaths($node->left, $curr_path+["left"]);
55+
$right_flat = merkleValuePaths($node->right, $curr_path+["right"]);
56+
$left_flat + $right_flat
57+
}
58+
}
59+
60+
// Verify merkle path inclusion proof
61+
fn merkleVerify($root_hash, $value, $proof) {
62+
fold(reverse($proof), hash::sha256($value), |$curr_hash, $merkle_step| {
63+
[ $step_hash, $step_side ] = $merkle_step;
64+
if $step_side == LEFT then PairCommit($curr_hash, $step_hash)
65+
else PairCommit($step_hash, $curr_hash)
66+
}) == $root_hash
67+
}
68+
69+
// Merkle proof formatted as Script witness stack elements, appended with the length
70+
// e.g. [ hash0, side0, hash1, side1, 2 ]
71+
fn merkleProofWitness($root, $value) {
72+
$proof = merkleProof($root, $value);
73+
flatten($proof) + [ len($proof) ]
74+
}
75+
76+
// Verify merkle path inclusion proof
77+
// stack in: <hash0> <side0> .. <hashN> <sideN> <total N> <value> <merkle root>
78+
fn OP::MERKLE_VERIFY($max_tree_depth) = `
79+
OP_TOALTSTACK // put <merkle root> aside
80+
OP_SHA256 // compute <value>'s leaf hash
81+
OP_SWAP // stack: .. <hashN> <sideN> <value's leaf hash> <total N>
82+
unrollFor($max_tree_depth, ` // loop <total N> times, up to $max_tree_depth
83+
// stack: .. <hashN> <sideN> <current hash> <counter N>
84+
OP_TOALTSTACK // put counter aside
85+
OP_SWAP OP_NOTIF OP_SWAP OP_ENDIF // bring <side>, order nodes based on it
86+
OP_PAIRCOMMIT // compute the branch hash
87+
OP_FROMALTSTACK // get counter back
88+
`) // stack: <final hash>
89+
OP_FROMALTSTACK OP_EQUALVERIFY // verify the path leads to the <merkle root>
90+
`;
91+
92+
//
93+
// Example use
94+
//
95+
96+
$merkle_root = merklize([100,101,102,103,104,105,106,107,108,109]);
97+
98+
$proof = merkleProof($merkle_root, 108);
99+
assert(merkleVerify($merkle_root->hash, 108, $proof));
100+
101+
// Fully executable Script, with both verification code and input data
102+
// Runnable on https://ide.scriptwiz.app/ (with the CAT-based PAIRCOMMIT)
103+
$script = `$proof len($proof) 108 $merkle_root->hash OP::MERKLE_VERIFY($merkle_root->depth)`;
104+
105+
// A locking script that verifies a merkle proof against the $merkle_root
106+
// witness: <merkle proof...> <value>
107+
$locking_script = `
108+
$merkle_root->hash OP::MERKLE_VERIFY($merkle_root->depth)
109+
OP_TRUE
110+
`;
111+
$tr = tr($locking_script);
112+
$address = address($tr);
113+
114+
// Spending tx with the merkle proof in its witness
115+
$spend_tx = tx[
116+
"input": [
117+
"prevout": 1a9f5fe129f1b6aaec9e852254b0ef4f23f95a65d7ef77f64067e68f040f2b57:1, // an output funding $address
118+
"witness": tr::script_witness($tr, $locking_script, merkleProofWitness($merkle_root, 105) + [ 105 ]),
119+
],
120+
"output": `OP_RETURN "OP_PAIRCOMMIT merkle proof \\o/"`:0,
121+
];
122+
$spend_tx_hex = hex($spend_tx);
123+
124+
125+
//
126+
// Key-value Lookup Table
127+
//
128+
129+
// Each element is kept in the tree as the hash of its key+value
130+
fn hashElement([ $key, $val ]) = PairCommit(bytes($key), bytes($val));
131+
132+
// Merkle tree of exp2() results for 4-bit numbers
133+
$exp2_values = fillArray(16, |$n| [$n, 2**$n]);
134+
$exp2_merkle = merklize(map($exp2_values, hashElement));
135+
136+
// Produce a witness proving the result of exp2($n)
137+
fn exp2::witness($n) {
138+
$exp2_result = 2**$n;
139+
$proof = merkleProofWitness($exp2_merkle, hashElement([ $n, $exp2_result ]));
140+
$proof + [ $exp2_result ]
141+
}
142+
143+
// stack in: <merkle proof...> <exp2 result> <exp2 input>
144+
// stack out: <exp2 result> (or fail)
145+
OP::EXP2 = `
146+
OP_SWAP OP_DUP OP_TOALTSTACK // keep a copy of the <exp2 result> aside
147+
OP_PAIRCOMMIT // compute the hashElement for the <exp2 input> + <exp2 result>
148+
$exp2_merkle->hash OP::MERKLE_VERIFY($exp2_merkle->depth) // verify merkle proof for hashElement
149+
OP_FROMALTSTACK // leave the <exp2 result> on stack
150+
`;
151+
152+
$script_exp2 = `exp2::witness(10) 10 OP::EXP2`; // -> 1024

0 commit comments

Comments
 (0)