|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +# Fund and spend an example "pay to multisig" SimplicityHL contract |
| 4 | +# on Liquid Testnet. |
| 5 | + |
| 6 | +# Dependencies: simc hal-simplicity jq curl |
| 7 | + |
| 8 | +pause() { echo -n "Press Enter to continue. " ; read -r; echo; echo; } |
| 9 | +# pause() { echo; echo; } |
| 10 | + |
| 11 | +# This demo has been updated to not require the use of elements-cli. |
| 12 | +# Some tasks could be simpler or more scalable in some sense if we used |
| 13 | +# a local elements-cli, but here we use the Liquid Testnet web API |
| 14 | +# instead to remove local dependencies. The main reason for this is that |
| 15 | +# elementsd will require multiple gigabytes of blockchain data. Having |
| 16 | +# a working local copy of elementsd can be useful for other development |
| 17 | +# tasks, but represents a larger time and disk space commitment. |
| 18 | + |
| 19 | +PROGRAM_SOURCE=~/src/SimplicityHL/examples/p2ms.simf |
| 20 | +WITNESS_FILE=~/src/SimplicityHL/examples/p2ms.wit |
| 21 | + |
| 22 | +# This is an unspendable public key address derived from BIP 0341. It is |
| 23 | +# semi-hardcoded in some Simplicity tools. You can change it in order |
| 24 | +# to make existing contract source code have a different address on the |
| 25 | +# blockchain but you must use a NUMS method to make sure that the key |
| 26 | +# is unspendable. BIP 0341 gives a method to modify the number below to |
| 27 | +# retain that property. |
| 28 | +# |
| 29 | +# If you are sending assets to a contract created by someone else, you |
| 30 | +# must make sure that the internal key used for that instance of the |
| 31 | +# contract is an unspendable value (if it is changed from this default). |
| 32 | +# Otherwise, the contract creator may be able to unilaterally steal |
| 33 | +# value from the contract. |
| 34 | +INTERNAL_KEY="50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" |
| 35 | +TMPDIR=$(mktemp -d) |
| 36 | + |
| 37 | + |
| 38 | +# Private keys of parties whose signatures can approve this transaction. |
| 39 | +PRIVKEY_1="0000000000000000000000000000000000000000000000000000000000000001" |
| 40 | +PRIVKEY_2="0000000000000000000000000000000000000000000000000000000000000002" |
| 41 | +PRIVKEY_3="0000000000000000000000000000000000000000000000000000000000000003" |
| 42 | + |
| 43 | + |
| 44 | +# Hardcoded address of the Liquid testnet for returning tLBTC |
| 45 | +# (so that they aren't wasted!) |
| 46 | +# We could also send these to our own wallet, but here we are choosing |
| 47 | +# to send them back. |
| 48 | +# Confidential address |
| 49 | +FAUCET_ADDRESS=tlq1qq2g07nju42l0nlx0erqa3wsel2l8prnq96rlnhml262mcj7pe8w6ndvvyg237japt83z24m8gu4v3yfhaqvrqxydadc9scsmw |
| 50 | +# FAUCET_ADDRESS=$("$ELEMENTS_CLI" validateaddress "$FAUCET_ADDRESS" | jq -r .unconfidential) |
| 51 | +# Unconfidential address |
| 52 | +FAUCET_ADDRESS=tex1qkkxzy9glfws4nc392an5w2kgjym7sxpshuwkjy |
| 53 | +# The ability to derive the unconfidential address without depending on elements-cli |
| 54 | +# will soon be added to "hal-simplicity address inspect". |
| 55 | + |
| 56 | +for variable in PROGRAM_SOURCE WITNESS_FILE INTERNAL_KEY PRIVKEY_1 PRIVKEY_2 PRIVKEY_3 FAUCET_ADDRESS |
| 57 | +do |
| 58 | +echo -n "$variable=" |
| 59 | +eval echo \$$variable |
| 60 | +done |
| 61 | + |
| 62 | +# This check is used for two different purposes. First, we need to be |
| 63 | +# able to actually download the transaction data in order to extract |
| 64 | +# some details. Second, we need the node that we will ultimately submit |
| 65 | +# our transaction to to have a copy of the input transaction (at least |
| 66 | +# in its mempool; not necessarily confirmed!). There are propagation |
| 67 | +# delays in the Liquid Network, so we cannot assume all nodes have heard |
| 68 | +# about all newly-submitted transactions within a matter of only 1-2 |
| 69 | +# seconds. |
| 70 | +# |
| 71 | +# An alternative using elements-cli would use the subcommand gettxout, |
| 72 | +# again polling to ensure that the local node has received a copy of the |
| 73 | +# relevant transaction! |
| 74 | + |
| 75 | +propagation_check(){ |
| 76 | + # TODO: Give a useful error if this times out. |
| 77 | + echo -n "Checking for transaction $FAUCET_TRANSACTION via Liquid API..." |
| 78 | + for _ in {1..60}; do |
| 79 | + if curl -sSL "$1""$FAUCET_TRANSACTION" | jq ".vout[0]" 2>/dev/null | tee "$TMPDIR"/faucet-tx-data.json | jq -e >/dev/null 2>&1 |
| 80 | + then |
| 81 | + echo " found." |
| 82 | + break |
| 83 | + else |
| 84 | + echo -n "." |
| 85 | + fi |
| 86 | + sleep 1 |
| 87 | + done |
| 88 | +} |
| 89 | + |
| 90 | +pause |
| 91 | + |
| 92 | +# Compile program |
| 93 | +echo simc "$PROGRAM_SOURCE" |
| 94 | +simc "$PROGRAM_SOURCE" |
| 95 | + |
| 96 | +pause |
| 97 | + |
| 98 | +# Extract the compiled program from the output of that command |
| 99 | +COMPILED_PROGRAM=$(simc "$PROGRAM_SOURCE" | tail -1) |
| 100 | + |
| 101 | +echo hal-simplicity simplicity info "$COMPILED_PROGRAM" |
| 102 | +hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq |
| 103 | +# You can get the human-readable display of the low-level combinators with |
| 104 | +# hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq -r .commit_decode |
| 105 | +# but this isn't used anywhere in the transaction. |
| 106 | +CMR=$(hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq -r .cmr) |
| 107 | +CONTRACT_ADDRESS=$(hal-simplicity simplicity info "$COMPILED_PROGRAM" | jq -r .liquid_testnet_address_unconf) |
| 108 | +echo |
| 109 | + |
| 110 | +for variable in CMR CONTRACT_ADDRESS |
| 111 | +do |
| 112 | +echo -n "$variable=" |
| 113 | +eval echo \$$variable |
| 114 | +done |
| 115 | + |
| 116 | +pause |
| 117 | + |
| 118 | +# Here we use a curl command to contact the Liquid Testnet faucet to |
| 119 | +# ask it to fund our contract. The HTML result is unstructured text |
| 120 | +# data, so we use a sed substitution to pull out the txid. |
| 121 | +echo Running curl to connect to Liquid Testnet faucet... |
| 122 | +FAUCET_TRANSACTION=$(curl "https://liquidtestnet.com/faucet?address=$CONTRACT_ADDRESS&action=lbtc" 2>/dev/null | sed -n "s/.*with transaction \([0-9a-f]*\)\..*$/\1/p") |
| 123 | + |
| 124 | +echo "FAUCET_TRANSACTION=$FAUCET_TRANSACTION" |
| 125 | + |
| 126 | +pause |
| 127 | + |
| 128 | +# Ask hal-simplicity to create a minimal PSET which asks to spend the |
| 129 | +# value that the faucet sent to our contract, by sending it back to |
| 130 | +# FAUCET_ADDRESS (the address the contract is asked to send this value |
| 131 | +# to) -- less a fee. |
| 132 | +# |
| 133 | +# Note that this hard-coded fee is higher than required. The concept of |
| 134 | +# weight can be used to calculate the minimum appropriate fee, but we'll |
| 135 | +# need other tools to determine it. |
| 136 | +echo hal-simplicity simplicity pset create '[ { "txid": "'"$FAUCET_TRANSACTION"'", "vout": 0 } ]' '[ { "'"$FAUCET_ADDRESS"'": 0.00099900 }, { "fee": 0.00000100 } ]' |
| 137 | +PSET=$(hal-simplicity simplicity pset create '[ { "txid": "'"$FAUCET_TRANSACTION"'", "vout": 0 } ]' '[ { "'"$FAUCET_ADDRESS"'": 0.00099900 }, { "fee": 0.00000100 } ]' | jq -r .pset) |
| 138 | + |
| 139 | +echo "Minimal PSET is $PSET" |
| 140 | + |
| 141 | +pause |
| 142 | + |
| 143 | +# Now we will attach a lot of stuff to this PSET. |
| 144 | + |
| 145 | +# First, we want to know more details related to the incoming transaction |
| 146 | +# that funded the contract and that we are now attempting to spend. |
| 147 | +echo "Looking up faucet transaction details." |
| 148 | + |
| 149 | +# Note that the elements-cli version would also require polling in a loop |
| 150 | +# until the local elementsd has a copy of the transaction in its mempool |
| 151 | +# (this isn't guaranteed to be true instantly). The JSON details are also |
| 152 | +# slightly different from the API in this case, including measuring the |
| 153 | +# value in a different base unit. |
| 154 | +# echo $ELEMENTS_CLI gettxout $FAUCET_TRANSACTION 0 |
| 155 | +# $ELEMENTS_CLI gettxout $FAUCET_TRANSACTION 0 | tee $TMPDIR/faucet-tx-data.orig.json | jq |
| 156 | +# HEX=$(jq -r .scriptPubKey.hex < $TMPDIR/faucet-tx-data.json) |
| 157 | +# ASSET=$(jq -r .asset < $TMPDIR/faucet-tx-data.json) |
| 158 | +# VALUE=$(jq -r .value < $TMPDIR/faucet-tx-data.json) |
| 159 | + |
| 160 | +propagation_check https://liquid.network/liquidtestnet/api/tx/ |
| 161 | +cat "$TMPDIR"/faucet-tx-data.json | jq |
| 162 | + |
| 163 | +HEX=$(jq -r .scriptpubkey < "$TMPDIR"/faucet-tx-data.json) |
| 164 | +ASSET=$(jq -r .asset < "$TMPDIR"/faucet-tx-data.json) |
| 165 | +VALUE=0.00$(jq -r .value < "$TMPDIR"/faucet-tx-data.json) |
| 166 | + |
| 167 | +echo "Extracted hex:asset:value parameters $HEX:$ASSET:$VALUE" |
| 168 | + |
| 169 | +pause |
| 170 | + |
| 171 | +echo hal-simplicity simplicity pset update-input "$PSET" 0 -i "$HEX:$ASSET:$VALUE" -c "$CMR" -p "$INTERNAL_KEY" |
| 172 | +hal-simplicity simplicity pset update-input "$PSET" 0 -i "$HEX:$ASSET:$VALUE" -c "$CMR" -p "$INTERNAL_KEY" | tee "$TMPDIR"/updated.json | jq |
| 173 | + |
| 174 | +PSET=$(cat "$TMPDIR"/updated.json | jq -r .pset) |
| 175 | + |
| 176 | +pause |
| 177 | + |
| 178 | +# Now we have to generate the sighash and then, for this case, make |
| 179 | +# signatures on it corresponding to the signatures of 2 out of 3 |
| 180 | +# authorized keys. This allows us to build the witness that will be |
| 181 | +# input to the Simplicity program, convincing it to approve this |
| 182 | +# transaction. |
| 183 | +# |
| 184 | +# Note that these signatures are specific to this individual |
| 185 | +# transaction (so, calculating them requires a copy of the PSET). |
| 186 | +# In the real-world case where these signatures are actually made |
| 187 | +# by different people or devices, the PSET should be sent to them |
| 188 | +# and they should make the signatures and send their respective |
| 189 | +# numerical signature values back. |
| 190 | + |
| 191 | +# Signature 1 |
| 192 | +echo "Signing on behalf of Alice using private key $PRIVKEY_1" |
| 193 | +echo hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_1" |
| 194 | +hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_1" | jq |
| 195 | +SIGNATURE_1=$(hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_1" | jq -r .signature) |
| 196 | +echo "Alice's signature is $SIGNATURE_1 (different from JSON due to signing nonce)" |
| 197 | + |
| 198 | +pause |
| 199 | + |
| 200 | +# No signature 2 |
| 201 | +echo "Bob's signature using private key $PRIVKEY_2 is" |
| 202 | +echo "intentionally absent in this demo." |
| 203 | + |
| 204 | +pause |
| 205 | + |
| 206 | +# Signature 3 |
| 207 | +echo "Signing on behalf of Charlie using private key $PRIVKEY_3" |
| 208 | +echo hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_3" |
| 209 | +hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_3" | jq |
| 210 | +SIGNATURE_3=$(hal-simplicity simplicity sighash "$PSET" 0 "$CMR" -x "$PRIVKEY_3" | jq -r .signature) |
| 211 | +echo "Charlie's signature is $SIGNATURE_3 (different from JSON due to signing nonce)" |
| 212 | + |
| 213 | +# Put the signatures into the appropriate place in the .wit file |
| 214 | +# (these substitutions are extremely hard-coded for this specific |
| 215 | +# .wit file) |
| 216 | +echo "Copying signatures into copy of witness file $WITNESS_FILE..." |
| 217 | +cp $WITNESS_FILE "$TMPDIR"/witness.wit |
| 218 | +sed -i "s/\[Some([^)]*)/[Some(0x$SIGNATURE_1)/" "$TMPDIR"/witness.wit |
| 219 | +sed -i "s/Some([^)]*)]/Some(0x$SIGNATURE_3)]/" "$TMPDIR"/witness.wit |
| 220 | + |
| 221 | +echo "Contents of witness:" |
| 222 | +cat "$TMPDIR"/witness.wit |
| 223 | + |
| 224 | +pause |
| 225 | + |
| 226 | +# The compiled program is not itself different when compiled with the |
| 227 | +# witness, but it's usual that it would be compiled once without a |
| 228 | +# witness when sending assets to the contract, as we do above, and once |
| 229 | +# with a witness when claiming assets from the contract, as we do here. |
| 230 | +# Those would usually be done by different people on different occasions. |
| 231 | +echo "Recompiling Simplicity program with attached populated witness file..." |
| 232 | +echo simc "$PROGRAM_SOURCE" "$TMPDIR"/witness.wit |
| 233 | +simc "$PROGRAM_SOURCE" "$TMPDIR"/witness.wit | tee "$TMPDIR"/compiled-with-witness |
| 234 | + |
| 235 | +# Maybe simc should also output structured data like JSON! |
| 236 | +PROGRAM=$(cat "$TMPDIR"/compiled-with-witness | sed '1d; 3,$d') |
| 237 | +WITNESS=$(cat "$TMPDIR"/compiled-with-witness | sed '1,3d; 5,$d') |
| 238 | + |
| 239 | +pause |
| 240 | + |
| 241 | +echo hal-simplicity simplicity pset finalize "$PSET" 0 "$PROGRAM" "$WITNESS" |
| 242 | +hal-simplicity simplicity pset finalize "$PSET" 0 "$PROGRAM" "$WITNESS" | jq |
| 243 | +PSET=$(hal-simplicity simplicity pset finalize "$PSET" 0 "$PROGRAM" "$WITNESS" | jq -r .pset) |
| 244 | + |
| 245 | +pause |
| 246 | + |
| 247 | +echo hal-simplicity simplicity pset extract "$PSET" |
| 248 | +hal-simplicity simplicity pset extract "$PSET" | jq |
| 249 | +RAW_TX=$(hal-simplicity simplicity pset extract "$PSET" | jq -r) |
| 250 | + |
| 251 | +echo "Raw transaction is $RAW_TX" |
| 252 | + |
| 253 | +pause |
| 254 | + |
| 255 | +propagation_check https://blockstream.info/liquidtestnet/api/tx/ |
| 256 | + |
| 257 | +# With a sufficiently up-to-date local copy of elementsd, we could also use |
| 258 | +# TXID=$("$ELEMENTS_CLI" sendrawtransaction "$RAW_TX") |
| 259 | +# instead of using the web API. |
| 260 | +echo "Submitting raw transaction via Liquid Testnet web API..." |
| 261 | +echo -n "Resulting transaction ID is " |
| 262 | +TXID=$(curl -X POST "https://blockstream.info/liquidtestnet/api/tx" -d "$RAW_TX" 2>/dev/null) |
| 263 | +echo "$TXID" |
| 264 | +echo |
| 265 | +echo "You can view it online at https://blockstream.info/liquidtestnet/tx/$TXID?expand" |
| 266 | + |
| 267 | +echo |
0 commit comments