class TimelockMarkReleaseHandler(BaseHandler):
    def __init__(self, oracle):
        self.oracle = oracle
        self.btc = oracle.btc
        self.kv = KeyValue(self.oracle.db)

    def handle_task(self, task):
        data = json.loads(task['json_data'])

        mark = data['mark']
        addr = data['address']

        mark_data = self.kv.get_by_section_key('mark_available',
                                               '{}#{}'.format(mark, addr))

        if not mark_data:
            return

        mark_history = self.kv.get_by_section_key('mark_history',
                                                  '{}#{}'.format(mark, addr))

        if not mark_history:
            self.kv.store('mark_history', '{}#{}'.format(mark, addr),
                          {'entries': []})

        mark_history_entries = self.kv.get_by_section_key(
            'mark_history', '{}#{}'.format(mark, addr))['entries']
        mark_history_entries.append({
            'mark':
            mark,
            'addr':
            addr,
            'ts':
            int(time.mktime(datetime.datetime.utcnow().timetuple()))
        })
        mark_history_dict = {"entries": mark_history_entries}
        self.kv.update('mark_history', '{}#{}'.format(mark, addr),
                       mark_history_dict)

        self.kv.update('mark_available', '{}#{}'.format(mark, addr),
                       {'available': True})
        logging.info("released mark {} from addr {}".format(mark, addr))

        info_msg = {
            'operation':
            'safe_timelock_released_mark',
            'in_reply_to':
            'none',
            'message_id':
            "%s-%s" % ("mark-release", str(randrange(1000000000, 9000000000))),
            'contract_id':
            "{}#{}".format(addr, mark),
        }

        self.oracle.broadcast_with_fastcast(json.dumps(info_msg))

    def verify_and_create_timelock(self, output):
        mark, address, value, txid, n = output

        mark_data = self.kv.get_by_section_key('mark_available',
                                               '{}#{}'.format(mark, address))
        if not mark_data:
            return

        if mark_data['available']:
            return

        return_address = mark_data['return_address']
        locktime = mark_data['locktime']
        oracle_fees = mark_data['oracle_fees']
        miners_fee_satoshi = mark_data['miners_fee_satoshi']
        req_sigs = mark_data['req_sigs']

        self.oracle.task_queue.save({
            "operation":
            'safe_timelock_create',
            "json_data":
            cjson.encode({
                'mark': mark,
                'return_address': return_address,
                'oracle_fees': oracle_fees,
                'req_sigs': req_sigs,
                'miners_fee_satoshi': miners_fee_satoshi,
                'address': address,
                'value': value,
                'txid': txid,
                'n': n
            }),
            "done":
            0,
            "next_check":
            locktime
        })

        logging.info("found transaction for mark:{} on address:{}".format(
            mark, address))
        info_msg = {
            'operation':
            'safe_timelock_found_transaction',
            'in_reply_to':
            'none',
            'message_id':
            "%s-%s" %
            ("locked_transaction", str(randrange(1000000000, 9000000000))),
            'contract_id':
            "{}#{}".format(address, mark),
        }

        self.oracle.broadcast_with_fastcast(json.dumps(info_msg))

    def get_observed_addresses(self):
        observed_addresses = self.kv.get_by_section_key(
            'safe_timelock', 'addresses')
        if not observed_addresses:
            self.kv.store('safe_timelock', 'addresses', {'addresses': []})

        observed_addresses = self.kv.get_by_section_key(
            'safe_timelock', 'addresses')
        observed_addresses = observed_addresses['addresses']
        return observed_addresses

    def handle_new_transactions(self, transactions):
        logging.info(transactions)

        our_addresses = self.kv.get_by_section_key('safe_timelock',
                                                   'addresses')
        if not our_addresses:
            return

        our_addresses = our_addresses['addresses']

        outputs = []
        for transaction in transactions:
            for vout in transaction['vout']:
                if not 'addresses' in vout['scriptPubKey']:
                    continue
                if len(vout['scriptPubKey']['addresses']) != 1:
                    continue
                if vout['scriptPubKey']['addresses'][0] in our_addresses:
                    outputs.append(
                        (value_to_mark(vout['value']),
                         vout['scriptPubKey']['addresses'][0], vout['value'],
                         transaction['txid'], vout['n']))
        for output in outputs:
            self.verify_and_create_timelock(output)
 def __init__(self, oracle):
     self.oracle = oracle
     self.btc = oracle.btc
     self.kv = KeyValue(self.oracle.db)
Esempio n. 3
0
class TransactionSigner(BaseHandler):
  def __init__(self, oracle):
    self.oracle = oracle
    self.btc = oracle.btc
    self.kv = KeyValue(self.oracle.db)


  def includes_me(self, prevtx):
    for tx in prevtx:
      if not 'redeemScript' in tx:
        return False
      my_turn = self.get_my_turn(tx['redeemScript'])
      if my_turn < 0:
        return False
    return True

  def get_my_turn(self, redeem_script):
    # oracles sign transactions based on the order of their signatures

    addresses = sorted(self.btc.decode_script(redeem_script)['addresses'])
    for idx, addr in enumerate(addresses):
      if self.btc.address_is_mine(addr):
        return idx
    return -1


  def is_proper_transaction(self, tx, prevtxs):
    logging.info('testing tx: %r' % tx)
    logging.info('with prevtxs: %r' % prevtxs)

    if not self.oracle.btc.is_valid_transaction(tx):
      logging.debug("transaction invalid")
      return False

    inputs, outputs = self.btc.get_inputs_outputs(tx)

    if not self.includes_me(prevtxs):
      logging.debug("transaction does not include me")
      return False

    if self.oracle.btc.transaction_already_signed(tx, prevtxs):
      logging.debug("transaction already signed")
      return False

    return True


  def sign(self, tx, pwtxid, inputs, req_sigs):
    # sign is being called by external contracts to initiate signing procedure
    # it marks the transaction as being ready to be signed if received from bitmessage
    # and schedules signing -- in case oracles previous in line didn't want to sign it

    logging.debug("tx: %r" % tx)

    tx_inputs, tx_outputs = self.btc.get_inputs_outputs(tx)

    #todo: shouldn't all the input scripts be guaranteed to be exactly the same by now?
    turns = [self.get_my_turn(vin['redeemScript']) for vin in inputs if 'redeemScript' in vin]

    my_turn = max(turns)
    add_time = (my_turn - 1) * TURN_LENGTH_TIME

    rq_hash = self.get_tx_hash(tx)

    try:
      self.kv.store( 'signable', rq_hash, { 'inputs':inputs, 'sigs_so_far':0, 'req_sigs': req_sigs , 'pwtxid' : pwtxid } )
    except:
      logging.warning('duplicate sign task? this try..except should be removed ultimately!')

    self.oracle.task_queue.save({
        "operation": 'sign',
        "json_data": json.dumps({"transaction": tx}),
        "next_check": time.time() + add_time,
        "done": 0,
    })

  def sign_now(self, tx):
    # sign now signs the transaction and broadcasts it over the network

    inputs, outputs = self.btc.get_inputs_outputs(tx)

    rq_hash = self.get_tx_hash(tx)

    rq_data = self.kv.get_by_section_key('signable', rq_hash)
    if rq_data is None:
      logging.debug("not scheduled to sign this")
      return

    inputs = rq_data['inputs']
    sigs_so_far = rq_data['sigs_so_far']
    req_sigs = rq_data['req_sigs']

    assert( self.is_proper_transaction(tx, inputs) )

    tx_sigs_count = self.btc.signatures_count(
        tx,
        inputs)

    logging.debug("sigs count so far: %r; req_sigs: %r" % (tx_sigs_count, req_sigs))

    if sigs_so_far > tx_sigs_count: # or > not >=? TODO
      logging.debug('already signed a transaction with more sigs')
      return

    rq_data['sigs_so_far'] = tx_sigs_count
    self.kv.update('signable', rq_hash, rq_data)
    # ^ let's remember the tx with most sigs that we've seen. 

    if tx_sigs_count >= req_sigs:
      logging.debug('already signed with enough keys')
      return

    pwtxid = rq_data['pwtxid']

    signed_transaction = self.btc.sign_transaction(tx, inputs)

    tx_new_sigs_count = self.btc.signatures_count(signed_transaction, inputs)

    if (tx_new_sigs_count == tx_sigs_count):
      logging.debug('failed signing transaction. already signed by me? aborting')
      return

    tx_sigs_count += 1

    body = { 'pwtxid': pwtxid, 'operation':'sign', 'transaction': signed_transaction, 'sigs': tx_sigs_count, 'req_sigs': req_sigs }

    logging.debug('broadcasting: %r' % body)

    subject = ('sign %s' % pwtxid)  if tx_sigs_count < req_sigs else ('final-sign %s' % pwtxid)

    if tx_sigs_count == req_sigs:
      logging.debug('pushing tx to Eligius. you might want to disable this in test systems')
      logging.debug(safe_pushtx(signed_transaction))

    self.oracle.communication.broadcast(subject, json.dumps(body))

    rq_data['sigs_so_far'] = tx_sigs_count
    self.kv.update('signable', rq_hash, rq_data)

  def handle_request(self, request):
    body = request.message
    # if the oracle received a transaction from bitmessage, it attempts to sign it
    # all the validity checks are being handled by sign_now 

    tx = body['transaction']

    self.sign_now(tx)

  def handle_task(self, task):
    # handles scheduled signing
    # in a perfect world only the first oracle would have to call this
    # and all the others would sign through handle_request

    self.oracle.task_queue.done(task)
    message = json.loads(task['json_data'])
    tx = message['transaction']

    rq_hash = self.get_tx_hash(tx)

    rq_data = self.kv.get_by_section_key('signable', rq_hash)
    assert(rq_data is not None)
 
    logging.info("rq_data: %r" % rq_data)

    if rq_data['sigs_so_far'] > 0:
      logging.debug('I already signed more popular txs')
      return

    self.sign_now(tx)
Esempio n. 4
0
 def __init__(self, oracle):
   self.oracle = oracle
   self.btc = oracle.btc
   self.kv = KeyValue(self.oracle.db)
class TimelockMarkReleaseHandler(BaseHandler):
  def __init__(self, oracle):
    self.oracle = oracle
    self.btc = oracle.btc
    self.kv = KeyValue(self.oracle.db)

  def handle_task(self, task):
    data = json.loads(task['json_data'])

    mark = data['mark']
    addr = data['address']

    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))

    if not mark_data:
      return

    mark_history = self.kv.get_by_section_key('mark_history', '{}#{}'.format(mark, addr))

    if not mark_history:
      self.kv.store('mark_history', '{}#{}'.format(mark, addr), {'entries':[]})

    mark_history_entries = self.kv.get_by_section_key('mark_history', '{}#{}'.format(mark, addr))['entries']
    mark_history_entries.append({'mark':mark,'addr':addr,'ts': int(time.mktime(datetime.datetime.utcnow().timetuple()))})
    mark_history_dict = {"entries":mark_history_entries}
    self.kv.update('mark_history', '{}#{}'.format(mark, addr), mark_history_dict)

    self.kv.update('mark_available', '{}#{}'.format(mark, addr), {'available':True})
    logging.info("released mark {} from addr {}".format(mark, addr))

  def verify_and_create_timelock(self, output):
    mark, address, value, txid, n = output

    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, address))
    if not mark_data:
      return

    if mark_data['available']:
      return

    return_address = mark_data['return_address']
    locktime = mark_data['locktime']
    oracle_fees = mark_data['oracle_fees']
    miners_fee_satoshi = mark_data['miners_fee_satoshi']
    req_sigs = mark_data['req_sigs']

    self.oracle.task_queue.save({
        "operation": 'safe_timelock_create',
        "json_data": cjson.encode({
            'mark': mark,
            'return_address': return_address,
            'oracle_fees': oracle_fees,
            'req_sigs': req_sigs,
            'miners_fee_satoshi': miners_fee_satoshi,
            'address': address,
            'value': value,
            'txid': txid,
            'n': n}),
        "done": 0,
        "next_check": locktime
    })

    logging.info("found transaction for mark:{} on address:{}".format(mark, address))

  def handle_new_block(self, block):
    transaction_ids = block['tx']

    our_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    if not our_addresses:
      return

    our_addresses = our_addresses['addresses']

    outputs = []
    for tx in transaction_ids:
      try:
        raw_transaction = self.btc.get_raw_transaction(tx)
      except ProtocolError:
        continue
      transaction = self.btc.decode_raw_transaction(raw_transaction)
      for vout in transaction['vout']:
        if not 'addresses' in vout['scriptPubKey']:
          continue
        if len(vout['scriptPubKey']['addresses']) != 1:
          continue
        if vout['scriptPubKey']['addresses'][0] in our_addresses:
          outputs.append((value_to_mark(vout['value']), vout['scriptPubKey']['addresses'][0], vout['value'], tx, vout['n']))

    for output in outputs:
      self.verify_and_create_timelock(output)
