|
| 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