├── .gitignore ├── demo ├── README.md ├── tx-result-demo.txt ├── demo.txt └── combineandfund.py ├── imgs ├── Liquidex_tx_A-B.png └── Liquidex_tx_USDT-LBTC.png ├── LICENSE ├── maker-cli.py ├── README.md └── taker-cli.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv* 2 | .idea/ 3 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Demo 2 | 3 | Pre PoC demo to ensure this work. 4 | -------------------------------------------------------------------------------- /imgs/Liquidex_tx_A-B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RCasatta/LiquiDEX/HEAD/imgs/Liquidex_tx_A-B.png -------------------------------------------------------------------------------- /imgs/Liquidex_tx_USDT-LBTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RCasatta/LiquiDEX/HEAD/imgs/Liquidex_tx_USDT-LBTC.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Riccardo Casatta, Leonardo Comandini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/tx-result-demo.txt: -------------------------------------------------------------------------------- 1 | 02000000000344e3c2cf78833ca3ba4c5bbcdbd670eaa79b706c9a0fef960accd0e2f4f600e7000000006a47304402202f4756a39fd4d7d3633f4dc081e78774a78a32baa3774f3b76263ae6f0eeceb802202754cd7a0bcc70cc5248672e1ac6052473d8812047ce75437246069cf2568935832102ef743a7afa34637e29a7b6f36322f45dd3a2eb8f0cdcd54415646d6629bad69e00000000ac81395474d6dfa237d968d79f141d61a367994da1fa9128c87db67d0d0a9cbf010000006a473044022072a22832719cf00026bc88cc542ba0a1ee374d0c4c92bef7eb8e37b22e581376022072a03b9146d0515adf7de73edd510e9488e40d89e511392c4478c3537b9b6e1e01210252ca3e708e32ec75704033730efad35047da42f4ca9f9c4e7ae5efcb4e426dadffffffffdc18c5fb9071825ce36c94e532d27828c94383a3afc5266071284f99080dfd87000000006a47304402205956094fa40d6f489ef7dfa6a29150460e7a74231c7e1ddb94e5b9e2141b34820220481ea26abe768324178d7f049e7034cdf6d0959e63ea71d6b89a6338d23c297201210252ca3e708e32ec75704033730efad35047da42f4ca9f9c4e7ae5efcb4e426dadffffffff0501eac5931e9e0c5097621537b7505d4c11fa26a42962ea5cc1615e9168c755c5d401000000000bebc200001976a914efbddc271c65897df7f31de46dc4b4fdf886b73488ac01a7060d3b396697bb4bbde00a58e6d620722969cdda779293481886a01b303c4c010000000005f5e100001976a91448bef0c5bfd4f5b9e987f65f7c89fd02ce9a4f8688ac01eac5931e9e0c5097621537b7505d4c11fa26a42962ea5cc1615e9168c755c5d401000000002faf0800001976a91448bef0c5bfd4f5b9e987f65f7c89fd02ce9a4f8688ac0125b251070e29ca19043cf33ccd7324e2ddab03ecc4ae0b5e77c4fc0e5cf6c95a01000000003b9a4d96001976a91448bef0c5bfd4f5b9e987f65f7c89fd02ce9a4f8688ac0125b251070e29ca19043cf33ccd7324e2ddab03ecc4ae0b5e77c4fc0e5cf6c95a010000000000001388000000000000 2 | -------------------------------------------------------------------------------- /demo/demo.txt: -------------------------------------------------------------------------------- 1 | PRIVATEKEY=cTr1NJdowsZQz1Rct5cxYo4ocgb1no7B7RX8SaKLPuvpN2hmpdDh # WIF 2 | SCRIPTPUBKEY=76a914efbddc271c65897df7f31de46dc4b4fdf886b73488ac 3 | ADDRESS=2dwHPL4vUj8eZTjW9tctDbFgMA7qahEsDGP 4 | 5 | AMOUNT_X=1 6 | ASSET_A=$(ec issueasset 10 0 | jq -r .asset) 7 | AMOUNT_Y=2 8 | ASSET_B=$(ec issueasset 10 0 | jq -r .asset) 9 | ec generatetoaddress 2 $(ec getnewaddress) 10 | 11 | TXID=$(ec sendtoaddress $ADDRESS $AMOUNT_X "" "" false true 1 "UNSET" $ASSET_A) 12 | VOUT=$(ec gettransaction $TXID | jq .details | jq .[0].vout) 13 | ec generatetoaddress 2 $(ec getnewaddress) 14 | 15 | TXU=$(ec createrawtransaction "[{\"txid\":\"$TXID\",\"vout\":$VOUT,\"sequence\":0}]" "[{\"$ADDRESS\":$AMOUNT_Y}]" 0 false "{\"$ADDRESS\": \"$ASSET_B\"}") 16 | 17 | TXS=$(ec signrawtransactionwithkey $TXU "[\"$PRIVATEKEY\"]" "[{\"txid\":\"$TXID\",\"vout\":$VOUT,\"scriptPubKey\":\"$SCRIPTPUBKEY\"}]" "SINGLE|ANYONECANPAY" | jq -r .hex) 18 | 19 | AMOUNT_B=10 20 | ADDRESS_TAKER=$(ec getaddressinfo $(ec getnewaddress "" "legacy") | jq -r .unconfidential) 21 | TXID_B=$(ec sendtoaddress $ADDRESS_TAKER $AMOUNT_B "" "" false true 1 "UNSET" $ASSET_B) 22 | VOUT_B=$(ec gettransaction $TXID_B | jq .details | jq .[0].vout) 23 | TXID_FEE=$(ec sendtoaddress $ADDRESS_TAKER $(ec getbalance | jq .bitcoin) "" "" true) 24 | VOUT_FEE=$(ec gettransaction $TXID_FEE | jq .details | jq .[0].vout) 25 | ec generatetoaddress 2 $(ec getnewaddress) 26 | AMOUNT_FEE=$(ec getbalance | jq .bitcoin) 27 | FEE=$(ec dumpassetlabels | jq -r .bitcoin) 28 | 29 | # from a virtualenv with wally installed 30 | TX_TAKER_U=$(python3 combineandfund.py $TXS $ADDRESS_TAKER $AMOUNT_X $ASSET_A $AMOUNT_Y $ASSET_B $FEE $TXID_B $VOUT_B $AMOUNT_B $TXID_FEE $VOUT_FEE $AMOUNT_FEE) 31 | 32 | PRIVATEKEY_TAKER=$(ec dumpprivkey $ADDRESS_TAKER) 33 | TX_TAKER_S=$(ec signrawtransactionwithkey $TX_TAKER_U "[\"$PRIVATEKEY_TAKER\"]" | jq -r .hex) 34 | ec testmempoolaccept "[\"$TX_TAKER_S\"]" 35 | -------------------------------------------------------------------------------- /demo/combineandfund.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import wallycore as wally 3 | 4 | txmaker, address_taker, x, A, y, B, FEE, txidB, voutB, amountB, txidFEE, voutFEE, amountFEE = sys.argv[1:] 5 | 6 | x = float(x) 7 | y = float(y) 8 | amountB = float(amountB) 9 | amountFEE = float(amountFEE) 10 | 11 | voutB = int(voutB) 12 | voutFEE = int(voutFEE) 13 | 14 | #decode tx 15 | tx = wally.tx_from_hex(txmaker, 3) 16 | scriptpubkey = wally.address_to_scriptpubkey(address_taker, wally.WALLY_NETWORK_LIQUID_REGTEST) 17 | 18 | def h2b_rev(h): 19 | return wally.hex_to_bytes(h)[::-1] 20 | 21 | def btc2sat(btc): 22 | return round(btc * 10**8) 23 | 24 | def add_unblinded_output(tx_, script, asset, amount): 25 | wally.tx_add_elements_raw_output( 26 | tx_, 27 | script, 28 | b'\x01' + h2b_rev(asset), 29 | wally.tx_confidential_value_from_satoshi(btc2sat(amount)), 30 | None, # nonce 31 | None, # surjection proof 32 | None, # range proof 33 | 0) 34 | 35 | def add_unsigned_input(tx_, txid, vout): 36 | wally.tx_add_elements_raw_input( 37 | tx_, 38 | h2b_rev(txid), 39 | vout, 40 | 0xffffffff, 41 | None, # scriptSig 42 | None, # witness 43 | None, # nonce 44 | None, # entropy 45 | None, # issuance amount 46 | None, # inflation keys 47 | None, # issuance amount rangeproof 48 | None, # inflation keys rangeproof 49 | None, # pegin witness 50 | 0) 51 | 52 | fixed_fee = 0.00005000 53 | 54 | #add output A 55 | add_unblinded_output(tx, scriptpubkey, A, x) 56 | #add output change B 57 | add_unblinded_output(tx, scriptpubkey, B, amountB - y) 58 | #add output change FEE 59 | add_unblinded_output(tx, scriptpubkey, FEE, amountFEE - fixed_fee) 60 | #add output FEE 61 | add_unblinded_output(tx, None, FEE, fixed_fee) 62 | #add input B 63 | add_unsigned_input(tx, txidB, voutB) 64 | #add input FEE 65 | add_unsigned_input(tx, txidFEE, voutFEE) 66 | #print tx 67 | print(wally.tx_to_hex(tx, 3)) 68 | -------------------------------------------------------------------------------- /maker-cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time, requests, json 3 | 4 | def btc2sat(btc): 5 | return round(btc * 10**8) 6 | 7 | def sat2btc(sat): 8 | return round(sat * 10**-8, 8) 9 | 10 | # adapted from https://github.com/Blockstream/liquid_multisig_issuance 11 | class RPCHost(object): 12 | def __init__(self, url): 13 | self.session = requests.Session() 14 | self.url = url 15 | self.headers = {"content-type": "application/json"} 16 | 17 | def call(self, rpc_method, *params): 18 | payload = json.dumps({"method": rpc_method, "params": list(params), "jsonrpc": "2.0"}) 19 | for i in range(5): 20 | try: 21 | response = self.session.post(self.url, headers=self.headers, data=payload) 22 | connected = True 23 | except requests.exceptions.ConnectionError: 24 | time.sleep(10) 25 | assert connected 26 | assert response.status_code in (200, 500), f"RPC connection failure: {response.status_code} {response.reason}" 27 | j = response.json() 28 | assert "error" not in j or j["error"] is None, f"Error : {j['error']}" 29 | return j['result'] 30 | 31 | 32 | def main(): 33 | parser = argparse.ArgumentParser() 34 | 35 | parser.add_argument("-n", "--node-url", help="Elements node URL, eg http://USERNAME:PASSWORD@HOST:PORT/", required=True) 36 | parser.add_argument("-u", "--utxo", help="txid:vout", required=True) 37 | parser.add_argument("-a", "--asset", help="asset to receive", required=True) 38 | parser.add_argument("-r", "--rate", type=float, help="price_asset_send/price_asset_receive", required=True) 39 | 40 | args = parser.parse_args() 41 | 42 | txid, vout = args.utxo.split(":") 43 | vout = int(vout) 44 | asset_receive, rate = args.asset, args.rate 45 | 46 | connection = RPCHost(args.node_url) 47 | 48 | unspents = connection.call("listunspent") 49 | utxo = [u for u in unspents if u["txid"] == txid and u["vout"] == vout][0] 50 | 51 | amount_receive = round(rate * utxo["amount"], 8) 52 | address = connection.call("getnewaddress") 53 | address = connection.call("getaddressinfo", address)["unconfidential"] 54 | 55 | tx = connection.call( 56 | "createrawtransaction", 57 | [{"txid": txid, "vout": vout, "sequence": 0xffffffff}], 58 | {address: amount_receive}, 59 | 0, 60 | False, 61 | {address: asset_receive}) 62 | 63 | ret = connection.call( 64 | "signrawtransactionwithwallet", 65 | tx, 66 | None, 67 | "SINGLE|ANYONECANPAY") 68 | 69 | assert ret["complete"] 70 | print(json.dumps({ 71 | "tx": ret["hex"], 72 | "inputs": [{ 73 | "asset": utxo["asset"], 74 | "amount": btc2sat(utxo["amount"]), 75 | "asset_blinder": utxo["assetblinder"], 76 | "amount_blinder": utxo["amountblinder"], 77 | }], 78 | "outputs": [{ 79 | "asset": asset_receive, 80 | "amount": btc2sat(amount_receive), 81 | "asset_blinder": "00" * 32, 82 | "amount_blinder": "00" * 32, 83 | }], 84 | }, separators=(',', ':'))) 85 | 86 | if __name__ == "__main__": 87 | main() 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiquiDEX 2 | 3 | **WARNING**: This is experimental software, do not use with real funds. 4 | 5 | A decentralized exchange for Liquid transactions. 6 | 7 | ## Naming 8 | 9 | - **Maker**: proposes the trade as a signed but partial transaction 10 | - **Taker**: accepts the trade, completes and broadcasts the transaction 11 | 12 | ## Flow 13 | 14 | Maker wants to propose to exchange amount `x` of asset `A` for amount `y` of 15 | asset `B`. 16 | 17 | Maker must have an utxo `U_xA` locking exactly amount `x` of asset `A`. 18 | 19 | Maker creates a transaction `T_xAyB` spending a single utxo `U_xA` and receiving 20 | a single output locking amount `y` of asset `B`. At this stage `T_xAyB` is 21 | partial and invalid. 22 | 23 | Maker signs the (only) input with `SIGHASH_SINGLE | SIHASH_ANYONECANPAY`. 24 | This allows the Taker to add more inputs and outputs, without invalidating the 25 | Maker signature. 26 | 27 | Maker posts `TX_xAyB` to the __LiquiDEX__. 28 | 29 | Taker sees `TX_xAyB` on the __LiquiDEX__, and decides to accept the trade. 30 | 31 | Taker does _whatever it wants_ to complete the trade, what follow is an example. 32 | 33 | Taker does some verifications, such as `U_xA` actually locks amount `x` of 34 | asset `A`. 35 | 36 | Taker adds an output locking amount `x` of asset `A`. 37 | 38 | Taker funds `TX_xAyB` (fee and asset `A`). 39 | 40 | Taker signs the newly added transacion inputs, possibly with `SIGHASH_ALL`. 41 | 42 | Taker broadcasts the `TX_xAyB`, and the trade is executed. 43 | 44 | ### Examples 45 | 46 | #### 10000 USDT in exchange of 1 LBTC 47 | 48 | ![Liquidex tx USDT-LBTC](imgs/Liquidex_tx_USDT-LBTC.png) 49 | 50 | #### 10 asset A in exchange of 15 asset B 51 | 52 | ![Liquidex tx A-B](imgs/Liquidex_tx_A-B.png) 53 | 54 | ## Test on Liquid Mainnet 55 | 56 | In the following example performed on Liquid Mainnet, the Maker propose a trade 57 | offering 1 sats of [Lager pints](https://blockstream.info/liquid/asset/8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de) 58 | in exchange of 1 sat of [this unnamed asset](https://blockstream.info/liquid/asset/8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de). 59 | 60 | ### Requirements 61 | 62 | `python3` with `requests` and `wallycore>=0.7.9` modules installed 63 | 64 | ### Maker 65 | ``` 66 | $ python3 maker-cli.py -n http://USER:PSW@IP:PORT/ -u 52b988dbbd4db1069de7183f72687d7a8d367f89fc0ca4dcad8ae89e9822db16:2 -a 1a57c66ec5e922285d8d261bafe6f8eee7ec37a60c80a7eca9ae85c7a62f01ca -r 1 67 | { 68 | "tx": "02000000010116db22989ee88aaddca40cfc897f368d7a7d68723f18e79d06b14dbddb88b95202000000171600144c8f2937d509c9bf899e271ebf45f022ede744eaffffffff0101ca012fa6c785aea9eca7800ca637ece7eef8e6af1b268d5d2822e9c56ec6571a0100000000000000010017a914ee144f68da1f9bd660beae702ea16176c84b0583870000000000000247304402202550a31425efa9d35742f18fd488d540a11a3d8faeddb098b9249a6affa3b97e0220174d78870770f283c00e2669e0bf412d101989c7532f6c44340f5fedd52788b4832102a520dca5668fe0d89531d436ecb4fc52f5c9243ab0b45f5e14a64f08ccd26efa000000", 69 | "inputs": [ 70 | { 71 | "asset": "8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de", 72 | "amount": 1, 73 | "asset_blinder": "ebf74cafa8f3811e09196ca9cd2c7bdbb07cd9f3c5dd481a719e66c87370326f", 74 | "amount_blinder": "2e675260821dc8e7ab4a3f910b4c655b32a58b0bdb20e630acb26d5b1ee5893a" 75 | } 76 | ], 77 | "outputs": [ 78 | { 79 | "asset": "1a57c66ec5e922285d8d261bafe6f8eee7ec37a60c80a7eca9ae85c7a62f01ca", 80 | "amount": 1, 81 | "asset_blinder": "0000000000000000000000000000000000000000000000000000000000000000", 82 | "amount_blinder": "0000000000000000000000000000000000000000000000000000000000000000" 83 | } 84 | ] 85 | } 86 | ``` 87 | 88 | ### Taker 89 | 90 | ``` 91 | $ python3 taker-cli.py -n http://USER:PSW@IP:PORT/ -p '{"tx":"02000000010116db22989ee88aaddca40cfc897f368d7a7d68723f18e79d06b14dbddb88b95202000000171600144c8f2937d509c9bf899e271ebf45f022ede744eaffffffff0101ca012fa6c785aea9eca7800ca637ece7eef8e6af1b268d5d2822e9c56ec6571a0100000000000000010017a914ee144f68da1f9bd660beae702ea16176c84b0583870000000000000247304402202550a31425efa9d35742f18fd488d540a11a3d8faeddb098b9249a6affa3b97e0220174d78870770f283c00e2669e0bf412d101989c7532f6c44340f5fedd52788b4832102a520dca5668fe0d89531d436ecb4fc52f5c9243ab0b45f5e14a64f08ccd26efa000000","inputs":[{"asset":"8026fa969633b7b6f504f99dde71335d633b43d18314c501055fcd88b9fcb8de","amount":1,"asset_blinder":"ebf74cafa8f3811e09196ca9cd2c7bdbb07cd9f3c5dd481a719e66c87370326f","amount_blinder":"2e675260821dc8e7ab4a3f910b4c655b32a58b0bdb20e630acb26d5b1ee5893a"}],"outputs":[{"asset":"1a57c66ec5e922285d8d261bafe6f8eee7ec37a60c80a7eca9ae85c7a62f01ca","amount":1,"asset_blinder":"0000000000000000000000000000000000000000000000000000000000000000","amount_blinder":"0000000000000000000000000000000000000000000000000000000000000000"}]}' > tx.txt 92 | ``` 93 | 94 | ### Result 95 | 96 | [e1004eeb2d12130c9e62e8522ecc23f498adeb7cedca65423215027ab806edfe](https://blockstream.info/liquid/tx/e1004eeb2d12130c9e62e8522ecc23f498adeb7cedca65423215027ab806edfe) 97 | 98 | ## Considerations 99 | 100 | [Existing protocol](https://github.com/Blockstream/liquid-swap/) is done in 3 steps while LiquiDEX use only 2 steps. 101 | Moreover, contrary to liquid-swap, LiquiDEX it's not interactive, meaning that the Maker does not have to be online when the trade executes. 102 | In LiquiDEX creating a trade proposal does not require an onchain tx, however, removing the proposal requires that the maker makes a tx, 103 | spending the input proposed as a trade and invalidating the proposal. 104 | 105 | ## Possible improvements: 106 | 107 | - Handle L-BTC as a trading asset. 108 | - Taker could potentially take multiple maker proposed transactions and complete 109 | those in a single tx. 110 | - Use PSET once there is a new Elements release supporting its finalized redesign. 111 | 112 | ## Copyright 113 | 114 | [MIT](LICENSE) 115 | -------------------------------------------------------------------------------- /taker-cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import time, requests, json, os 3 | 4 | import wallycore as wally 5 | 6 | h2b = wally.hex_to_bytes 7 | b2h = wally.hex_from_bytes 8 | h2b_rev = lambda h : wally.hex_to_bytes(h)[::-1] 9 | b2h_rev = lambda b : wally.hex_from_bytes(b[::-1]) 10 | 11 | def btc2sat(btc): 12 | return round(btc * 10**8) 13 | 14 | def sat2btc(sat): 15 | return round(sat * 10**-8, 8) 16 | 17 | def rawblindrawtransaction(tx_hex, 18 | input_amount_blinders, input_amounts, 19 | input_assets, input_asset_blinders, 20 | output_amount_blinders, output_amounts, 21 | output_assets, output_asset_blinders): 22 | """Expects inputs as `elements-cli rawblindrawtransaction` 23 | 24 | and data about already blinded outputs. 25 | Inputs types and order are questionable but consistent with the rpc call. 26 | """ 27 | 28 | tx = wally.tx_from_hex(tx_hex, wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS) 29 | 30 | input_values = [btc2sat(i) for i in input_amounts] 31 | input_assets = [h2b_rev(h) for h in input_assets] 32 | input_abfs = [h2b_rev(h) for h in input_asset_blinders] 33 | input_vbfs = [h2b_rev(h) for h in input_amount_blinders] 34 | input_ags = [wally.asset_generator_from_bytes(a, bf) for a, bf in zip(input_assets, input_abfs)] 35 | 36 | input_assets_concat = b''.join(input_assets) 37 | input_abfs_concat = b''.join(input_abfs) 38 | input_ags_concat = b''.join(input_ags) 39 | 40 | min_value = 1 41 | ct_exp = 0 42 | # TODO: assert all output amounts are in the supported range 43 | ct_bits = 52 44 | 45 | # TODO: add general support for non-fee unblinded outputs, for those: 46 | # - do not generate blinders 47 | # - do not set *proof and nonce 48 | out_num = wally.tx_get_num_outputs(tx) 49 | output_blinded_values = [] 50 | output_abfs = [] 51 | output_vbfs = [] 52 | assert len(output_amount_blinders) == len(output_amounts) == len(output_asset_blinders) == len(output_assets) == out_num 53 | for out_idx in range(out_num): 54 | given = bool(output_amount_blinders[out_idx]) 55 | if wally.tx_get_output_nonce(tx, out_idx) == b'\x00' * 33: # unblinded 56 | if out_idx == out_num - 1: # fee 57 | continue 58 | 59 | # If given, 0-th output might be unblinded 60 | assert out_idx == 0 and given 61 | 62 | output_abfs.append(h2b_rev(output_asset_blinders[out_idx]) if given else os.urandom(32)) 63 | output_vbfs.append(h2b_rev(output_amount_blinders[out_idx]) if given else os.urandom(32)) 64 | output_blinded_values.append(btc2sat(output_amounts[out_idx]) if given else wally.tx_confidential_value_to_satoshi(wally.tx_get_output_value(tx, out_idx))) 65 | 66 | output_vbfs.pop(-1) 67 | output_vbfs.append(wally.asset_final_vbf( 68 | input_values + output_blinded_values, wally.tx_get_num_inputs(tx), 69 | b''.join(input_abfs + output_abfs), b''.join(input_vbfs + output_vbfs))) 70 | 71 | for out_idx in range(out_num - 1): 72 | given = bool(output_amount_blinders[out_idx]) 73 | 74 | blinding_pubkey = wally.tx_get_output_nonce(tx, out_idx) 75 | scriptpubkey = wally.tx_get_output_script(tx, out_idx) 76 | assert scriptpubkey 77 | 78 | if given: 79 | asset = h2b_rev(output_assets[out_idx]) 80 | value_satoshi = btc2sat(output_amounts[out_idx]) 81 | else: 82 | asset_prefixed = wally.tx_get_output_asset(tx, out_idx) 83 | value_bytes = wally.tx_get_output_value(tx, out_idx) 84 | 85 | assert asset_prefixed[0] == 1 and value_bytes[0] == 1 86 | value_satoshi = wally.tx_confidential_value_to_satoshi(value_bytes) 87 | asset = asset_prefixed[1:] 88 | 89 | output_abf = output_abfs[out_idx] 90 | output_vbf = output_vbfs[out_idx] 91 | blinded = output_abf != b'\x00' * 32 and output_vbf != b'\x00' * 32 92 | 93 | if not blinded: 94 | continue 95 | 96 | eph_key_prv = os.urandom(32) 97 | eph_key_pub = wally.ec_public_key_from_private_key(eph_key_prv) 98 | blinding_nonce = wally.sha256(wally.ecdh(blinding_pubkey, eph_key_prv)) 99 | 100 | output_generator = wally.asset_generator_from_bytes(asset, output_abf) 101 | output_value_commitment = wally.asset_value_commitment( 102 | value_satoshi, output_vbf, output_generator) 103 | 104 | rangeproof = wally.asset_rangeproof_with_nonce( 105 | value_satoshi, blinding_nonce, asset, output_abf, output_vbf, 106 | output_value_commitment, scriptpubkey, output_generator, min_value, 107 | ct_exp, ct_bits) 108 | 109 | surjectionproof = wally.asset_surjectionproof( 110 | asset, output_abf, output_generator, os.urandom(32), 111 | input_assets_concat, input_abfs_concat, input_ags_concat) 112 | 113 | if not given: 114 | wally.tx_set_output_asset(tx, out_idx, output_generator) 115 | wally.tx_set_output_value(tx, out_idx, output_value_commitment) 116 | wally.tx_set_output_nonce(tx, out_idx, eph_key_pub) 117 | wally.tx_set_output_surjectionproof(tx, out_idx, surjectionproof) 118 | wally.tx_set_output_rangeproof(tx, out_idx, rangeproof) 119 | 120 | return wally.tx_to_hex(tx, wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS) 121 | 122 | # adapted from https://github.com/Blockstream/liquid_multisig_issuance 123 | class RPCHost(object): 124 | def __init__(self, url): 125 | self.session = requests.Session() 126 | self.url = url 127 | self.headers = {"content-type": "application/json"} 128 | 129 | def call(self, rpc_method, *params): 130 | payload = json.dumps({"method": rpc_method, "params": list(params), "jsonrpc": "2.0"}) 131 | for i in range(5): 132 | try: 133 | response = self.session.post(self.url, headers=self.headers, data=payload) 134 | connected = True 135 | except requests.exceptions.ConnectionError: 136 | time.sleep(10) 137 | assert connected 138 | assert response.status_code in (200, 500), f"RPC connection failure: {response.status_code} {response.reason}" 139 | j = response.json() 140 | assert "error" not in j or j["error"] is None, f"Error : {j['error']}" 141 | return j['result'] 142 | 143 | def main(): 144 | parser = argparse.ArgumentParser() 145 | 146 | parser.add_argument("-n", "--node-url", help="Elements node URL, eg http://USERNAME:PASSWORD@HOST:PORT/", required=True) 147 | parser.add_argument("-p", "--proposal", help="Proposal to match", required=True) 148 | 149 | args = parser.parse_args() 150 | 151 | j = json.loads(args.proposal) 152 | tx = j["tx"] 153 | assert len(j["inputs"]) == len(j["outputs"]) == 1 154 | maker_input = j["inputs"][0] 155 | maker_output = j["outputs"][0] 156 | x = maker_input["amount"] 157 | A = maker_input["asset"] 158 | input_amount_blinders = [maker_input["amount_blinder"]] 159 | input_amounts = [sat2btc(x)] 160 | input_assets = [A] 161 | input_asset_blinders = [maker_input["asset_blinder"]] 162 | y = maker_output["amount"] 163 | B = maker_output["asset"] 164 | 165 | tx = wally.tx_from_hex(tx, wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS) 166 | 167 | connection = RPCHost(args.node_url) 168 | 169 | assert wally.tx_get_num_inputs(tx) == 1 170 | txid = b2h_rev(wally.tx_get_input_txhash(tx, 0)) 171 | vout = wally.tx_get_input_index(tx, 0) 172 | ret = connection.call("gettxout", txid, vout) 173 | assert ret["confirmations"] > 1 174 | if "value" in ret: 175 | assert ret["value"] == sat2btc(x) 176 | assert ret["asset"] == A 177 | assert maker_input["amount_blinder"] == maker_input["asset_blinder"] == "0" * 64 178 | else: 179 | asset_commitment = wally.asset_generator_from_bytes(h2b_rev(A), h2b_rev(maker_input["asset_blinder"])) 180 | amount_commitment = wally.asset_value_commitment(x, h2b_rev(maker_input["amount_blinder"]), asset_commitment) 181 | assert b2h(asset_commitment) == ret["assetcommitment"] 182 | assert b2h(amount_commitment) == ret["valuecommitment"] 183 | 184 | assert wally.tx_get_num_outputs(tx) == 1 185 | asset_commitment = wally.tx_get_output_asset(tx, 0) 186 | amount_commitment = wally.tx_get_output_value(tx, 0) 187 | if asset_commitment[0] == 1: 188 | assert amount_commitment[0] == 1 189 | assert asset_commitment[1:] == h2b_rev(B) 190 | assert amount_commitment == wally.tx_confidential_value_from_satoshi(y) 191 | else: 192 | asset_commitment_ = wally.asset_generator_from_bytes(h2b_rev(B), h2b_rev(maker_output["asset_blinder"])) 193 | amount_commitment_ = wally.asset_value_commitment(y, h2b_rev(maker_output["amount_blinder"]), asset_commitment_) 194 | assert asset_commitment == asset_commitment_ 195 | assert amount_commitment == amount_commitment_ 196 | 197 | unspents = connection.call("listunspent") 198 | unspents = [u for u in unspents if u["spendable"]] 199 | utxos_B = [u for u in unspents if u["asset"] == B] 200 | assert sum(btc2sat(u["amount"]) for u in utxos_B) >= y 201 | fixed_fee = 5000 202 | FEE = connection.call("dumpassetlabels")["bitcoin"] 203 | utxos_FEE = [u for u in unspents if u["asset"] == FEE] 204 | assert sum(btc2sat(u["amount"]) for u in utxos_FEE) >= fixed_fee 205 | 206 | def add_unblinded_output(tx_, script, asset, sat, blinding_pubkey=None): 207 | wally.tx_add_elements_raw_output( 208 | tx_, 209 | script, 210 | b'\x01' + h2b_rev(asset), 211 | wally.tx_confidential_value_from_satoshi(sat), 212 | blinding_pubkey, # nonce 213 | None, # surjection proof 214 | None, # range proof 215 | 0) 216 | 217 | def add_unsigned_input(tx_, txid, vout): 218 | wally.tx_add_elements_raw_input( 219 | tx_, 220 | h2b_rev(txid), 221 | vout, 222 | 0xffffffff, 223 | None, # scriptSig 224 | None, # witness 225 | None, # nonce 226 | None, # entropy 227 | None, # issuance amount 228 | None, # inflation keys 229 | None, # issuance amount rangeproof 230 | None, # inflation keys rangeproof 231 | None, # pegin witness 232 | 0) 233 | 234 | def get_new_scriptpubkey_and_blinding_pubkey(connection): 235 | address = connection.call("getnewaddress") 236 | info = connection.call("getaddressinfo", address) 237 | scriptpubkey = h2b(info["scriptPubKey"]) 238 | blinding_pubkey = h2b(info["confidential_key"]) if info["confidential_key"] else None 239 | return (scriptpubkey, blinding_pubkey) 240 | 241 | # add output (A, x) 242 | scriptpubkey_Ax, blinding_pubkey_Ax = get_new_scriptpubkey_and_blinding_pubkey(connection) 243 | add_unblinded_output(tx, scriptpubkey_Ax, A, x, blinding_pubkey_Ax) 244 | # add inputs (B, y+c) 245 | tot_in_B = 0 246 | for u in utxos_B: 247 | add_unsigned_input(tx, u["txid"], u["vout"]) 248 | input_amount_blinders.append(u["amountblinder"]) 249 | input_amounts.append(u["amount"]) 250 | input_asset_blinders.append(u["assetblinder"]) 251 | input_assets.append(u["asset"]) 252 | tot_in_B += btc2sat(u["amount"]) 253 | if tot_in_B >= y: 254 | break 255 | # add change output (B, c) 256 | if tot_in_B > y: 257 | scriptpubkey_Bchange, blinding_pubkey_Bchange = get_new_scriptpubkey_and_blinding_pubkey(connection) 258 | add_unblinded_output(tx, scriptpubkey_Bchange, B, tot_in_B - y, blinding_pubkey_Bchange) 259 | # add inputs (FEE, fixed_fee+c) 260 | tot_in_FEE = 0 261 | for u in utxos_FEE: 262 | add_unsigned_input(tx, u["txid"], u["vout"]) 263 | input_amount_blinders.append(u["amountblinder"]) 264 | input_amounts.append(u["amount"]) 265 | input_asset_blinders.append(u["assetblinder"]) 266 | input_assets.append(u["asset"]) 267 | tot_in_FEE += btc2sat(u["amount"]) 268 | if tot_in_FEE >= fixed_fee: 269 | break 270 | # add change output (FEE, c) 271 | if tot_in_FEE > fixed_fee: 272 | scriptpubkey_FEEchange, blinding_pubkey_FEEchange = get_new_scriptpubkey_and_blinding_pubkey(connection) 273 | add_unblinded_output(tx, scriptpubkey_FEEchange, FEE, tot_in_FEE - fixed_fee, blinding_pubkey_FEEchange) 274 | # add output for fee 275 | add_unblinded_output(tx, None, FEE, fixed_fee) 276 | 277 | # TODO: shuffle added inputs and outpus, otherwise blinding partially pointless... 278 | 279 | tx_hex = wally.tx_to_hex(tx, wally.WALLY_TX_FLAG_USE_WITNESS | wally.WALLY_TX_FLAG_USE_ELEMENTS) 280 | num_out = wally.tx_get_num_outputs(tx) 281 | tx_hex = rawblindrawtransaction( 282 | tx_hex, 283 | input_amount_blinders, 284 | input_amounts, 285 | input_assets, 286 | input_asset_blinders, 287 | [maker_output["amount_blinder"]] + [None] * (num_out - 1), 288 | [sat2btc(maker_output["amount"])] + [None] * (num_out - 1), 289 | [maker_output["asset"]] + [None] * (num_out - 1), 290 | [maker_output["asset_blinder"]] + [None] * (num_out - 1), 291 | ) 292 | 293 | ret = connection.call("signrawtransactionwithwallet", tx_hex) 294 | assert ret["complete"] 295 | tx_hex = ret["hex"] 296 | 297 | ret = connection.call("testmempoolaccept", [tx_hex]) 298 | assert all(e["allowed"] for e in ret), ret 299 | 300 | print(tx_hex) 301 | 302 | if __name__ == "__main__": 303 | main() 304 | --------------------------------------------------------------------------------