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)
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 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))
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)