Add fuzzing for amounts

* +- 0 to 1%, or +- 1 sat for tiny amounts
* Ensure balance cannot go negative
This commit is contained in:
Râu Cao 2025-05-02 17:35:25 +04:00
parent 874c984c6d
commit c16db7bbb2
Signed by: raucao
GPG Key ID: 37036C356E56CC51

View File

@ -17,7 +17,7 @@ for (const key of ["DB_USER", "DB_PASSWORD", "DB_NAME", "ANONYMIZATION_KEY"]) {
// Adjust based on database capacity // Adjust based on database capacity
const BATCH_SIZE = parseInt(env.BATCH_SIZE) || 10; const BATCH_SIZE = parseInt(env.BATCH_SIZE) || 10;
// Maximum number of invoices to process per user // 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; const MAX_INVOICES = parseInt(env.MAX_INVOICES) || 21000;
// Database connection configuration from .env or environment variables // 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) // Write headers to CSV (create file or overwrite if it exists)
await Deno.writeTextFile(csvFilePath, csvHeaders.join(",") + "\n"); 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) { async function anonymizeUserId(userId) {
const keyData = new TextEncoder().encode(env.ANONYMIZATION_KEY); const keyData = new TextEncoder().encode(env.ANONYMIZATION_KEY);
const data = new TextEncoder().encode(userId.toString()); 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(""); 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) { async function processUser(userId, client) {
// Check the number of settled invoices for the user // Check the number of settled invoices for the user
const countResult = await client.queryObject( const countResult = await client.queryObject(
@ -123,16 +135,34 @@ async function processUser(userId, client) {
for (const invoice of invoices) { for (const invoice of invoices) {
const day = invoice.settled_at.toISOString().split("T")[0]; // YYYY-MM-DD 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 // Get fuzzing factor for this invoice
const effectiveAmount = invoice.type === "incoming" const fuzzFactor = getFuzzFactor(Number(invoice.amount));
? amount
: amount + serviceFee + routingFee; // Add fees for outgoing // Convert amounts to BigInt
const signedAmount = invoice.type === "incoming" ? effectiveAmount : -effectiveAmount; 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]) { if (!dailyData[day]) {
dailyData[day] = { dailyData[day] = {