Merge pull request #2167 from antonilol/taproot-fee-tooltip
Add Taproot transaction feature tooltip with fee saving information
This commit is contained in:
		
						commit
						5663cd1444
					
				@ -5,13 +5,18 @@ const P2SH_P2WSH_COST  = 35 * 4; // the WU cost for the non-witness part of P2SH
 | 
			
		||||
 | 
			
		||||
export function calcSegwitFeeGains(tx: Transaction) {
 | 
			
		||||
  // calculated in weight units
 | 
			
		||||
  let realizedGains = 0;
 | 
			
		||||
  let realizedBech32Gains = 0;
 | 
			
		||||
  let potentialBech32Gains = 0;
 | 
			
		||||
  let potentialP2shGains = 0;
 | 
			
		||||
  let potentialTaprootGains = 0;
 | 
			
		||||
  let realizedTaprootGains = 0;
 | 
			
		||||
 | 
			
		||||
  for (const vin of tx.vin) {
 | 
			
		||||
    if (!vin.prevout) { continue; }
 | 
			
		||||
 | 
			
		||||
    const isP2pk         = vin.prevout.scriptpubkey_type === 'p2pk';
 | 
			
		||||
    // const isBareMultisig = vin.prevout.scriptpubkey_type === 'multisig'; // type will be unknown, so use the multisig helper from the address labels
 | 
			
		||||
    const isBareMultisig = !!parseMultisigScript(vin.prevout.scriptpubkey_asm);
 | 
			
		||||
    const isP2pkh        = vin.prevout.scriptpubkey_type === 'p2pkh';
 | 
			
		||||
    const isP2sh         = vin.prevout.scriptpubkey_type === 'p2sh';
 | 
			
		||||
    const isP2wsh        = vin.prevout.scriptpubkey_type === 'v0_p2wsh';
 | 
			
		||||
@ -28,43 +33,125 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
			
		||||
      case isP2wsh:
 | 
			
		||||
      case isP2tr:
 | 
			
		||||
        // maximal gains: the scriptSig is moved entirely to the witness part
 | 
			
		||||
        realizedGains += witnessSize(vin) * 3;
 | 
			
		||||
        realizedBech32Gains += witnessSize(vin) * 3;
 | 
			
		||||
        // XXX P2WSH output creation is more expensive, should we take this into consideration?
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Backward compatible Segwit - P2SH-P2WPKH
 | 
			
		||||
      case isP2sh2Wpkh:
 | 
			
		||||
        // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU)
 | 
			
		||||
        realizedGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
 | 
			
		||||
        realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
 | 
			
		||||
        potentialBech32Gains += P2SH_P2WPKH_COST;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Backward compatible Segwit - P2SH-P2WSH
 | 
			
		||||
      case isP2sh2Wsh:
 | 
			
		||||
        // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes
 | 
			
		||||
        realizedGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
 | 
			
		||||
        realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
 | 
			
		||||
        potentialBech32Gains += P2SH_P2WSH_COST;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Non-segwit P2PKH/P2SH
 | 
			
		||||
      // Non-segwit P2PKH/P2SH/P2PK/bare multisig
 | 
			
		||||
      case isP2pkh:
 | 
			
		||||
      case isP2sh:
 | 
			
		||||
      case isP2pk:
 | 
			
		||||
      case isBareMultisig: {
 | 
			
		||||
        const fullGains = scriptSigSize(vin) * 3;
 | 
			
		||||
        potentialBech32Gains += fullGains;
 | 
			
		||||
        potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: should we also consider P2PK and pay-to-bare-script (non-p2sh-wrapped) as upgradable to P2WPKH and P2WSH?
 | 
			
		||||
    if (isP2tr) {
 | 
			
		||||
      if (vin.witness.length === 1) {
 | 
			
		||||
        // key path spend
 | 
			
		||||
        // we don't know if this was a multisig or single sig (the goal of taproot :)),
 | 
			
		||||
        // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%"
 | 
			
		||||
        // the witness size of P2WPKH is 1 (stack size) + 1 (size) + 72 (low s signature) + 1 (size) + 33 (pubkey) = 108 WU
 | 
			
		||||
        // the witness size of key path P2TR is 1 (stack size) + 1 (size) + 64 (signature) = 66 WU
 | 
			
		||||
        realizedTaprootGains += 42;
 | 
			
		||||
      } else {
 | 
			
		||||
        // script path spend
 | 
			
		||||
        // complex scripts with multiple spending paths can often be made around 2x to 3x smaller with the Taproot script tree
 | 
			
		||||
        // because only the hash of the alternative spending path has the be in the witness data, not the entire script,
 | 
			
		||||
        // but only assumptions can be made because the scripts themselves are unknown (again, the goal of taproot :))
 | 
			
		||||
        // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const script = isP2sh2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
 | 
			
		||||
      let replacementSize: number;
 | 
			
		||||
      if (
 | 
			
		||||
        // single sig
 | 
			
		||||
        isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh ||
 | 
			
		||||
        // multisig
 | 
			
		||||
        isBareMultisig || parseMultisigScript(script)
 | 
			
		||||
      ) {
 | 
			
		||||
        // the scriptSig and scriptWitness can all be replaced by a 66 witness WU with taproot
 | 
			
		||||
        replacementSize = 66;
 | 
			
		||||
      } else if (script) {
 | 
			
		||||
        // not single sig, not multisig: the complex scripts
 | 
			
		||||
        // rough calculations on spending paths
 | 
			
		||||
        // every OP_IF and OP_NOTIF indicates an _extra_ spending path, so add 1
 | 
			
		||||
        const spendingPaths = script.split(' ').filter(op => /^(OP_IF|OP_NOTIF)$/g.test(op)).length + 1;
 | 
			
		||||
        // now assume the script could have been split in ${spendingPaths} equal tapleaves
 | 
			
		||||
        replacementSize = script.length / 2 / spendingPaths +
 | 
			
		||||
        // but account for the leaf and branch hashes and internal key in the control block
 | 
			
		||||
          32 * Math.log2((spendingPaths - 1) || 1) + 33;
 | 
			
		||||
      }
 | 
			
		||||
      potentialTaprootGains += witnessSize(vin) + scriptSigSize(vin) * 4 - replacementSize;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // returned as percentage of the total tx weight
 | 
			
		||||
  return { realizedGains: realizedGains / (tx.weight + realizedGains) // percent of the pre-segwit tx size
 | 
			
		||||
         , potentialBech32Gains: potentialBech32Gains / tx.weight
 | 
			
		||||
         , potentialP2shGains: potentialP2shGains / tx.weight
 | 
			
		||||
  return {
 | 
			
		||||
    realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size
 | 
			
		||||
    potentialBech32Gains: potentialBech32Gains / tx.weight,
 | 
			
		||||
    potentialP2shGains: potentialP2shGains / tx.weight,
 | 
			
		||||
    potentialTaprootGains: potentialTaprootGains / tx.weight,
 | 
			
		||||
    realizedTaprootGains: realizedTaprootGains / tx.weight
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
 | 
			
		||||
export function parseMultisigScript(script: string): void | { m: number, n: number } {
 | 
			
		||||
  if (!script) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const ops = script.split(' ');
 | 
			
		||||
  if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const opN = ops.pop();
 | 
			
		||||
  if (!opN.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const n = parseInt(opN.match(/[0-9]+/)[0], 10);
 | 
			
		||||
  if (ops.length < n * 2 + 1) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // pop n public keys
 | 
			
		||||
  for (let i = 0; i < n; i++) {
 | 
			
		||||
    if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const opM = ops.pop();
 | 
			
		||||
  if (!opM.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const m = parseInt(opM.match(/[0-9]+/)[0], 10);
 | 
			
		||||
 | 
			
		||||
  if (ops.length) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { m, n };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/shesek/move-decimal-point
 | 
			
		||||
export function moveDec(num: number, n: number) {
 | 
			
		||||
  let frac, int, neg, ref;
 | 
			
		||||
@ -128,7 +215,7 @@ export const formatNumber = (s, precision = null) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Utilities for segwitFeeGains
 | 
			
		||||
const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0);
 | 
			
		||||
const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S + (w.length / 2), 0) : 0;
 | 
			
		||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
 | 
			
		||||
 | 
			
		||||
// Power of ten wrapper
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
 | 
			
		||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { parseMultisigScript } from 'src/app/bitcoin.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-address-labels',
 | 
			
		||||
@ -98,41 +99,11 @@ export class AddressLabelsComponent implements OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  detectMultisig(script: string) {
 | 
			
		||||
    if (!script) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const ops = script.split(' ');
 | 
			
		||||
    if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const opN = ops.pop();
 | 
			
		||||
    if (!opN.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const n = parseInt(opN.match(/[0-9]+/)[0], 10);
 | 
			
		||||
    if (ops.length < n * 2 + 1) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // pop n public keys
 | 
			
		||||
    for (let i = 0; i < n; i++) {
 | 
			
		||||
      if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const opM = ops.pop();
 | 
			
		||||
    if (!opM.startsWith('OP_PUSHNUM_')) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const m = parseInt(opM.match(/[0-9]+/)[0], 10);
 | 
			
		||||
    const ms = parseMultisigScript(script);
 | 
			
		||||
 | 
			
		||||
    if (ops.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    if (ms) {
 | 
			
		||||
      this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVout() {
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,21 @@
 | 
			
		||||
<span *ngIf="segwitGains.realizedGains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number:  '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
			
		||||
<span *ngIf="segwitGains.realizedBech32Gains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number:  '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
			
		||||
<ng-template #segwitTwo>
 | 
			
		||||
  <span *ngIf="segwitGains.realizedGains && segwitGains.potentialBech32Gains else potentialP2shGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number:  '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
			
		||||
  <span *ngIf="segwitGains.realizedBech32Gains && segwitGains.potentialBech32Gains; else potentialP2shGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number:  '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
			
		||||
  <ng-template #potentialP2shGains>
 | 
			
		||||
    <span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number:  '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<span *ngIf="isTaproot; else noTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Taproot tooltip" ngbTooltip="This transaction uses Taproot" placement="bottom" i18n="tx-features.tag.taproot">Taproot</span>
 | 
			
		||||
<ng-template #noTaproot>
 | 
			
		||||
  <span class="badge badge-danger mr-1" i18n-ngbTooltip="No Taproot tooltip" ngbTooltip="This transaction could save on fees and improve privacy by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot">Taproot</del></span>
 | 
			
		||||
 | 
			
		||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and the user's thereby increased privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
			
		||||
<ng-template #notFullyTaproot>
 | 
			
		||||
  <span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
			
		||||
  <ng-template #noTaproot>
 | 
			
		||||
    <span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about privacy and fees that could be saved with taproot" ngbTooltip="This transaction could increase the user's privacy and save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
 | 
			
		||||
    <ng-template #taprootButNoGains>
 | 
			
		||||
      <span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
<span *ngIf="isRbfTransaction; else rbfDisabled" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
 | 
			
		||||
 | 
			
		||||
<span *ngIf="isRbfTransaction; else rbfDisabled" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction supports Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
 | 
			
		||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1" i18n-ngbTooltip="RBF disabled tooltip" ngbTooltip="This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method" placement="bottom"><del i18n="tx-features.tag.rbf|RBF">RBF</del></span></ng-template>
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,11 @@ export class TxFeaturesComponent implements OnChanges {
 | 
			
		||||
  @Input() tx: Transaction;
 | 
			
		||||
 | 
			
		||||
  segwitGains = {
 | 
			
		||||
    realizedGains: 0,
 | 
			
		||||
    realizedBech32Gains: 0,
 | 
			
		||||
    potentialBech32Gains: 0,
 | 
			
		||||
    potentialP2shGains: 0,
 | 
			
		||||
    potentialTaprootGains: 0,
 | 
			
		||||
    realizedTaprootGains: 0
 | 
			
		||||
  };
 | 
			
		||||
  isRbfTransaction: boolean;
 | 
			
		||||
  isTaproot: boolean;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user