From 512589dc79fde70a1dec5f6ed443b4b1d84aa789 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 14 Dec 2023 11:26:17 +0000 Subject: [PATCH] Add fake pubkey filter --- backend/src/api/common.ts | 32 ++++++++-- backend/src/mempool.interfaces.ts | 2 +- backend/src/utils/secp256k1.ts | 74 ++++++++++++++++++++++++ frontend/src/app/shared/filters.utils.ts | 6 +- 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 backend/src/utils/secp256k1.ts diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 42dae7eb0..751bab5a3 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -6,6 +6,7 @@ import { NodeSocket } from '../repositories/NodesSocketsRepository'; import { isIP } from 'net'; import rbfCache from './rbf-cache'; import transactionUtils from './transaction-utils'; +import { isPoint } from '../utils/secp256k1'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -211,6 +212,15 @@ export class Common { } } + static isBurnKey(pubkey: string): boolean { + return [ + '022222222222222222222222222222222222222222222222222222222222222222', + '033333333333333333333333333333333333333333333333333333333333333333', + '020202020202020202020202020202020202020202020202020202020202020202', + '030303030303030303030303030303030303030303030303030303030303030303', + ].includes(pubkey); + } + static getTransactionFlags(tx: TransactionExtended): number { let flags = 0n; if (tx.version === 1) { @@ -249,8 +259,8 @@ export class Common { flags |= this.setSchnorrSighashFlags(flags, vin.witness); } else if (vin.witness) { flags |= this.setSegwitSighashFlags(flags, vin.witness); - } else if (vin.scriptsig_asm) { - flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm); + } else if (vin.scriptsig?.length) { + flags |= this.setLegacySighashFlags(flags, vin.scriptsig_asm || transactionUtils.convertScriptSigAsm(vin.scriptsig)); } if (vin.prevout?.scriptpubkey_address) { @@ -263,12 +273,23 @@ export class Common { } else { flags |= TransactionFlags.no_rbf; } + let hasFakePubkey = false; for (const vout of tx.vout) { switch (vout.scriptpubkey_type) { - case 'p2pk': flags |= TransactionFlags.p2pk; break; + case 'p2pk': { + flags |= TransactionFlags.p2pk; + // detect fake pubkey (i.e. not a valid DER point on the secp256k1 curve) + hasFakePubkey = hasFakePubkey || !isPoint(vout.scriptpubkey.slice(2, -2)); + } break; case 'multisig': { flags |= TransactionFlags.p2ms; - // TODO - detect fake multisig data embedding + // detect fake pubkeys (i.e. not valid DER points on the secp256k1 curve) + const asm = vout.scriptpubkey_asm || transactionUtils.convertScriptSigAsm(vout.scriptpubkey); + for (const key of (asm?.split(' ') || [])) { + if (!hasFakePubkey && !key.startsWith('OP_')) { + hasFakePubkey = hasFakePubkey || this.isBurnKey(key) || !isPoint(key); + } + } } break; case 'p2pkh': flags |= TransactionFlags.p2pkh; break; case 'p2sh': flags |= TransactionFlags.p2sh; break; @@ -282,6 +303,9 @@ export class Common { } outValues[vout.value || Math.random()] = (outValues[vout.value || Math.random()] || 0) + 1; } + if (hasFakePubkey) { + flags |= TransactionFlags.fake_pubkey; + } if (tx.ancestors?.length) { flags |= TransactionFlags.cpfp_child; } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index f50274304..4a630f1e4 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -215,7 +215,7 @@ export const TransactionFlags = { replacement: 0b00000100_00000000_00000000n, // data op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, inscription: 0b00000100_00000000_00000000_00000000n, // heuristics coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, diff --git a/backend/src/utils/secp256k1.ts b/backend/src/utils/secp256k1.ts new file mode 100644 index 000000000..cc731f17d --- /dev/null +++ b/backend/src/utils/secp256k1.ts @@ -0,0 +1,74 @@ +function powMod(x: bigint, power: number, modulo: bigint): bigint { + for (let i = 0; i < power; i++) { + x = (x * x) % modulo; + } + return x; +} + +function sqrtMod(x: bigint, P: bigint): bigint { + const b2 = (x * x * x) % P; + const b3 = (b2 * b2 * x) % P; + const b6 = (powMod(b3, 3, P) * b3) % P; + const b9 = (powMod(b6, 3, P) * b3) % P; + const b11 = (powMod(b9, 2, P) * b2) % P; + const b22 = (powMod(b11, 11, P) * b11) % P; + const b44 = (powMod(b22, 22, P) * b22) % P; + const b88 = (powMod(b44, 44, P) * b44) % P; + const b176 = (powMod(b88, 88, P) * b88) % P; + const b220 = (powMod(b176, 44, P) * b44) % P; + const b223 = (powMod(b220, 3, P) * b3) % P; + const t1 = (powMod(b223, 23, P) * b22) % P; + const t2 = (powMod(t1, 6, P) * b2) % P; + const root = powMod(t2, 2, P); + return root; +} + +const curveP = BigInt(`0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F`); + +/** + * This function tells whether the point given is a DER encoded point on the ECDSA curve. + * @param {string} pointHex The point as a hex string (*must not* include a '0x' prefix) + * @returns {boolean} true if the point is on the SECP256K1 curve + */ +export function isPoint(pointHex: string): boolean { + if ( + !( + // is uncompressed + ( + (pointHex.length === 130 && pointHex.startsWith('04')) || + // OR is compressed + (pointHex.length === 66 && + (pointHex.startsWith('02') || pointHex.startsWith('03'))) + ) + ) + ) { + return false; + } + + // Function modified slightly from noble-curves + + + // Now we know that pointHex is a 33 or 65 byte hex string. + const isCompressed = pointHex.length === 66; + + const x = BigInt(`0x${pointHex.slice(2, 66)}`); + if (x >= curveP) { + return false; + } + + if (!isCompressed) { + const y = BigInt(`0x${pointHex.slice(66, 130)}`); + if (y >= curveP) { + return false; + } + // Just check y^2 = x^3 + 7 (secp256k1 curve) + return (y * y) % curveP === (x * x * x + 7n) % curveP; + } else { + // Get unaltered y^2 (no mod p) + const ySquared = (x * x * x + 7n) % curveP; + // Try to sqrt it, it will round down if not perfect root + const y = sqrtMod(ySquared, curveP); + // If we square and it's equal, then it was a perfect root and valid point. + return (y * y) % curveP === ySquared; + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index 4a8cb6a15..0b652a192 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -29,7 +29,7 @@ export const TransactionFlags = { replacement: 0b00000100_00000000_00000000n, // data op_return: 0b00000001_00000000_00000000_00000000n, - fake_multisig: 0b00000010_00000000_00000000_00000000n, + fake_pubkey: 0b00000010_00000000_00000000_00000000n, inscription: 0b00000100_00000000_00000000_00000000n, // heuristics coinjoin: 0b00000001_00000000_00000000_00000000_00000000n, @@ -64,7 +64,7 @@ export const TransactionFilters: { [key: string]: Filter } = { replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true }, /* data */ op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true }, - // fake_multisig: { key: 'fake_multisig', label: 'Fake multisig', flag: TransactionFlags.fake_multisig }, + fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey }, inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true }, /* heuristics */ coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true }, @@ -82,7 +82,7 @@ export const FilterGroups: { label: string, filters: Filter[]}[] = [ { label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] }, { label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] }, { label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] }, - { label: 'Data', filters: ['op_return', 'fake_multisig', 'inscription'] }, + { label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] }, { label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] }, { label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] }, ].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) })); \ No newline at end of file