From c16db7bbb205607d01b76b4dc9cbbd4f20377106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Fri, 2 May 2025 17:35:25 +0400 Subject: [PATCH] Add fuzzing for amounts * +- 0 to 1%, or +- 1 sat for tiny amounts * Ensure balance cannot go negative --- generate-stats.ts | 54 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/generate-stats.ts b/generate-stats.ts index 96a5a10..9259a68 100644 --- a/generate-stats.ts +++ b/generate-stats.ts @@ -17,7 +17,7 @@ for (const key of ["DB_USER", "DB_PASSWORD", "DB_NAME", "ANONYMIZATION_KEY"]) { // Adjust based on database capacity const BATCH_SIZE = parseInt(env.BATCH_SIZE) || 10; // Maximum number of invoices to process per user -// Skip user if exceeded (likely the case for streaming payments e.g.) +// Skip user if exceeded (likely the case for streaming payments) const MAX_INVOICES = parseInt(env.MAX_INVOICES) || 21000; // Database connection configuration from .env or environment variables @@ -72,7 +72,7 @@ const csvHeaders = [ // Write headers to CSV (create file or overwrite if it exists) await Deno.writeTextFile(csvFilePath, csvHeaders.join(",") + "\n"); -// Function to compute HMAC-SHA256 hash of user_id, truncated to 48 bits (12 hex chars) +// Compute HMAC-SHA256 hash of user_id, truncated to 48 bits (12 hex chars) async function anonymizeUserId(userId) { const keyData = new TextEncoder().encode(env.ANONYMIZATION_KEY); const data = new TextEncoder().encode(userId.toString()); @@ -88,7 +88,19 @@ async function anonymizeUserId(userId) { return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); } -// Function to process a single user and return CSV rows +// Generate a random fuzzing factor +function getFuzzFactor(amount) { + const bytes = new Uint8Array(4); + crypto.getRandomValues(bytes); + const randomValue = new Uint32Array(bytes.buffer)[0]; + const normalized = randomValue / 0xFFFFFFFF; + if (amount < 100) { + return normalized < 0.5 ? -1 : 1; // ±1 satoshi for small amounts + } + return normalized * 0.02 - 0.01; // -1% to +1% for larger amounts +} + +// Process a single user and return CSV rows async function processUser(userId, client) { // Check the number of settled invoices for the user const countResult = await client.queryObject( @@ -123,16 +135,34 @@ async function processUser(userId, client) { for (const invoice of invoices) { const day = invoice.settled_at.toISOString().split("T")[0]; // YYYY-MM-DD - // Convert amounts to BigInt, handling NULL fees - const amount = BigInt(invoice.amount); - const serviceFee = invoice.service_fee ? BigInt(invoice.service_fee) : BigInt(0); - const routingFee = invoice.routing_fee ? BigInt(invoice.routing_fee) : BigInt(0); - // Calculate effective amount: include fees for outgoing, not for incoming - const effectiveAmount = invoice.type === "incoming" - ? amount - : amount + serviceFee + routingFee; // Add fees for outgoing - const signedAmount = invoice.type === "incoming" ? effectiveAmount : -effectiveAmount; + // Get fuzzing factor for this invoice + const fuzzFactor = getFuzzFactor(Number(invoice.amount)); + + // Convert amounts to BigInt + const amount = BigInt(invoice.amount); + const serviceFee = BigInt(invoice.service_fee); + const routingFee = BigInt(invoice.routing_fee); + + // Apply fuzzing: value * (1 + fuzzFactor) or ±1 sat, rounded to nearest integer + let fuzzedAmount = amount < 100 + ? amount + BigInt(fuzzFactor) + : BigInt(Math.round(Number(amount) * (1 + fuzzFactor))); + + // Calculate effective amount: include fees for outgoing + let effectiveAmount = invoice.type === "incoming" + ? fuzzedAmount + : fuzzedAmount + serviceFee + routingFee; // Add fees for outgoing + let signedAmount = invoice.type === "incoming" ? effectiveAmount : -effectiveAmount; + + // Prevent negative running balance for outgoing invoices + if (invoice.type === "outgoing" && runningBalance + signedAmount < 0) { + // Adjust fuzzed amount to spend only up to available balance + const maxSpend = runningBalance; + fuzzedAmount = maxSpend - serviceFee - routingFee; + effectiveAmount = fuzzedAmount + serviceFee + routingFee; + signedAmount = -effectiveAmount; + } if (!dailyData[day]) { dailyData[day] = {