diff --git a/README.md b/README.md index f204ef3..06b6a81 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ # ZBXCAT + A work-in-progress for Zcash Bitcoin Cross-Chain Atomic Transactions Contains basic scripts we're still testing in regtest mode on both networks. This may all be refactored as we go. Bitcoin scripts use the rpc proxy code in python-bitcoinlib, and Zcash script will use python-zcashlib (a Zcash fork of python-bitcoinlib). -## BTC p2sh HTLC script +## Setup -The script `btc-p2sh-htlc.py` creates and redeems a p2sh transaction on Bitcoin regtest using a preimage. TODO: Locktime scripting still needs work. - -To successfully run it, you'll need python3, the dependencies installed, and a bitcoin daemon running in regtest mode. +To successfully run this, you'll need python3, the dependencies installed, and a bitcoin daemon running in regtest mode. To install python3 in a virtualenv, run this command from the top level of the directory: ``` @@ -22,15 +21,6 @@ To install dependencies, run: pip install -r requirements.txt ``` -To run a bitcoin daemon in regtest mode, with the ability to inspect transactions outside your wallet (useful for testing purposes), use the command -``` -bitcoind -regtest -txindex=1 --daemon -``` - -## ZEC p2sh HTLC script - -The script `zec-p2sh-htlc.py` is the same as the BTC script, but uses python-zcashlib, which is still a work in progress. - To install python-zcashlib for testing and editing, clone the repository to your local filesystem. It is currently on a branch of python-bitcoinlib maintained by @arcalinea. ``` @@ -45,13 +35,26 @@ To install python-zcashlib from your local filesystem path in editable mode: `pip install --editable (-e) ` +## Run Zcash and Bitcoin daemons locally + +To test, run a Zcash daemon and bitcoin daemon in regtest mode. You may have to change the port one of them runs on, for example with the flag `-port=18445`. + +To run a bitcoin daemon in regtest mode, with the ability to inspect transactions outside your wallet (useful for testing purposes), use the command +``` +bitcoind -regtest -txindex=1 -daemon -port=18445 +``` + Be sure to run a Zcash daemon in regtest mode. ``` zcashd -regtest -txindex=1 --daemon ``` +## XCAT CLI interface + +Run `xcat.py` to go through the protocol. `xcat.py new` creats a new trade, `xcat.py seller` progresses with the seller's next step, `xcat.py buyer` progresses with the buyer's next step. + +To test the entire script workflow, run `test.py`. + ## Misc -`protocol-pseudocode.py` is guaranteed to not run. It was written as a brainstorm/sketch of a hypothetical xcat protocol using @ebfull's fork of Zcash/Bitcoin that supports createhtlc as an rpc command. Including here in case it's useful in any way. - I used the module [future](http://python-future.org/futurize.html) to make existing python2 code for the rpc interface compatible with python3. diff --git a/bXcat.py b/bXcat.py index 80a846c..ebec5b2 100644 --- a/bXcat.py +++ b/bXcat.py @@ -12,7 +12,7 @@ from bitcoin.core import b2x, lx, b2lx, x, COIN, COutPoint, CMutableTxOut, CMuta from bitcoin.base58 import decode from bitcoin.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP, OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH -from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret +from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret, P2SHBitcoinAddress, P2PKHBitcoinAddress from utils import * @@ -27,6 +27,13 @@ FEE = 0.001*COIN zcashd = zcash.rpc.Proxy() +def parse_secret(txid): + decoded = bitcoind.getrawtransaction(lx(txid), 1) + print("Decoded", decoded) + # decoded = bitcoind.decoderawtransaction(raw) + asm = decoded['vin'][0]['scriptSig']['asm'].split(" ") + print(asm[2]) + def get_keys(funder_address, redeemer_address): fundpubkey = CBitcoinAddress(funder_address) redeempubkey = CBitcoinAddress(redeemer_address) @@ -42,18 +49,21 @@ def hashtimelockcontract(funder, redeemer, secret, locktime): redeemerAddr = CBitcoinAddress(redeemer) h = sha256(secret) blocknum = bitcoind.getblockcount() + print("Current blocknum", blocknum) redeemblocknum = blocknum + locktime - zec_redeemScript = CScript([OP_IF, OP_SHA256, h, OP_EQUALVERIFY,OP_DUP, OP_HASH160, + print("REDEEMBLOCKNUM BITCOIN", redeemblocknum) + redeemScript = CScript([OP_IF, OP_SHA256, h, OP_EQUALVERIFY,OP_DUP, OP_HASH160, redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160, funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG]) - txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() + print("Redeem script for p2sh contract on Bitcoin blockchain:", b2x(redeemScript)) + txin_scriptPubKey = redeemScript.to_p2sh_scriptPubKey() # Convert the P2SH scriptPubKey to a base58 Bitcoin address txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey) p2sh = str(txin_p2sh_address) - return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'zec_redeemScript': b2x(zec_redeemScript), 'redeemer': redeemer, 'funder': funder} + return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'redeemScript': b2x(redeemScript), 'redeemer': redeemer, 'funder': funder} def fund_htlc(p2sh, amount): - send_amount = amount*COIN + send_amount = float(amount) * COIN fund_txid = bitcoind.sendtoaddress(p2sh, send_amount) txid = b2x(lx(b2x(fund_txid))) return txid @@ -87,75 +97,135 @@ def search_p2sh(block, p2sh): print("Address to p2sh found in transaction!", addr) print("Returning from search_p2sh") - def get_tx_details(txid): # must convert txid string to bytes x(txid) fund_txinfo = bitcoind.gettransaction(lx(txid)) return fund_txinfo['details'][0] -def redeem(p2sh, action): - # ensure p2sh is imported - bitcoind.importaddress(p2sh, '', False) +# redeems automatically after buyer has funded tx, by scanning for transaction to the p2sh +# i.e., doesn't require buyer telling us fund txid +def auto_redeem(contract, secret): + # How to find redeemScript and redeemblocknum from blockchain? + print("Contract in auto redeem", contract.__dict__) + p2sh = contract.p2sh + #checking there are funds in the address + amount = check_funds(p2sh) + if(amount == 0): + print("address ", p2sh, " not funded") + quit() + fundtx = find_transaction_to_address(p2sh) + amount = fundtx['amount'] / COIN + print("Found fundtx:", fundtx) + p2sh = P2SHBitcoinAddress(p2sh) + if fundtx['address'] == p2sh: + print("Found {0} in p2sh {1}, redeeming...".format(amount, p2sh)) - contracts = get_contract() - trade = get_trade() - for key in contracts: - if key == p2sh: - contract = contracts[key] - if contract: - print("Redeeming tx in p2sh", p2sh) - # TODO: Have to get tx info from saved contract p2sh - redeemblocknum = contract['redeemblocknum'] - zec_redeemScript = contract['zec_redeemScript'] + # Parsing redeemblocknum from the redeemscript of the p2sh + redeemblocknum = find_redeemblocknum(contract) + blockcount = bitcoind.getblockcount() + print("\nCurrent blocknum at time of redeem on Bitcoin:", blockcount) + if blockcount < redeemblocknum: + redeemPubKey = find_redeemAddr(contract) + print('redeemPubKey', redeemPubKey) + else: + print("nLocktime exceeded, refunding") + redeemPubKey = find_refundAddr(contract) + print('refundPubKey', redeemPubKey) + # redeemPubKey = CBitcoinAddress.from_scriptPubKey(redeemPubKey) + # exit() - txid = trade[action]['fund_tx'] - details = get_tx_details(txid) - print("Txid for fund tx", txid) - # must be little endian hex - txin = CMutableTxIn(COutPoint(lx(txid), details['vout'])) - redeemPubKey = CBitcoinAddress(contract['redeemer']) - amount = trade[action]['amount'] * COIN - print("amount: {0}, fee: {1}".format(amount, FEE)) - txout = CMutableTxOut(amount - FEE, redeemPubKey.to_scriptPubKey()) + zec_redeemScript = CScript(x(contract.redeemScript)) + txin = CMutableTxIn(fundtx['outpoint']) + txout = CMutableTxOut(fundtx['amount'] - FEE, redeemPubKey.to_scriptPubKey()) # Create the unsigned raw transaction. tx = CMutableTransaction([txin], [txout]) # nLockTime needs to be at least as large as parameter of CHECKLOCKTIMEVERIFY for script to verify # TODO: these things like redeemblocknum should really be properties of a tx class... # Need: redeemblocknum, zec_redeemScript, secret (for creator...), txid, redeemer... - # Is stored as hex, must convert to bytes - zec_redeemScript = CScript(x(zec_redeemScript)) - - tx.nLockTime = redeemblocknum + if blockcount >= redeemblocknum: + print("\nLocktime exceeded") + tx.nLockTime = redeemblocknum # Ariel: This is only needed when redeeming with the timelock sighash = SignatureHash(zec_redeemScript, tx, 0, SIGHASH_ALL) - # TODO: figure out how to better protect privkey? + # TODO: figure out how to better protect privkey privkey = bitcoind.dumpprivkey(redeemPubKey) sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) - # TODO: Figure out where to store secret preimage securely. Parse from scriptsig of redeemtx - secret = trade['sell']['secret'] + print("SECRET", secret) preimage = secret.encode('utf-8') - print('preimage', preimage) - - # print('zec_redeemScript', zec_redeemScript) txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript]) - # print("Redeem tx hex:", b2x(tx.serialize())) - # Can only call to_p2sh_scriptPubKey on CScript obj + # exit() + + print("txin.scriptSig", b2x(txin.scriptSig)) txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() - - # print("txin.scriptSig", b2x(txin.scriptSig)) - # print("txin_scriptPubKey", b2x(txin_scriptPubKey)) - # print('tx', tx) + print('Redeem txhex', b2x(tx.serialize())) VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) - print("Script verified, sending raw tx...") - print("Raw tx of prepared redeem tx: ", b2x(tx.serialize())) + print("script verified, sending raw tx") txid = bitcoind.sendrawtransaction(tx) - txhex = b2x(lx(b2x(txid))) - print("Txid of submitted redeem tx: ", txhex) - return txhex + print("Txid of submitted redeem tx: ", b2x(lx(b2x(txid)))) + return b2x(lx(b2x(txid))) else: print("No contract for this p2sh found in database", p2sh) +# takes hex and returns array of decoded script op codes +def parse_script(script_hex): + redeemScript = zcashd.decodescript(script_hex) + scriptarray = redeemScript['asm'].split(' ') + return scriptarray + +def find_redeemblocknum(contract): + scriptarray = parse_script(contract.redeemScript) + redeemblocknum = scriptarray[8] + return int(redeemblocknum) + +def find_redeemAddr(contract): + scriptarray = parse_script(contract.redeemScript) + redeemer = scriptarray[6] + redeemAddr = P2PKHBitcoinAddress.from_bytes(x(redeemer)) + return redeemAddr + +def find_refundAddr(contract): + scriptarray = parse_script(contract.redeemScript) + funder = scriptarray[13] + refundAddr = P2PKHBitcoinAddress.from_bytes(x(funder)) + return refundAddr + +# def find_recipient(contract): + # initiator = CBitcoinAddress(contract.initiator) + # fulfiller = CBitcoinAddress(contract.fulfiller) + # print("Initiator", b2x(initiator)) + # print("Fulfiler", b2x(fulfiller)) + # make this dependent on actual fund tx to p2sh, not contract + # print("Contract fund_tx", contract.fund_tx) + # txid = contract.fund_tx + # raw = bitcoind.gettransaction(lx(txid))['hex'] + # print("Raw tx", raw) + # # print("Raw", raw) + # decoded = zcashd.decoderawtransaction(raw + '00') + # scriptSig = decoded['vin'][0]['scriptSig'] + # print("Decoded", scriptSig) + # asm = scriptSig['asm'].split(" ") + # pubkey = asm[1] + # print('pubkey', pubkey) + # redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey)) + # print('redeemPubkey', redeemPubkey) + +def find_transaction_to_address(p2sh): + bitcoind.importaddress(p2sh, "", False) + txs = bitcoind.listunspent() + for tx in txs: + # print("tx addr:", tx['address']) + # print(type(tx['address'])) + # print(type(p2sh)) + if tx['address'] == CBitcoinAddress(p2sh): + print("Found tx to p2sh", p2sh) + print(tx) + return tx + def new_bitcoin_addr(): addr = bitcoind.getnewaddress() print('new btc addr', addr.to_scriptPubKey) return addr.to_scriptPubKey() + +def generate(num): + blocks = bitcoind.generate(num) + return blocks diff --git a/checktx.py b/checktx.py deleted file mode 100755 index 76ae3dd..0000000 --- a/checktx.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -import sys -import subprocess - -subprocess.Popen("source /home/jay/Zcash/python3-xcat/venv/bin/activate", shell=True) - -import bitcoin -import bitcoin.rpc -SelectParams('regtest') -bitcoind = bitcoin.rpc.Proxy() - -txid = sys.argv[1] -print("Incoming txid:", txid) -tx = bitcoind.gettransaction(txid, 0) -print(tx) diff --git a/contract.json b/contract.json deleted file mode 100644 index 5c40c4b..0000000 --- a/contract.json +++ /dev/null @@ -1 +0,0 @@ -{"t2QrLFUqmp1v1xQSE3hmgwcYuinRb3BRWMm": {"funder": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY", "redeemer": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", "redeemblocknum": 182, "zec_redeemScript": "63a820343e398e4e99a68e7de6ec57f00b6a14a8e6d0a7dd714efbab4dcbd385f4f3038876a9143ea29256c9d2888ca23de42a8b8e69ca2ec235b16702b600b17576a914c5acca6ef39c843c7a9c3ad01b2da95fe2edf5ba6888ac", "p2sh": "t2QrLFUqmp1v1xQSE3hmgwcYuinRb3BRWMm"}, "2MuWU5BpLpqJvvzkCPq8gFHA4VFFGyvjaJf": {"funder": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", "redeemer": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", "redeemblocknum": 146, "zec_redeemScript": "63a820343e398e4e99a68e7de6ec57f00b6a14a8e6d0a7dd714efbab4dcbd385f4f3038876a9147788b4511a25fba1092e67b307a6dcdb6da125d967029200b17576a914c7043e62a7391596116f54f6a64c8548e97d3fd96888ac", "p2sh": "2MuWU5BpLpqJvvzkCPq8gFHA4VFFGyvjaJf"}} \ No newline at end of file diff --git a/contract.py b/contract.py deleted file mode 100644 index be5c6f9..0000000 --- a/contract.py +++ /dev/null @@ -1,8 +0,0 @@ - - -class Contract(object): - def __init__(self, funder=None, redeemer=None, blockchain=None): - '''Create a new hash time-locked contract''' - self.funder = funder - self.redeemer = redeemer - self.blockchain = blockchain diff --git a/protocol-pseudocode.py b/protocol-pseudocode.py deleted file mode 100644 index de353ea..0000000 --- a/protocol-pseudocode.py +++ /dev/null @@ -1,70 +0,0 @@ -from rpc.ZDaemon import * -import argparse, textwrap -import hashlib - -# How to prevent this program from having access to local bitcoind or zcashd? Have to just allow it? -# A bit like a GUI wallet that you download, then use as a local app? Running its own instance of zcashd, bitcoind? (bitcoin SPV client possible?) -# -# UI: Seller puts in their address, buyer puts in theirs. (Job of DEX exchange can be to match orders...) -# Interface extracts pubkeys -# Interface determines locktime, and gives seller their "secret redeem code" for their funds (hash preimage) -# Interface creates the HTLC with preimage -# Interface imports address for the redeem script, for buyer, and for seller. (needs access to local zcashd/bitcoind) -# Buyer sends to p2sh address, funding -# Interface checks that it's in wallet, then creates the redeem transaction (rawtx) for buyer and seller -# After set time passes, interface tries to complete the transaction -# If falls through, uses redeem transactions. - -zd = ZDaemon(network=TESTNET) - -def get_pubkey_from_taddr(): - taddr = raw_input("Enter the taddr you would like to send from: ") - resp = zd.validateaddress(taddr) - if resp['pubkey']: - pubkey = resp['pubkey'] - print "The pubkey for the address you entered is: ", pubkey - return pubkey - -def create_htlc(pubkey, sellerpubkey): - # UI is going to be opinionated about timelocks, and provide secret for you. - secret = raw_input("Enter a secret to lock your funds: ") - # convert to bytes - secret_bytes = str.encode(secret) - digest = hashlib.sha256(preimage).digest() - time = 10 - # need to add this rpc call, assume is running zcashd on zkcp branch - htlc = zd.createhtlc(pubkey, sellerpubkey, secret_bytes, time) - return htlc - -def import_address(htlc): - fund_addr = zd.importaddress(htlc['redeemScript']) - txs = zd.listunspent() - for tx in txs: - if tx['address'] == htlc['address'] - return tx['address'] - - -def fund_p2sh(p2sh): - fund_tx = zd.sendtoaddress(p2sh) - return fund_tx - -def redeem_p2sh(fund_tx): - for tx in txs: - if tx['address'] == htlc['address'] - return tx['address'] - - return tx['txid'], tx['vout'] - # write this function too - rawtx = zd.createrawtransaction(txid, vout, selleraddress, amount) - # Buyer has to sign raw tx - - -# out of band: sellerpubkey, selleraddress -pubkey = get_pubkey_from_taddr() -# Wait for pubkey from buyer. Assume some messaging layer, or out of band communication, with seller. -htlc = create_htlc(pubkey, sellerpubkey) -# import address for both buyer and seller -addr = import_address(htlc) -# Buyer funds tx -fund_tx = fund_p2sh(addr) -# Now seller must redeem diff --git a/secret.json b/secret.json new file mode 100644 index 0000000..160c7de --- /dev/null +++ b/secret.json @@ -0,0 +1 @@ +Hm0Mla5n \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..28bfbf0 --- /dev/null +++ b/test.py @@ -0,0 +1,103 @@ +import zXcat +import bXcat +from xcat import * + +htlcTrade = Trade() +print("Starting test of xcat...") + +def get_initiator_addresses(): + return {'bitcoin': 'myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp', 'zcash': 'tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ'} + +def get_fulfiller_addresses(): + return {'bitcoin': 'mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z', 'zcash': 'tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY'} + +def initiate(trade): + # Get amounts + amounts = {"sell": {"currency": "bitcoin", "amount": "0.5"}, "buy": {"currency": "zcash", "amount": "1.12"}} + sell = amounts['sell'] + buy = amounts['buy'] + sell_currency = sell['currency'] + buy_currency = buy['currency'] + # Get addresses + init_addrs = get_initiator_addresses() + sell['initiator'] = init_addrs[sell_currency] + buy['initiator'] = init_addrs[buy_currency] + fulfill_addrs = get_fulfiller_addresses() + sell['fulfiller'] = fulfill_addrs[sell_currency] + buy['fulfiller'] = fulfill_addrs[buy_currency] + # initializing contract classes with addresses, currencies, and amounts + trade.sellContract = Contract(sell) + trade.buyContract = Contract(buy) + print(trade.sellContract.__dict__) + print(trade.buyContract.__dict__) + + secret = generate_password() + print("Generating secret to lock funds:", secret) + save_secret(secret) + # TODO: Implement locktimes and mock block passage of time + sell_locktime = 2 + buy_locktime = 4 # Must be more than first tx + + create_sell_p2sh(trade, secret, sell_locktime) + txid = fund_sell_contract(trade) + print("Sent") + create_buy_p2sh(trade, secret, buy_locktime) + +def fulfill(trade): + buy = trade.buyContract + sell = trade.sellContract + buy_p2sh_balance = check_p2sh(buy.currency, buy.p2sh) + sell_p2sh_balance = check_p2sh(sell.currency, sell.p2sh) + + if buy_p2sh_balance == 0: + print("Buy amt:", buy.amount) + txid = fund_buy_contract(trade) + print("Fund tx txid:", txid) + else: + raise ValueError("Sell p2sh not funded, buyer cannot redeem") + +def redeem_one(trade): + buy = trade.buyContract + if trade.sellContract.get_status() == 'redeemed': + raise RuntimeError("Sell contract status was already redeemed before seller could redeem buyer's tx") + else: + secret = get_secret() + print("GETTING SECRET IN TEST:", secret) + tx_type, txid = redeem_p2sh(trade.buyContract, secret) + print("\nTX Type", tx_type) + setattr(trade.buyContract, tx_type, txid) + save(trade) + print("You have redeemed {0} {1}!".format(buy.amount, buy.currency)) + +def redeem_two(trade): + if trade.sellContract.get_status() == 'redeemed': + raise RuntimeError("Sell contract was redeemed before buyer could retrieve funds") + elif trade.buyContract.get_status() == 'refunded': + print("buyContract was refunded to buyer") + else: + # Buy contract is where seller disclosed secret in redeeming + if trade.buyContract.currency == 'bitcoin': + secret = bXcat.parse_secret(trade.buyContract.redeem_tx) + else: + secret = zXcat.parse_secret(trade.buyContract.redeem_tx) + print("Found secret in seller's redeem tx", secret) + redeem_tx = redeem_p2sh(trade.sellContract, secret) + setattr(trade.sellContract, 'redeem_tx', redeem_tx) + save(trade) + +def generate_blocks(num): + bXcat.generate(num) + zXcat.generate(num) + +initiate(htlcTrade) +fulfill(htlcTrade) + +generate_blocks(6) + +redeem_one(htlcTrade) +redeem_two(htlcTrade) + +# addr = CBitcoinAddress('tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ') +# print(addr) +# # print(b2x('tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ')) +# print(b2x(addr)) diff --git a/trades.py b/trades.py new file mode 100644 index 0000000..1602b3a --- /dev/null +++ b/trades.py @@ -0,0 +1,27 @@ +class Trade(object): + def __init__(self, sellContract=None, buyContract=None): + '''Create a new trade with a sell contract and buy contract across two chains''' + self.sellContract = sellContract + self.buyContract = buyContract + +class Contract(object): + def __init__(self, data): + # Keep track of funding and redeem tx? + allowed = ('fulfiller', 'initiator', 'currency', 'p2sh', 'amount', 'fund_tx', 'redeem_tx', 'secret', 'redeemScript', 'redeemblocknum') + for key in data: + if key in allowed: + setattr(self, key, data[key]) + + def get_status(self): + # keep as function or set as property? + if hasattr(self, 'redeem_tx'): + return 'redeemed' + elif hasattr(self, 'refund_tx'): + return 'refunded' + elif hasattr(self, 'fund_tx'): + # Do additional validation here to check amts on blockchain + return 'funded' + else: + return 'empty' + +# other classes; transactions? users? diff --git a/user.py b/user.py deleted file mode 100644 index e69de29..0000000 diff --git a/userInput.py b/userInput.py new file mode 100644 index 0000000..193ab14 --- /dev/null +++ b/userInput.py @@ -0,0 +1,79 @@ +from utils import * + +def get_trade_amounts(): + print("in user input") + amounts = {} + sell_currency = input("Which currency would you like to trade out of (bitcoin or zcash)? ") + if sell_currency == '': + sell_currency = 'bitcoin' + if sell_currency == 'bitcoin': + buy_currency = 'zcash' + else: + buy_currency = 'bitcoin' + print(sell_currency) + sell_amt = input("How much {0} do you want to sell? ".format(sell_currency)) + sell_amt = 3.5 + print(sell_amt) + buy_amt = input("How much {0} do you want to receive in exchange? ".format(buy_currency)) + buy_amt = 1.2 + print(buy_amt) + sell = {'currency': sell_currency, 'amount': sell_amt} + buy = {'currency': buy_currency, 'amount': buy_amt} + amounts['sell'] = sell + amounts['buy'] = buy + return amounts + +def create_password(): + secret = input("Initiating trade: Create a password to place the funds in escrow: ") + # TODO: hash and store secret only locally. + if secret == '': + secret = generate_password() + print('Remember your password:', secret) + # Saving secret locally for now + save_secret(secret) + return secret + +def retrieve_password(): + secret = input("Enter the secret you used to lock the funds in order to redeem:") + if secret == '': + secret = get_secret() + print(secret) + return secret + +def authorize_fund_sell(htlcTrade): + print('To complete your sell, send {0} {1} to this p2sh: {2}'.format(htlcTrade.sellContract.amount, htlcTrade.sellContract.currency, htlcTrade.sellContract.p2sh)) + response = input("Type 'enter' to allow this program to send funds on your behalf.") + +def get_initiator_addresses(): + btc_addr = input("Enter your bitcoin address: ") + # btc_addr = bXcat.new_bitcoin_addr() + btc_addr = 'myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp' + print(btc_addr) + zec_addr = input("Enter your zcash address: ") + # zec_addr = zXcat.new_zcash_addr() + zec_addr = 'tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ' + print(zec_addr) + addresses = {'bitcoin': btc_addr, 'zcash': zec_addr} + return addresses + +def get_fulfiller_addresses(): + btc_addr = input("Enter the bitcoin address of the party you want to trade with: ") + # btc_addr = bXcat.new_bitcoin_addr() + btc_addr = 'mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z' + print(btc_addr) + zec_addr = input("Enter the zcash address of the party you want to trade with: ") + # zec_addr = zXcat.new_zcash_addr() + zec_addr = 'tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY' + print(zec_addr) + addresses = {'bitcoin': btc_addr, 'zcash': zec_addr} + return addresses + +def authorize_buyer_fulfill(sell_p2sh_balance, sell_currency, buy_p2sh_balance, buy_currency): + input("The seller's p2sh is funded with {0} {1}, type 'enter' if this is the amount you want to buy in {1}.".format(sell_p2sh_balance, sell_currency)) + input("You have not send funds to the contract to buy {1} (requested amount: {0}), type 'enter' to allow this program to send the agreed upon funds on your behalf.".format(buy_p2sh_balance, buy_currency)) + +def authorize_seller_redeem(buy): + input("Buyer funded the contract where you offered to buy {0}, type 'enter' to redeem {1} {0} from {2}.".format(buy.currency, buy.amount, buy.p2sh)) + +def authorize_buyer_redeem(trade): + input("Seller funded the contract where you paid them in {0} to buy {1}, type 'enter' to redeem {2} {1} from {3}.".format(trade.buyContract.currency, trade.sellContract.currency, trade.sellContract.amount, trade.sellContract.p2sh)) diff --git a/utils.py b/utils.py index bbb7bd4..03d0eec 100644 --- a/utils.py +++ b/utils.py @@ -1,11 +1,22 @@ import hashlib import json +import random +import binascii + +def hex2str(hexstring): + return binascii.unhexlify(hexstring).decode('utf-8') def sha256(secret): preimage = secret.encode('utf8') h = hashlib.sha256(preimage).digest() return h +def generate_password(): + s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" + passlen = 8 + p = "".join(random.sample(s,passlen)) + return p + # TODO: Port these over to leveldb or some other database def save_trade(trade): with open('xcat.json', 'w') as outfile: @@ -23,11 +34,20 @@ def erase_trade(): with open('xcat.json', 'w') as outfile: outfile.write('') -def get_contract(): - with open('contract.json') as data_file: - contractdb = json.load(data_file) - return contractdb +# caching the secret locally for now... +def get_secret(): + with open('secret.json') as data_file: + for line in data_file: + return line.strip('\n') -def save_contract(contracts): - with open('contract.json', 'w') as outfile: - json.dump(contracts, outfile) +def save_secret(secret): + with open('secret.json', 'w') as outfile: + outfile.write(secret) + +def save(trade): + print("Saving trade") + trade = { + 'sell': trade.sellContract.__dict__, + 'buy': trade.buyContract.__dict__ + } + save_trade(trade) diff --git a/watchdata b/watchdata deleted file mode 100644 index cd25575..0000000 --- a/watchdata +++ /dev/null @@ -1,2 +0,0 @@ -7af1b1d333431217958032adc11b84278939d400a7368d33bab4e8455b4203f0 -7997c2eafeda0815b840bfbd60e5ad70d8879aac252bd594c732a04d6a7b85e9 diff --git a/watchtx.sh b/watchtx.sh deleted file mode 100755 index e1e71c5..0000000 --- a/watchtx.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# /home/jay/Zcash/python3-xcat/protocol/checktx.py $@ -echo "$@" >> "/home/jay/Zcash/python3-xcat/protocol/watchdata" diff --git a/xcat.json b/xcat.json index da70bd3..1891b41 100644 --- a/xcat.json +++ b/xcat.json @@ -1 +1 @@ -{"id": 1, "buy": {"initiator": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", "fund_tx": "9df973f468ce0b4398a9db2e8710b0ec268a3b3118f996f67f808af97e53a161", "fulfiller": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY", "p2sh": "t2QrLFUqmp1v1xQSE3hmgwcYuinRb3BRWMm", "amount": 1.2, "currency": "zcash"}, "sell": {"status": "funded", "fund_tx": "d1f679a3ee51fd563224e8114bed1d79fa6dc54d48163ec12ba17ba1d9e76baa", "p2sh": "2MuWU5BpLpqJvvzkCPq8gFHA4VFFGyvjaJf", "fulfiller": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", "initiator": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", "amount": 3.5, "currency": "bitcoin", "secret": "rabbits"}} \ No newline at end of file +{"sell": {"currency": "bitcoin", "fulfiller": "mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z", "initiator": "myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp", "amount": "0.5", "redeemScript": "63a82033fb8f68c7e079a2a35cfcd8827279f8a55fa1be04c99debd8ed3e54954e08228876a9147788b4511a25fba1092e67b307a6dcdb6da125d96702bd01b17576a914c7043e62a7391596116f54f6a64c8548e97d3fd96888ac", "fund_tx": "6cca31678d7fe6461277cea7e0614844e62ccc3ae4e247ca77d62bdd741fabef", "p2sh": "2N3rA4r6VSx65FeBTcgWWcR9HaW69DYDbrp", "redeemblocknum": 445}, "buy": {"currency": "zcash", "fulfiller": "tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY", "initiator": "tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ", "amount": "1.12", "refund_tx": "5e34f0c634fb6207e9c85ed52528629404d25db29b68931b4e0278377ee776d6", "redeemScript": "63a82033fb8f68c7e079a2a35cfcd8827279f8a55fa1be04c99debd8ed3e54954e08228876a9143ea29256c9d2888ca23de42a8b8e69ca2ec235b16702e301b17576a914c5acca6ef39c843c7a9c3ad01b2da95fe2edf5ba6888ac", "fund_tx": "8c9f9581a8cab00836b54237b30a6d68125067e3ffe6ae04413da2bdc9146daa", "p2sh": "t2UkYvcnigZt7FFp86UvTZ78qdGeLXw7tet", "redeemblocknum": 483}} \ No newline at end of file diff --git a/xcat.py b/xcat.py index c73984b..e6cff8e 100644 --- a/xcat.py +++ b/xcat.py @@ -6,6 +6,8 @@ from time import sleep import json import os, sys from pprint import pprint +from trades import Contract, Trade +import userInput def check_p2sh(currency, address): if currency == 'bitcoin': @@ -15,24 +17,6 @@ def check_p2sh(currency, address): print("Checking funds in Zcash p2sh") return zXcat.check_funds(address) -def set_price(): - trade = {} - #TODO: make currencies interchangeable. Save to a tuple? - sell = input("Which currency would you like to trade out of? (bitcoin)") - sell = 'bitcoin' - buy = 'zcash' - sell_amt = input("How much {0} do you want to sell?".format(sell)) - sell_amt = 3.5 - print(sell_amt) - buy_amt = input("How much {0} do you want to receive in exchange?".format(buy)) - buy_amt = 1.2 - print(buy_amt) - sell = {'currency': sell, 'amount': sell_amt} - buy = {'currency': buy, 'amount': buy_amt} - trade['sell'] = sell - trade['buy'] = buy - save_trade(trade) - def create_htlc(currency, funder, redeemer, secret, locktime): if currency == 'bitcoin': sell_p2sh = bXcat.hashtimelockcontract(funder, redeemer, secret, locktime) @@ -47,177 +31,166 @@ def fund_htlc(currency, p2sh, amount): txid = zXcat.fund_htlc(p2sh, amount) return txid -def initiate_trade(): - trade = get_trade() - currency = trade['sell']['currency'] - secret = input("Initiating trade: Enter a password to place the {0} you want to sell in escrow: ".format(currency)) - # TODO: hash and store secret only locally. - # secret = 'test' - print('Remember your password:', secret) - locktime = 20 # Must be more than first tx +def fund_buy_contract(trade): + buy = trade.buyContract + txid = fund_htlc(buy.currency, buy.p2sh, buy.amount) + setattr(trade.buyContract, 'fund_tx', txid) + save(trade) + return txid - # Returns contract obj - contracts = {} - contract = create_htlc(currency, trade['sell']['initiator'], trade['sell']['fulfiller'], secret, locktime) - sell_p2sh = contract['p2sh'] - contracts[contract['p2sh']] = contract - save_contract(contracts) +def fund_sell_contract(trade): + sell = trade.sellContract + txid = fund_htlc(sell.currency, sell.p2sh, sell.amount) + setattr(trade.sellContract, 'fund_tx', txid) + save(trade) + return txid - print('To complete your sell, send {0} {1} to this p2sh: {2}'.format(trade['sell']['amount'], currency, contract['p2sh'])) - response = input("Type 'enter' to allow this program to send funds on your behalf.") - print("Sent") +def create_sell_p2sh(trade, secret, locktime): + # CREATE SELL CONTRACT + sell = trade.sellContract + contract = create_htlc(sell.currency, sell.initiator, sell.fulfiller, secret, locktime) + print("sell contract", contract) + setattr(trade.sellContract, 'p2sh', contract['p2sh']) + setattr(trade.sellContract, 'redeemScript', contract['redeemScript']) + setattr(trade.sellContract, 'redeemblocknum', contract['redeemblocknum']) + save(trade) - sell_amt = trade['sell']['amount'] - txid = fund_htlc(currency, sell_p2sh, sell_amt) +def create_buy_p2sh(trade, secret, locktime): + ## CREATE BUY CONTRACT + buy = trade.buyContract + print("Now creating buy contract on the {0} blockchain where you will wait for the buyer to send funds...".format(buy.currency)) + print("HTLC DETAILS", buy.currency, buy.fulfiller, buy.initiator, secret, locktime) + buy_contract = create_htlc(buy.currency, buy.fulfiller, buy.initiator, secret, locktime) + print("Buy contract", buy_contract) - trade['sell']['p2sh'] = sell_p2sh - trade['sell']['fund_tx'] = txid - trade['sell']['status'] = 'funded' - # TODO: Save secret locally for seller - trade['sell']['secret'] = secret + setattr(trade.buyContract, 'p2sh', buy_contract['p2sh']) + setattr(trade.buyContract, 'redeemScript', buy_contract['redeemScript']) + setattr(trade.buyContract, 'redeemblocknum', buy_contract['redeemblocknum']) + print("Now contact the buyer and tell them to send funds to this p2sh: ", trade.buyContract.p2sh) - save_trade(trade) + save(trade) - buy_currency = trade['buy']['currency'] - buy_initiator = trade['buy']['initiator'] - buy_fulfiller = trade['buy']['fulfiller'] - print("Now creating buy contract on the {0} blockchain where you will wait for the buyer to send funds...".format(buy_currency)) - buy_contract = create_htlc(buy_currency, buy_fulfiller, buy_initiator, secret, locktime) - buy_p2sh = buy_contract['p2sh'] - contracts[buy_contract['p2sh']] = buy_contract - save_contract(contracts) - print("Now contact the buyer and tell them to send funds to this p2sh: ", buy_p2sh) - - trade['buy']['p2sh'] = buy_p2sh - - save_trade(trade) - -def get_addresses(): - trade = get_trade() - sell = trade['sell']['currency'] - buy = trade['buy']['currency'] - - init_offer_addr = input("Enter your {0} address: ".format(sell)) - # init_offer_addr = bXcat.new_bitcoin_addr() - init_offer_addr = 'myfFr5twPYNwgeXyjCmGcrzXtCmfmWXKYp' - print(init_offer_addr) - init_bid_addr = input("Enter your {0} address: ".format(buy)) - # init_bid_addr = zXcat.new_zcash_addr() - init_bid_addr = 'tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ' - print(init_bid_addr) - trade['sell']['initiator'] = init_offer_addr - trade['buy']['initiator'] = init_bid_addr - - fulfill_offer_addr = input("Enter the {0} address of the party you want to trade with: ".format(sell)) - # fulfill_offer_addr = bXcat.new_bitcoin_addr() - fulfill_offer_addr = 'mrQzUGU1dwsWRx5gsKKSDPNtrsP65vCA3Z' - print(fulfill_offer_addr) - fulfill_bid_addr = input("Enter the {0} address of the party you want to trade with: ".format(buy)) - # fulfill_bid_addr = zXcat.new_zcash_addr() - fulfill_bid_addr = 'tmTjZSg4pX2Us6V5HttiwFZwj464fD2ZgpY' - print(fulfill_bid_addr) - trade['sell']['fulfiller'] = fulfill_offer_addr - trade['buy']['fulfiller'] = fulfill_bid_addr - - # zec_funder, zec_redeemer = zXcat.get_keys(zec_fund_addr, zec_redeem_addr) - trade['id'] = 1 - - save_trade(trade) - -def buyer_fulfill(): - trade = get_trade() - - buy_p2sh = trade['buy']['p2sh'] - sell_p2sh = trade['sell']['p2sh'] - - buy_amount = check_p2sh(trade['buy']['currency'], buy_p2sh) - sell_amount = check_p2sh(trade['sell']['currency'], sell_p2sh) - - - amount = trade['buy']['amount'] - currency = trade['buy']['currency'] - if buy_amount == 0: - input("The seller's p2sh is funded with {0} {1}, type 'enter' if this is the amount you want to buy in {1}.".format(trade['sell']['amount'], trade['sell']['currency'])) - input("You have not send funds to the contract to buy {1} (requested amount: {0}), type 'enter' to allow this program to send the agreed upon funds on your behalf.".format(amount, currency)) - p2sh = trade['buy']['p2sh'] - txid = fund_htlc(currency, p2sh, amount) - trade['buy']['fund_tx'] = txid - - save_trade(trade) - else: - print("It looks like you've already funded the contract to buy {1}, the amount in escrow in the p2sh is {0}.".format(amount, currency)) - print("Please wait for the seller to remove your funds from escrow to complete the trade.") - - -def check_blocks(p2sh): - # blocks = [] - with open('watchdata', 'r') as infile: - for line in infile: - res = bXcat.search_p2sh(line.strip('\n'), p2sh) - # blocks.append(line.strip('\n')) - # print(blocks) - # for block in blocks: - # res = bXcat.search_p2sh(block, p2sh) - -def redeem_p2sh(currency, p2sh, action): - # action is buy or sell +def redeem_p2sh(contract, secret): + currency = contract.currency if currency == 'bitcoin': - res = bXcat.redeem(p2sh, action) + res = bXcat.auto_redeem(contract, secret) else: - res = zXcat.redeem(p2sh, action) + res = zXcat.auto_redeem(contract, secret) return res -def seller_redeem(): - # add locktime as variable? +def print_trade(role): + print("\nTrade status for {0}:".format(role)) trade = get_trade() - if 'status' in trade['buy'] and trade['buy']['status'] == 'redeemed': - print("You already redeemed the funds and acquired {0} {1}".format(trade['buy']['amount'], trade['buy']['currency'])) - exit() - else: - # Seller redeems buyer's funded tx (contract in p2sh) - p2sh = trade['buy']['p2sh'] - currency = trade['buy']['currency'] - redeem_tx = redeem_p2sh(currency, p2sh, 'buy') - trade['buy']['redeem_tx'] = redeem_tx - trade['buy']['status'] = 'redeemed' - save_trade(trade) + pprint(trade) -def buyer_redeem(): - trade = get_trade() - if 'status' in trade['sell'] and trade['sell']['status'] == 'redeemed': - print("You already redeemed the funds and acquired {0} {1}".format(trade['sell']['amount'], trade['sell']['currency'])) +#### Main functions determining user flow from command line +def buyer_redeem(trade): + userInput.authorize_buyer_redeem(trade) + if trade.sellContract.get_status() == 'redeemed': + print("You already redeemed the funds and acquired {0} {1}".format(trade.sellContract.amount, trade.sellContract.currency)) exit() else: # Buyer redeems seller's funded tx - p2sh = trade['sell']['p2sh'] - currency = trade['sell']['currency'] - redeem_tx = redeem_p2sh(currency, p2sh, 'sell') - trade['sell']['redeem_tx'] = redeem_tx - trade['sell']['status'] = 'redeemed' - save_trade(trade) + p2sh = trade.sellContract.p2sh + currency = trade.sellContract.currency + # Buy contract is where seller disclosed secret in redeeming + if trade.buyContract.currency == 'bitcoin': + secret = bXcat.parse_secret(trade.buyContract.redeem_tx) + else: + secret = zXcat.parse_secret(trade.buyContract.redeem_tx) + print("Found secret in seller's redeem tx", secret) + redeem_tx = redeem_p2sh(trade.sellContract, secret) + setattr(trade.sellContract, 'redeem_tx', redeem_tx) + save(trade) + exit() -def print_trade(role): - print("Trade status:") - trade = get_trade() - if role == 'seller': - pprint(trade) +def seller_redeem(trade): + buy = trade.buyContract + userInput.authorize_seller_redeem(buy) + + if trade.sellContract.get_status() == 'redeemed': + print("You already redeemed the funds and acquired {0} {1}".format(buy.amount, buy.currency)) + exit() else: - del trade['sell']['secret'] - pprint(trade) + # Seller redeems buyer's funded tx (contract in p2sh) + secret = userInput.retrieve_password() + tx_type, txid = redeem_p2sh(trade.buyContract, secret) + setattr(trade.buyContract, tx_type, txid) + save(trade) + print("You have redeemed {0} {1}!".format(buy.amount, buy.currency)) + print_trade('seller') + +def buyer_fulfill(trade): + buy = trade.buyContract + sell = trade.sellContract + buy_p2sh_balance = check_p2sh(buy.currency, buy.p2sh) + sell_p2sh_balance = check_p2sh(sell.currency, sell.p2sh) + + if buy_p2sh_balance == 0: + userInput.authorize_buyer_fulfill(sell_p2sh_balance, sell.currency, buy_p2sh_balance, buy.currency) + print("Buy amt:", buy.amount) + txid = fund_buy_contract(trade) + print("Fund tx txid:", txid) + else: + print("It looks like you've already funded the contract to buy {1}, the amount in escrow in the p2sh is {0}.".format(buy_p2sh_balance, buy.currency)) + print("Please wait for the seller to remove your funds from escrow to complete the trade.") + print_trade('buyer') + +def seller_initiate(trade): + # Get amounts + amounts = userInput.get_trade_amounts() + sell = amounts['sell'] + buy = amounts['buy'] + sell_currency = sell['currency'] + buy_currency = buy['currency'] + # Get addresses + init_addrs = userInput.get_initiator_addresses() + sell['initiator'] = init_addrs[sell_currency] + buy['initiator'] = init_addrs[buy_currency] + fulfill_addrs = userInput.get_fulfiller_addresses() + sell['fulfiller'] = fulfill_addrs[sell_currency] + buy['fulfiller'] = fulfill_addrs[buy_currency] + # initializing contract classes with addresses, currencies, and amounts + trade.sellContract = Contract(sell) + trade.buyContract = Contract(buy) + print(trade.sellContract.__dict__) + print(trade.buyContract.__dict__) + + secret = userInput.create_password() + # TODO: Implement locktimes and mock block passage of time + sell_locktime = 5 + buy_locktime = 10 # Must be more than first tx + + create_sell_p2sh(trade, secret, sell_locktime) + + userInput.authorize_fund_sell(trade) + + txid = fund_sell_contract(trade) + print("Sent") + + create_buy_p2sh(trade, secret, buy_locktime) + print_trade('seller') if __name__ == '__main__': print("ZEC <-> BTC XCAT (Cross-Chain Atomic Transactions)") - # TODO: Get trade indicated by id number - # TODO: pass trade into functions? - # TODO: workflow framed as currency you're trading out of being sell. appropriate? - # Have initiator propose amounts to trade + print("=" * 50) + trade = get_trade() + if trade == None: + htlcTrade = Trade() + print("New empty trade") + else: + buyContract = Contract(trade['buy']) + sellContract = Contract(trade['sell']) + htlcTrade = Trade(buyContract=buyContract, sellContract=sellContract) + try: if sys.argv[1] == 'new': erase_trade() role = 'seller' - trade = get_trade() + htlcTrade = Trade() + print("Creating new XCAT transaction...") else: role = sys.argv[1] print("Your role in demo:", role) @@ -233,46 +206,31 @@ if __name__ == '__main__': print("Trade exists, run script as buyer or seller to complete trade.") exit() - if trade is not None: - if trade['sell']['status'] == 'redeemed' and trade['buy']['status'] == 'redeemed': + if htlcTrade.buyContract is not None and htlcTrade.sellContract is not None: + if htlcTrade.sellContract.get_status() == 'redeemed' and htlcTrade.buyContract.get_status() == 'redeemed': print("This trade is already complete! Trade details:") pprint(trade) exit() if role == "seller": - if trade == None or 'status' not in trade['sell']: - set_price() - get_addresses() - initiate_trade() - print_trade('seller') - elif 'status' in trade['sell']: - if 'fund_tx' in trade['buy']: - # Means buyer has already funded the currency the transaction initiator wants to exchange into - print("Buyer funded the contract where you offered to buy {0}, redeeming funds from {1}...".format(trade['buy']['currency'], trade['buy']['p2sh'])) - seller_redeem() - print("You have redeemed {0} {1}!".format(trade['buy']['amount'], trade['buy']['currency'])) - print_trade('seller') - else: - print("Buyer has not yet funded the contract where you offered to buy {0}, please wait for them to complete their part.".format(trade['buy']['currency'])) - print_trade('seller') + if htlcTrade.sellContract == None: + seller_initiate(htlcTrade) + elif htlcTrade.buyContract.get_status() == 'funded': + seller_redeem(htlcTrade) + elif htlcTrade.buyContract.get_status() == 'empty': + print("Buyer has not yet funded the contract where you offered to buy {0}, please wait for them to complete their part.".format(htlcTrade.buyContract.currency)) else: # Need better way of preventing buyer from having secret - if 'status' not in trade['buy'] and trade['sell']['status'] == 'funded': + # if 'status' not in trade['buy'] and trade['sell']['status'] == 'funded': + if htlcTrade.sellContract.get_status() == 'funded' and htlcTrade.buyContract.get_status() != 'redeemed': print("One active trade available, fulfilling buyer contract...") - trade = get_trade() - buyer_fulfill() + buyer_fulfill(htlcTrade) # How to monitor if txs are included in blocks -- should use blocknotify and a monitor daemon? - # For regtest, can mock in a function # p2sh = trade['buy']['p2sh'] # check_blocks(p2sh) - print_trade('buyer') - elif trade['buy']['status'] == 'redeemed': + elif htlcTrade.buyContract.get_status() == 'redeemed': # Seller has redeemed buyer's tx, buyer can now redeem. - print("The seller has redeemed the contract where you paid them in {0}, now redeeming your funds from {1}".format(trade['buy']['currency'], trade['sell']['p2sh'])) - buyer_redeem() + buyer_redeem(htlcTrade) print("XCAT trade complete!") - print_trade('buyer') - - # Note: there is some little endian weirdness in the bXcat and zXcat files, need to handle the endianness of txids better & more consistently diff --git a/zXcat.py b/zXcat.py index 496db22..76e7be8 100644 --- a/zXcat.py +++ b/zXcat.py @@ -14,7 +14,7 @@ from zcash import SelectParams from zcash.core import b2x, lx, x, b2lx, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, Hash160 from zcash.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL, OP_FALSE, OP_DROP, OP_CHECKLOCKTIMEVERIFY, OP_SHA256, OP_TRUE from zcash.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH -from zcash.wallet import CBitcoinAddress, CBitcoinSecret +from zcash.wallet import CBitcoinAddress, CBitcoinSecret, P2SHBitcoinAddress, P2PKHBitcoinAddress from utils import * @@ -39,7 +39,9 @@ def hashtimelockcontract(funder, redeemer, secret, locktime): redeemerAddr = CBitcoinAddress(redeemer) h = sha256(secret) blocknum = zcashd.getblockcount() + print("Current blocknum", blocknum) redeemblocknum = blocknum + locktime + print("REDEEMBLOCKNUM ZCASH", redeemblocknum) zec_redeemScript = CScript([OP_IF, OP_SHA256, h, OP_EQUALVERIFY,OP_DUP, OP_HASH160, redeemerAddr, OP_ELSE, redeemblocknum, OP_CHECKLOCKTIMEVERIFY, OP_DROP, OP_DUP, OP_HASH160, funderAddr, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG]) @@ -49,10 +51,10 @@ def hashtimelockcontract(funder, redeemer, secret, locktime): txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey) p2sh = str(txin_p2sh_address) # Returning all this to be saved locally in p2sh.json - return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'zec_redeemScript': b2x(zec_redeemScript), 'redeemer': redeemer, 'funder': funder} + return {'p2sh': p2sh, 'redeemblocknum': redeemblocknum, 'redeemScript': b2x(zec_redeemScript), 'redeemer': redeemer, 'funder': funder} def fund_htlc(p2sh, amount): - send_amount = amount*COIN + send_amount = float(amount)*COIN fund_txid = zcashd.sendtoaddress(p2sh, send_amount) txid = b2x(lx(b2x(fund_txid))) return txid @@ -70,68 +72,168 @@ def get_tx_details(txid): fund_txinfo = zcashd.gettransaction(txid) return fund_txinfo['details'][0] -def redeem(p2sh, action): - contracts = get_contract() - trade = get_trade() - for key in contracts: - if key == p2sh: - contract = contracts[key] - if contract: - print("Redeeming tx in p2sh", p2sh) - # TODO: Have to get tx info from saved contract p2sh - redeemblocknum = contract['redeemblocknum'] - zec_redeemScript = contract['zec_redeemScript'] +def find_transaction_to_address(p2sh): + zcashd.importaddress(p2sh, "", False) + txs = zcashd.listunspent() + for tx in txs: + # print("tx addr:", tx['address']) + # print(type(tx['address'])) + # print(type(p2sh)) + if tx['address'] == CBitcoinAddress(p2sh): + print("Found tx to p2sh", p2sh) + print(tx) + return tx - txid = trade[action]['fund_tx'] - details = get_tx_details(txid) - print("Txid for fund tx", txid) - # must be little endian hex - txin = CMutableTxIn(COutPoint(lx(txid), details['vout'])) - redeemPubKey = CBitcoinAddress(contract['redeemer']) - amount = trade[action]['amount'] * COIN - print("amount: {0}, fee: {1}".format(amount, FEE)) - txout = CMutableTxOut(amount - FEE, redeemPubKey.to_scriptPubKey()) - # Create the unsigned raw transaction. - tx = CMutableTransaction([txin], [txout]) - # nLockTime needs to be at least as large as parameter of CHECKLOCKTIMEVERIFY for script to verify - # TODO: these things like redeemblocknum should really be properties of a tx class... - # Need: redeemblocknum, zec_redeemScript, secret (for creator...), txid, redeemer... - # Is stored as hex, must convert to bytes - zec_redeemScript = CScript(x(zec_redeemScript)) +# def get_tx_details(txid): +# # This method is problematic I haven't gotten the type conversions right +# print(bytearray.fromhex(txid)) +# print(b2x(bytearray.fromhex(txid))) +# fund_txinfo = zcashd.gettransaction(bytearray.fromhex(txid)) +# print(fund_txinfo) +# +# return fund_txinfo['details'][0] - tx.nLockTime = redeemblocknum - sighash = SignatureHash(zec_redeemScript, tx, 0, SIGHASH_ALL) - # TODO: figure out how to better protect privkey? - privkey = zcashd.dumpprivkey(redeemPubKey) - sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) - - # TODO: Figure out where to store secret preimage securely. Parse from scriptsig of redeemtx - secret = trade['sell']['secret'] - preimage = secret.encode('utf-8') - print('preimage', preimage) +def find_secret(p2sh): + return parse_secret('4c25b5db9f3df48e48306891d8437c69308afa122f92416df1a3ba0d3604882f') + zcashd.importaddress(p2sh, "", False) + # is this working? + txs = zcashd.listtransactions() + for tx in txs: + # print("tx addr:", tx['address']) + # print(type(tx['address'])) + # print(type(p2sh)) + if (tx['address'] == p2sh ) and (tx['category'] == "send"): + print(type(tx['txid'])) + print(str.encode(tx['txid'])) + raw = zcashd.getrawtransaction(lx(tx['txid']),True)['hex'] + decoded = zcashd.decoderawtransaction(raw) + print("deo:", decoded['vin'][0]['scriptSig']['asm']) - # print('zec_redeemScript', zec_redeemScript) - txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript]) - # print("Redeem tx hex:", b2x(tx.serialize())) +def parse_secret(txid): + raw = zcashd.gettransaction(lx(txid), True)['hex'] + # print("Raw", raw) + decoded = zcashd.decoderawtransaction(raw) + scriptSig = decoded['vin'][0]['scriptSig'] + print("Decoded", scriptSig) + asm = scriptSig['asm'].split(" ") + pubkey = asm[1] + secret = hex2str(asm[2]) + redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey)) + print('redeemPubkey', redeemPubkey) + print(secret) + return secret - # Can only call to_p2sh_scriptPubKey on CScript obj - txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() +# redeems automatically after buyer has funded tx, by scanning for transaction to the p2sh +# i.e., doesn't require buyer telling us fund txid - # print("txin.scriptSig", b2x(txin.scriptSig)) - # print("txin_scriptPubKey", b2x(txin_scriptPubKey)) - # print('tx', tx) - VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) - print("Script verified, sending raw tx...") - print("Raw tx of prepared redeem tx: ", b2x(tx.serialize())) - txid = zcashd.sendrawtransaction(tx) - txhex = b2x(lx(b2x(txid))) - print("Txid of submitted redeem tx: ", txhex) - return txhex +def auto_redeem(contract, secret): + # How to find redeemScript and redeemblocknum from blockchain? + print("Contract in auto redeem", contract.__dict__) + p2sh = contract.p2sh + #checking there are funds in the address + amount = check_funds(p2sh) + if(amount == 0): + print("address ", p2sh, " not funded") + quit() + fundtx = find_transaction_to_address(p2sh) + amount = fundtx['amount'] / COIN + print("Found fundtx:", fundtx) + p2sh = P2SHBitcoinAddress(p2sh) + if fundtx['address'] == p2sh: + print("Found {0} in p2sh {1}, redeeming...".format(amount, p2sh)) + + # Where can you find redeemblocknum in the transaction? + redeemblocknum = find_redeemblocknum(contract) + blockcount = zcashd.getblockcount() + print("\nCurrent blocknum at time of redeem on Zcash:", blockcount) + if blockcount < redeemblocknum: + redeemPubKey = find_redeemAddr(contract) + print('redeemPubKey', redeemPubKey) + zec_redeemScript = CScript(x(contract.redeemScript)) + txin = CMutableTxIn(fundtx['outpoint']) + txout = CMutableTxOut(fundtx['amount'] - FEE, redeemPubKey.to_scriptPubKey()) + # Create the unsigned raw transaction. + tx = CMutableTransaction([txin], [txout]) + sighash = SignatureHash(zec_redeemScript, tx, 0, SIGHASH_ALL) + # TODO: figure out how to better protect privkey + privkey = zcashd.dumpprivkey(redeemPubKey) + sig = privkey.sign(sighash) + bytes([SIGHASH_ALL]) + print("SECRET", secret) + preimage = secret.encode('utf-8') + txin.scriptSig = CScript([sig, privkey.pub, preimage, OP_TRUE, zec_redeemScript]) + + print("txin.scriptSig", b2x(txin.scriptSig)) + txin_scriptPubKey = zec_redeemScript.to_p2sh_scriptPubKey() + print('Redeem txhex', b2x(tx.serialize())) + VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,)) + print("script verified, sending raw tx") + txid = zcashd.sendrawtransaction(tx) + print("Txid of submitted redeem tx: ", b2x(lx(b2x(txid)))) + print("TXID SUCCESSFULLY REDEEMED") + return 'redeem_tx', b2x(lx(b2x(txid))) + else: + # if blockcount >= redeemblocknum: + # tx.nLockTime = redeemblocknum + print("nLocktime exceeded, refunding") + refundPubKey = find_refundAddr(contract) + print('refundPubKey', refundPubKey) + txid = zcashd.sendtoaddress(refundPubKey, fundtx['amount'] - FEE) + print("Txid of refund tx:", b2x(lx(b2x(txid)))) + print("TXID SUCCESSFULLY REFUNDED") + return 'refund_tx', b2x(lx(b2x(txid))) else: print("No contract for this p2sh found in database", p2sh) +def parse_script(script_hex): + redeemScript = zcashd.decodescript(script_hex) + scriptarray = redeemScript['asm'].split(' ') + return scriptarray + +def find_redeemblocknum(contract): + scriptarray = parse_script(contract.redeemScript) + redeemblocknum = scriptarray[8] + return int(redeemblocknum) + +def find_redeemAddr(contract): + scriptarray = parse_script(contract.redeemScript) + redeemer = scriptarray[6] + redeemAddr = P2PKHBitcoinAddress.from_bytes(x(redeemer)) + return redeemAddr + +def find_refundAddr(contract): + scriptarray = parse_script(contract.redeemScript) + funder = scriptarray[13] + refundAddr = P2PKHBitcoinAddress.from_bytes(x(funder)) + return refundAddr + +def find_recipient(contract): + # make this dependent on actual fund tx to p2sh, not contract + txid = contract.fund_tx + raw = zcashd.gettransaction(lx(txid), True)['hex'] + # print("Raw", raw) + decoded = zcashd.decoderawtransaction(raw) + scriptSig = decoded['vin'][0]['scriptSig'] + print("Decoded", scriptSig) + asm = scriptSig['asm'].split(" ") + pubkey = asm[1] + initiator = CBitcoinAddress(contract.initiator) + fulfiller = CBitcoinAddress(contract.fulfiller) + print("Initiator", b2x(initiator)) + print("Fulfiler", b2x(fulfiller)) + print('pubkey', pubkey) + redeemPubkey = P2PKHBitcoinAddress.from_pubkey(x(pubkey)) + print('redeemPubkey', redeemPubkey) + +# addr = CBitcoinAddress('tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ') +# print(addr) +# # print(b2x('tmFRXyju7ANM7A9mg75ZjyhFW1UJEhUPwfQ')) +# print(b2x(addr)) def new_zcash_addr(): addr = zcashd.getnewaddress() print('new ZEC addr', addr.to_p2sh_scriptPubKey) return addr.to_scriptPubKey() + +def generate(num): + blocks = zcashd.generate(num) + return blocks diff --git a/zcashcli.py b/zcashcli.py index e3ab6b5..32bd8ab 100644 --- a/zcashcli.py +++ b/zcashcli.py @@ -16,6 +16,7 @@ from zcash.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH from zcash.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH from zcash.wallet import CBitcoinAddress, CBitcoinSecret import hashlib +import binascii # SelectParams('testnet') SelectParams('regtest')