Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python
- """
- bitpaint.py
- ~~~~~~~~~~~
- Simple command-line utility to use the Bitcoin blockchain to track so-called "colored coins", or "smart contracts/securities".
- Create "colored coin", receive colored coin, pay to owners of colored coins.
- Features:
- - Tracking using bitcoind and blockchain.info
- - Generate address for colored coin (so you can store
- coins outside your normal wallet, so that the satoshi
- client doesn't accidentally spend them)
- - Start tracking a "colored coin": "paint" a coin
- - Generate list of holders of colored coin (btc-address and
- amount of coin held)
- - Transfer colored coin from one address to another
- Upcoming features:
- - Pay from wallet to colored coin owners
- - Forward non-colored payments from asset-addresses to
- wallet-addresses
- Quick guide - issue "security":
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1. Create a holding address:
- $ python bitpaint.py --new-address
- 2. Transfer the coins you want to color to that address from your
- regular wallet.
- 3. Make a note of the transaction id <txid> and the output number
- that transferred the coins to the holding address <n>.
- 4. Start tracking from the output of that transaction:
- $ python bitpaint.py --paint <name>:<txid>:<n>
- After these steps, the address you generated holds the full colored
- coin. <name> is any name you would like to give this colored coin.
- <name> only exists locally.
- Quick guide - transfer ownership
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1. Ask receiver to download this script and generate a holding
- address, <receiver_address>
- 2. Transfer the colored coin:
- $ python --transfer-from <addr>:<txid>:<n> --transfer-to <receiver_address1>:<amount1>,<receiver_address2>:<amount2>,...
- Quick guide - list current owners
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1. Update the list of owners of an asset <name>:
- $ python --update-ownership <name>
- 2. Show the list of owners:
- $ python --owners <name>
- Quick guide - list "colored coins" I own
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 1. Show the list of colored coins owned by my addresses:
- $ python --my-holdings
- Caution:
- - Private keys are stored in plaintext in your configuration file
- - Do not allow the "colored coin" to be used in a transaction with
- multiple inputs. That would mess up the tracking. I suggest avoid
- using holding-addresses in your normal wallet, and just use this
- client to manage your colored coins.
- - Since there is no transaction fee included, you cannot be sure that
- the transaction will be confirmed
- Donations welcome: 1GsnaaAWYMb7yPwBQ19bSLLMTtsfSyjqg3
- """
- # Import libraries
- import urllib2, ConfigParser, ctypes, ctypes.util, hashlib, simplejson as json, os
- from jsonrpc import ServiceProxy
- from optparse import OptionParser
- ### Start: Generic helpers
- def JSONtoAmount(value):
- return long(round(value * 1e8))
- def AmountToJSON(amount):
- return float(amount / 1e8)
- ### End: Generic helpers
- ### Start: Create/Read Config
- # Here we create a configuration file if one does not exist already.
- # User is asked for details about his bitcoind so the script can connect.
- config_file = "bitpaint.conf"
- basic_bitpaint_conf = """[bitcoind]
- rpchost = %s
- rpcport = %s
- rpcuser = %s
- rpcpwd = %s
- [HoldingAddresses]
- addresses =
- private_keys =
- """
- # If the config file does not exists, ask user for details and write one
- if not os.path.exists(config_file):
- print "Configuration file bitpaint.conf not found. Creating one..."
- host = raw_input("bitcoind rpc host (default: 127.0.0.1): ")
- if len(host) == 0: host = "127.0.0.1"
- port = raw_input("bitcoind rpc port (default: 8332): ")
- if len(port) == 0: port = "8332"
- user = raw_input("bitcoind rpc username (default: <blank>: ")
- pwd = raw_input("bitcoind rpc password (default: <blank>: ")
- f = open(config_file, 'w')
- f.write(basic_bitpaint_conf % (host,port,user,pwd))
- f.close()
- # Parse the config file
- config = ConfigParser.ConfigParser()
- config.read(config_file)
- rpchost = config.get('bitcoind', 'rpchost')
- rpcport = config.get('bitcoind', 'rpcport')
- rpcuser = config.get('bitcoind', 'rpcuser')
- rpcpwd = config.get('bitcoind', 'rpcpwd')
- # Connect to bitcoind
- bitcoind_connection_string = "http://%s:%s@%s:%s" % (rpcuser,rpcpwd,rpchost,rpcport)
- sp = ServiceProxy(bitcoind_connection_string)
- ### End: Create/Read Config
- ### Start: Address Generation code
- # The following code was yoinked from addrgen.py
- # Thanks to: Joric/bitcoin-dev, june 2012, public domain
- ssl = ctypes.cdll.LoadLibrary (ctypes.util.find_library ('ssl') or 'libeay32')
- def check_result (val, func, args):
- if val == 0: raise ValueError
- else: return ctypes.c_void_p (val)
- ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p
- ssl.EC_KEY_new_by_curve_name.errcheck = check_result
- class KEY:
- def __init__(self):
- NID_secp256k1 = 714
- self.k = ssl.EC_KEY_new_by_curve_name(NID_secp256k1)
- self.compressed = False
- self.POINT_CONVERSION_COMPRESSED = 2
- self.POINT_CONVERSION_UNCOMPRESSED = 4
- def __del__(self):
- if ssl:
- ssl.EC_KEY_free(self.k)
- self.k = None
- def generate(self, secret=None):
- if secret:
- self.prikey = secret
- priv_key = ssl.BN_bin2bn(secret, 32, ssl.BN_new())
- group = ssl.EC_KEY_get0_group(self.k)
- pub_key = ssl.EC_POINT_new(group)
- ctx = ssl.BN_CTX_new()
- ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx)
- ssl.EC_KEY_set_private_key(self.k, priv_key)
- ssl.EC_KEY_set_public_key(self.k, pub_key)
- ssl.EC_POINT_free(pub_key)
- ssl.BN_CTX_free(ctx)
- return self.k
- else:
- return ssl.EC_KEY_generate_key(self.k)
- def get_pubkey(self):
- size = ssl.i2o_ECPublicKey(self.k, 0)
- mb = ctypes.create_string_buffer(size)
- ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb)))
- return mb.raw
- def get_secret(self):
- bn = ssl.EC_KEY_get0_private_key(self.k);
- bytes = (ssl.BN_num_bits(bn) + 7) / 8
- mb = ctypes.create_string_buffer(bytes)
- n = ssl.BN_bn2bin(bn, mb);
- return mb.raw.rjust(32, chr(0))
- def set_compressed(self, compressed):
- self.compressed = compressed
- if compressed:
- form = self.POINT_CONVERSION_COMPRESSED
- else:
- form = self.POINT_CONVERSION_UNCOMPRESSED
- ssl.EC_KEY_set_conv_form(self.k, form)
- def dhash(s):
- return hashlib.sha256(hashlib.sha256(s).digest()).digest()
- def rhash(s):
- h1 = hashlib.new('ripemd160')
- h1.update(hashlib.sha256(s).digest())
- return h1.digest()
- b58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
- def base58_encode(n):
- l = []
- while n > 0:
- n, r = divmod(n, 58)
- l.insert(0,(b58_digits[r]))
- return ''.join(l)
- def base58_decode(s):
- n = 0
- for ch in s:
- n *= 58
- digit = b58_digits.index(ch)
- n += digit
- return n
- def base58_encode_padded(s):
- res = base58_encode(int('0x' + s.encode('hex'), 16))
- pad = 0
- for c in s:
- if c == chr(0):
- pad += 1
- else:
- break
- return b58_digits[0] * pad + res
- def base58_decode_padded(s):
- pad = 0
- for c in s:
- if c == b58_digits[0]:
- pad += 1
- else:
- break
- h = '%x' % base58_decode(s)
- if len(h) % 2:
- h = '0' + h
- res = h.decode('hex')
- return chr(0) * pad + res
- def base58_check_encode(s, version=0):
- vs = chr(version) + s
- check = dhash(vs)[:4]
- return base58_encode_padded(vs + check)
- def base58_check_decode(s, version=0):
- k = base58_decode_padded(s)
- v0, data, check0 = k[0], k[1:-4], k[-4:]
- check1 = dhash(v0 + data)[:4]
- if check0 != check1:
- raise BaseException('checksum error')
- if version != ord(v0):
- raise BaseException('version mismatch')
- return data
- def gen_eckey(passphrase=None, secret=None, pkey=None, compressed=False, rounds=1):
- k = KEY()
- if passphrase:
- secret = passphrase.encode('utf8')
- for i in xrange(rounds):
- secret = hashlib.sha256(secret).digest()
- if pkey:
- secret = base58_check_decode(pkey, 128)
- compressed = len(secret) == 33
- secret = secret[0:32]
- k.generate(secret)
- k.set_compressed(compressed)
- return k
- def get_addr(k):
- pubkey = k.get_pubkey()
- secret = k.get_secret()
- hash160 = rhash(pubkey)
- addr = base58_check_encode(hash160)
- payload = secret
- if k.compressed:
- payload = secret + chr(0)
- pkey = base58_check_encode(payload, 128)
- return addr, pkey
- ### End: Address Generation code
- ### Start: Transaction code
- def bc_address_to_hash_160(addr):
- bytes = b58decode(addr, 25)
- return bytes[1:21]
- __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
- __b58base = len(__b58chars)
- def b58decode(v, length):
- """ decode v into a string of len bytes
- """
- long_value = 0L
- for (i, c) in enumerate(v[::-1]):
- long_value += __b58chars.find(c) * (__b58base**i)
- result = ''
- while long_value >= 256:
- div, mod = divmod(long_value, 256)
- result = chr(mod) + result
- long_value = div
- result = chr(long_value) + result
- nPad = 0
- for c in v:
- if c == __b58chars[0]: nPad += 1
- else: break
- result = chr(0)*nPad + result
- if length is not None and len(result) != length:
- return None
- return result
- def makek():
- # Create a dictionary with address as key and private-key
- # as value
- a = config.get('HoldingAddresses', 'addresses').split("+")
- p = config.get('HoldingAddresses', 'private_keys').split("+")
- k = {}
- for a in zip(a,p):
- k[a[0]] = a[1]
- return k
- def maketx(inputs, outputs, send=False):
- # Create a transaction, sign it - possibly send it - but
- # in either case return the raw hex
- # inputs: [('a7813e20045b2f2caf612c589adc7e985029167106a300b6a7157084c26967f5', 1, '1PPgZP53BrcG7hyXdWT9fugewmxL1H8LS3'),...]
- # outputs: [('1KRavVCsvaLi7ZzktHSCE3hPUvhPDhQKhz', 8000000),...]
- ip = []
- for txid,vout,_ in inputs:
- ip.append({"txid": txid, "vout": vout})
- op = {}
- for addr,amnt in outputs:
- op[addr] = AmountToJSON(amnt)
- tx = sp.createrawtransaction(ip,op)
- k = makek()
- ip = []
- pkeys = []
- for txid,vout,addr in inputs:
- ip.append({"txid": txid, "vout": vout, "scriptPubKey": '76a914'+bc_address_to_hash_160(addr).encode('hex')+'88ac'})
- pkeys.append(k[addr])
- final_t = sp.signrawtransaction(tx,ip,pkeys)
- if send:
- sp.sendrawtransaction(tx)
- return final_t['hex']
- ### End: Transaction code
- ### Start: Blockchain Inspection/Traversion code
- def gettx(txid):
- # Get the information of a single transaction, using
- # the bitcoind API
- tx_raw = sp.getrawtransaction(txid)
- tx = sp.decoderawtransaction(tx_raw)
- return tx
- def getaddresstxs(address):
- # Get all transactions associated with an address.
- # Uses blockchain.info to get this, bitcoind API
- # apparently has no equivalent function.
- address_info = json.loads(urllib2.urlopen("http://blockchain.info/address/%s?format=json"%(address,) ).read())
- tx_list = []
- for tx in address_info['txs']:
- tx_list.append(tx['hash'])
- return tx_list
- def getholderschange(txid):
- # Get a list of the new holders and old holders represented by a
- # single transaction, given as the tx-id.
- tid = txid.split(":")
- tx = gettx(tid[0])
- new_holders = []
- old_holders = []
- for i in tx['vin']:
- old_holders.append(i['txid']+":"+str(i['vout']))
- for o in tx['vout']:
- new_holders.append((o['scriptPubKey']['addresses'][0], o['value']))
- return new_holders, old_holders
- def spentby(tx_out, single_input = False):
- # Return the id of the transaction which spent the given txid/#
- # if single_input is true, it only returns if the tx_out was used as a single
- # input to the transaction.
- # This is because it is not possible to follow a colored coin across a transaction
- # with multiple inputs
- tid = tx_out.split(":")
- tx = gettx(tid[0])
- address = tx['vout'][int(tid[1])]['scriptPubKey']['addresses'][0]
- tx_list = getaddresstxs(address)
- for t in tx_list:
- outputs,inputs = getholderschange(t)
- if single_input and not len(inputs) == 1:
- print t,"used with multiple inputs: tracking lost at address",address
- continue
- for i in inputs:
- if i == tx_out:
- return t
- return None
- def rec(prevout_txid):
- # Recursive function to traverse the "tree of ownership", depth-first,
- # head-recursion.
- holder_list = []
- spent_by = spentby(prevout_txid, single_input = True)
- if spent_by is None:
- return None
- tx_data = gettx(spent_by)
- for o in tx_data['vout']:
- ntxid = spent_by+":"+str(o['n'])
- hl = rec(ntxid)
- if hl is None:
- holder_list.append((o['scriptPubKey']['addresses'][0],o['value'],ntxid))
- else:
- for h in hl:
- holder_list.append(h)
- return holder_list
- def get_current_holders(root_tx_out):
- # Get the current holders of the "colored coin" with
- # the given root (a string with txid+":"+n_output)
- return rec(root_tx_out)
- ### End: Blockchain Inspection/Traversion code
- ### Start: "User-facing" methods
- def generate_holding_address():
- # Generate an address, add it to the config file
- addr, pkey = get_addr(gen_eckey())
- addresses = config.get('HoldingAddresses', 'addresses')
- private_keys = config.get('HoldingAddresses', 'private_keys')
- if len(addresses) > 5:
- config.set('HoldingAddresses', 'addresses', '+'.join([addresses,addr]))
- config.set('HoldingAddresses', 'private_keys', '+'.join([private_keys,pkey]))
- else:
- config.set('HoldingAddresses', 'addresses', addr)
- config.set('HoldingAddresses', 'private_keys', pkey)
- config.write(open(config_file,'w'))
- return "Address added: "+addr
- def update_tracked_coins(name):
- # Update the list of owners of a tracked coin
- # and write to the config file
- root_tx = config.get(name, "root_tx")
- current_holders = get_current_holders(root_tx)
- holding_addresses = ""
- holding_amounts = ""
- holding_txids = ""
- total = 0.0
- for h in current_holders:
- holding_addresses += "+" + h[0]
- holding_amounts += "+" + str(h[1])
- holding_txids += "+" + h[2]
- config.set(name, "holders", holding_addresses[1:])
- config.set(name, "amounts", holding_amounts[1:])
- config.set(name, "txid", holding_txids[1:])
- config.write(open(config_file,'w'))
- def start_tracking_coins(name,txid):
- # Give a name of a tracked coin, together with a
- # root output that will be used to track it.
- # Write this to the config file, and update the
- # list of owners.
- if name in config.sections():
- return name+" already exists."
- config.add_section(name)
- config.set(name, "root_tx", txid)
- config.set(name, "holders", "")
- config.set(name, "amounts", "")
- config.set(name, "txid", "")
- config.write(open(config_file,'w'))
- update_tracked_coins(name)
- def show_holders(name):
- holders = config.get(name, "holders").split("+")
- amounts = config.get(name, "amounts").split("+")
- txids = config.get(name,"txid").split("+")
- total = 0.0
- print "*** %s ***" % (name,)
- for h in zip(holders,amounts,txids):
- print h[0],h[1],h[2]
- total += float(h[1])
- print "** Total %s: %f **" % (name,total)
- def show_my_holdings():
- sections = config.sections()
- reserved_sections = ['bitcoind', 'HoldingAddresses']
- my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
- for s in sections:
- if s in reserved_sections: continue
- holders = config.get(s, "holders").split("+")
- amounts = config.get(s, "amounts").split("+")
- txids = config.get(s, "txid").split("+")
- for h in holders:
- if h in my_holding_addresses:
- print h, amounts[holders.index(h)],txids[holders.index(h)]
- def show_my_holding_addresses():
- my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
- for a in my_holding_addresses:
- print a
- def transfer_asset(sender, receivers):
- address,txid,n = sender.split(":")
- tx_input = (txid, int(n), address)
- tx_outputs = []
- for l in receivers.split(","):
- address,amount = l.split(":")
- tx_outputs.append((address,int(float(amount)*1e8)))
- print maketx(tx_input, tx_outputs)
- if __name__ == '__main__':
- ######
- # Upcoming:
- # - GUI
- # - Paint coins
- # - Create holding address
- # - Transfer asset
- # - Forward dividends to wallet
- # - Proportional payments to asset holders
- # - Add options for losing track: cutoff, remove, proportional follow.
- ######
- parser = OptionParser()
- parser.add_option('-p', '--paint', help='Paint coins for tracking', dest='paint_txid', action='store')
- parser.add_option('-n', '--new-address', help='Create new holding address for colored coins', dest='gen_address', default=False, action='store_true')
- parser.add_option('-l', '--list-colors', help='List of names of painted coins being tracked', dest='list_colors', default=False, action='store_true')
- parser.add_option('-u', '--update-ownership', help='Update ownership info for painted coins', dest='update_name', action='store')
- parser.add_option('-o', '--owners', help='Show owners of painted coins', dest="holders_name", action="store")
- parser.add_option('-m', '--my-holdings', help='Show holdings at my addresses', dest="show_holdings", action="store_true")
- parser.add_option('-a', '--holding-addresses', help='Show my holding addresses', dest="show_addresses", action="store_true")
- parser.add_option('-f', '--transfer-from', help='Asset to transfer to another address. address:txid:n', dest='transfer_from', action="store")
- parser.add_option('-t', '--transfer-to', help='Address to transfer asset to. address:amount,...', dest='transfer_to', action="store")
- # Not implemented yet - function to safely transfer other unrelated funds (e.g. dividends, coupons, etc.) AWAY from addresses
- # holding some asset:
- #parser.add_option('-x', '--transfer-other-from', help='Transfer bitcoins UNRELATED to the tracked address/coins away from this address', dest="transfer_other_from", action="store")
- #parser.add_option('-y', '--transfer-other-to', help='Transfer bitcoins UNRELATED to the tracked address/coins to this address', dest="transfer_other_to", action="store")
- opts, args = parser.parse_args()
- if opts.gen_address:
- print generate_holding_address()
- if opts.paint_txid:
- name,txid,n = opts.paint_txid.split(":")
- print start_tracking_coins(name,txid+":"+n)
- if opts.holders_name:
- show_holders(opts.holders_name)
- if opts.update_name:
- update_tracked_coins(opts.update_name)
- if opts.show_holdings:
- show_my_holdings()
- if opts.show_addresses:
- show_my_holding_addresses()
- if opts.transfer_from:
- if opts.transfer_to:
- transfer_asset(opts.transfer_from, opts.transfer_to)
- else:
- print "Missing address to transfer to"
- if opts.transfer_to:
- if opts.transfer_from:
- transfer_asset(opts.transfer_from, opts.transfer_to)
- else:
- print "Missing address/input-txid to transfer from"
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement