Advertisement
Guest User

bitpaint.py

a guest
Oct 10th, 2012
142
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.29 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3. """
  4. bitpaint.py
  5. ~~~~~~~~~~~
  6. Simple command-line utility to use the Bitcoin blockchain to track so-called "colored coins", or "smart contracts/securities".
  7.  
  8. Create "colored coin", receive colored coin, pay to owners of colored coins.
  9.  
  10. Features:
  11. - Tracking using bitcoind and blockchain.info
  12. - Generate address for colored coin (so you can store
  13.  coins outside your normal wallet, so that the satoshi
  14.  client doesn't accidentally spend them)
  15. - Start tracking a "colored coin": "paint" a coin
  16. - Generate list of holders of colored coin (btc-address and
  17.  amount of coin held)
  18. - Transfer colored coin from one address to another
  19.  
  20. Upcoming features:
  21. - Pay from wallet to colored coin owners
  22. - Forward non-colored payments from asset-addresses to
  23.  wallet-addresses
  24.  
  25.  
  26. Quick guide - issue "security":
  27. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  28. 1. Create a holding address:
  29.   $ python bitpaint.py --new-address
  30. 2. Transfer the coins you want to color to that address from your
  31.   regular wallet.
  32. 3. Make a note of the transaction id <txid> and the output number
  33.   that transferred the coins to the holding address <n>.
  34. 4. Start tracking from the output of that transaction:
  35.   $ python bitpaint.py --paint <name>:<txid>:<n>
  36.  
  37. After these steps, the address you generated holds the full colored
  38. coin. <name> is any name you would like to give this colored coin.
  39. <name> only exists locally.
  40.  
  41.  
  42. Quick guide - transfer ownership
  43. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  44. 1. Ask receiver to download this script and generate a holding
  45.   address, <receiver_address>
  46. 2. Transfer the colored coin:
  47.   $ python --transfer-from <addr>:<txid>:<n> --transfer-to <receiver_address1>:<amount1>,<receiver_address2>:<amount2>,...
  48.  
  49.  
  50. Quick guide - list current owners
  51. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  52. 1. Update the list of owners of an asset <name>:
  53.   $ python --update-ownership <name>
  54. 2. Show the list of owners:
  55.   $ python --owners <name>
  56.  
  57.  
  58. Quick guide - list "colored coins" I own
  59. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  60. 1. Show the list of colored coins owned by my addresses:
  61.   $ python --my-holdings
  62.  
  63.  
  64. Caution:
  65. - Private keys are stored in plaintext in your configuration file
  66. - Do not allow the "colored coin" to be used in a transaction with
  67.  multiple inputs. That would mess up the tracking. I suggest avoid
  68.  using holding-addresses in your normal wallet, and just use this
  69.  client to manage your colored coins.
  70. - Since there is no transaction fee included, you cannot be sure that
  71.  the transaction will be confirmed
  72.  
  73. Donations welcome: 1GsnaaAWYMb7yPwBQ19bSLLMTtsfSyjqg3
  74.  
  75. """
  76.  
  77. # Import libraries
  78. import urllib2, ConfigParser, ctypes, ctypes.util, hashlib, simplejson as json, os
  79. from jsonrpc import ServiceProxy
  80. from optparse import OptionParser
  81.  
  82. ### Start: Generic helpers
  83. def JSONtoAmount(value):
  84.     return long(round(value * 1e8))
  85. def AmountToJSON(amount):
  86.     return float(amount / 1e8)
  87. ### End: Generic helpers
  88.  
  89.  
  90. ### Start: Create/Read Config
  91. # Here we create a configuration file if one does not exist already.
  92. # User is asked for details about his bitcoind so the script can connect.
  93. config_file = "bitpaint.conf"
  94. basic_bitpaint_conf = """[bitcoind]
  95. rpchost = %s
  96. rpcport = %s
  97. rpcuser = %s
  98. rpcpwd = %s
  99.  
  100. [HoldingAddresses]
  101. addresses =
  102. private_keys =
  103. """
  104. # If the config file does not exists, ask user for details and write one
  105. if not os.path.exists(config_file):
  106.     print "Configuration file bitpaint.conf not found. Creating one..."
  107.     host = raw_input("bitcoind rpc host (default: 127.0.0.1): ")
  108.     if len(host) == 0: host = "127.0.0.1"
  109.     port = raw_input("bitcoind rpc port (default: 8332): ")
  110.     if len(port) == 0: port = "8332"
  111.     user = raw_input("bitcoind rpc username (default: <blank>: ")
  112.     pwd = raw_input("bitcoind rpc password (default: <blank>: ")
  113.     f = open(config_file, 'w')
  114.     f.write(basic_bitpaint_conf % (host,port,user,pwd))
  115.     f.close()
  116.  
  117. # Parse the config file
  118. config = ConfigParser.ConfigParser()
  119. config.read(config_file)
  120. rpchost = config.get('bitcoind', 'rpchost')
  121. rpcport = config.get('bitcoind', 'rpcport')
  122. rpcuser = config.get('bitcoind', 'rpcuser')
  123. rpcpwd  = config.get('bitcoind', 'rpcpwd')
  124.  
  125. # Connect to bitcoind
  126. bitcoind_connection_string = "http://%s:%s@%s:%s" % (rpcuser,rpcpwd,rpchost,rpcport)
  127. sp = ServiceProxy(bitcoind_connection_string)
  128.  
  129. ### End: Create/Read Config
  130.  
  131.  
  132. ### Start: Address Generation code
  133. # The following code was yoinked from addrgen.py
  134. # Thanks to: Joric/bitcoin-dev, june 2012, public domain
  135. ssl = ctypes.cdll.LoadLibrary (ctypes.util.find_library ('ssl') or 'libeay32')
  136.  
  137. def check_result (val, func, args):
  138.     if val == 0: raise ValueError
  139.     else: return ctypes.c_void_p (val)
  140.  
  141. ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p
  142. ssl.EC_KEY_new_by_curve_name.errcheck = check_result
  143.  
  144. class KEY:
  145.     def __init__(self):
  146.         NID_secp256k1 = 714
  147.         self.k = ssl.EC_KEY_new_by_curve_name(NID_secp256k1)
  148.         self.compressed = False
  149.         self.POINT_CONVERSION_COMPRESSED = 2
  150.         self.POINT_CONVERSION_UNCOMPRESSED = 4
  151.  
  152.     def __del__(self):
  153.         if ssl:
  154.             ssl.EC_KEY_free(self.k)
  155.         self.k = None
  156.  
  157.     def generate(self, secret=None):
  158.         if secret:
  159.             self.prikey = secret
  160.             priv_key = ssl.BN_bin2bn(secret, 32, ssl.BN_new())
  161.             group = ssl.EC_KEY_get0_group(self.k)
  162.             pub_key = ssl.EC_POINT_new(group)
  163.             ctx = ssl.BN_CTX_new()
  164.             ssl.EC_POINT_mul(group, pub_key, priv_key, None, None, ctx)
  165.             ssl.EC_KEY_set_private_key(self.k, priv_key)
  166.             ssl.EC_KEY_set_public_key(self.k, pub_key)
  167.             ssl.EC_POINT_free(pub_key)
  168.             ssl.BN_CTX_free(ctx)
  169.             return self.k
  170.         else:
  171.             return ssl.EC_KEY_generate_key(self.k)
  172.  
  173.     def get_pubkey(self):
  174.         size = ssl.i2o_ECPublicKey(self.k, 0)
  175.         mb = ctypes.create_string_buffer(size)
  176.         ssl.i2o_ECPublicKey(self.k, ctypes.byref(ctypes.pointer(mb)))
  177.         return mb.raw
  178.  
  179.     def get_secret(self):
  180.         bn = ssl.EC_KEY_get0_private_key(self.k);
  181.         bytes = (ssl.BN_num_bits(bn) + 7) / 8
  182.         mb = ctypes.create_string_buffer(bytes)
  183.         n = ssl.BN_bn2bin(bn, mb);
  184.         return mb.raw.rjust(32, chr(0))
  185.  
  186.     def set_compressed(self, compressed):
  187.         self.compressed = compressed
  188.         if compressed:
  189.             form = self.POINT_CONVERSION_COMPRESSED
  190.         else:
  191.             form = self.POINT_CONVERSION_UNCOMPRESSED
  192.         ssl.EC_KEY_set_conv_form(self.k, form)
  193.  
  194. def dhash(s):
  195.     return hashlib.sha256(hashlib.sha256(s).digest()).digest()
  196.  
  197. def rhash(s):
  198.     h1 = hashlib.new('ripemd160')
  199.     h1.update(hashlib.sha256(s).digest())
  200.     return h1.digest()
  201.  
  202. b58_digits = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
  203.  
  204. def base58_encode(n):
  205.     l = []
  206.     while n > 0:
  207.         n, r = divmod(n, 58)
  208.         l.insert(0,(b58_digits[r]))
  209.     return ''.join(l)
  210.  
  211. def base58_decode(s):
  212.     n = 0
  213.     for ch in s:
  214.         n *= 58
  215.         digit = b58_digits.index(ch)
  216.         n += digit
  217.     return n
  218.  
  219. def base58_encode_padded(s):
  220.     res = base58_encode(int('0x' + s.encode('hex'), 16))
  221.     pad = 0
  222.     for c in s:
  223.         if c == chr(0):
  224.             pad += 1
  225.         else:
  226.             break
  227.     return b58_digits[0] * pad + res
  228.  
  229. def base58_decode_padded(s):
  230.     pad = 0
  231.     for c in s:
  232.         if c == b58_digits[0]:
  233.             pad += 1
  234.         else:
  235.             break
  236.     h = '%x' % base58_decode(s)
  237.     if len(h) % 2:
  238.         h = '0' + h
  239.     res = h.decode('hex')
  240.     return chr(0) * pad + res
  241.  
  242. def base58_check_encode(s, version=0):
  243.     vs = chr(version) + s
  244.     check = dhash(vs)[:4]
  245.     return base58_encode_padded(vs + check)
  246.  
  247. def base58_check_decode(s, version=0):
  248.     k = base58_decode_padded(s)
  249.     v0, data, check0 = k[0], k[1:-4], k[-4:]
  250.     check1 = dhash(v0 + data)[:4]
  251.     if check0 != check1:
  252.         raise BaseException('checksum error')
  253.     if version != ord(v0):
  254.         raise BaseException('version mismatch')
  255.     return data
  256.  
  257. def gen_eckey(passphrase=None, secret=None, pkey=None, compressed=False, rounds=1):
  258.     k = KEY()
  259.     if passphrase:
  260.         secret = passphrase.encode('utf8')
  261.         for i in xrange(rounds):
  262.             secret = hashlib.sha256(secret).digest()
  263.     if pkey:
  264.         secret = base58_check_decode(pkey, 128)
  265.         compressed = len(secret) == 33
  266.         secret = secret[0:32]
  267.     k.generate(secret)
  268.     k.set_compressed(compressed)
  269.     return k
  270.  
  271. def get_addr(k):
  272.     pubkey = k.get_pubkey()
  273.     secret = k.get_secret()
  274.     hash160 = rhash(pubkey)
  275.     addr = base58_check_encode(hash160)
  276.     payload = secret
  277.     if k.compressed:
  278.         payload = secret + chr(0)
  279.     pkey = base58_check_encode(payload, 128)
  280.     return addr, pkey
  281. ### End: Address Generation code
  282.  
  283.  
  284. ### Start: Transaction code
  285. def bc_address_to_hash_160(addr):
  286.     bytes = b58decode(addr, 25)
  287.     return bytes[1:21]
  288.  
  289. __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
  290. __b58base = len(__b58chars)
  291.  
  292. def b58decode(v, length):
  293.     """ decode v into a string of len bytes
  294.     """
  295.     long_value = 0L
  296.     for (i, c) in enumerate(v[::-1]):
  297.         long_value += __b58chars.find(c) * (__b58base**i)
  298.     result = ''
  299.     while long_value >= 256:
  300.         div, mod = divmod(long_value, 256)
  301.         result = chr(mod) + result
  302.         long_value = div
  303.     result = chr(long_value) + result
  304.     nPad = 0
  305.     for c in v:
  306.         if c == __b58chars[0]: nPad += 1
  307.         else: break
  308.     result = chr(0)*nPad + result
  309.     if length is not None and len(result) != length:
  310.         return None
  311.     return result
  312.  
  313. def makek():
  314.     # Create a dictionary with address as key and private-key
  315.     # as value
  316.     a = config.get('HoldingAddresses', 'addresses').split("+")
  317.     p = config.get('HoldingAddresses', 'private_keys').split("+")
  318.     k = {}
  319.     for a in zip(a,p):
  320.         k[a[0]] = a[1]
  321.     return k
  322.  
  323. def maketx(inputs, outputs, send=False):
  324.     # Create a transaction, sign it - possibly send it - but
  325.     # in either case return the raw hex
  326.     # inputs: [('a7813e20045b2f2caf612c589adc7e985029167106a300b6a7157084c26967f5', 1, '1PPgZP53BrcG7hyXdWT9fugewmxL1H8LS3'),...]
  327.     # outputs: [('1KRavVCsvaLi7ZzktHSCE3hPUvhPDhQKhz', 8000000),...]
  328.     ip = []
  329.     for txid,vout,_ in inputs:
  330.         ip.append({"txid": txid, "vout": vout})
  331.     op = {}
  332.     for addr,amnt in outputs:
  333.         op[addr] = AmountToJSON(amnt)
  334.     tx = sp.createrawtransaction(ip,op)
  335.     k = makek()
  336.     ip = []
  337.     pkeys = []
  338.     for txid,vout,addr in inputs:
  339.         ip.append({"txid": txid, "vout": vout, "scriptPubKey": '76a914'+bc_address_to_hash_160(addr).encode('hex')+'88ac'})
  340.         pkeys.append(k[addr])
  341.     final_t = sp.signrawtransaction(tx,ip,pkeys)
  342.     if send:
  343.         sp.sendrawtransaction(tx)
  344.     return final_t['hex']
  345.  
  346. ### End: Transaction code
  347.  
  348.  
  349. ### Start: Blockchain Inspection/Traversion code
  350. def gettx(txid):
  351.     # Get the information of a single transaction, using
  352.     # the bitcoind API
  353.     tx_raw = sp.getrawtransaction(txid)
  354.     tx = sp.decoderawtransaction(tx_raw)
  355.     return tx
  356.  
  357. def getaddresstxs(address):
  358.     # Get all transactions associated with an address.
  359.     # Uses blockchain.info to get this, bitcoind API
  360.     # apparently has no equivalent function.
  361.     address_info = json.loads(urllib2.urlopen("http://blockchain.info/address/%s?format=json"%(address,) ).read())
  362.     tx_list = []
  363.     for tx in address_info['txs']:
  364.         tx_list.append(tx['hash'])
  365.     return tx_list
  366.  
  367. def getholderschange(txid):
  368.     # Get a list of the new holders and old holders represented by a
  369.     # single transaction, given as the tx-id.
  370.     tid = txid.split(":")
  371.     tx = gettx(tid[0])
  372.     new_holders = []
  373.     old_holders = []
  374.     for i in tx['vin']:
  375.         old_holders.append(i['txid']+":"+str(i['vout']))
  376.     for o in tx['vout']:
  377.         new_holders.append((o['scriptPubKey']['addresses'][0], o['value']))
  378.     return new_holders, old_holders
  379.  
  380. def spentby(tx_out, single_input = False):
  381.     # Return the id of the transaction which spent the given txid/#
  382.     # if single_input is true, it only returns if the tx_out was used as a single
  383.     # input to the transaction.
  384.     # This is because it is not possible to follow a colored coin across a transaction
  385.     # with multiple inputs
  386.     tid = tx_out.split(":")
  387.     tx = gettx(tid[0])
  388.     address = tx['vout'][int(tid[1])]['scriptPubKey']['addresses'][0]
  389.     tx_list = getaddresstxs(address)
  390.     for t in tx_list:
  391.         outputs,inputs = getholderschange(t)
  392.         if single_input and not len(inputs) == 1:
  393.             print t,"used with multiple inputs: tracking lost at address",address
  394.             continue
  395.         for i in inputs:
  396.             if i == tx_out:
  397.                 return t
  398.     return None
  399.  
  400. def rec(prevout_txid):
  401.     # Recursive function to traverse the "tree of ownership", depth-first,
  402.     # head-recursion.
  403.     holder_list = []
  404.    
  405.     spent_by = spentby(prevout_txid, single_input = True)
  406.     if spent_by is None:
  407.         return None
  408.     tx_data = gettx(spent_by)
  409.  
  410.     for o in tx_data['vout']:
  411.         ntxid = spent_by+":"+str(o['n'])
  412.         hl = rec(ntxid)
  413.         if hl is None:
  414.             holder_list.append((o['scriptPubKey']['addresses'][0],o['value'],ntxid))
  415.         else:
  416.             for h in hl:
  417.                 holder_list.append(h)
  418.  
  419.     return holder_list
  420.  
  421. def get_current_holders(root_tx_out):
  422.     # Get the current holders of the "colored coin" with
  423.     # the given root (a string with txid+":"+n_output)
  424.     return rec(root_tx_out)
  425. ### End: Blockchain Inspection/Traversion code
  426.  
  427.  
  428. ### Start: "User-facing" methods
  429. def generate_holding_address():
  430.     # Generate an address, add it to the config file
  431.     addr, pkey = get_addr(gen_eckey())
  432.     addresses = config.get('HoldingAddresses', 'addresses')
  433.     private_keys = config.get('HoldingAddresses', 'private_keys')
  434.     if len(addresses) > 5:
  435.         config.set('HoldingAddresses', 'addresses', '+'.join([addresses,addr]))
  436.         config.set('HoldingAddresses', 'private_keys', '+'.join([private_keys,pkey]))
  437.     else:
  438.         config.set('HoldingAddresses', 'addresses', addr)
  439.         config.set('HoldingAddresses', 'private_keys', pkey)
  440.     config.write(open(config_file,'w'))
  441.     return "Address added: "+addr
  442.  
  443. def update_tracked_coins(name):
  444.     # Update the list of owners of a tracked coin
  445.     # and write to the config file
  446.     root_tx = config.get(name, "root_tx")
  447.     current_holders = get_current_holders(root_tx)
  448.     holding_addresses = ""
  449.     holding_amounts = ""
  450.     holding_txids = ""
  451.     total = 0.0
  452.     for h in current_holders:
  453.         holding_addresses += "+" + h[0]
  454.         holding_amounts += "+" + str(h[1])
  455.         holding_txids += "+" + h[2]
  456.     config.set(name, "holders", holding_addresses[1:])
  457.     config.set(name, "amounts", holding_amounts[1:])
  458.     config.set(name, "txid", holding_txids[1:])
  459.     config.write(open(config_file,'w'))
  460.  
  461. def start_tracking_coins(name,txid):
  462.     # Give a name of a tracked coin, together with a
  463.     # root output that will be used to track it.
  464.     # Write this to the config file, and update the
  465.     # list of owners.
  466.     if name in config.sections():
  467.         return name+" already exists."
  468.     config.add_section(name)
  469.     config.set(name, "root_tx", txid)
  470.     config.set(name, "holders", "")
  471.     config.set(name, "amounts", "")
  472.     config.set(name, "txid", "")
  473.     config.write(open(config_file,'w'))
  474.     update_tracked_coins(name)
  475.  
  476. def show_holders(name):
  477.     holders = config.get(name, "holders").split("+")
  478.     amounts = config.get(name, "amounts").split("+")
  479.     txids = config.get(name,"txid").split("+")
  480.     total = 0.0
  481.     print "*** %s ***" % (name,)
  482.     for h in zip(holders,amounts,txids):
  483.         print h[0],h[1],h[2]
  484.         total += float(h[1])
  485.     print "** Total %s: %f **" % (name,total)
  486.  
  487. def show_my_holdings():
  488.     sections = config.sections()
  489.     reserved_sections = ['bitcoind', 'HoldingAddresses']
  490.     my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
  491.     for s in sections:
  492.         if s in reserved_sections: continue
  493.         holders = config.get(s, "holders").split("+")
  494.         amounts = config.get(s, "amounts").split("+")
  495.         txids = config.get(s, "txid").split("+")
  496.         for h in holders:
  497.             if h in my_holding_addresses:
  498.                 print h, amounts[holders.index(h)],txids[holders.index(h)]
  499.  
  500. def show_my_holding_addresses():
  501.     my_holding_addresses = config.get('HoldingAddresses', 'addresses').split("+")
  502.     for a in my_holding_addresses:
  503.         print a
  504.  
  505. def transfer_asset(sender, receivers):
  506.     address,txid,n = sender.split(":")
  507.     tx_input = (txid, int(n), address)
  508.     tx_outputs = []
  509.     for l in receivers.split(","):
  510.         address,amount = l.split(":")
  511.         tx_outputs.append((address,int(float(amount)*1e8)))
  512.     print maketx(tx_input, tx_outputs)
  513.  
  514. if __name__ == '__main__':
  515.     ######
  516.     # Upcoming:
  517.     # - GUI
  518.     # - Paint coins
  519.     # - Create holding address
  520.     # - Transfer asset
  521.     # - Forward dividends to wallet
  522.     # - Proportional payments to asset holders
  523.     # - Add options for losing track: cutoff, remove, proportional follow.
  524.     ######
  525.  
  526.     parser = OptionParser()
  527.     parser.add_option('-p', '--paint', help='Paint coins for tracking', dest='paint_txid', action='store')
  528.     parser.add_option('-n', '--new-address', help='Create new holding address for colored coins', dest='gen_address', default=False, action='store_true')
  529.     parser.add_option('-l', '--list-colors', help='List of names of painted coins being tracked', dest='list_colors', default=False, action='store_true')
  530.     parser.add_option('-u', '--update-ownership', help='Update ownership info for painted coins', dest='update_name', action='store')
  531.     parser.add_option('-o', '--owners', help='Show owners of painted coins', dest="holders_name", action="store")
  532.     parser.add_option('-m', '--my-holdings', help='Show holdings at my addresses', dest="show_holdings", action="store_true")
  533.     parser.add_option('-a', '--holding-addresses', help='Show my holding addresses', dest="show_addresses", action="store_true")
  534.     parser.add_option('-f', '--transfer-from', help='Asset to transfer to another address. address:txid:n', dest='transfer_from', action="store")
  535.     parser.add_option('-t', '--transfer-to', help='Address to transfer asset to. address:amount,...', dest='transfer_to', action="store")
  536.  
  537.     # Not implemented yet - function to safely transfer other unrelated funds (e.g. dividends, coupons, etc.) AWAY from addresses
  538.     # holding some asset:
  539.     #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")
  540.     #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")
  541.     opts, args = parser.parse_args()
  542.    
  543.     if opts.gen_address:
  544.         print generate_holding_address()
  545.     if opts.paint_txid:
  546.         name,txid,n = opts.paint_txid.split(":")
  547.         print start_tracking_coins(name,txid+":"+n)
  548.     if opts.holders_name:
  549.         show_holders(opts.holders_name)
  550.     if opts.update_name:
  551.         update_tracked_coins(opts.update_name)
  552.     if opts.show_holdings:
  553.         show_my_holdings()
  554.     if opts.show_addresses:
  555.         show_my_holding_addresses()
  556.     if opts.transfer_from:
  557.         if opts.transfer_to:
  558.             transfer_asset(opts.transfer_from, opts.transfer_to)
  559.         else:
  560.             print "Missing address to transfer to"
  561.     if opts.transfer_to:
  562.         if opts.transfer_from:
  563.             transfer_asset(opts.transfer_from, opts.transfer_to)
  564.         else:
  565.             print "Missing address/input-txid to transfer from"
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement