Fuzz Testing

Property-based testing and fuzzing techniques to discover edge cases and unexpected input handling issues in the Keythings Wallet.

Overview

Fuzz testing uses automated input generation to discover edge cases, boundary conditions, and unexpected behaviors. The Keythings Wallet employs three fuzzing techniques:

  • Property-Based Testing — Generates inputs based on mathematical properties
  • Strategic Testing — Handcrafted test cases for known edge cases
  • Dumb Fuzzing — Random ASCII input generation

Running Fuzz Tests

bun run security:fuzz

# Output:
# Testing parseDecimalStringToBigInt with:
# - 750 property-based cases
# - 12 strategic cases
# - 500 random fuzzing cases

1. Property-Based Testing

Property-based testing generates inputs that satisfy mathematical properties, ensuring correctness across a wide range of valid inputs.

Test Case Generation

import fc from 'fast-check'

const digitsArbitrary = (minLength, maxLength) =>
  fc.array(
    fc.integer({ min: 0, max: 9 }),
    { minLength, maxLength }
  ).map(digits => digits.join(''))

const decimalRecord = fc.record({
  fieldType: fc.constantFrom('decimals', 'decimalPlaces'),
  decimals: fc.integer({ min: 0, max: 18 }),
  leadingWhitespace: fc.constantFrom('', ' ', '\t'),
  trailingWhitespace: fc.constantFrom('', ' ', '\t'),
  sign: fc.constantFrom('', '+', '-'),
  integerPart: digitsArbitrary(1, 24),
  fractionalPart: digitsArbitrary(0, 24),
  hasDecimalPoint: fc.boolean(),
})

const samples = fc.sample(decimalRecord, 750)

Expected Value Computation

function computeExpectedValue(
  fieldType,
  decimals,
  sign,
  integerPart,
  fractionalPart
) {
  const signValue = sign === '-' ? -1n : 1n
  const safeInteger = integerPart || '0'

  if (fieldType === 'decimals') {
    const truncated = fractionalPart.slice(0, decimals)
    const padded = truncated.padEnd(decimals, '0')
    const combined = `${safeInteger}${padded}`
    return signValue * BigInt(combined || '0')
  }

  const multiplier = 10n ** BigInt(decimals)
  const truncated = fractionalPart.slice(0, decimals)
  const padded = truncated.padEnd(decimals, '0')
  const fractionalValue = padded ? BigInt(padded) : 0n

  return signValue * (
    BigInt(safeInteger) * multiplier + fractionalValue
  )
}

Property Validation

for (const sample of samples) {
  const result = parseDecimalStringToBigInt(
    sample.input,
    sample.decimals,
    sample.fieldType
  )

  const expected = computeExpectedValue(
    sample.fieldType,
    sample.decimals,
    sample.sign,
    sample.integerPart,
    sample.hasDecimalPoint ? sample.fractionalPart : ''
  )

  if (result !== expected) {
    recordFailure({
      technique: 'property-based',
      input: sample.input,
      expected: expected.toString(),
      received: result.toString(),
      error: 'Value mismatch'
    })
  }
}

2. Strategic Testing

Strategic testing uses handcrafted test cases for known edge cases and boundary conditions.

Edge Case Test Cases

const strategicCases = [
  {
    input: '',
    decimals: 0,
    fieldType: 'decimals',
    note: 'Empty string should normalize to zero',
    expectation: 'ok',
    expectedValue: 0n
  },
  {
    input: '.',
    decimals: 2,
    fieldType: 'decimalPlaces',
    note: 'Missing integer digits must be rejected',
    expectation: 'error'
  },
  {
    input: '   ',
    decimals: 4,
    fieldType: 'decimalPlaces',
    note: 'Whitespace-only should collapse to zero',
    expectation: 'ok',
    expectedValue: 0n
  },
  {
    input: '.25',
    decimals: 4,
    fieldType: 'decimals',
    note: 'No integer digits should parse as fractional',
    expectation: 'ok',
    expectedValue: 2500n
  },
  {
    input: '1 234',
    decimals: 0,
    fieldType: 'decimals',
    note: 'Embedded whitespace not allowed',
    expectation: 'error'
  },
  {
    input: '01',
    decimals: 0,
    fieldType: 'decimals',
    note: 'Full-width unicode numerals must be rejected',
    expectation: 'error'
  },
  {
    input: '1e5',
    decimals: 0,
    fieldType: 'decimals',
    note: 'Scientific notation unsupported',
    expectation: 'error'
  },
  {
    input: '0x10',
    decimals: 0,
    fieldType: 'decimals',
    note: 'Hexadecimal prefix invalid',
    expectation: 'error'
  },
  {
    input: '-.5',
    decimals: 4,
    fieldType: 'decimals',
    note: 'Negative fractional without leading zero',
    expectation: 'ok',
    expectedValue: -5000n
  },
]

Execution

for (const testCase of strategicCases) {
  try {
    const result = parseDecimalStringToBigInt(
      testCase.input,
      testCase.decimals,
      testCase.fieldType
    )

    if (testCase.expectation === 'error') {
      recordFailure({
        technique: 'strategic',
        input: testCase.input,
        error: 'Expected failure but succeeded'
      })
      continue
    }

    if (testCase.expectedValue !== undefined &&
        result !== testCase.expectedValue) {
      recordFailure({
        technique: 'strategic',
        input: testCase.input,
        expected: testCase.expectedValue.toString(),
        received: result.toString(),
        error: 'Mismatched result'
      })
    }
  } catch (error) {
    if (testCase.expectation === 'ok') {
      recordFailure({
        technique: 'strategic',
        input: testCase.input,
        error: error instanceof Error ? error.message : String(error)
      })
    }
  }
}

3. Dumb Fuzzing

Random ASCII fuzzing uncovers edge cases that deterministic approaches might miss.

Fuzz Generator

function randomAsciiString(length: number) {
  const chars = ' 	
+-.,0123456789abcdefABCDEFxX'
  let result = ''

  for (let i = 0; i < length; i++) {
    const idx = Math.floor(Math.random() * chars.length)
    result += chars[idx]
  }

  return result
}

function runDumbFuzzing(iterations = 500) {
  for (let i = 0; i < iterations; i++) {
    const length = Math.floor(Math.random() * 32)
    const input = randomAsciiString(length)

    try {
      parseDecimalStringToBigInt(input, 18, 'decimals')
    } catch (error) {
      if (!(error instanceof ParseDecimalError)) {
        recordFailure({
          technique: 'dumb-fuzz',
          input,
          error: error instanceof Error ? error.message : String(error)
        })
      }
    }
  }
}

Fuzz Orchestrator

export async function fuzzParseDecimalStringToBigInt() {
  const findings: Array<{ technique: string; input: string; error: string } & Record<string, unknown>> = []

  const recordFailure = (finding) => {
    findings.push({
      timestamp: new Date().toISOString(),
      ...finding
    })
  }

  // Property-based testing
  for (const sample of samples) {
    const result = parseDecimalStringToBigInt(
      sample.input,
      sample.decimals,
      sample.fieldType
    )

    const expected = computeExpectedValue(
      sample.fieldType,
      sample.decimals,
      sample.sign,
      sample.integerPart,
      sample.hasDecimalPoint ? sample.fractionalPart : ''
    )

    if (result !== expected) {
      recordFailure({
        technique: 'property-based',
        input: sample.input,
        expected: expected.toString(),
        received: result.toString(),
        error: 'Value mismatch'
      })
    }
  }

  // Strategic testing
  for (const testCase of strategicCases) {
    try {
      const result = parseDecimalStringToBigInt(
        testCase.input,
        testCase.decimals,
        testCase.fieldType
      )

      if (testCase.expectation === 'error') {
        recordFailure({
          technique: 'strategic',
          input: testCase.input,
          error: 'Expected failure but succeeded'
        })
      }
    } catch (error) {
      if (testCase.expectation === 'ok') {
        recordFailure({
          technique: 'strategic',
          input: testCase.input,
          error: error instanceof Error ? error.message : String(error)
        })
      }
    }
  }

  // Dumb fuzzing
  runDumbFuzzing()

  return findings
}

Reporting

Each technique produces structured findings that include the failing input, technique name, and error description.

Example Finding

{
  "technique": "property-based",
  "input": "-0.000000000000000001",
  "expected": "-1",
  "received": "-2",
  "error": "Value mismatch",
  "timestamp": "2025-10-09T18:35:02.190Z"
}

Best Practices

  • Run the fuzz suite after significant changes to parsing or numeric handling logic
  • Investigate timing variations to ensure there are no performance regressions
  • Persist findings for auditing and regression tracking
  • Expand strategic cases when new bugs are discovered