示例#1
0
def propose(obj, asset_p, amount_p, asset_r, amount_r, output, fee_rate):
    """Propose a swap

    A swap consists in sending AMOUNT_P of ASSET_P and receiving AMOUNT_R of
    ASSET_R.
    """

    with ConnCtx(obj.credentials, critical) as cc:
        connection = cc.connection
        do_initial_checks(connection, obj.is_mainnet)

        proposal = swap.propose(btc2sat(amount_p), asset_p, btc2sat(amount_r),
                                asset_r, connection, fee_rate)
        encoded_payload = encode_payload(proposal)
        click.echo(encoded_payload, file=output)
示例#2
0
    def generate_proposal(self, parent):
        asset_p = self.comboBoxSendAsset.currentData()
        amount_p = btc2sat(float(self.lineEditSendAmount.text() or 0))
        asset_r = self.comboBoxReceiveAsset.currentData()
        amount_r = btc2sat(float(self.lineEditReceiveAmount.text() or 0))

        with ConnCtx(parent.credentials, parent.critical) as cc:
            connection = cc.connection
            fee_rate = float(connection.getnetworkinfo()['relayfee'])

            proposal = swap.propose(amount_p=amount_p,
                                    asset_p=asset_p,
                                    amount_r=amount_r,
                                    asset_r=asset_r,
                                    connection=connection,
                                    fee_rate=fee_rate)

            encoded_payload = encode_payload(proposal)
            parent.copy_dialog(text=encoded_payload,
                               suggested_name='proposal.txt')
