BRC
Algo Testing
cryptec · BrowserCoin PoW lab

v5 vs Sandglass

Technical comparison of live consensus PoW (v5) and the experimental Sandglass candidate measured on this site. Sandglass is not activated on-chain yet — activating it would be a hard fork.

Goal

BrowserCoin wants mining that stays viable in a browser tab / laptop CPU (WASM, ordinary RAM, a handful of Web Workers), while making it harder for GPUs to run thousands of independent hash lanes from huge VRAM bandwidth.

v5 already uses memory-hard Argon2id. Sandglass keeps that gate and adds a data-dependent scratch mix so a single hash cannot be efficiently SIMD/batch-pipelined the way pure Argon2 batches can on CUDA.

At a glance

Before — v5 (live) After — Sandglass mid
Status Consensus PoW today Experimental candidate (lab only)
Salt / domain browsercoin-pow-v5 browsercoin-pow-v6-sandglass + domain tag
Core primitive Argon2id only Argon2id → BLAKE2b scratch → dependent reads → BLAKE2b-256
Argon2 memory 32 MiB 32 MiB (mid preset)
Argon2 passes t 1 2
Parallelism p 1 1
Extra working set 4 MiB BLAKE2b scratch + 2048 random reads
Output 32-byte Argon2 tag 32-byte BLAKE2b-256 digest
Browser path WASM Argon2id in Web Workers Same + JS/WASM BLAKE2b mix
Native GPU path Argon2 CUDA batches scale well Hybrid: Argon2 CUDA + sequential mix on CPU (mix caps H/s)

mid is the main public board. production is a separate page (64 MiB Argon2, 8 MiB scratch, 4096 rounds) with its own board — mid history stays valid.

Pipeline (Sandglass)

header (148 bytes)
        │
        ▼
┌───────────────────────────┐
│ 1. Argon2id gate          │  password=header, m=32MiB, t=2, p=1
│    → tag T (32 bytes)     │
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 2. Scratch expand         │  4 MiB = 65536 × 64-byte blocks
│    B_i = BLAKE2b-512(…)   │  B_i = BLAKE2b-512(T ‖ u64be(i))
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 3. Dependent sand mix     │  2048 rounds
│    idx ← state; read B_idx│  each round needs previous state
│    state ← BLAKE2b-512(…) │  → hard to batch / hide latency
└─────────────┬─────────────┘
              │
              ▼
┌───────────────────────────┐
│ 4. Finalize               │  BLAKE2b-256(state ‖ "out")
│    → 32-byte PoW digest   │
└───────────────────────────┘

Before — BrowserCoin v5

Live consensus hash: one Argon2id call over the encoded block header. Salt is fixed network-wide so a version bump can hard-fork cleanly.

// src/crypto/pow.ts — consensus today
const SALT = new TextEncoder().encode('browsercoin-pow-v5');

export const POW_PARAMS = {
  memorySize: 32 * 1024, // KiB → 32 MiB
  iterations: 1,
  parallelism: 1,
  hashLength: 32,
} as const;

export async function powHash(headerBytes: Uint8Array): Promise<Uint8Array> {
  const argon2id = await loadArgon2id();
  return argon2id({
    password: headerBytes,
    salt: SALT,
    parallelism: POW_PARAMS.parallelism,
    passes: POW_PARAMS.iterations,
    memorySize: POW_PARAMS.memorySize,
    tagLength: POW_PARAMS.hashLength,
  });
}

Pseudocode:

pow_v5(header) =
  Argon2id(
    password = header,
    salt     = "browsercoin-pow-v5",
    m        = 32 MiB,
    t        = 1,
    p        = 1,
    tagLen   = 32
  )

Strengths: simple, WASM-friendly, memory-hard enough that mid-range GPU L2 cannot hold many lanes. Weakness for the GPU threat model: once Argon2 is implemented well on CUDA, thousands of independent headers can still be hashed in parallel with high occupancy — there is no long serial dependency after the memory-hard step.

After — Sandglass (mid preset)

Reference implementation lives in src/crypto/pow-v6-sandglass.ts. The mid preset used by this site:

// Mid preset (what the bench / board measure)
const SANDGLASS_MID = {
  argonMemoryKiB: 32 * 1024,   // 32 MiB Argon2id
  argonPasses: 2,              // t=2 (heavier than v5's t=1)
  argonParallelism: 1,
  scratchBytes: 4 * 1024 * 1024, // 4 MiB BLAKE2b scratch
  mixRounds: 2048,             // dependent random reads
  hashLength: 32,
};

Core of the hash (simplified from the reference module):

// src/crypto/pow-v6-sandglass.ts (structure)
const DOMAIN = 'browsercoin-sandglass-v1';
const SALT = new TextEncoder().encode('browsercoin-pow-v6-sandglass');
const BLOCK = 64; // BLAKE2b-512 width

export async function sandglassHash(header, params = SANDGLASS_MID) {
  // 1) Memory-hard gate — same family as v5, different salt / passes
  const tag = argon2id({
    password: header,
    salt: SALT,
    parallelism: params.argonParallelism,
    passes: params.argonPasses,
    memorySize: params.argonMemoryKiB,
    tagLength: 32,
  });

  // 2) Expand tag into a scratchpad of 64-byte blocks
  const nBlocks = params.scratchBytes / BLOCK;
  const scratch = new Uint8Array(params.scratchBytes);
  for (let i = 0; i < nBlocks; i++) {
    scratch.set(blake2b512(concat([tag, u64be(i)])), i * BLOCK);
  }

  // 3) Data-dependent mix — each round's index comes from prior state
  let state = blake2b512(concat([domain, tag, mixTag]));
  for (let r = 0; r < params.mixRounds; r++) {
    const idx = Number(asU64LE(state.subarray(0, 8)) % BigInt(nBlocks));
    const slice = scratch.subarray(idx * BLOCK, idx * BLOCK + BLOCK);
    state = blake2b512(concat([state, slice, u64be(r)]));
  }

  // 4) Finalize
  return blake2b256(concat([state, outTag])).subarray(0, 32);
}

BLAKE2b comes from @noble/hashes/blake2b; Argon2id from the same WASM stack as v5 (openpgpjs/argon2id).

sandglass_mid(header) =
  T ← Argon2id(header, salt=v6-sandglass, m=32MiB, t=2, p=1)
  for i in 0 .. 65535:
      B[i] ← BLAKE2b-512(T ‖ u64be(i))          // 4 MiB scratch
  state ← BLAKE2b-512("browsercoin-sandglass-v1" ‖ T ‖ "mix")
  for r in 0 .. 2047:
      idx ← U64LE(state[0:8]) mod 65536
      state ← BLAKE2b-512(state ‖ B[idx] ‖ u64be(r))
  return BLAKE2b-256(state ‖ "out")[0:32]

Why the mix hurts GPUs more than phones

Sandglass does not claim to stop “datacenters” in general — a rack of CPUs still wins over one laptop. It targets parallel accelerators, not enterprise as a class.

How this site measures

Activation (if ever)

Not consensus today. A real switch would be a hard fork roughly like:

// Conceptual — not implemented
- new network / salt id: browsercoin-pow-v6
- replace powHash() call sites with sandglassHash(…, SANDGLASS_*)
- retarget / genesis difficulty for the new cost model
- miners (browser, pool, CUDA) ship matching bytecode / kernels

Until then, treat Sandglass numbers as a lab signal for whether the candidate is worth the fork cost.

mid bench → production bench →