LivePublic submissionsChallenge repo ↗

Solidity Fixed-Point Normal CDF

Find the most accurate 18-decimal fixed-point standard normal CDF implementation over [0, 8.835109788175395786] in Solidity.

Current best
4,412,972,275
steventhornton · 2.9% below baseline
Score
huber
lower is better
gas (tie-break)
566
lower is better
SubmitChecking local sign-in…
Leaderboard
#Solverhubergasavg_gasmax_errSubmission
1steventhorntonsteventhornton4,412,972,2755665573165545850124Download
Record progression
4.56B4.48B4.4Bbaseline 4,546,363,466
record (huber, lower is better)baseline
Objective

Implement the most accurate standard normal cumulative distribution function Φ(x)\Phi(x) you can, in Solidity, over the domain [0,b][0, b] with b=8.835109788175395786b = 8.835109788175395786, using 18-decimal fixed-point (PRB-Math SD59x18).

Φ(x)=12erfc(x2)\Phi(x) = \frac{1}{2}\,\mathrm{erfc}\left(\frac{-x}{\sqrt{2}}\right)

A valid submission compiles under a pinned toolchain, stays inside a strict opcode allowlist and a 512-byte bytecode budget, hits both endpoints exactly, and never reverts on a domain input. Accuracy decides the ranking; gas only breaks an exact tie.

What you submit

A single Solidity source file (not bytecode) defining exactly:

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19;
import { SD59x18 } from "@prb/math/src/SD59x18.sol";

library CDFImpl {
    function _cdf(SD59x18 x) internal pure returns (SD59x18) {
        // your implementation, for x in [0, b]
    }
}

The verifier compiles it against a fixed, canonical NormalCDF contract that you cannot modify. That contract exposes the public entry point cdf(SD59x18) and calls your _cdf for x in [0,b][0, b]. Outside [0,b][0, b] it supplies the exact tail values and the reflection identity Φ(x)=1Φ(x)\Phi(x) = 1 - \Phi(-x); those regions are fixed and are not scored.

You MUST import PRB-Math's SD59x18 exactly as shown, and no other external import is allowed. Your own internal helpers and constants belong in the same file. Inside _cdf you may do anything the Constraints allow: PRB-Math's math functions are optional, and dropping to raw int256 arithmetic is fine.

Domain and encoding

A ULP (unit in the last place) is the smallest representable step at 18 decimals: 101810^{-18}, the integer 1.

  • 18-decimal fixed point: a real rr is the integer round(r1018)\mathrm{round}(r \cdot 10^{18}).
  • Domain: x[0,b]x \in [0, b] with b=8.835109788175395786b = 8.835109788175395786 (integer 8835109788175395786). This is one ULP below x=8.835109788175395787x^{*} = 8.835109788175395787, the smallest input whose Φ\Phi rounds to 11 at 18 decimals.
  • Φ(0)=0.5\Phi(0) = 0.5 \rightarrow 500000000000000000.
  • Φ(b)\Phi(b) \rightarrow 999999999999999999 (that is 110181 - 10^{-18}).

Compilation (fixed)

The verifier compiles with, exactly:

  • solc 0.8.33 (solc-js build 0.8.33+commit.64118f21.Emscripten.clang), with --optimize --optimize-runs 200, --evm-version osaka, --via-ir, and --no-cbor-metadata. solc-js is a pure-JS build of that exact compiler, so the bytecode is reproducible from the pinned npm version with no external solc binary.
  • PRB-Math v4.1.2, installed from npm (@prb/math@4.1.2).
Constraints

Throughout, b = 8.835109788175395786 is the right endpoint of the domain, defined under What you submit.

Validity rules (any failure means invalid)

  1. Compiles under the pinned toolchain and produces a deployable NormalCDF with cdf(int256) pure.
  2. Opcode allowlist: the whole deployed runtime uses only the opcodes listed below. It must be a pure computation: no storage, block or transaction context, external code, gas introspection, logs, calls, contract creation, or KECCAK256. Checked statically over the whole runtime, and again dynamically under an opcode-recording tracer on a small diverse subset of calls, so a banned opcode hidden from the static sweep is still caught when it executes.
  3. Bytecode size at most 512 bytes, on the whole deployed runtime.
  4. Endpoints (exact): cdf(0) == 500000000000000000 and cdf(b) == 999999999999999999.
  5. Monotonic: the sampled outputs, sorted by x, are non-decreasing. The check is on the N sample points only; monotonicity over the whole domain is infeasible to test.
  6. In range: every sampled output lies in [500000000000000000, 999999999999999999].
  7. Total: cdf reverts on no sampled input and runs out of gas on none. Like rules 5, 6 and 8 this is checked over the N sample points, not proved over the whole domain.
  8. Gas: max execution gas per call over the sample at most 1024 (gas_used - 21000 - calldata_cost).
  9. No Yul verbatim: the source must not use the Yul verbatim builtin, which injects raw bytecode that would bypass the opcode allowlist. Ordinary inline assembly { } is allowed.
  10. Source size at most 65536 bytes.

Opcode allowlist

The canonical list is verifier/opcodes.json, and it is closed-world: anything not listed, including opcodes added by future hardforks, is rejected. CALLVALUE is allowed only because solc emits it, with ISZERO and revert, as the non-payable guard on every non-payable function.

GroupOpcodes
Halt / returnSTOP RETURN REVERT INVALID
ArithmeticADD MUL SUB DIV SDIV MOD SMOD ADDMOD MULMOD EXP SIGNEXTEND
Comparison / bitwiseLT GT SLT SGT EQ ISZERO AND OR XOR NOT BYTE SHL SHR SAR
Read calldataCALLDATALOAD CALLDATASIZE CALLDATACOPY
Non-payable guardCALLVALUE
Stack / memory / local flowPOP MLOAD MSTORE MSTORE8 MSIZE MCOPY JUMP JUMPI JUMPDEST PC PUSH0
PushPUSH1 .. PUSH32
DupDUP1 .. DUP16
SwapSWAP1 .. SWAP16
Score

Two metrics, ranked lexicographically. Accuracy always decides; gas only breaks an exact accuracy tie.

  1. huber (minimize) - the Huber loss of the output-ULP error against the banker's-rounded (round-half-even) reference, over a seeded sample of the domain.
  2. gas (minimize) - worst-case execution gas over that same sample (gas_used - 21000 - calldata_cost).

huber

Per point, the error and its Huber term are:

erri=cdf(xi)roundeven(Φ(xi)1018),Hδ(e)={e2eδδ(2eδ)e>δ\text{err}_i = \text{cdf}(x_i) - \mathrm{round}_{\text{even}}\big(\Phi(x_i)\cdot 10^{18}\big), \qquad H_\delta(e) = \begin{cases} e^2 & |e| \le \delta \\ \delta\,(2|e| - \delta) & |e| > \delta \end{cases}

with knee δ=109\delta = 10^9 ULP. The reported score is 1NiHδ(erri)\sqrt{\tfrac{1}{N}\sum_i H_\delta(\text{err}_i)} over N=1,000,000N = 1{,}000{,}000 points, rounded to the nearest integer. Scores run 10710^7 to 10910^9, so a sub-1-ULP difference is an exact tie and gas decides.

Huber rather than plain RMSE keeps the score robust: errors up to δ\delta count quadratically, but the rare deep-tail spikes count only linearly, so a handful of large errors cannot dominate.

The sample

The output range Φ[0.5,1]\Phi \in [0.5, 1] is split into 10 equal-width bands, and each band gets the same number of points, drawn uniformly in xx within the band (the band edges are grid(probit) of the boundaries, ends pinned to 0 and b). Equal weight per band means the deep tail, out to x=bx = b, is exercised as much as the head rather than starved.

The draw uses a fixed public seed, the number 42, the same for every submission, so scoring is deterministic and reproducible. That is also what lets gas break an exact accuracy tie: two submissions draw the identical sample.

Ranking

A new record must be strictly better on the first metric where it differs from the current best. Any accuracy gain, however small, outranks any gas gain; gas only separates submissions whose accuracy is exactly equal, for example the same approximation with and without an accuracy-neutral dead branch. A submission that ties on both metrics does not displace the incumbent, so exact ties go to the earliest submission. There is no minimum-improvement margin.

Every parameter above (NN, the seed, the band edges, the Huber knee, the domain, and the size, gas and source-size caps) is frozen in verifier/config.ts.

Verifier

Compiles your CDFImpl against the fixed NormalCDF wrapper under the pinned toolchain, enforces every rule in Constraints, then scores the result. It enforces those rules and nothing more: the verifier is canonical, it IS the spec.

Determinism and duplicates

  • Sample seed. The sample is drawn by a SHA-256 counter-mode PRNG from a fixed public seed, the number 42 as a 32-byte value (SAMPLE_SEED_HEX in verifier/config.ts), the same for every submission. Scoring is therefore deterministic and reproducible.
  • Fingerprint (the dedup key). SHA-256 of your deployed runtime bytecode with the trailing CBOR metadata stripped (the pin includes --no-cbor-metadata, and any trailer is stripped as a fallback). A submission whose runtime bytecode is byte-identical to an earlier one is rejected; it would score identically and lose the earliest-wins tie anyway. Only real logic changes move the runtime bytecode, so comments, whitespace and formatting cannot dodge dedup.

The verifier is the canonical spec. View verifier source →