示例#3
0
def parse_accepted(signed_tx, blinding_keys, u_address_p, u_address_r,
                   connection):
    """Parse an accepted swap proposal

    Proposer checks correctness of the accepted proposal and deduce its
    details.
    """

    logging.info('Parsing accepted swap proposal')
    logging.debug('Blinding keys: {}'.format(blinding_keys))
    logging.debug('Proposer address: {}'.format(u_address_p))
    logging.debug('Receiver address: {}'.format(u_address_r))

    # There should be a way for the proposer to recognize his proposal, e.g.
    # proposer sends his proposal signed, receiver includes it in what he sends
    # back, proposer verify that signed tx actually includes what he proposed

    is_proposer = is_mine(u_address_p, connection)
    is_receiver = is_mine(u_address_r, connection)

    if is_receiver:
        # FIXME: once this will be implemented, the case where the node owns
        # both addresses must be handled
        raise OwnProposalError('Parsing an own accepted proposal is not '
                               'implemented yet.')
    elif not is_proposer:
        raise UnexpectedValueError('Unable to proceed: both proposer and '
                                   'receiver addresses ({}, {}) are not owned '
                                   'by the wallet'.format(
                                       u_address_p, u_address_r))

    amount_p = amount_r = 0
    asset_p = asset_r = ''

    logging.debug('Importing blinding keys')
    # import blinding keys to fully unblind the transaction
    for c_address, blinding_key in blinding_keys.items():
        connection.importblindingkey(c_address, blinding_key)

    logging.debug('Unblinding transaction to analyze it')
    unblinded_tx = connection.unblindrawtransaction(signed_tx)['hex']
    decoded_tx = connection.decoderawtransaction(unblinded_tx)
    logging.debug('Decoded unblinded transaction: {}'.format(decoded_tx))

    if decoded_tx['locktime'] != NLOCKTIME:
        msg = 'Unexpected nLocktime value: expected {}, found {}'.format(
            decoded_tx['nlocktime'], NLOCKTIME)
        raise UnexpectedValueError(msg)

    # TODO: the same thing should happen for IS_REPLACEABLE

    # deduce tx inputs owned by receiver
    receiver_unspents = connection.listunspent()
    amounts_in = dict()
    for input_ in decoded_tx['vin']:
        for unspent in receiver_unspents:
            if (input_['txid'] == unspent['txid']
                    and input_['vout'] == unspent['vout']):
                asset = unspent['asset']
                amount = btc2sat(unspent['amount'])

                amounts_in.update({asset: amounts_in.get(asset, 0) + amount})
                break

    # deduce tx outputs owned by receiver
    amounts_out = dict()
    for output in decoded_tx['vout']:
        if 'value' not in output:
            raise UnblindError('Transaction is not fully unblinded')

        if 'addresses' in output['scriptPubKey']:
            u_address = output['scriptPubKey']['addresses'][0]
            amount = btc2sat(output['value'])
            asset = output['asset']

            if u_address == u_address_r:
                if asset_p:
                    raise UnexpectedValueError('Found more than one receiver '
                                               'address')
                amount_p = amount
                asset_p = asset
            elif u_address == u_address_p:
                if asset_r:
                    raise UnexpectedValueError('Found more than one proposer '
                                               'address')
                amount_r = amount
                asset_r = asset

            if connection.getaddressinfo(u_address)['ismine']:
                amounts_out.update({asset: amounts_out.get(asset, 0) + amount})

        elif output['scriptPubKey']['type'] == 'fee':
            asset_fee = output['asset']
            fee_tot = btc2sat(output['value'])

    if amount_p == 0 or asset_p == '':
        raise ValueError('Missing receiver address in transaction')
    if amount_r == 0 or asset_r == '':
        raise ValueError('Missing proposer address in transaction')

    tx_balance = {
        k: amounts_out.get(k, 0) - amounts_in.get(k, 0)
        for k in set(list(amounts_in) + list(amounts_out))
    }

    expected_keys = set([asset_r, asset_p, asset_fee])
    if expected_keys != set(tx_balance):
        msg = 'Unexpected keys in transaction balance: found {}, expected {}' \
              ', balance'.format(tx_balance.keys(), expected_keys, tx_balance)
        raise UnexpectedValueError(msg)

    if asset_fee not in (asset_p, asset_r):
        fee_p = -tx_balance[asset_fee]
        fee_r = fee_tot - fee_p

    # in case one of the asset is the same as the fee, deduce fees from the
    # (unsafe) amount given to the function, if fees are acceptable, the amount
    # is assumed to be acceptable too
    if asset_fee == asset_p:
        fee_p = -tx_balance[asset_p] - amount_p
        fee_r = fee_tot - fee_p

    if asset_fee == asset_r:
        fee_p = amount_r - tx_balance[asset_r]
        fee_r = fee_tot - fee_p

    if fee_tot <= 0 or fee_p <= 0 or fee_r <= 0:
        raise UnexpectedValueError('Unexpected fees: tot {}, proposer {}, '
                                   'receiver {}'.format(fee_tot, fee_p, fee_r))

    if asset_fee != asset_p and -tx_balance[asset_p] != amount_p:
        raise UnexpectedValueError(
            'Unexpected amount sent by proposer: found {}, expected {}'.format(
                -tx_balance[asset_p], amount_p))

    if asset_fee != asset_r and tx_balance[asset_r] != amount_r:
        raise UnexpectedValueError(
            'Unexpected amount sent by receiver: found {}, expected {}'.format(
                tx_balance[asset_r], amount_r))

    logging.debug('Proposer: amount {} (sat), asset {}, fee {} (sat)'.format(
        amount_p, asset_p, fee_p))
    logging.debug('Receiver: amount {} (sat), asset {}, fee {} (sat)'.format(
        amount_r, asset_r, fee_r))
    return (signed_tx, amount_p, asset_p, fee_p, amount_r, asset_r, fee_r)