class TimelockMarkReleaseHandler(BaseHandler):
  def __init__(self, oracle):
    self.oracle = oracle
    self.btc = oracle.btc
    self.kv = KeyValue(self.oracle.db)

  def handle_task(self, task):
    data = json.loads(task['json_data'])

    mark = data['mark']
    addr = data['address']

    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))

    if not mark_data:
      return

    mark_history = self.kv.get_by_section_key('mark_history', '{}#{}'.format(mark, addr))

    if not mark_history:
      self.kv.store('mark_history', '{}#{}'.format(mark, addr), {'entries':[]})

    mark_history_entries = self.kv.get_by_section_key('mark_history', '{}#{}'.format(mark, addr))['entries']
    mark_history_entries.append({'mark':mark,'addr':addr,'ts': int(time.mktime(datetime.datetime.utcnow().timetuple()))})
    mark_history_dict = {"entries":mark_history_entries}
    self.kv.update('mark_history', '{}#{}'.format(mark, addr), mark_history_dict)

    self.kv.update('mark_available', '{}#{}'.format(mark, addr), {'available':True})
    logging.info("released mark {} from addr {}".format(mark, addr))

    info_msg = {
      'operation': 'safe_timelock_released_mark',
      'in_reply_to': 'none',
      'message_id': "%s-%s" % ("mark-release", str(randrange(1000000000,9000000000))),
      'contract_id' : "{}#{}".format(addr, mark),
    }

    self.oracle.broadcast_with_fastcast(json.dumps(info_msg))

  def verify_and_create_timelock(self, output):
    mark, address, value, txid, n = output

    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, address))
    if not mark_data:
      return

    if mark_data['available']:
      return

    return_address = mark_data['return_address']
    locktime = mark_data['locktime']
    oracle_fees = mark_data['oracle_fees']
    miners_fee_satoshi = mark_data['miners_fee_satoshi']
    req_sigs = mark_data['req_sigs']

    self.oracle.task_queue.save({
        "operation": 'safe_timelock_create',
        "json_data": cjson.encode({
            'mark': mark,
            'return_address': return_address,
            'oracle_fees': oracle_fees,
            'req_sigs': req_sigs,
            'miners_fee_satoshi': miners_fee_satoshi,
            'address': address,
            'value': value,
            'txid': txid,
            'n': n}),
        "done": 0,
        "next_check": locktime
    })

    logging.info("found transaction for mark:{} on address:{}".format(mark, address))
    info_msg = {
      'operation': 'safe_timelock_found_transaction',
      'in_reply_to': 'none',
      'message_id': "%s-%s" % ("locked_transaction", str(randrange(1000000000,9000000000))),
      'contract_id' : "{}#{}".format(address, mark),
    }

    self.oracle.broadcast_with_fastcast(json.dumps(info_msg))

  def get_observed_addresses(self):
    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    if not observed_addresses:
      self.kv.store('safe_timelock', 'addresses', {'addresses':[]})

    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    observed_addresses = observed_addresses['addresses']
    return observed_addresses

  def handle_new_transactions(self, transactions):
    logging.info(transactions)

    our_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    if not our_addresses:
      return

    our_addresses = our_addresses['addresses']

    outputs = []
    for transaction in transactions:
      for vout in transaction['vout']:
        if not 'addresses' in vout['scriptPubKey']:
          continue
        if len(vout['scriptPubKey']['addresses']) != 1:
          continue
        if vout['scriptPubKey']['addresses'][0] in our_addresses:
          outputs.append((value_to_mark(vout['value']), vout['scriptPubKey']['addresses'][0], vout['value'], transaction['txid'], vout['n']))
    for output in outputs:
      self.verify_and_create_timelock(output)
