mirror of
https://github.com/mempool/mempool.git
synced 2025-03-01 17:17:09 +01:00
139 lines
No EOL
5.1 KiB
TypeScript
139 lines
No EOL
5.1 KiB
TypeScript
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; }
|
|
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(`Invalid blinding data.`)
|
|
}
|
|
tx._deduced = false; // invalidate cache so deduction is attempted again
|
|
return tx;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
return this.tryUnblindTx(tx);
|
|
}
|
|
}
|
|
return tx;
|
|
}
|
|
} |