示例#4
0
def propose(amount_p, asset_p, amount_r, asset_r, connection, fee_rate=None):
    """Propose a swap

    Proposer (p) sends amount_p of asset_p.
    Receiver (r) is asked to send amount_r of asset_r.
    """

    logging.info('Proposing a swap [1/3]')
    logging.info('Send {} (sat) of asset {}'.format(amount_p, asset_p))
    logging.info('Receive {} (sat) of asset {}'.format(amount_r, asset_r))
    logging.info('Fee rate: {}'.format(fee_rate or 'default'))

    if not all([amount_p, asset_p, amount_r, asset_r]):
        raise MissingValueError('Missing or zero parameter')

    if asset_p == asset_r:
        raise SameAssetError('Swaps between the same asset are currently not '
                             'supported.')

    is_mainnet = connection.getblockchaininfo().get('chain') == 'liquidv1'

    c_address_p = connection.getnewaddress()
    u_address_p = connection.getaddressinfo(c_address_p)['unconfidential']

    txu = connection.createrawtransaction(
        [], {DUMMY_ADDRESS_CONFIDENTIAL[is_mainnet]: sat2btc(amount_p)},
        NLOCKTIME, IS_REPLACEABLE,
        {DUMMY_ADDRESS_CONFIDENTIAL[is_mainnet]: asset_p})

    # FIXME: consider locking unspents.
    details = {
        # 'lockUnspents': False,
    }
    if fee_rate is not None:
        details.update({'feeRate': fee_rate})

    logging.debug('Selecting inputs to fund swap transaction (proposer)')
    txf = connection.fundrawtransaction(txu, details)['hex']

    inputs = connection.decoderawtransaction(txf)['vin']
    outputs = connection.decoderawtransaction(txf)['vout']

    # collect details (keys) for each input
    keys = ['txid', 'vout', 'amount', 'asset', 'amountblinder', 'assetblinder']
    unspents = connection.listunspent()
    unspents_details = list()
    for input_ in inputs:
        for unspent in unspents:
            if (input_['txid'] == unspent['txid']
                    and input_['vout'] == unspent['vout']):
                unspent['amount'] = btc2sat(unspent.pop('amount'))
                unspents_details.append({k: unspent[k] for k in keys})

    if len(inputs) != len(unspents_details):
        raise MissingValueError('Unable to collect unspent details')

    # construct maps for amount and asset to feed (again) createrawtransaction
    map_amount = dict()
    map_asset = dict()

    # map unconfidential addresses to confidential addresses
    map_confidential = dict()

    for output in outputs:
        if 'addresses' in output['scriptPubKey']:
            u_address = output['scriptPubKey']['addresses'][0]

            if u_address == DUMMY_ADDRESS[is_mainnet]:
                key = u_address
            else:
                c_address = connection.getaddressinfo(
                    u_address)['confidential']
                map_confidential.update({u_address: c_address})
                key = c_address

            map_amount.update({key: btc2sat(output['value'])})
            map_asset.update({key: output['asset']})

        elif output['scriptPubKey']['type'] == 'fee':
            map_amount.update({'fee': btc2sat(output['value'])})

    # add asset and amount to receive
    map_amount.update({c_address_p: amount_r})
    map_asset.update({c_address_p: asset_r})
    map_confidential.update({u_address_p: c_address_p})

    tx = connection.createrawtransaction(inputs, values2btc(map_amount),
                                         NLOCKTIME, IS_REPLACEABLE, map_asset)

    logging.debug('Proposer address: {}'.format(u_address_p))
    logging.debug('Address map: {}'.format(map_confidential))
    logging.debug('Unspents_details: {}'.format(unspents_details))
    return {
        'tx': tx,
        'u_address_p': u_address_p,
        'map_confidential': map_confidential,
        'unspents_details': unspents_details,
    }