class SafeTimelockCreateHandler(BaseHandler):
  def __init__(self, oracle):
    self.oracle = oracle
    self.btc = oracle.btc
    self.kv = KeyValue(self.oracle.db)

  def mark_unavailable(self, mark, addr):
    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))
    if not mark_data:
      return False

    available = mark_data['available']
    return not available

  def claim_mark(self, mark, addr, return_address, locktime, oracle_fees, miners_fee_satoshi, req_sigs):
    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))
    if not mark_data:
      self.kv.store('mark_available', '{}#{}'.format(mark, addr), {'available':True})

    self.kv.update('mark_available', '{}#{}'.format(mark, addr), {
      'available': False,
      'return_address': return_address,
      'ts': int(time.mktime(datetime.datetime.utcnow().timetuple())),
      'locktime': locktime,
      'oracle_fees': oracle_fees,
      'miners_fee_satoshi': miners_fee_satoshi,
      'req_sigs': req_sigs
    })
    logging.info("claimed mark {} for addr {}".format(mark, return_address))

  def extend_observed_addresses(self, address):
    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    if not observed_addresses:
      self.kv.store('safe_timelock', 'addresses', {'addresses':[]})

    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    observed_addresses = observed_addresses['addresses']
    if address in observed_addresses:
      return

    observed_addresses.append(address)
    logging.info("extending observed address {}".format(address))
    self.kv.update('safe_timelock', 'addresses', {'addresses':observed_addresses})

  def save_redeem(self, addr, redeem):
    try:
      self.kv.store('safe_timelock_redeem', addr, {'redeem':redeem})
    except AssertionError:
      # Already saved
      pass

  def handle_request(self, request):
    message = request.message

    return_address = message['return_address']
    mark = get_mark_for_address(return_address)
    address_to_pay_on = self.oracle.btc.add_multisig_address(message['req_sigs'], message['pubkey_list'])
    retval = self.oracle.btc.create_multisig_address(message['req_sigs'], message['pubkey_list'])
    redeemScript = retval['redeemScript']

    self.save_redeem(address_to_pay_on, redeemScript)

    self.extend_observed_addresses(address_to_pay_on)

    locktime = int(message['locktime'])
    oracle_fees = message['oracle_fees']
    miners_fee_satoshi = message['miners_fee_satoshi']
    req_sigs = message['req_sigs']

    if self.mark_unavailable(mark, address_to_pay_on):
      reply_msg = {
        'operation': 'safe_timelock_error',
        'in_reply_to': message['message_id'],
        'comment': 'A marker for this address is currently unavailable. Try again in a couple minutes, or try a different return address. See: https://github.com/orisi/orisi/issues/88',
        'contract_id' : '{}#{}'.format(address_to_pay_on, mark),
        'message_id': "%s-%s" % (address_to_pay_on, str(randrange(1000000000,9000000000)))
      }
      logging.info("mark {} unavailable".format(mark))

      self.oracle.broadcast_with_fastcast(json.dumps(reply_msg))
      return

    # For now oracles are running single-thread so there is no race condition
    self.claim_mark(mark, address_to_pay_on, return_address, locktime, oracle_fees, miners_fee_satoshi, req_sigs)

    reply_msg = { 'operation' : 'safe_timelock_created',
        'contract_id' : '{}#{}'.format(address_to_pay_on, mark),
        'comment': 'mark claimed, use {} as value sufix, you have {} minutes to send cash to address {}'.format(mark, int(TIME_FOR_TRANSACTION / 60), address_to_pay_on),
        'in_reply_to' : message['message_id'],
        'message_id': "%s-%s" % (address_to_pay_on, str(randrange(1000000000,9000000000))),
        'mark': mark,
        'addr': address_to_pay_on,
        'time': TIME_FOR_TRANSACTION}

    self.oracle.broadcast_with_fastcast(json.dumps(reply_msg))

    message['contract_id'] = '{}#{}'.format(address_to_pay_on, mark)

    release_time = int(time.time()) + TIME_FOR_TRANSACTION + NUMBER_OF_CONFIRMATIONS * TIME_FOR_CONFIRMATION

    self.oracle.task_queue.save({
        "operation": 'timelock_mark_release',
        "json_data": json.dumps({'mark': mark, 'address': address_to_pay_on}),
        "done": 0,
        "next_check": release_time
    })

  def handle_task(self, task):
    message = cjson.decode(task['json_data'])

    txid = message['txid']
    n = message['n']
    redeemScript = self.kv.get_by_section_key('safe_timelock_redeem', message['address'])['redeem']
    tx = self.btc.get_raw_transaction(txid)
    transaction = self.btc.decode_raw_transaction(tx)
    vout = None
    for v in transaction['vout']:
      if v['n'] == n:
        vout = v
        break

    if not vout:
      logging.info("missing vout for txid {} n {}".format(txid, n))
      return

    sum_satoshi = int(round(vout['value'] * 100000000))
    message['sum_satoshi'] = sum_satoshi

    scriptPubKey = vout['scriptPubKey']['hex']

    prevtx = {
        'txid': txid,
        'vout': n,
        'redeemScript': redeemScript,
        'scriptPubKey': scriptPubKey
    }
    prevtxs = [prevtx,]
    message['prevtxs'] = prevtxs

    message['outputs'] = message['oracle_fees']

    logging.debug('outputs: %r' % message['outputs'])

    try:
      future_transaction = self.try_prepare_raw_transaction(message)
    except:

      msg_id = "%s-%s" % ("timelock_signature", str(randrange(1000000000,9000000000)))
      contract_id = "{}#{}".format(message['address'], message['mark'])

      info_msg = {
        'operation': 'timelock_signing_fail',
        'in_reply_to': '',
        'message_id': msg_id,
        'contract_id' : contract_id,
      }
      self.oracle.broadcast_with_fastcast(json.dumps(info_msg))

      logging.exception('problem signing tx: %r ; %r' % (contract_id, msg_id))
      return

    if not future_transaction:
      return

    logging.info(future_transaction)

    try:
      pwtxid = self.get_tx_hash(future_transaction)
    except:
      logging.error("Failed to create tx hash")
      return

    assert(future_transaction is not None) # should've been verified gracefully in handle_request

    self.oracle.signer.sign(future_transaction, pwtxid, prevtxs, message['req_sigs'])

    info_msg = {
      'operation': 'safe_timelock_signed',
      'in_reply_to': '',
      'message_id': "%s-%s" % ("timelock_signature", str(randrange(1000000000,9000000000))),
      'contract_id' : "{}#{}".format(message['address'], message['mark']),
    }

    self.oracle.broadcast_with_fastcast(json.dumps(info_msg))
