Refactor to use python-bitcoinlib rpc proxy

This commit is contained in:
Jay Graber 2017-05-12 15:59:12 -07:00
parent 3e1bb1fbaf
commit 75c253994a
23 changed files with 78 additions and 464 deletions

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# 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.
The ZBXCAT directory contains BitcoinRPC and ZcashRPC, wrappers around the rpc interface that can be imported as Python modules.
The settings.py file in BitcoinRPC and ZcashRPC parse the config files for username/password and set the network ports.
Most functions are named exactly the same as the rpc methods, except for a few additional custom functions that do things like return only the version number.
**EDIT**: The scripts now use the rpc proxy code in python-bitcoinlib, and ZDaemon's functions will be refactored into python-zcashlib (a Zcash fork of python-bitcoinlib)
## Current status of scripts
Run `redeem-preimage-p2sh.py` to test. It creates and redeems a p2sh transaction using a preimage. To successfully run it, you need python3, the dependencies installed, and a bitcoin daemon running in regtest mode.
(Currently only tested on Bitcoin. Need to verify that the Zcash fork of python-bitcoinlib, one of the dependencies, works properly, then figure out the best way to install it.)
`bitcoin-swap.py` contains all the functions that use a proxy to interact with a Bitcoin daemon.
Use python3 to test. To create a virtualenv for python3, run this command from the top level of the directory:
```
virtualenv -p python3 venv
source venv/bin/activate
```
Install dependencies for ZBXCAT: `pip install -r requirements.txt`
## Installing python-zcashlib for testing and editing
The Zcash fork of python-bitcoinlib that is currently in progress:
`git clone https://github.com/arcalinea/python-bitcoinlib/tree/zcashlib`
You can install this module locally through pip, in editable mode, so that changes you make are applied immediately. For install from local filesystem path:
`pip install --editable (-e) <path-to-zcashlib>`
## 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.

View File

@ -1,103 +0,0 @@
from __future__ import print_function
from __future__ import absolute_import
import requests
import json
from .settings import *
class BDaemon(object):
id_count = 0
def __init__(self, network=NETWORK, user=RPCUSER, password=RPCPASSWORD, timeout=TIMEOUT):
#TODO: check utf safety
url = network_url(network)
print('In mode:', network)
self.network = url
self.user = user.encode('utf8')
self.password = password.encode('utf8')
self.timeout = timeout
def _call(self, method, *args):
jsondata = json.dumps({ 'version': '2',
'method': method,
'params': args,
'id': self.id_count})
r = requests.post(self.network, auth=(self.user,self.password), data=jsondata, timeout=self.timeout)
self.id_count += 1
resp = json.loads(r.text)
#TODO: deal with errors better.
error = resp['error']
if error:
print(error)
return resp['result']
# REGTEST
def generate(self, blocknum):
return self._call('generate', blocknum)
def importaddress(self, script):
return self._call('importaddress', script)
#Block Info
def getBlockHash(self, blockheight):
return self._call('getblockhash', blockheight)
def getBlockByHash(self, blockhash):
return self._call('getblock', blockhash)
def getBlockByHeight(self, blockheight):
return self.getBlockByHash(self.getBlockHash(blockheight))
# Custom methods to get Network Info
def getNetworkHeight(self):
return self._call('getblockcount')
def getNetworkDifficulty(self):
return self._call('getdifficulty')
def getVersion(self):
info = self._call('getnetworkinfo')
client = info['subversion']
version = client.strip('/').split(':')[1]
return version
def getConnectionCount(self):
return self._call('getconnectioncount')
# Wallet Info
def getbalance(self):
return self._call('getbalance')
def listunspent(self, minconf=1):
return self._call('listunspent', minconf)
#Raw Txs
def gettransaction(self, txid):
return self._call('gettransaction', txid)
def getrawtransaction(self, txid, verbose=0):
# default verbose=0 returns serialized, hex-encoded data
# verbose=1, returns a JSON obj of tx
return self._call('getrawtransaction', txid, verbose)
def decoderawtransaction(self, txhex):
return self._call('decoderawtransaction', txhex)
def sendrawtransaction(self, txhex):
return self._call('sendrawtransaction', txhex)
def getnewaddress(self):
return self._call('getnewaddress')
def sendtoaddress(self, taddress, amount):
return self._call('sendtoaddress', taddress, amount)
def listunspent(self):
return self._call('listunspent')

