def process_proposals(self, proposals):
        """ This is the "meat" of the SNICKERReceiver service.
        It parses proposals and creates and broadcasts transactions
        with the wallet, assuming all conditions are met.
        Note that this is ONLY called from the proposals poll loop.

        Each entry in `proposals` is of form:
        encrypted_proposal - base64 string
        key - hex encoded compressed pubkey, or ''
        if the key is not null, we attempt to decrypt and
        process according to that key, else cycles over all keys.

        If all SNICKER validations succeed, the decision to spend is
        entirely dependent on self.acceptance_callback.
        If the callback returns True, we co-sign and broadcast the
        transaction and also update the wallet with the new
        imported key (TODO: future versions will enable searching
        for these keys using history + HD tree; note the jmbitcoin
        snicker.py module DOES insist on ECDH being correctly used,
        so this will always be possible for transactions created here.

        Returned is a list of txids of any transactions which
        were broadcast, unless a critical error occurs, in which case
        False is returned (to minimize this function's trust in other
        parts of the code being executed, if something appears to be
        inconsistent, we trigger immediate halt with this return).
        """

        for kp in proposals:
            # handle empty list entries:
            if not kp:
                continue
            try:
                p, k = kp.split(',')
            except:
                # could argue for info or warning debug level,
                # but potential for a lot of unwanted output.
                jlog.debug("Invalid proposal string, ignoring: " + kp)
                continue
            if k is not None:
                # note that this operation will succeed as long as
                # the key is in the wallet._script_map, which will
                # be true if the key is at an HD index lower than
                # the current wallet.index_cache
                k = hextobin(k)
                addr = self.wallet_service.pubkey_to_addr(k)
                if not self.wallet_service.is_known_addr(addr):
                    jlog.debug("Key not recognized as part of our "
                               "wallet, ignoring.")
                    continue
                result = self.wallet_service.parse_proposal_to_signed_tx(
                    addr, p, self.acceptance_callback)
                if result[0] is not None:
                    tx, tweak, out_spk = result
                    # We will: rederive the key as a sanity check,
                    # and see if it matches the claimed spk.
                    # Then, we import the key into the wallet
                    # (even though it's re-derivable from history, this
                    # is the easiest for a first implementation).
                    # Finally, we co-sign, then push.
                    # (Again, simplest function: checks already passed,
                    # so do it automatically).
                    tweaked_key = btc.snicker_pubkey_tweak(k, tweak)
                    tweaked_spk = self.wallet_service.pubkey_to_script(
                        tweaked_key)
                    # Derive original path to make sure we change
                    # mixdepth:
                    source_path = self.wallet_service.script_to_path(
                        self.wallet_service.pubkey_to_script(k))
                    # NB This will give the correct source mixdepth independent
                    # of whether the key is imported or not:
                    source_mixdepth = self.wallet_service.get_details(
                        source_path)[0]
                    if not tweaked_spk == out_spk:
                        jlog.error("The spk derived from the pubkey does "
                                   "not match the scriptPubkey returned from "
                                   "the snicker module - code error.")
                        return False
                    # before import, we should derive the tweaked *private* key
                    # from the tweak, also; failure of this critical sanity check
                    # is a code error. If the recreated private key matches, we
                    # import to the wallet. Note that this happens *before* pushing
                    # the coinjoin transaction to the network, which is advisably
                    # conservative (never possible to have broadcast a tx without
                    # having already stored the output's key).
                    success, msg = self.wallet_service.check_tweak_matches_and_import(
                        addr, tweak, tweaked_key, source_mixdepth)
                    if not success:
                        jlog.error(msg)
                        return False

                    # TODO condition on automatic brdcst or not
                    if not jm_single().bc_interface.pushtx(tx.serialize()):
                        # this represents an error about state (or conceivably,
                        # an ultra-short window in which the spent utxo was
                        # consumed in another transaction), but not really
                        # an internal logic error, so we do NOT return False
                        jlog.error("Failed to broadcast SNICKER coinjoin: " +\
                                   bintohex(tx.GetTxid()[::-1]))
                        jlog.info(btc.human_readable_transaction(tx))
                    jlog.info("Successfully broadcast SNICKER coinjoin: " +\
                                  bintohex(tx.GetTxid()[::-1]))
                    self.log_successful_tx(tx)
                else:
                    jlog.debug('Failed to parse proposal: ' + result[1])
            else:
                # Some extra work to implement checking all possible
                # keys.
                jlog.info("Proposal without pubkey was not processed.")

        # Completed processing all proposals without any logic
        # errors (whether the proposals were valid or accepted
        # or not).
        return True