class SafeTimelockCreateHandler(BaseHandler):
  def __init__(self, oracle):
    self.oracle = oracle
    self.btc = oracle.btc
    self.kv = KeyValue(self.oracle.db)

  def mark_unavailable(self, mark, addr):
    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))
    if not mark_data:
      return False

    available = mark_data['available']
    return not available

  def claim_mark(self, mark, addr, return_address, locktime, oracle_fees, miners_fee_satoshi, req_sigs):
    mark_data = self.kv.get_by_section_key('mark_available', '{}#{}'.format(mark, addr))
    if not mark_data:
      self.kv.store('mark_available', '{}#{}'.format(mark, addr), {'available':True})

    self.kv.update('mark_available', '{}#{}'.format(mark, addr), {
      'available': False,
      'return_address': return_address,
      'ts': int(time.mktime(datetime.datetime.utcnow().timetuple())),
      'locktime': locktime,
      'oracle_fees': oracle_fees,
      'miners_fee_satoshi': miners_fee_satoshi,
      'req_sigs': req_sigs
    })
    logging.info("claimed mark {} for addr {}".format(mark, return_address))

  def extend_observed_addresses(self, address):
    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    if not observed_addresses:
      self.kv.store('safe_timelock', 'addresses', {'addresses':[]})

    observed_addresses = self.kv.get_by_section_key('safe_timelock', 'addresses')
    observed_addresses = observed_addresses['addresses']
    if address in observed_addresses:
      return

    observed_addresses.append(address)
    logging.info("extending observed address {}".format(address))
    self.kv.update('safe_timelock', 'addresses', {'addresses':observed_addresses})

  def save_redeem(self, addr, redeem):
    try:
      self.kv.store('safe_timelock_redeem', addr, {'redeem':redeem})
    except AssertionError:
      # Already saved
      pass

  def handle_request(self, request):
    message = request.message

    return_address = message['return_address']
    mark = get_mark_for_address(return_address)
    address_to_pay_on = self.oracle.btc.add_multisig_address(message['req_sigs'], message['pubkey_list'])
    retval = self.oracle.btc.create_multisig_address(message['req_sigs'], message['pubkey_list'])
    redeemScript = retval['redeemScript']

    self.save_redeem(address_to_pay_on, redeemScript)

    self.extend_observed_addresses(address_to_pay_on)

    locktime = int(message['locktime'])
    oracle_fees = message['oracle_fees']
    miners_fee_satoshi = message['miners_fee_satoshi']
    req_sigs = message['req_sigs']

    if self.mark_unavailable(mark, address_to_pay_on):
      reply_msg = {
        'operation': 'safe_timelock_error',
        'in_reply_to': message['message_id'],
        'comment': 'mark for this address is currently unavailable - please try again in several minutes',
        'contract_id' : '{}#{}'.format(address_to_pay_on, mark),
        'message_id': "%s-%s" % (address_to_pay_on, str(randrange(1000000000,9000000000)))
      }
      logging.info("mark {} unavailable".format(mark))

      self.oracle.broadcast_with_fastcast(json.dumps(reply_msg))
      return

    # For now oracles are running single-thread so there is no race condition
    self.claim_mark(mark, address_to_pay_on, return_address, locktime, oracle_fees, miners_fee_satoshi, req_sigs)

    reply_msg = { 'operation' : 'safe_timelock_created',
        'contract_id' : '{}#{}'.format(address_to_pay_on, mark),
        'comment': 'mark claimed, use {} as value sufix, you have {} minutes to send cash to address {}'.format(mark, int(TIME_FOR_TRANSACTION / 60), address_to_pay_on),
        'in_reply_to' : message['message_id'],
        'message_id': "%s-%s" % (address_to_pay_on, str(randrange(1000000000,9000000000))),
        'mark': mark,
        'addr': address_to_pay_on,
        'time': TIME_FOR_TRANSACTION}

    self.oracle.broadcast_with_fastcast(json.dumps(reply_msg))

    message['contract_id'] = '{}#{}'.format(address_to_pay_on, mark)

    release_time = int(time.time()) + TIME_FOR_TRANSACTION + NUMBER_OF_CONFIRMATIONS * TIME_FOR_CONFIRMATION

    self.oracle.task_queue.save({
        "operation": 'timelock_mark_release',
        "json_data": json.dumps({'mark': mark, 'address': address_to_pay_on}),
        "done": 0,
        "next_check": release_time
    })

  def handle_task(self, task):
    message = cjson.decode(task['json_data'])

    txid = message['txid']
    n = message['n']
    redeemScript = self.kv.get_by_section_key('safe_timelock_redeem', message['address'])['redeem']
    tx = self.btc.get_raw_transaction(txid)
    transaction = self.btc.decode_raw_transaction(tx)
    vout = None
    for v in transaction['vout']:
      if v['n'] == n:
        vout = v
        break

    if not vout:
      logging.info("missing vout for txid {} n {}".format(txid, n))
      return

    sum_satoshi = int(round(vout['value'] * 100000000))
    message['sum_satoshi'] = sum_satoshi

    scriptPubKey = vout['scriptPubKey']['hex']

    prevtx = {
        'txid': txid,
        'vout': n,
        'redeemScript': redeemScript,
        'scriptPubKey': scriptPubKey
    }
    prevtxs = [prevtx,]
    message['prevtxs'] = prevtxs

    message['outputs'] = message['oracle_fees']

    future_transaction = self.try_prepare_raw_transaction(message)
    if not future_transaction:
      return
    logging.info(future_transaction)

    try:
      pwtxid = self.get_tx_hash(future_transaction)
    except:
      logging.error("Failed to create tx hash")
      return

    assert(future_transaction is not None) # should've been verified gracefully in handle_request

    self.oracle.signer.sign(future_transaction, pwtxid, prevtxs, message['req_sigs'])

    info_msg = {
      'operation': 'safe_timelock_signed',
      'in_reply_to': '',
      'message_id': "%s-%s" % ("timelock_signature", str(randrange(1000000000,9000000000))),
      'contract_id' : "{}#{}".format(message['address'], message['mark']),
    }

    self.oracle.broadcast_with_fastcast(json.dumps(info_msg))