View File

@ -1,28 +0,0 @@
import os, re
TIMEOUT = 100
#Default fee to use on network for txs.
DEFAULT_FEE = 0.01
# Default network is testnet
NETWORK = 'TESTNET'
def network_url(network):
if network == 'TESTNET':
return "http://localhost:18332"
if network == 'REGTEST':
return "http://localhost:18332"
if network == 'MAINNET':
return "http://localhost:8332"
bitcoinconf = os.path.expanduser('~/.bitcoin/bitcoin.conf')
def read_config(filename):
f = open(filename)
for line in f:
if re.match('rpcuser', line):
user = line.strip('\n').split('=')[1]
if re.match('rpcpassword', line):
password = line.strip('\n').split('=')[1]
return (user, password)
config = read_config(bitcoinconf)
# from bitcoin.conf
RPCUSER = config[0]
RPCPASSWORD = config[1]

View File

@ -1 +0,0 @@
Python wrapper for bitcoind and zcashd, created for ZBXCAT - Zcash Bitcoin Cross-Chain Atomic Transactions.

View File

@ -1,160 +0,0 @@
from __future__ import print_function
from __future__ import absolute_import
import requests
import json
from .settings import *
class ZDaemon(object):
id_count = 0
def __init__(self, network=NETWORK, user=RPCUSER, password=RPCPASSWORD, timeout=TIMEOUT):
#TODO: check utf safety
url = network_url(network)
print('In mode:', network)
self.network = url
self.user = user.encode('utf8')
self.password = password.encode('utf8')
self.timeout = timeout
def _call(self, method, *args):
jsondata = json.dumps({ 'version': '2',
'method': method,
'params': args,
'id': self.id_count})
print("jsondata", jsondata)
r = requests.post(self.network, auth=(self.user,self.password), data=jsondata, timeout=self.timeout)
self.id_count += 1
resp = json.loads(r.text)
#TODO: deal with errors better.
error = resp['error']
if error:
print(error)
return resp['result']
# REGTEST
def generate(self, blocknum):
return self._call('generate', blocknum)
def importaddress(self, script):
return self._call('importaddress', script)
#Block Info
def getBlockHash(self, blockheight):
return self._call('getblockhash', blockheight)
def getBlockByHash(self, blockhash):
return self._call('getblock', blockhash)
def getBlockByHeight(self, blockheight):
return self.getBlockByHash(self.getBlockHash(blockheight))
# Custom methods to get Network Info
def getNetworkHeight(self):
return self._call('getblockcount')
def getNetworkDifficulty(self):
return self._call('getdifficulty')
def getVersion(self):
info = self._call('getnetworkinfo')
client = info['subversion']
version = client.strip('/').split(':')[1]
return version
def getConnectionCount(self):
return self._call('getconnectioncount')
# Wallet Info (transparent)
def getbalance(self):
return self._call('getbalance')
def listunspent(self, minconf=1):
return self._call('listunspent', minconf)
#Raw Txs
def gettransaction(self, txid):
return self._call('gettransaction', txid)
def getrawtransaction(self, txid, verbose=0):
# default verbose=0 returns serialized, hex-encoded data
# verbose=1, returns a JSON obj of tx
return self._call('getrawtransaction', txid, verbose)
def decoderawtransaction(self, txhex):
return self._call('decoderawtransaction', txhex)
def sendrawtransaction(self, txhex):
return self._call('sendrawtransaction', txhex)
# taddr methods
def getnewaddress(self):
return self._call('getnewaddress')
def sendtoaddress(self, taddress, amount):
return self._call('sendtoaddress', taddress, amount)
def listunspent(self):
return self._call('listunspent')
# Custom method to find a taddr with spendable utxos for z_sendmany
def find_taddr_with_unspent(self):
unspent = self._call('listunspent')
for utxo in unspent:
if utxo['spendable'] == True and utxo['amount'] > 0.1:
# Check that it's not a coinbase tx
tx = zd.gettransaction(utxo['txid'])
if 'generated' not in tx:
return tx['address']
def sweep_coinbase(self, zaddr):
cb = []
utxos = self.listunspent()
for utxo in utxos:
tx = self.gettransaction(utxo['txid'])
if 'generated' in tx and tx['generated'] == True:
cb.append(utxo)
for coin in cb:
amount = coin['amount'] - 0.0001
opid = self.z_sendmany(coin['address'], zaddr, amount)
print("OPID of z_sendmany: ", opid)
status = self.z_getoperationstatus(opid)
print("Status: ", status[0]['status'])
# zaddr methods
def z_gettotalbalance(self):
return self._call('z_gettotalbalance')
def z_getnewaddress(self):
return self._call('z_getnewaddress')
def z_listaddresses(self):
return self._call('z_listaddresses')
def z_listreceivedbyaddress(self, zaddr, minconf=1):
return self._call('z_listreceivedbyaddress', zaddr, minconf)
def z_getoperationstatus(self, opid):
return self._call('z_getoperationstatus', ["{0}".format(opid)])
def z_getoperationresult(self, opid):
return self._call('z_getoperationresult', ["{0}".format(opid)])
# With addition of encrypted memo field
def z_sendmany(self, sender, receiver, amount=0.0001, memo=''):
amts_array = []
if memo == '':
amounts = {"address": receiver, "amount": amount}
else:
memo = memo.encode('hex')
amounts = {"address": receiver, "amount": amount, "memo": memo}
amts_array.append(amounts)
return self._call('z_sendmany', sender, amts_array)

