OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / relay-bandwidth-accounting.test.js

76 lines · javascript ref:
import test from 'brittle'
import { Relay } from '../../packages/core/core/relay-node/relay.js'

// Covers the per-frame circuit byte accounting hot path (relay.js):
//  - per-circuit byte cap still enforced
//  - relay-wide bandwidth cap still enforced (check happens before the
//    current frame is added, matching the pre-refactor semantics)
//  - the bucket prune is amortized to at most once per wall-clock second
//    instead of running twice on every data frame
// These pin the behaviour so the prune amortization stays behaviour-preserving.

test('recordCircuitBytes enforces the per-circuit byte cap', async (t) => {
  const relay = new Relay(null, { maxConnections: 10 })
  await relay.start()
  relay.registerCircuit('c1', 'peerA', 100) // 100-byte cap on this circuit
  t.teardown(() => relay.closeCircuit('c1'))

  t.ok(relay.recordCircuitBytes('c1', 60), 'first 60 bytes accepted')
  t.absent(relay.recordCircuitBytes('c1', 60), '60+60 > 100 rejected')
  t.ok(relay.recordCircuitBytes('c1', 30), '60+30 <= 100 still accepted')
  t.is(relay.circuits.get('c1').bytesRelayed, 90, 'rejected frame did not count')
})

test('recordCircuitBytes enforces the relay-wide bandwidth cap', async (t) => {
  const relay = new Relay(null, { maxConnections: 10 })
  await relay.start()
  relay.maxBandwidthBytes = 1000 // tiny window cap for the test
  relay.registerCircuit('c1', 'peerA', 1_000_000_000) // byte cap out of the way
  t.teardown(() => relay.closeCircuit('c1'))

  t.ok(relay.recordCircuitBytes('c1', 600), 'total 0 -> under cap, accepted (now 600)')
  t.ok(relay.recordCircuitBytes('c1', 600), 'total 600 -> under cap, accepted (now 1200)')
  t.absent(relay.recordCircuitBytes('c1', 600), 'total 1200 > 1000 -> rejected')
})

test('recordCircuitBytes rejects unknown circuit', async (t) => {
  const relay = new Relay(null, { maxConnections: 10 })
  await relay.start()
  t.absent(relay.recordCircuitBytes('nope', 10), 'no such circuit')
})

test('_pruneBandwidth drops expired buckets and is amortized within a second', async (t) => {
  const relay = new Relay(null, {})
  await relay.start()
  // Monotonic timestamps — production only ever inserts increasing seconds,
  // which is what lets the prune loop `break` on the first live bucket.
  relay._addBandwidth(100, 1000)
  relay._addBandwidth(100, 1001)
  relay._addBandwidth(100, 1065)
  t.is(relay._bandwidthTotal, 300, 'three buckets recorded')

  // Prune at 1065: cutoff = 1065 - 60 = 1005, so 1000 and 1001 expire.
  relay._pruneBandwidth(1065)
  t.is(relay._bandwidthBuckets.size, 1, 'two expired buckets dropped')
  t.is(relay._bandwidthTotal, 100, 'total reflects only the live bucket')
  t.is(relay._bandwidthLastPruneSec, 1065, 'prune second recorded')

  // Amortization: a second prune within the SAME second must not re-sweep
  // the bucket Map. Spy on iteration to prove the loop body is skipped —
  // this is the per-frame work the refactor removes.
  let sweeps = 0
  const buckets = relay._bandwidthBuckets
  relay._bandwidthBuckets = {
    [Symbol.iterator] () { sweeps++; return buckets[Symbol.iterator]() },
    get size () { return buckets.size },
    get: (k) => buckets.get(k),
    set: (k, v) => buckets.set(k, v),
    delete: (k) => buckets.delete(k)
  }
  relay._pruneBandwidth(1065) // same second -> guarded, no sweep
  t.is(sweeps, 0, 'no re-sweep within the same second')
  relay._pruneBandwidth(1066) // second advances -> sweep runs once
  t.is(sweeps, 1, 'sweep runs once the second advances')
  relay._bandwidthBuckets = buckets
})