예제 #2
0
def main():
    parser = OptionParser(usage='usage: %prog [options] walletname',
                          description=description)
    parser.add_option('-m',
                      '--mixdepth',
                      action='store',
                      type='int',
                      dest='mixdepth',
                      default=0,
                      help="mixdepth to source coins from")
    parser.add_option('-a',
                      '--amtmixdepths',
                      action='store',
                      type='int',
                      dest='amtmixdepths',
                      help='number of mixdepths in wallet, default 5',
                      default=5)
    parser.add_option('-g',
                      '--gap-limit',
                      type="int",
                      action='store',
                      dest='gaplimit',
                      help='gap limit for wallet, default=6',
                      default=6)
    add_base_options(parser)
    (options, args) = parser.parse_args()
    load_program_config(config_path=options.datadir)
    check_regtest()
    if len(args) != 1:
        log.error("Invalid arguments, see --help")
        sys.exit(EXIT_ARGERROR)
    wallet_name = args[0]
    wallet_path = get_wallet_path(wallet_name, None)
    max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1])
    wallet = open_test_wallet_maybe(
        wallet_path,
        wallet_name,
        max_mix_depth,
        wallet_password_stdin=options.wallet_password_stdin,
        gap_limit=options.gaplimit)
    wallet_service = WalletService(wallet)

    # step 1: do a full recovery style sync. this will pick up
    # all addresses that we expect to match transactions against,
    # from a blank slate Core wallet that originally had no imports.
    if not options.recoversync:
        jmprint("Recovery sync was not set, but using it anyway.")
    while not wallet_service.synced:
        wallet_service.sync_wallet(fast=False)
    # Note that the user may be interrupted above by the rescan
    # request; this is as for normal scripts; after the rescan is done
    # (usually, only once, but, this *IS* needed here, unlike a normal
    # wallet generation event), we just try again.

    # Now all address from HD are imported, we need to grab
    # all the transactions for those addresses; this includes txs
    # that *spend* as well as receive our coins, so will include
    # "first-out" SNICKER txs as well as ordinary spends and JM coinjoins.
    seed_transactions = wallet_service.get_all_transactions()

    # Search for SNICKER txs and add them if they match.
    # We proceed recursively; we find all one-out matches, then
    # all 2-out matches, until we find no new ones and stop.

    if len(seed_transactions) == 0:
        jmprint("No transactions were found for this wallet. Did you rescan?")
        return False

    new_txs = []
    current_block_heights = set()
    for tx in seed_transactions:
        if btc.is_snicker_tx(tx):
            jmprint("Found a snicker tx: {}".format(
                bintohex(tx.GetTxid()[::-1])))
            equal_outs = btc.get_equal_outs(tx)
            if not equal_outs:
                continue
            if all([
                    wallet_service.is_known_script(x.scriptPubKey) == False
                    for x in [a[1] for a in equal_outs]
            ]):
                # it is now *very* likely that one of the two equal
                # outputs is our SNICKER custom output
                # script; notice that in this case, the transaction *must*
                # have spent our inputs, since it didn't recognize ownership
                # of either coinjoin output (and if it did recognize the change,
                # it would have recognized the cj output also).
                # We try to regenerate one of the outputs, but warn if
                # we can't.
                my_indices = get_pubs_and_indices_of_inputs(tx,
                                                            wallet_service,
                                                            ours=True)
                for mypub, mi in my_indices:
                    for eo in equal_outs:
                        for (other_pub, i) in get_pubs_and_indices_of_inputs(
                                tx, wallet_service, ours=False):
                            for (our_pub,
                                 j) in get_pubs_and_indices_of_ancestor_inputs(
                                     tx.vin[mi], wallet_service, ours=True):
                                our_spk = wallet_service.pubkey_to_script(
                                    our_pub)
                                our_priv = wallet_service.get_key_from_addr(
                                    wallet_service.script_to_addr(our_spk))
                                tweak_bytes = btc.ecdh(our_priv[:-1],
                                                       other_pub)
                                tweaked_pub = btc.snicker_pubkey_tweak(
                                    our_pub, tweak_bytes)
                                tweaked_spk = wallet_service.pubkey_to_script(
                                    tweaked_pub)
                                if tweaked_spk == eo[1].scriptPubKey:
                                    # TODO wallet.script_to_addr has a dubious assertion, that's why
                                    # we use btc method directly:
                                    address_found = str(
                                        btc.CCoinAddress.from_scriptPubKey(
                                            btc.CScript(tweaked_spk)))
                                    #address_found = wallet_service.script_to_addr(tweaked_spk)
                                    jmprint(
                                        "Found a new SNICKER output belonging to us."
                                    )
                                    jmprint(
                                        "Output address {} in the following transaction:"
                                        .format(address_found))
                                    jmprint(btc.human_readable_transaction(tx))
                                    jmprint(
                                        "Importing the address into the joinmarket wallet..."
                                    )
                                    # NB for a recovery we accept putting any imported keys all into
                                    # the same mixdepth (0); TODO investigate correcting this, it will
                                    # be a little complicated.
                                    success, msg = wallet_service.check_tweak_matches_and_import(
                                        wallet_service.script_to_addr(our_spk),
                                        tweak_bytes, tweaked_pub,
                                        wallet_service.mixdepth)
                                    if not success:
                                        jmprint(
                                            "Failed to import SNICKER key: {}".
                                            format(msg), "error")
                                        return False
                                    else:
                                        jmprint("... success.")
                                    # we want the blockheight to track where the next-round rescan
                                    # must start from
                                    current_block_heights.add(
                                        wallet_service.
                                        get_transaction_block_height(tx))
                                    # add this transaction to the next round.
                                    new_txs.append(tx)
    if len(new_txs) == 0:
        return True
    seed_transactions.extend(new_txs)
    earliest_new_blockheight = min(current_block_heights)
    jmprint("New SNICKER addresses were imported to the Core wallet; "
            "do rescanblockchain again, starting from block {}, before "
            "restarting this script.".format(earliest_new_blockheight))
    return False
    def process_proposals(self, proposals):
        """ Each entry in `proposals` is of form:
        encrypted_proposal - base64 string
        key - hex encoded compressed pubkey, or ''
        if the key is not null, we attempt to decrypt and
        process according to that key, else cycles over all keys.

        If all SNICKER validations succeed, the decision to spend is
        entirely dependent on self.acceptance_callback.
        If the callback returns True, we co-sign and broadcast the
        transaction and also update the wallet with the new
        imported key (TODO: future versions will enable searching
        for these keys using history + HD tree; note the jmbitcoin
        snicker.py module DOES insist on ECDH being correctly used,
        so this will always be possible for transactions created here.

        Returned is a list of txids of any transactions which
        were broadcast, unless a critical error occurs, in which case
        False is returned (to minimize this function's trust in other
        parts of the code being executed, if something appears to be
        inconsistent, we trigger immediate halt with this return).
        """

        for kp in proposals:
            try:
                p, k = kp.split(b',')
            except:
                jlog.error("Invalid proposal string, ignoring: " + kp)
            if k is not None:
                # note that this operation will succeed as long as
                # the key is in the wallet._script_map, which will
                # be true if the key is at an HD index lower than
                # the current wallet.index_cache
                k = hextobin(k.decode('utf-8'))
                addr = self.wallet_service.pubkey_to_addr(k)
                if not self.wallet_service.is_known_addr(addr):
                    jlog.debug("Key not recognized as part of our "
                               "wallet, ignoring.")
                    continue
                # TODO: interface/API of SNICKERWalletMixin would better take
                # address as argument here, not privkey:
                priv = self.wallet_service.get_key_from_addr(addr)
                result = self.wallet_service.parse_proposal_to_signed_tx(
                    priv, p, self.acceptance_callback)
                if result[0] is not None:
                    tx, tweak, out_spk = result

                    # We will: rederive the key as a sanity check,
                    # and see if it matches the claimed spk.
                    # Then, we import the key into the wallet
                    # (even though it's re-derivable from history, this
                    # is the easiest for a first implementation).
                    # Finally, we co-sign, then push.
                    # (Again, simplest function: checks already passed,
                    # so do it automatically).
                    # TODO: the more sophisticated actions.
                    tweaked_key = btc.snicker_pubkey_tweak(k, tweak)
                    tweaked_spk = btc.pubkey_to_p2sh_p2wpkh_script(tweaked_key)
                    if not tweaked_spk == out_spk:
                        jlog.error("The spk derived from the pubkey does "
                                   "not match the scriptPubkey returned from "
                                   "the snicker module - code error.")
                        return False
                    # before import, we should derive the tweaked *private* key
                    # from the tweak, also:
                    tweaked_privkey = btc.snicker_privkey_tweak(priv, tweak)
                    if not btc.privkey_to_pubkey(
                            tweaked_privkey) == tweaked_key:
                        jlog.error("Was not able to recover tweaked pubkey "
                                   "from tweaked privkey - code error.")
                        jlog.error("Expected: " + bintohex(tweaked_key))
                        jlog.error(
                            "Got: " +
                            bintohex(btc.privkey_to_pubkey(tweaked_privkey)))
                        return False
                    # the recreated private key matches, so we import to the wallet,
                    # note that type = None here is because we use the same
                    # scriptPubKey type as the wallet, this has been implicitly
                    # checked above by deriving the scriptPubKey.
                    self.wallet_service.import_private_key(
                        self.import_branch,
                        self.wallet_service._ENGINE.privkey_to_wif(
                            tweaked_privkey))

                    # TODO condition on automatic brdcst or not
                    if not jm_single().bc_interface.pushtx(tx.serialize()):
                        jlog.error("Failed to broadcast SNICKER CJ.")
                        return False
                    self.successful_txs.append(tx)
                    return True
                else:
                    jlog.debug('Failed to parse proposal: ' + result[1])
                    continue
            else:
                # Some extra work to implement checking all possible
                # keys.
                raise NotImplementedError()

        # Completed processing all proposals without any logic
        # errors (whether the proposals were valid or accepted
        # or not).
        return True