Binary file not shown.

Binary file not shown.

View File

@ -1,38 +0,0 @@
import os, re
#Timeout needs to be high for any pour operations
TIMEOUT = 600
#Default fee to use on network for txs.
DEFAULT_FEE = 0.01
# Default is testnet
NETWORK = 'TESTNET'
def network_url(network):
if network == 'TESTNET':
return "http://localhost:18232"
if network == 'REGTEST':
return "http://localhost:18232"
if network == 'MAINNET':
return "http://localhost:18444"
zcashconf = os.path.expanduser('~/.zcash/zcash.conf')
def read_config(filename):
f = open(filename)
for line in f:
if re.match('rpcuser', line):
user = line.strip('\n').split('=')[1]
if re.match('rpcpassword', line):
password = line.strip('\n').split('=')[1]
return (user, password)
config = read_config(zcashconf)
# from zcash conf
RPCUSER = config[0]
RPCPASSWORD = config[1]
#TESTS
#for tests (sample data here - replace with your own)
TEST_TXID = ''
TEST_ZADDR = ""
TEST_TADDR = ""
TEST_ZSECRET = ""

Binary file not shown.

View File

Binary file not shown.

View File

@ -1,2 +0,0 @@
requests
python-bitcoinlib

View File

@ -1,38 +0,0 @@
from __future__ import print_function
from __future__ import absolute_import
import os.path
import sys
from .rpc.ZDaemon import *
from settings import *
def test_daemon():
zd = ZDaemon()
# Network tests
print(zd.getBlockHash(100))
print(zd.getBlockByHash(zd.getBlockHash(100)))
print(zd.getBlockByHeight(100))
print(zd.getNetworkHeight())
print(zd.getNetworkDifficulty())
print(zd.getVersion())
print(zd.getConnectionCount())
# Taddr Wallet tests
print(zd.getbalance())
print(zd.listunspent())
print(zd.getnewaddress())
# Zaddr wallet tests
print(zd.z_getnewaddress())
zaddrs = zd.z_listaddresses()
print(zaddrs)
zaddr_received = zd.z_listreceivedbyaddress(zaddr)
print(zaddr_received)
# TODO: test z_sendmany, and use regtest
if __name__ == "__main__":
# test_daemon()
zd = ZDaemon()
print(zd.getVersion())

