PHP WebShell

Текущая директория: /opt/BitGoJS/modules/abstract-utxo/src/transaction/fixedScript

Просмотр файла: parseOutput.ts

import debugLib from 'debug';
import _ from 'lodash';
import {
  AddressVerificationData,
  IRequestTracer,
  InvalidAddressDerivationPropertyError,
  IWallet,
  TransactionPrebuild,
  UnexpectedAddressError,
  VerificationOptions,
  ITransactionRecipient,
  Triple,
} from '@bitgo/sdk-core';

import { AbstractUtxoCoin, Output, isWalletOutput } from '../../abstractUtxoCoin';

const debug = debugLib('bitgo:v2:parseoutput');

interface HandleVerifyAddressErrorResponse {
  external: boolean;
  needsCustomChangeKeySignatureVerification?: boolean;
}

/**
 * Check an address which failed initial validation to see if it's the base address of a migrated v1 bch wallet.
 *
 * The wallet in question could be a migrated SafeHD BCH wallet, and the transaction we
 * are currently parsing is trying to spend change back to the v1 wallet base address.
 *
 * It does this since we don't allow new address creation for these wallets,
 * and instead return the base address from the v1 wallet when a new address is requested.
 * If this new address is requested for the purposes of spending change back to the wallet,
 * the change will go to the v1 wallet base address. This address *is* on the wallet,
 * but it will still cause an error to be thrown by verifyAddress, since the derivation path
 * used for this address is non-standard. (I have seen these addresses derived using paths m/0/0 and m/101,
 * whereas the v2 addresses are derived using path  m/0/0/${chain}/${index}).
 *
 * This means we need to check for this case explicitly in this catch block, and classify
 * these types of outputs as internal instead of external. Failing to do so would cause the
 * transaction's implicit external outputs (ie, outputs which go to addresses not specified in
 * the recipients array) to add up to more than the 150 basis point limit which we enforce on
 * pay-as-you-go outputs (which should be the only implicit external outputs on our transactions).
 *
 * The 150 basis point limit for implicit external sends is enforced in verifyTransaction,
 * which calls this function to get information on the total external/internal spend amounts
 * for a transaction. The idea here is to protect from the transaction being maliciously modified
 * to add more implicit external spends (eg, to an attacker-controlled wallet).
 *
 * See verifyTransaction for more information on how transaction prebuilds are verified before signing.
 *
 * @param wallet {Wallet} wallet which is making the transaction
 * @param currentAddress {string} address to check for externality relative to v1 wallet base address
 */
function isMigratedAddress(wallet: IWallet, currentAddress: string): boolean {
  if (_.isString(wallet.migratedFrom()) && wallet.migratedFrom() === currentAddress) {
    debug('found address %s which was migrated from v1 wallet, address is not external', currentAddress);
    return true;
  }

  return false;
}

interface VerifyCustomChangeAddressOptions {
  coin: AbstractUtxoCoin;
  customChangeKeys: HandleVerifyAddressErrorOptions['customChangeKeys'];
  addressType: HandleVerifyAddressErrorOptions['addressType'];
  addressDetails: HandleVerifyAddressErrorOptions['addressDetails'];
  currentAddress: HandleVerifyAddressErrorOptions['currentAddress'];
}

/**
 * Check to see if an address is derived from the given custom change keys
 * @param {VerifyCustomChangeAddressOptions} params
 * @return {boolean}
 */
async function verifyCustomChangeAddress(params: VerifyCustomChangeAddressOptions): Promise<boolean> {
  const { coin, customChangeKeys, addressType, addressDetails, currentAddress } = params;
  try {
    return await coin.verifyAddress(
      _.extend({ addressType }, addressDetails, {
        keychains: customChangeKeys,
        address: currentAddress,
      })
    );
  } catch (e) {
    debug('failed to verify custom change address %s', currentAddress);
    return false;
  }
}

interface HandleVerifyAddressErrorOptions {
  e: Error;
  currentAddress: string;
  wallet: IWallet;
  txParams: {
    changeAddress?: string;
  };
  customChangeKeys?: CustomChangeOptions['keys'];
  coin: AbstractUtxoCoin;
  addressDetails?: any;
  addressType?: string;
  considerMigratedFromAddressInternal?: boolean;
}