Esempio n. 9
0
class TransactionSigner(BaseHandler):
    def __init__(self, oracle):
        self.oracle = oracle
        self.btc = oracle.btc
        self.kv = KeyValue(self.oracle.db)

    def includes_me(self, prevtx):
        for tx in prevtx:
            if not 'redeemScript' in tx:
                return False
            my_turn = self.get_my_turn(tx['redeemScript'])
            if my_turn < 0:
                return False
        return True

    def get_my_turn(self, redeem_script):
        # oracles sign transactions based on the order of their signatures

        addresses = sorted(self.btc.decode_script(redeem_script)['addresses'])
        for idx, addr in enumerate(addresses):
            if self.btc.address_is_mine(addr):
                return idx
        return -1

    def is_proper_transaction(self, tx, prevtxs):
        logging.info('testing tx: %r' % tx)
        logging.info('with prevtxs: %r' % prevtxs)

        if not self.oracle.btc.is_valid_transaction(tx):
            logging.debug("transaction invalid")
            return False

        inputs, outputs = self.btc.get_inputs_outputs(tx)

        if not self.includes_me(prevtxs):
            logging.debug("transaction does not include me")
            return False

        if self.oracle.btc.transaction_already_signed(tx, prevtxs):
            logging.debug("transaction already signed")
            return False

        return True

    def sign(self, tx, pwtxid, inputs, req_sigs):
        # sign is being called by external contracts to initiate signing procedure
        # it marks the transaction as being ready to be signed if received from fastcast
        # and schedules signing -- in case oracles previous in line didn't want to sign it

        logging.debug("tx: %r" % tx)

        tx_inputs, tx_outputs = self.btc.get_inputs_outputs(tx)

        #todo: shouldn't all the input scripts be guaranteed to be exactly the same by now?
        turns = [
            self.get_my_turn(vin['redeemScript']) for vin in inputs
            if 'redeemScript' in vin
        ]

        my_turn = max(turns)
        add_time = (my_turn - 1) * TURN_LENGTH_TIME

        rq_hash = self.get_tx_hash(tx)
        logging.info("sign -> rq_hash: {}".format(rq_hash))

        try:
            self.kv.store(
                'signable', rq_hash, {
                    'inputs': inputs,
                    'sigs_so_far': 0,
                    'req_sigs': req_sigs,
                    'pwtxid': pwtxid
                })
        except:
            logging.warning(
                'duplicate sign task? this try..except should be removed ultimately!'
            )

        self.oracle.task_queue.save({
            "operation":
            'sign',
            "json_data":
            json.dumps({"transaction": tx}),
            "next_check":
            time.time() + add_time,
            "done":
            0,
        })

    def sign_now(self, tx):
        # sign now signs the transaction and broadcasts it over the network

        inputs, outputs = self.btc.get_inputs_outputs(tx)

        rq_hash = self.get_tx_hash(tx)

        rq_data = self.kv.get_by_section_key('signable', rq_hash)
        if rq_data is None:
            logging.debug("not scheduled to sign this")
            return

        inputs = rq_data['inputs']
        sigs_so_far = rq_data['sigs_so_far']
        req_sigs = rq_data['req_sigs']

        assert (self.is_proper_transaction(tx, inputs))

        tx_sigs_count = self.btc.signatures_count(tx, inputs)

        logging.debug("sigs count so far: %r; req_sigs: %r" %
                      (tx_sigs_count, req_sigs))

        if sigs_so_far > tx_sigs_count:  # or > not >=? TODO
            logging.debug('already signed a transaction with more sigs')
            return

        rq_data['sigs_so_far'] = tx_sigs_count
        self.kv.update('signable', rq_hash, rq_data)
        # ^ let's remember the tx with most sigs that we've seen.

        if tx_sigs_count >= req_sigs:
            logging.debug('already signed with enough keys')
            return

        pwtxid = rq_data['pwtxid']

        signed_transaction = self.btc.sign_transaction(tx, inputs)

        tx_new_sigs_count = self.btc.signatures_count(signed_transaction,
                                                      inputs)

        if (tx_new_sigs_count == tx_sigs_count):
            logging.debug(
                'failed signing transaction. already signed by me? aborting')
            return

        tx_sigs_count += 1

        body = {
            'pwtxid': pwtxid,
            'operation': 'sign',
            'transaction': signed_transaction,
            'sigs': tx_sigs_count,
            'req_sigs': req_sigs
        }
        logging.debug('broadcasting: %r' % body)
        self.oracle.broadcast_with_fastcast(json.dumps(body))

        if tx_sigs_count == req_sigs:
            safe_pushtx(signed_transaction)
            self.oracle.btc.send_transaction(signed_transaction)

        rq_data['sigs_so_far'] = tx_sigs_count
        self.kv.update('signable', rq_hash, rq_data)

    def handle_request(self, request):
        body = request.message
        # if the oracle received a transaction from fastcast, it attempts to sign it
        # all the validity checks are being handled by sign_now

        tx = body['transaction']

        self.sign_now(tx)

    def handle_task(self, task):
        # handles scheduled signing
        # in a perfect world only the first oracle would have to call this
        # and all the others would sign through handle_request

        self.oracle.task_queue.done(task)
        message = json.loads(task['json_data'])
        tx = message['transaction']

        rq_hash = self.get_tx_hash(tx)

        rq_data = self.kv.get_by_section_key('signable', rq_hash)
        assert (rq_data is not None)

        logging.info("rq_data: %r" % rq_data)

        if rq_data['sigs_so_far'] > 0:
            logging.debug('I already signed more popular txs')
            return

        self.sign_now(tx)