View File

@ -1,35 +0,0 @@
from ZBXCAT.BitcoinRPC.BDaemon import *
bd = BDaemon('REGTEST')
# v = bd.getVersion()
# print(v)
def generate(num):
gen = bd.generate(num)
print("Generated blocks", gen)
def fund_p2sh(p2sh, amount):
fund_tx = bd.sendtoaddress(p2sh, amount)
return fund_tx
def tx_details(txid):
tx = bd.gettransaction(txid)
details = tx['details'][0]
return details
# These two methods are placeholders
def get_recipient_address():
address = bd.getnewaddress()
return address
def get_sender_address():
address = bd.getnewaddress()
return address
def importaddress(addr):
res = bd.importaddress(addr)
return res
def sendrawtx(hex):
txid = bd.sendrawtransaction(hex)
return txid

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3
# Based on spend-p2sh-txout.py from python-bitcoinlib.
# Copyright (C) 2014 The python-bitcoinlib developers
# Copyright (C) 2017 The Zcash developers
import sys
@ -9,6 +8,8 @@ if sys.version_info.major < 3:
sys.stderr.write('Sorry, Python 3.x required by this example.\n')
sys.exit(1)
import bitcoin
import bitcoin.rpc
from bitcoin import SelectParams
from bitcoin.core import b2x, lx, COIN, COutPoint, CMutableTxOut, CMutableTxIn, CMutableTransaction, Hash160
from bitcoin.core.script import CScript, OP_DUP, OP_IF, OP_ELSE, OP_ENDIF, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG, SignatureHash, SIGHASH_ALL
@ -17,23 +18,21 @@ from bitcoin.core.scripteval import VerifyScript, SCRIPT_VERIFY_P2SH
from bitcoin.wallet import CBitcoinAddress, CBitcoinSecret
import hashlib
from daemon import *
SelectParams('testnet')
# SelectParams('testnet')
# To get transactions not in your wallet, must set -txindex=1
SelectParams('regtest')
proxy = bitcoin.rpc.Proxy()
# The parameters needed for the htlc - hash preimage, sender/seller address, recipient/buyer address, num of blocks for timeout
preimage = b'preimage'
h = hashlib.sha256(preimage).digest()
seckey = CBitcoinSecret('cShLodcaVA7JjDXnRaK5jsmQhPJmBDeXPd4Fzx8dRD9ih6gmXKH6')
# AUTOMATE - get recipient addr
recipient_address = get_recipient_address()
print(' recipient_address (newly generated)', recipient_address)
sender_address = get_sender_address()
print(' sender_address (newly generated)', sender_address)
recipientpubkey = CBitcoinAddress('mheZcjatFMjcHX5hVQdAY4Lvxm7q7rXuU2')
senderpubkey = CBitcoinAddress('mheZcjatFMjcHX5hVQdAY4Lvxm7q7rXuU2')
# proxy.getnewaddress() returns CBitcoinAddress
recipientpubkey = proxy.getnewaddress()
senderpubkey = proxy.getnewaddress()
# privkey of the recipient, used to sign the redeemTx
seckey = proxy.dumpprivkey(recipientpubkey)
blocknum = 7
# Create a htlc redeemScript. Similar to a scriptPubKey the redeemScript must be
@ -43,55 +42,36 @@ txin_redeemScript = CScript([OP_IF, OP_SHA256, h, OP_EQUALVERIFY,OP_DUP, OP_HASH
senderpubkey, OP_ENDIF,OP_EQUALVERIFY, OP_CHECKSIG])
print("redeem script:", b2x(txin_redeemScript))
#print("scriptpubey:", b2x(recipientpubkey.to_scriptPubKey()))
# Create P2SH scriptPubKey from redeemScript.
# Create P2SH scriptPubKey from redeemScript.
txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey()
print("p2sh_scriptPubKey", b2x(txin_scriptPubKey))
# Convert the P2SH scriptPubKey to a base58 Bitcoin address and print it.
# You'll need to send some funds to it to create a txout to spend.
# Convert the P2SH scriptPubKey to a base58 Bitcoin address
txin_p2sh_address = CBitcoinAddress.from_scriptPubKey(txin_scriptPubKey)
print('Pay to:',str(txin_p2sh_address))
p2sh = str(txin_p2sh_address)
print('Pay to:', p2sh)
#AUTOMATE funding tx, seller sends funds
amount = 1.0
fund_tx = fund_p2sh(p2sh, amount)
print("TXID", fund_tx)
# AUTOMATE import this address?
importaddress(p2sh)
print('p2sh address imported')
# MINE THE FUNDED TX
generate(1)
# AUTOMATE Send funds to p2sh
amount = 1.0*COIN
fund_tx = proxy.sendtoaddress(txin_p2sh_address, amount)
print('Now redeeming.........')
# lx() takes *little-endian* hex and converts it to bytes; in Bitcoin
# transaction hashes are shown little-endian rather than the usual big-endian.
# AUTOMATE
txid = lx(fund_tx)
details = tx_details(fund_tx)
print('get details of fund_tx', details)
# AUTOMATE getting vout of funding tx
txinfo = proxy.gettransaction(fund_tx)
details = txinfo['details'][0]
vout = details['vout']
print('vout', vout)
# Create the txin structure, which includes the outpoint. The scriptSig
# defaults to being empty.
txin = CMutableTxIn(COutPoint(txid, vout))
# Create the txin structure. scriptSig defaults to being empty.
# The input is the p2sh funding transaction txid, vout is its index
txin = CMutableTxIn(COutPoint(fund_tx, vout))
# Create the txout. This time we create the scriptPubKey from a Bitcoin
# address.
# AUTOMATE: amount and address set above
redeemed = amount*COIN
fee = 0.001*COIN
txout = CMutableTxOut(redeemed - fee, CBitcoinAddress(recipient_address).to_scriptPubKey())
# Create the txout. Pays out to recipient, so uses recipient's pubkey
# Withdraw full amount minus fee
default_fee = 0.001*COIN
txout = CMutableTxOut(amount - default_fee, recipientpubkey.to_scriptPubKey())
# Create the unsigned transaction.
# Create the unsigned raw transaction.
tx = CMutableTransaction([txin], [txout])
# Calculate the signature hash for that transaction. Note how the script we use
@ -103,19 +83,13 @@ sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL)
sig = seckey.sign(sighash) + bytes([SIGHASH_ALL])
# Set the scriptSig of our transaction input appropriately.
txin.scriptSig = CScript([ sig,seckey.pub, preimage, OP_TRUE, txin_redeemScript])
txin.scriptSig = CScript([ sig, seckey.pub, preimage, OP_TRUE, txin_redeemScript])
print("scriptSig:", b2x(txin.scriptSig))
print("Redeem tx hex:", b2x(tx.serialize()))
# Verify the signature worked.
VerifyScript(txin.scriptSig, txin_scriptPubKey, tx, 0, (SCRIPT_VERIFY_P2SH,))
# Tx to hex
hextx = b2x(tx.serialize())
print(hextx)
# AUTOMATE: Send raw redeem tx
txid = sendrawtx(hextx)
print("txid of submitted redeem tx", txid)
generate(1)
print("Now sending redeem transaction.......")
txid = proxy.sendrawtransaction(tx)
print("Txid of submitted redeem tx: ", b2x(txid))