async function handleVerifyAddressError({
  e,
  currentAddress,
  wallet,
  txParams,
  customChangeKeys,
  coin,
  addressDetails,
  addressType,
  considerMigratedFromAddressInternal,
}: HandleVerifyAddressErrorOptions): Promise<HandleVerifyAddressErrorResponse> {
  // Todo: name server-side errors to avoid message-based checking [BG-5124]
  const walletAddressNotFound = e.message.includes('wallet address not found');
  const unexpectedAddress = e instanceof UnexpectedAddressError;
  if (walletAddressNotFound || unexpectedAddress) {
    if (unexpectedAddress && !walletAddressNotFound) {
      // check to see if this is a migrated v1 bch address - it could be internal
      const isMigrated = isMigratedAddress(wallet, currentAddress);
      if (isMigrated) {
        return { external: considerMigratedFromAddressInternal === false };
      }

      debug('Address %s was found on wallet but could not be reconstructed', currentAddress);

      // attempt to verify address using custom change address keys if the wallet has that feature enabled
      if (
        customChangeKeys &&
        (await verifyCustomChangeAddress({ coin, addressDetails, addressType, currentAddress, customChangeKeys }))
      ) {
        // address is valid against the custom change keys. Mark address as not external
        // and request signature verification for the custom change keys
        debug('Address %s verified as derived from the custom change keys', currentAddress);
        return { external: false, needsCustomChangeKeySignatureVerification: true };
      }
    }

    // the address was found, but not on the wallet, which simply means it's external
    debug('Address %s presumed external', currentAddress);
    return { external: true };
  } else if (e instanceof InvalidAddressDerivationPropertyError && currentAddress === txParams.changeAddress) {
    // expect to see this error when passing in a custom changeAddress with no chain or index
    return { external: false };
  }

  console.error('Address classification failed for address', currentAddress);
  console.trace(e);
  /**
   * It might be a completely invalid address or a bad validation attempt or something else completely, in
   * which case we do not proceed and rather rethrow the error, which is safer than assuming that the address
   * validation failed simply because it's external to the wallet.
   */
  throw e;
}

interface FetchAddressDetailsOptions {
  reqId?: IRequestTracer;
  disableNetworking: boolean;
  addressDetailsPrebuild: any;
  addressDetailsVerification: any;
  currentAddress: string;
  wallet: IWallet;
}

async function fetchAddressDetails({
  reqId,
  disableNetworking,
  addressDetailsPrebuild,
  addressDetailsVerification,
  currentAddress,
  wallet,
}: FetchAddressDetailsOptions) {
  let addressDetails = _.extend({}, addressDetailsPrebuild, addressDetailsVerification);
  debug('Locally available address %s details: %O', currentAddress, addressDetails);
  if (_.isEmpty(addressDetails) && !disableNetworking) {
    addressDetails = await wallet.getAddress({ address: currentAddress, reqId });
    debug('Downloaded address %s details: %O', currentAddress, addressDetails);
  }
  return addressDetails;
}

export interface CustomChangeOptions {
  keys: Triple<{ pub: string }>;
  signatures: Triple<string>;
}

export interface ParseOutputOptions {
  currentOutput: Output;
  coin: AbstractUtxoCoin;
  txPrebuild: TransactionPrebuild;
  verification: VerificationOptions;
  keychainArray: Triple<{ pub: string }>;
  wallet: IWallet;
  txParams: {
    recipients: ITransactionRecipient[];
    changeAddress?: string;
  };
  customChange?: CustomChangeOptions;
  reqId?: IRequestTracer;
}

