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
Score
huber
lower is better
gas (tie-break)
566
lower is better
Leaderboard
#SolverScore (huber)gasavg_gasmax_err
1steventhorntonsteventhornton4,412,972,2755665573165545850124

Click a row to view the submitted code.

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)

What you submit

A Solidity library that defines the implementation of the _cdf function:

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

The verifier compiles your file together with a fixed, canonical NormalCDF contract (which you cannot modify). That contract exposes the public entry point cdf(SD59x18) and calls your _cdf on the contest domain. Outside [0,b][0, b] the canonical wrapper 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.

Inside _cdf you may do anything allowed within the limits (see the Constraints below): you need not use PRB-Math's math functions and may drop to raw int256 arithmetic. Only the opcode allowlist, size, and gas limits constrain you.

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}).

Score: accuracy first (Huber), then gas

The primary metric is accuracy, measured by the Huber loss of the output-ULP error against the correctly rounded reference, over a large random sample of the domain. 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)} (lower is better), rounded to the nearest integer. Scores are on the order of 10710^7 to 10910^9, so the score is never reported or ranked at a finer resolution than 1 ULP: a sub-1-ULP difference is an exact tie (and then gas decides). Huber, rather than plain RMSE, keeps the score robust: errors up to δ\delta count quadratically (normal least-squares), but the rare deep-tail spikes count only linearly, so a handful of large errors cannot dominate the score.

The sample is drawn in 10 equal-weight bands across the output range: the output Φ[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. Equal weight per band means the upper decile (the deep tail, out to x=bx = b) counts as much as the lower decile, so the tail is exercised rather than starved. The draw uses a fixed public seed (42), the same for every submission, so your score is deterministic and reproducible.

Submissions are ranked lexicographically on two metrics:

  1. huber (lower is better): accuracy, and it always wins first.
  2. gas (lower is better): the worst-case exec gas over the sample, used only to break an exact accuracy tie.

Any accuracy gain, however small, outranks any gas gain. Gas only decides submissions whose accuracy score is exactly equal (for example, the same approximation with and without an accuracy-neutral dead branch: the leaner one ranks higher). A new record must be strictly better on the first metric where it differs; a submission that ties on both metrics does not displace the incumbent, so exact ties go to the earliest submission.

Constraints

Domain

The contest domain is x in [0, b] (18-decimal fixed point), with

  • b = 8.835109788175395786 (integer 8835109788175395786), one ULP below the smallest input whose Φ rounds to 1 at 18 decimals.
  • Φ(0) = 0.5 -> 500000000000000000, and Φ(b) = 1 - 1e-18 -> 999999999999999999.

Submission format

  • 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) {
            // ...
        }
    }
    
  • You MUST import PRB-Math's SD59x18 (import { SD59x18 } from "@prb/math/src/SD59x18.sol"). You may define your own internal helpers/constants in the same file. No other external imports.
  • The verifier supplies and deploys the canonical NormalCDF contract; you cannot change it. It calls CDFImpl._cdf(x) for x in [0, b].

Compilation (fixed)

Compiled by the verifier 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 no external solc binary is needed and the bytecode is reproducible from the pinned npm version.
  • PRB-Math v4.1.2, installed from npm (@prb/math@4.1.2).

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 must use only the allowed opcodes (see the list below). It must be a pure computation: no storage, block/tx context, external code, gas introspection, logs, calls, contract creation, or KECCAK256. Enforced both statically (a sweep of the whole runtime) and dynamically (a small, diverse subset of calls is executed under an opcode-recording tracer, so any banned opcode that executes on those paths is caught even if it was hidden from the static sweep).
  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. True monotonicity over the whole domain is not tested (it is infeasible); the check is on the N sample points sorted by x.
  6. In range: every sampled output lies in [500000000000000000, 999999999999999999].
  7. Total: cdf never reverts or runs out of gas on a domain input.
  8. Gas: max execution gas per call over the sample at most 1024 (gas_used - 21000 - calldata_cost).
  9. No Yul verbatim: the submission source must not use the Yul verbatim builtin (it 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. It is closed-world: anything not listed, including opcodes added by future hardforks, is rejected. CALLVALUE is allowed because solc emits it (with ISZERO + 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

Scoring

  • Two ranked metrics, in priority order:
    1. huber (lower wins) - the Huber loss of the output-ULP error vs the banker's-rounded reference, reported as sqrt(mean Huber loss) rounded to the nearest integer (resolution 1 ULP; scores are 1e7 to 1e9, so a sub-1-ULP difference is a tie). Per point, err = |cdf(x) - round_half_even(Phi(x)*1e18)|, and the Huber term is err^2 if err <= 1e9 (the knee), else 1e9*(2*err - 1e9). Measured over the seeded, output-banded / uniform-in-x sample of N = 1,000,000 points. Huber keeps the score robust: the rare deep-tail spikes count only linearly, so they cannot dominate.
    2. gas (lower wins) - worst-case exec gas over the sample; a tie-breaker only.
  • Ranking is strictly lexicographic: a new record must be strictly better on the first metric where it differs from the current best. Accuracy always decides first; gas only separates submissions with exactly equal accuracy. A submission that ties on both metrics does not displace the incumbent (earliest submission wins). There is no minimum-improvement margin.
  • Sampling. The output range Phi in [0.5, 1] is split into 10 equal-width bands; each band gets an equal share of the N points, drawn uniformly in x within the band (the band edges are grid(probit) of the band boundaries, ends pinned to 0 and b). Equal weight per band means the deep tail (top band, out to x = b) is exercised as much as the head, so no region is starved.
  • These parameters (N, the sample seed, the band edges, the Huber knee, the size cap, the gas cap, the source-size cap, the domain, the toolchain pin) are the verifier's frozen spec and live in verifier/config.ts. The verifier is canonical: it IS the spec.

Determinism, the sample seed, and the bytecode hash

  • Sample seed (fixed and public). The sample is drawn by a SHA-256 counter-mode PRNG from a fixed public seed, the number 42 (as a 32-byte value; see verifier/config.ts, SAMPLE_SEED_HEX), the same for every submission. Scoring is therefore fully deterministic and reproducible, and the fixed seed is what lets gas break an exact accuracy tie (two submissions with the same accuracy logic draw the identical sample, so their accuracy score is exactly equal).
  • Duplicate rejection (runtime bytecode hash). Independently of the seed, the dedup key is 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). Because only real logic changes move the runtime bytecode (comments, whitespace, and formatting do not), cosmetic edits do not dodge dedup.
Verifier

Compiles the submitted CDFImpl library with a fixed public wrapper under a pinned solc 0.8.33 + PRB-Math toolchain (via-ir), enforces the opcode allowlist (static plus a small-subset execution trace), a bytecode-size cap, a source-size cap, a ban on Yul verbatim, a per-call gas cap, exact endpoints, and monotonicity, then scores the Huber loss of the output-ULP error against a banker's-rounded reference over a seeded, output-banded / uniform-in-x sample.

The verifier is the canonical spec. View verifier source →