示例#5
0
def accept(tx_p,
           c_address_p,
           amount_p,
           asset_p,
           fee_p,
           amount_r,
           asset_r,
           map_amount_p,
           map_asset_p,
           unspents_details_p,
           connection,
           fee_rate=None):
    """Accept a (parsed) swap proposal

    Fund, blind and sign the transaction. Should be used with outputs from
    parse_proposed.
    """

    logging.info('Accepting swap proposal [2/3]')

    c_address_r = connection.getnewaddress()
    u_address_r = connection.validateaddress(c_address_r)['unconfidential']
    u_address_p = connection.validateaddress(c_address_p)['unconfidential']
    logging.debug('Receiver address: {}'.format(u_address_r))
    logging.debug('Proposer address: {}'.format(u_address_p))

    txu = connection.createrawtransaction([], {c_address_p: sat2btc(amount_r)},
                                          NLOCKTIME, IS_REPLACEABLE,
                                          {c_address_p: asset_r})

    details = {
        # 'lockUnspents': False,
    }
    if fee_rate is not None:
        details.update({'feeRate': fee_rate})

    logging.debug('Selecting inputs to fund swap transaction (receiver)')
    tx_r = connection.fundrawtransaction(
        txu,
        details,
    )['hex']
    inputs_r = connection.decoderawtransaction(tx_r)['vin']
    outputs_r = connection.decoderawtransaction(tx_r)['vout']

    # collect details (keys) for each input
    keys = ['txid', 'vout', 'amount', 'asset', 'amountblinder', 'assetblinder']
    unspents_r = connection.listunspent()
    unspents_details_r = list()
    for input_ in inputs_r:
        for unspent in unspents_r:
            if (input_['txid'] == unspent['txid']
                    and input_['vout'] == unspent['vout']):
                unspent['amount'] = btc2sat(unspent.pop('amount'))
                unspents_details_r.append({k: unspent[k] for k in keys})

    unspents_details = unspents_details_p + unspents_details_r

    # deduce fees and maps for outputs and amounts from tx_r
    map_amount_r = dict()
    map_asset_r = dict()

    for output in outputs_r:
        if 'addresses' in output['scriptPubKey']:
            u_address = output['scriptPubKey']['addresses'][0]

            if u_address == u_address_p:
                c_address = c_address_p
            else:
                c_address = connection.getaddressinfo(
                    u_address)['confidential']

            map_amount_r.update({c_address: btc2sat(output['value'])})
            map_asset_r.update({c_address: output['asset']})

        elif output['scriptPubKey']['type'] == 'fee':
            # map_amount_r is updated later with the cumulative fee
            fee_r = btc2sat(output['value'])

    # proposer maps do not include the dummy address
    map_amount_p.update({c_address_r: amount_p})
    map_asset_p.update({c_address_r: asset_p})

    # join inputs and outputs from p and r
    inputs_p = connection.decoderawtransaction(tx_p)['vin']

    inputs = inputs_p + inputs_r
    random.seed(os.urandom(32))
    random.shuffle(inputs)

    map_amount = dict()
    map_asset = dict()

    map_amount.update(map_amount_p)
    map_amount.update(map_amount_r)
    map_amount.update({'fee': fee_p + fee_r})

    map_asset.update(map_asset_p)
    map_asset.update(map_asset_r)

    logging.debug('Creating swap transaction')
    # createrawtransaction with the inputs from the 2 transactions
    tx = connection.createrawtransaction(inputs,
                                         sort_dict(values2btc(map_amount)),
                                         NLOCKTIME, IS_REPLACEABLE,
                                         sort_dict(map_asset))

    # inputs may be reordered by createrawtransaction
    inputs = connection.decoderawtransaction(tx)['vin']

    # pack blinders, assetblinders, assets and amounts
    amountblinders = list()
    assetblinders = list()
    assets = list()
    amounts = list()

    for input_ in inputs:
        for unspent_details in unspents_details:
            if (input_['txid'] == unspent_details['txid']
                    and input_['vout'] == unspent_details['vout']):
                amountblinders.append(unspent_details['amountblinder'])
                assetblinders.append(unspent_details['assetblinder'])
                assets.append(unspent_details['asset'])
                amounts.append(unspent_details['amount'])

    if len(inputs) != len(amounts):
        raise MissingValueError('Missing data in unspent details')

    logging.debug('Blinding swap transaction')
    # blind transaction
    btx = connection.rawblindrawtransaction(tx, amountblinders,
                                            [sat2btc(v) for v in amounts],
                                            assets, assetblinders)

    logging.debug('Signing swap transaction (receiver inputs)')
    # sign transaction
    stx = connection.signrawtransactionwithwallet(btx)['hex']

    logging.debug('Dumping blinding keys (so that the swap partner can fully '
                  'unblind the transaction)')
    # dump bliding private keys
    blinding_keys = dict()
    for c_address in map_amount:
        if (c_address != 'fee'
                and connection.getaddressinfo(c_address)['ismine']):
            blinding_key = connection.dumpblindingkey(c_address)
            blinding_keys.update({c_address: blinding_key})

    min_relay_fee = connection.getnetworkinfo()['relayfee']
    dtx = connection.decoderawtransaction(stx)
    # TODO: estimate impact of missing signatures
    estimated_vsize_vb = dtx['vsize']
    estimated_fee_rate = (fee_p + fee_r) * 10**-5 / estimated_vsize_vb

    if estimated_fee_rate < min_relay_fee:
        msg = 'Fee rate too low: please specify an higher fee rate or reject' \
              ' the proposal ({:.8f}, min: {})'.format(estimated_fee_rate,
                                                       min_relay_fee)
        raise FeeRateError(msg)

    logging.debug('Blinding keys: {}'.format(blinding_keys))
    return {
        'tx': stx,
        'blinding_keys': blinding_keys,
        'u_address_p': u_address_p,
        'u_address_r': u_address_r,
    }