export async function parseOutput({
  currentOutput,
  coin,
  txPrebuild,
  verification,
  keychainArray,
  wallet,
  txParams,
  customChange,
  reqId,
}: ParseOutputOptions): Promise<Output> {
  const disableNetworking = !!verification.disableNetworking;
  const currentAddress = currentOutput.address;

  if (currentAddress === undefined) {
    // In the case that the address is undefined, it means that the output has a non-encodeable scriptPubkey
    // If this is the case, then we need to check that the amount is 0 and we can skip the rest.
    if (currentOutput.amount.toString() !== '0') {
      throw new Error('output with undefined address must have amount of 0');
    }
    return currentOutput;
  }

  // attempt to grab the address details from either the prebuilt tx, or the verification params.
  // If both of these are empty, then we will try to get the address details from bitgo instead
  const addressDetailsPrebuild = _.get(txPrebuild, `txInfo.walletAddressDetails.${currentAddress}`, {});
  const addressDetailsVerification: AddressVerificationData = verification?.addresses?.[currentAddress] ?? {};
  debug('Parsing address details for %s', currentAddress);
  let currentAddressDetails = undefined;
  let currentAddressType: string | undefined = undefined;
  const RECIPIENT_THRESHOLD = 1000;
  try {
    // In the case of PSBTs, we can already determine the internal/external status of the output addresses
    // based on the derivation information being included in the PSBT. We can short circuit GET v2.wallet.address
    // and save on network requests. Since we have the derivation information already, we can still verify the address
    if (currentOutput.external !== undefined) {
      // In the case that we have a custom change wallet, we need to verify the address against the custom change keys
      // and not the wallet keys. This check is done in the handleVerifyAddressError function if this error is thrown.
      if (customChange !== undefined) {
        throw new UnexpectedAddressError('`address validation failure');
      }
      // If it is an internal address, we can skip the network request and just verify the address locally with the
      // derivation information we have. Otherwise, if the address is external, which is the only remaining case, we
      // can just return the current output as is without contacting the server.
      if (isWalletOutput(currentOutput)) {
        const res = await coin.isWalletAddress({
          addressType: AbstractUtxoCoin.inferAddressType({ chain: currentOutput.chain }) || undefined,
          keychains: keychainArray,
          address: currentAddress,
          chain: currentOutput.chain,
          index: currentOutput.index,
        });
        if (!res) {
          throw new UnexpectedAddressError();
        }
      }
      return currentOutput;
    }
    /**
     * The only way to determine whether an address is known on the wallet is to initiate a network request and
     * fetch it. Should the request fail and return a 404, it will throw and therefore has to be caught. For that
     * reason, address wallet ownership detection is wrapped in a try/catch. Additionally, once the address
     * details are fetched on the wallet, a local address validation is run, whose errors however are generated
     * client-side and can therefore be analyzed with more granularity and type checking.
     */

    /**
     * In order to minimize API requests, we assume that explicit recipients are always external when the
     * recipient list is > 1000 This is not always a valid assumption and could lead greater apparent spend (but never lower)
     */
    if (txParams.recipients !== undefined && txParams.recipients.length > RECIPIENT_THRESHOLD) {
      const isCurrentAddressInRecipients = txParams.recipients.some((recipient) =>
        recipient.address.includes(currentAddress)
      );

      if (isCurrentAddressInRecipients) {
        return { ...currentOutput };
      }
    }

    const addressDetails = await fetchAddressDetails({
      reqId,
      addressDetailsVerification,
      addressDetailsPrebuild,
      currentAddress,
      disableNetworking,
      wallet,
    });
    // verify that the address is on the wallet. verifyAddress throws if
    // it fails to correctly rederive the address, meaning it's external
    currentAddressType = AbstractUtxoCoin.inferAddressType(addressDetails) || undefined;
    currentAddressDetails = addressDetails;
    await coin.verifyAddress(
      _.extend({ addressType: currentAddressType }, addressDetails, {
        keychains: keychainArray,
        address: currentAddress,
      })
    );
    debug('Address %s verification passed', currentAddress);

    // verify address succeeded without throwing, so the address was
    // correctly rederived from the wallet keychains, making it not external
    return _.extend({}, currentOutput, addressDetails, { external: false });
  } catch (e) {
    debug('Address %s verification threw an error:', currentAddress, e);
    return _.extend(
      {},
      currentOutput,
      await handleVerifyAddressError({
        e,
        coin,
        currentAddress,
        wallet,
        txParams,
        customChangeKeys: customChange && customChange.keys,
        addressDetails: currentAddressDetails,
        addressType: currentAddressType,
        considerMigratedFromAddressInternal: verification.considerMigratedFromAddressInternal,
      })
    );
  }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!