diff --git a/client/src/driver/blinding.js b/client/src/driver/blinding.js index f58a8a02..f3b55b84 100644 --- a/client/src/driver/blinding.js +++ b/client/src/driver/blinding.js @@ -85,8 +85,8 @@ function parseBlinders(str) { } function verifyNum(num) { - if (!+num) throw new Error('Invalid blinding data (invalid number)') - return +num + if (typeof num != 'string' || !/^[1-9][0-9]*$/.test(num)) throw new Error('Invalid blinding data (invalid number)') + return BigInt(num) } function verifyHex32(str) { if (!str || !/^[0-9a-f]{64}$/i.test(str)) throw new Error('Invalid blinding data (invalid hex)') diff --git a/client/src/lib/deduce-blinded.js b/client/src/lib/deduce-blinded.js index cb6cb062..9229adfa 100644 --- a/client/src/lib/deduce-blinded.js +++ b/client/src/lib/deduce-blinded.js @@ -15,10 +15,10 @@ export function deduceBlinded(tx) { 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)) + totals.set(prevout.asset, addAmounts(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)) + totals.set(vout.asset, addAmounts(totals.get(vout.asset) || 0, negateAmount(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 @@ -35,7 +35,15 @@ export function deduceBlinded(tx) { } else { if (!unknown_ins.length) throw new Error('expected unknown input') unknown_ins[0].prevout.asset = blinded_asset - unknown_ins[0].prevout.value = blinded_value * -1 + unknown_ins[0].prevout.value = negateAmount(blinded_value) } } } + +const addAmounts = (a, b) => + typeof a == 'bigint' || typeof b == 'bigint' + ? BigInt(a) + BigInt(b) + : a + b + +const negateAmount = value => + typeof value == 'bigint' ? -value : value * -1 diff --git a/client/src/lib/libwally.js b/client/src/lib/libwally.js index e880c5a5..3ee2b36d 100644 --- a/client/src/lib/libwally.js +++ b/client/src/lib/libwally.js @@ -3,6 +3,8 @@ const WALLY_OK = 0 , ASSET_GENERATOR_LEN = 33 , ASSET_TAG_LEN = 32 , BLINDING_FACTOR_LEN = 32 + , UINT32_MASK = BigInt('0xffffffff') + , UINT64_MAX = BigInt('0xffffffffffffffff') const STATIC_ROOT = process.env.STATIC_ROOT || '' , WASM_URL = process.env.LIBWALLY_WASM_URL || `${STATIC_ROOT}libwally/wallycore.js` @@ -51,7 +53,7 @@ export function asset_generator_from_bytes(asset, asset_blinder) { export function asset_value_commitment(value, value_blinder, asset_commitment) { // Emscripten transforms int64 function arguments into two int32 arguments, see: // https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-pass-int64-t-and-uint64-t-values-from-js-into-wasm-functions - const [value_lo, value_hi] = split_int52_lo_hi(value) + const [value_lo, value_hi] = split_uint64_lo_hi(value) const value_commitment_ptr = Module._malloc(ASSET_COMMITMENT_LEN) checkCode(Module.ccall('wally_asset_value_commitment' @@ -79,18 +81,16 @@ function readBytes(ptr, size) { return bytes } -// Split a 52-bit JavaScript number into two 32-bits numbers for the low and high bits -// https://stackoverflow.com/a/19274574 -function split_int52_lo_hi(i) { - let lo = i | 0 - if (lo < 0) lo += 4294967296 +// Split a uint64 value into two exact 32-bit numbers for Emscripten's i64 ABI. +function split_uint64_lo_hi(i) { + if (typeof i != 'bigint') throw new Error('not a bigint: ' + i) - let hi = i - lo - hi /= 4294967296 + if (i < BigInt(0) || i > UINT64_MAX) throw new Error('not a uint64: ' + i) - if ((hi < 0) || (hi >= 1048576)) throw new Error ("not an int52: "+i) + const lo = Number(i & UINT32_MASK) + , hi = Number((i >> BigInt(32)) & UINT32_MASK) - return [ lo, hi ] + return [ lo, hi ] } function encodeHex(bytes) { diff --git a/client/src/lib/privacy-analysis.js b/client/src/lib/privacy-analysis.js index 97fff628..b34c15f4 100644 --- a/client/src/lib/privacy-analysis.js +++ b/client/src/lib/privacy-analysis.js @@ -53,18 +53,21 @@ export default function getPrivacyAnalysis(tx) { // if the transaction could've avoided the smallest input and still have enough to fund // any of the two outputs, the transaction has what appears to be an unnecessary input. - const minusSmallestIn = sumInputs(tx.vin) - smallestInput(tx.vin) - , largeOut = Math.max(o1.value, o2.value) - , smallOut = Math.min(o1.value, o2.value) - - if (minusSmallestIn >= largeOut + tx.fee) { - // UIH2: if it still covers the larger output and fee, this implies this was - // a non-standard transaction that added extra inputs for exotic reasons - detected.push('exotic-detection-uih2') - } else if (minusSmallestIn >= smallOut + tx.fee) { - // UIH1: if it still covers the small output and fee, this implies the smaller - // output was the change and not the payment - detected.push('change-detection-uih1') + const smallestIn = smallestInput(tx.vin) + if (smallestIn != null) { + const minusSmallestIn = subtractAmounts(sumInputs(tx.vin), smallestIn) + , largeOut = maxAmount(o1.value, o2.value) + , smallOut = minAmount(o1.value, o2.value) + + if (minusSmallestIn >= addAmounts(largeOut, tx.fee)) { + // UIH2: if it still covers the larger output and fee, this implies this was + // a non-standard transaction that added extra inputs for exotic reasons + detected.push('exotic-detection-uih2') + } else if (minusSmallestIn >= addAmounts(smallOut, tx.fee)) { + // UIH1: if it still covers the small output and fee, this implies the smaller + // output was the change and not the payment + detected.push('change-detection-uih1') + } } } } @@ -92,8 +95,39 @@ export default function getPrivacyAnalysis(tx) { // Utilities -const sumInputs = ins => ins.reduce((T, vin) => T + (vin.prevout && vin.prevout.value || 0), 0) - , smallestInput = ins => Math.min(...ins.map(vin => vin.prevout && vin.prevout.value || Math.Infinity)) +const sumInputs = ins => ins.reduce((T, vin) => + addAmounts(T, vin.prevout && vin.prevout.value != null ? vin.prevout.value : 0), 0) + +const smallestInput = ins => ins.reduce((smallest, vin) => { + if (!vin.prevout || vin.prevout.value == null) return smallest + return smallest == null ? vin.prevout.value : minAmount(smallest, vin.prevout.value) +}, null) + +const addAmounts = (a, b) => + hasBigInt(a, b) ? toBigIntAmount(a) + toBigIntAmount(b) : a + b + +const subtractAmounts = (a, b) => + hasBigInt(a, b) ? toBigIntAmount(a) - toBigIntAmount(b) : a - b + +const hasBigInt = (a, b) => typeof a == 'bigint' || typeof b == 'bigint' + +const toBigIntAmount = value => BigInt(value == null ? 0 : value) + +const minAmount = (a, b) => { + if (!hasBigInt(a, b)) return a < b ? a : b + + const aBig = toBigIntAmount(a) + , bBig = toBigIntAmount(b) + return aBig < bBig ? aBig : bBig +} + +const maxAmount = (a, b) => { + if (!hasBigInt(a, b)) return a > b ? a : b + + const aBig = toBigIntAmount(a) + , bBig = toBigIntAmount(b) + return aBig > bBig ? aBig : bBig +} // checks if there's at least one previous output of this type const inputsHasType = (ins, scriptpubkey_type) => @@ -106,7 +140,11 @@ const lostPrecision = num => { if (num == 0) return 0; let count = 0 - for (let d=10; num%d==0; ++count, d*=10); + if (typeof num == 'bigint') { + for (let d=BigInt(10); num%d==0; ++count, d*=BigInt(10)); + } else { + for (let d=10; num%d==0; ++count, d*=10); + } return count } diff --git a/client/src/util.js b/client/src/util.js index 0cc723f0..2efe85ac 100644 --- a/client/src/util.js +++ b/client/src/util.js @@ -56,7 +56,10 @@ export const isRbf = tx => tx.vin.some(vin => vin.sequence < 0xfffffffe) export const isAllNative = tx => tx.vout.every(isNativeOut) -export const outTotal = tx => tx.vout.reduce((N, vout) => N + (vout.value || 0), 0) +export const outTotal = tx => + tx.vout.some(vout => typeof vout.value == 'bigint') + ? tx.vout.reduce((N, vout) => N + (vout.value == null ? BigInt(0) : BigInt(vout.value)), BigInt(0)) + : tx.vout.reduce((N, vout) => N + (vout.value || 0), 0) export const isNativeOut = vout => (!vout.asset && !vout.assetcommitment) || vout.asset === nativeAssetId