Merge pull request #725 from mempool/simon/liquid-unblinding-refactor
Refactored liquid unblinding code into a new file.
This commit is contained in:
		
						commit
						b165339230
					
				
							
								
								
									
										138
									
								
								frontend/src/app/components/transaction/liquid-ublinding.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								frontend/src/app/components/transaction/liquid-ublinding.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
			
		||||
import { Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
 | 
			
		||||
  // Parse the blinders data from a string encoded as a comma separated list, in the following format:
 | 
			
		||||
  // <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex>
 | 
			
		||||
  // This can be repeated with a comma separator to specify blinders for multiple outputs.
 | 
			
		||||
export class LiquidUnblinding {
 | 
			
		||||
  commitments: Map<any, any>;
 | 
			
		||||
 | 
			
		||||
  parseBlinders(str: string) {
 | 
			
		||||
    const parts = str.split(',');
 | 
			
		||||
    const blinders = [];
 | 
			
		||||
    while (parts.length) {
 | 
			
		||||
      blinders.push({
 | 
			
		||||
        value: this.verifyNum(parts.shift()),
 | 
			
		||||
        asset: this.verifyHex32(parts.shift()),
 | 
			
		||||
        value_blinder: this.verifyHex32(parts.shift()),
 | 
			
		||||
        asset_blinder: this.verifyHex32(parts.shift()),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return blinders;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  verifyNum(num: string) {
 | 
			
		||||
    if (!+num) {
 | 
			
		||||
      throw new Error('Invalid blinding data (invalid number)');
 | 
			
		||||
    }
 | 
			
		||||
    return +num;
 | 
			
		||||
  }
 | 
			
		||||
  verifyHex32(str: string) {
 | 
			
		||||
    if (!str || !/^[0-9a-f]{64}$/i.test(str)) {
 | 
			
		||||
      throw new Error('Invalid blinding data (invalid hex)');
 | 
			
		||||
    }
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeCommitmentMap(blinders: any) {
 | 
			
		||||
    const libwally = await import('./libwally.js');
 | 
			
		||||
    await libwally.load();
 | 
			
		||||
    const commitments = new Map();
 | 
			
		||||
    blinders.forEach(b => {
 | 
			
		||||
      const { asset_commitment, value_commitment } =
 | 
			
		||||
      libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder);
 | 
			
		||||
 | 
			
		||||
      commitments.set(`${asset_commitment}:${value_commitment}`, {
 | 
			
		||||
        asset: b.asset,
 | 
			
		||||
        value: b.value,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return commitments;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Look for the given output, returning an { value, asset } object
 | 
			
		||||
  find(vout: any) {
 | 
			
		||||
    return vout.assetcommitment && vout.valuecommitment &&
 | 
			
		||||
      this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Lookup all transaction inputs/outputs and attach the unblinded data
 | 
			
		||||
  tryUnblindTx(tx: Transaction) {
 | 
			
		||||
    if (tx) {
 | 
			
		||||
      if (tx._unblinded) { return tx._unblinded; }
 | 
			
		||||
      let matched = 0;
 | 
			
		||||
      if (tx.vout !== undefined) {
 | 
			
		||||
        tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout));
 | 
			
		||||
        tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout));
 | 
			
		||||
      }
 | 
			
		||||
      if (this.commitments !== undefined) {
 | 
			
		||||
        tx._unblinded = { matched, total: this.commitments.size };
 | 
			
		||||
        this.deduceBlinded(tx);
 | 
			
		||||
        if (matched < this.commitments.size) {
 | 
			
		||||
          throw new Error(`Error: Invalid blinding data.`)
 | 
			
		||||
        }
 | 
			
		||||
        tx._deduced = false; // invalidate cache so deduction is attempted again
 | 
			
		||||
        return tx._unblinded;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Look the given output and attach the unblinded data
 | 
			
		||||
  tryUnblindOut(vout: any) {
 | 
			
		||||
    const unblinded = this.find(vout);
 | 
			
		||||
    if (unblinded) { Object.assign(vout, unblinded); }
 | 
			
		||||
    return !!unblinded;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Attempt to deduce the blinded input/output based on the available information
 | 
			
		||||
  deduceBlinded(tx: any) {
 | 
			
		||||
    if (tx._deduced) { return; }
 | 
			
		||||
    tx._deduced = true;
 | 
			
		||||
 | 
			
		||||
    // Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment)
 | 
			
		||||
    const unknownIns = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null);
 | 
			
		||||
    const unknownOuts = tx.vout.filter(vout => vout.value == null);
 | 
			
		||||
 | 
			
		||||
    // If the transaction has a single unknown input/output, we can deduce its asset/amount
 | 
			
		||||
    // based on the other known inputs/outputs.
 | 
			
		||||
    if (unknownIns.length + unknownOuts.length === 1) {
 | 
			
		||||
 | 
			
		||||
      // Keep a per-asset tally of all known input amounts, minus all known output amounts
 | 
			
		||||
      const totals = new Map();
 | 
			
		||||
      tx.vin.filter(vin => vin.prevout && vin.prevout.value != null)
 | 
			
		||||
        .forEach(({ prevout }) =>
 | 
			
		||||
          totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value));
 | 
			
		||||
      tx.vout.filter(vout => vout.value != null)
 | 
			
		||||
        .forEach(vout =>
 | 
			
		||||
          totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value));
 | 
			
		||||
 | 
			
		||||
      // There should only be a single asset where the inputs and outputs amounts mismatch,
 | 
			
		||||
      // which is the asset of the blinded input/output
 | 
			
		||||
      const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value !== 0);
 | 
			
		||||
      if (remainder.length !== 1) { throw new Error('unexpected remainder while deducing blinded tx'); }
 | 
			
		||||
      const [ blindedAsset, blindedValue ] = remainder[0];
 | 
			
		||||
 | 
			
		||||
      // A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output,
 | 
			
		||||
      // a negative one is the input.
 | 
			
		||||
      if (blindedValue > 0) {
 | 
			
		||||
        if (!unknownOuts.length) { throw new Error('expected unknown output'); }
 | 
			
		||||
        unknownOuts[0].asset = blindedAsset;
 | 
			
		||||
        unknownOuts[0].value = blindedValue;
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!unknownIns.length) { throw new Error('expected unknown input'); }
 | 
			
		||||
        unknownIns[0].prevout.asset = blindedAsset;
 | 
			
		||||
        unknownIns[0].prevout.value = blindedValue * -1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkUnblindedTx(tx: Transaction) {
 | 
			
		||||
    const windowLocationHash = window.location.hash.substring('#blinded='.length);
 | 
			
		||||
    if (windowLocationHash.length > 0) {
 | 
			
		||||
      const blinders = this.parseBlinders(windowLocationHash);
 | 
			
		||||
      if (blinders) {
 | 
			
		||||
        this.commitments = await this.makeCommitmentMap(blinders);
 | 
			
		||||
        this.tryUnblindTx(tx);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -17,6 +17,7 @@ import { AudioService } from 'src/app/services/audio.service';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { LiquidUnblinding } from './liquid-ublinding';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-transaction',
 | 
			
		||||
@ -40,9 +41,9 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
  cpfpInfo: CpfpInfo | null;
 | 
			
		||||
  showCpfpDetails = false;
 | 
			
		||||
  fetchCpfp$ = new Subject<string>();
 | 
			
		||||
  commitments: Map<any, any>;
 | 
			
		||||
  now = new Date().getTime();
 | 
			
		||||
  timeAvg$: Observable<number>;
 | 
			
		||||
  liquidUnblinding = new LiquidUnblinding();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
@ -123,10 +124,8 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    this.subscription = this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap(async (params: ParamMap) => {
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.txId = params.get('id') || '';
 | 
			
		||||
 | 
			
		||||
          await this.checkUnblindedTx();
 | 
			
		||||
          this.seoService.setTitle(
 | 
			
		||||
            $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
 | 
			
		||||
          );
 | 
			
		||||
@ -157,8 +156,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(
 | 
			
		||||
        async (tx: Transaction) => {
 | 
			
		||||
      .subscribe((tx: Transaction) => {
 | 
			
		||||
          if (!tx) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
@ -199,7 +197,12 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
              this.fetchCpfp$.next(this.tx.txid);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          await this.checkUnblindedTx();
 | 
			
		||||
          if (this.network === 'liquid') {
 | 
			
		||||
            this.liquidUnblinding.checkUnblindedTx(this.tx)
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                this.errorUnblinded = error;
 | 
			
		||||
              });
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        (error) => {
 | 
			
		||||
          this.error = error;
 | 
			
		||||
@ -294,145 +297,4 @@ export class TransactionComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.fetchCpfpSubscription.unsubscribe();
 | 
			
		||||
    this.leaveTransaction();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Parse the blinders data from a string encoded as a comma separated list, in the following format:
 | 
			
		||||
  // <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex>
 | 
			
		||||
  // This can be repeated with a comma separator to specify blinders for multiple outputs.
 | 
			
		||||
 | 
			
		||||
  parseBlinders(str: string) {
 | 
			
		||||
    const parts = str.split(',');
 | 
			
		||||
    const blinders = [];
 | 
			
		||||
    while (parts.length) {
 | 
			
		||||
      blinders.push({
 | 
			
		||||
        value: this.verifyNum(parts.shift()),
 | 
			
		||||
        asset: this.verifyHex32(parts.shift()),
 | 
			
		||||
        value_blinder: this.verifyHex32(parts.shift()),
 | 
			
		||||
        asset_blinder: this.verifyHex32(parts.shift()),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return blinders;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  verifyNum(num: string) {
 | 
			
		||||
    if (!+num) {
 | 
			
		||||
      throw new Error('Invalid blinding data (invalid number)');
 | 
			
		||||
    }
 | 
			
		||||
    return +num;
 | 
			
		||||
  }
 | 
			
		||||
  verifyHex32(str: string) {
 | 
			
		||||
    if (!str || !/^[0-9a-f]{64}$/i.test(str)) {
 | 
			
		||||
      throw new Error('Invalid blinding data (invalid hex)');
 | 
			
		||||
    }
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async makeCommitmentMap(blinders: any) {
 | 
			
		||||
    const libwally = await import('./libwally.js');
 | 
			
		||||
    await libwally.load();
 | 
			
		||||
    const commitments = new Map();
 | 
			
		||||
    blinders.forEach(b => {
 | 
			
		||||
      const { asset_commitment, value_commitment } =
 | 
			
		||||
      libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder);
 | 
			
		||||
 | 
			
		||||
      commitments.set(`${asset_commitment}:${value_commitment}`, {
 | 
			
		||||
        asset: b.asset,
 | 
			
		||||
        value: b.value,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return commitments;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Look for the given output, returning an { value, asset } object
 | 
			
		||||
  find(vout: any) {
 | 
			
		||||
    return vout.assetcommitment && vout.valuecommitment &&
 | 
			
		||||
      this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Lookup all transaction inputs/outputs and attach the unblinded data
 | 
			
		||||
  tryUnblindTx(tx: any) {
 | 
			
		||||
    if (tx) {
 | 
			
		||||
      if (tx._unblinded) { return tx._unblinded; }
 | 
			
		||||
      let matched = 0;
 | 
			
		||||
      if (tx.vout !== undefined) {
 | 
			
		||||
        tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout));
 | 
			
		||||
        tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout));
 | 
			
		||||
      }
 | 
			
		||||
      if (this.commitments !== undefined) {
 | 
			
		||||
        tx._unblinded = { matched, total: this.commitments.size };
 | 
			
		||||
        this.deduceBlinded(tx);
 | 
			
		||||
        if (matched < this.commitments.size) {
 | 
			
		||||
          this.errorUnblinded = `Error: Invalid blinding data.`;
 | 
			
		||||
        }
 | 
			
		||||
        tx._deduced = false; // invalidate cache so deduction is attempted again
 | 
			
		||||
        return tx._unblinded;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Look the given output and attach the unblinded data
 | 
			
		||||
  tryUnblindOut(vout: any) {
 | 
			
		||||
    const unblinded = this.find(vout);
 | 
			
		||||
    if (unblinded) { Object.assign(vout, unblinded); }
 | 
			
		||||
    return !!unblinded;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Attempt to deduce the blinded input/output based on the available information
 | 
			
		||||
  deduceBlinded(tx: any) {
 | 
			
		||||
    if (tx._deduced) { return; }
 | 
			
		||||
    tx._deduced = true;
 | 
			
		||||
 | 
			
		||||
    // Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment)
 | 
			
		||||
    const unknownIns = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null);
 | 
			
		||||
    const unknownOuts = tx.vout.filter(vout => vout.value == null);
 | 
			
		||||
 | 
			
		||||
    // If the transaction has a single unknown input/output, we can deduce its asset/amount
 | 
			
		||||
    // based on the other known inputs/outputs.
 | 
			
		||||
    if (unknownIns.length + unknownOuts.length === 1) {
 | 
			
		||||
 | 
			
		||||
      // Keep a per-asset tally of all known input amounts, minus all known output amounts
 | 
			
		||||
      const totals = new Map();
 | 
			
		||||
      tx.vin.filter(vin => vin.prevout && vin.prevout.value != null)
 | 
			
		||||
        .forEach(({ prevout }) =>
 | 
			
		||||
          totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value));
 | 
			
		||||
      tx.vout.filter(vout => vout.value != null)
 | 
			
		||||
        .forEach(vout =>
 | 
			
		||||
          totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value));
 | 
			
		||||
 | 
			
		||||
      // There should only be a single asset where the inputs and outputs amounts mismatch,
 | 
			
		||||
      // which is the asset of the blinded input/output
 | 
			
		||||
      const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value !== 0);
 | 
			
		||||
      if (remainder.length !== 1) { throw new Error('unexpected remainder while deducing blinded tx'); }
 | 
			
		||||
      const [ blindedAsset, blindedValue ] = remainder[0];
 | 
			
		||||
 | 
			
		||||
      // A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output,
 | 
			
		||||
      // a negative one is the input.
 | 
			
		||||
      if (blindedValue > 0) {
 | 
			
		||||
        if (!unknownOuts.length) { throw new Error('expected unknown output'); }
 | 
			
		||||
        unknownOuts[0].asset = blindedAsset;
 | 
			
		||||
        unknownOuts[0].value = blindedValue;
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!unknownIns.length) { throw new Error('expected unknown input'); }
 | 
			
		||||
        unknownIns[0].prevout.asset = blindedAsset;
 | 
			
		||||
        unknownIns[0].prevout.value = blindedValue * -1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkUnblindedTx() {
 | 
			
		||||
    try {
 | 
			
		||||
      if (this.network === 'liquid') {
 | 
			
		||||
        const windowLocationHash = window.location.hash.substring('#blinded='.length);
 | 
			
		||||
        if (windowLocationHash.length > 0) {
 | 
			
		||||
 | 
			
		||||
          const blinders = this.parseBlinders(windowLocationHash);
 | 
			
		||||
          if (blinders) {
 | 
			
		||||
            this.commitments = await this.makeCommitmentMap(blinders);
 | 
			
		||||
            this.tryUnblindTx(this.tx);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.errorUnblinded = error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user