示例#6
0
def parse_proposed(tx, u_address_p, map_confidential, unspents_details,
                   connection):
    """Parse a proposed swap

    Receiver checks correctness of proposal and deduce its details.
    """

    logging.info('Parsing swap proposal')
    logging.debug('Proposer address: {}'.format(u_address_p))
    logging.debug('Address map: {}'.format(map_confidential))
    logging.debug('Unspents_details: {}'.format(unspents_details))

    is_mainnet = connection.getblockchaininfo().get('chain') == 'liquidv1'

    is_proposer = is_mine(u_address_p, connection)
    if is_proposer:
        logging.info('Parsing own proposal')

    dtx = connection.decoderawtransaction(tx)
    logging.debug('Decoded proposer transaction: {}'.format(dtx))
    outputs = dtx['vout']

    # Deduce details of the swap (amount_p,r, asset_p,r) from the tx
    # Construct maps to feed createrawtransaction

    amount_p = amount_r = fee_p = 0
    asset_p = asset_r = ''

    map_amount_p = dict()
    map_asset_p = dict()

    for output in outputs:
        if 'addresses' in output['scriptPubKey']:
            u_address = output['scriptPubKey']['addresses'][0]

            if u_address == DUMMY_ADDRESS[is_mainnet]:
                if asset_p:
                    raise UnexpectedValueError('Found more than one dummy '
                                               'address')
                amount_p = btc2sat(output['value'])
                asset_p = output['asset']
                logging.debug('Proposer: amount {} (sat), asset {}'.format(
                    amount_p, asset_p))

                # dummy address will be replaced, do not include it in the maps
                continue

            elif u_address == u_address_p:
                if asset_r:
                    raise UnexpectedValueError('Found more than one proposer '
                                               'address')
                amount_r = btc2sat(output['value'])
                asset_r = output['asset']
                logging.debug('Receiver: amount {} (sat), asset {}'.format(
                    amount_r, asset_r))

            c_address = map_confidential.get(u_address, '')
            if not c_address:
                raise MissingValueError('Missing address in map_confidential')

            if not connection.validateaddress(c_address)['isvalid']:
                raise InvalidAddressError(
                    'Invalid address: {}'.format(c_address))

            if u_address != connection.getaddressinfo(
                    c_address)['unconfidential']:
                msg = 'Unmatching confidential-unconfidential address: {}, ' \
                      '{}'.format(c_address, u_address)
                raise UnexpectedValueError(msg)

            map_amount_p.update({c_address: btc2sat(output['value'])})
            map_asset_p.update({c_address: output['asset']})

        elif output['scriptPubKey']['type'] == 'fee':
            fee_p = btc2sat(output['value'])
            logging.debug('Proposer: fee {} (sat)'.format(fee_p))

    if amount_p == 0 or asset_p == '':
        raise MissingValueError('Missing dummy address')
    elif amount_r == 0 or asset_r == '':
        raise MissingValueError('Missing proposer address in the transaction')
    elif fee_p == 0:
        raise MissingValueError('Missing fee')

    c_address_p = map_confidential[u_address_p]

    logging.debug('Proposer map amount: {}'.format(map_amount_p))
    logging.debug('Proposer map asset: {}'.format(map_asset_p))

    return (tx, c_address_p, amount_p, asset_p, fee_p, amount_r, asset_r,
            map_amount_p, map_asset_p, unspents_details)