Skip to content

fix(serder)!: #147 ample(3) diverges from keripy — port the exact f1/f2 threshold formula#152

Merged
joeldsouzax merged 1 commit into
mainfrom
fix/147-ample-keripy-parity
Jul 11, 2026
Merged

fix(serder)!: #147 ample(3) diverges from keripy — port the exact f1/f2 threshold formula#152
joeldsouzax merged 1 commit into
mainfrom
fix/147-ample-keripy-parity

Conversation

@joeldsouzax

Copy link
Copy Markdown
Contributor

Summary

serder::ample computed the simplified max(1, ceil(2n/3)), which diverges from keripy's ample() at exactly n = 3: keripy returns 3, we returned 2. A 3-witness inception built without an explicit witness threshold emitted "bt":"2" where keripy emits "bt":"3" — byte-divergent from the oracle and a weakened BFT margin.

This ports the real formula (keripy eventing.py:69-106 @ de59bc7d, f=None, weak=True defaults): minimize m over both the floor and ceiling candidates for the maximum fault count f subject to n >= 3f+1 and (n+f+1)/2 <= m <= n-f.

Breaking change (0.x MINOR)

ample(n: usize) now returns Result<u32, SerderError> instead of u32:

  • the unwrap_or(u32::MAX) sentinel conversion (banned pattern) is replaced with a typed SerderError::Validation when the threshold exceeds u32;
  • all intermediate arithmetic is checked (checked_sub/checked_add), no bare ops;
  • InceptionBuilder/DelegatedInceptionBuilder propagate the error through their existing Result (no builder-surface change).

Called out in CHANGELOG.md under Unreleased.

Acceptance criteria (from #147)

  • Table test asserting keripy's test_eventing_v1.py ample() expectations for n = 0..13
  • Proptest vs a verbatim-transcribed keripy oracle over n = 0..=256 — plus a deterministic exhaustive sweep of the same range (random sampling passed against the buggy code ~37% of the time, so sampling alone is a flaky bug-probe)
  • ample_three_witnesses flipped to assert 3 (verified failing against the old formula before the fix)
  • Sentinel conversion replaced with a checked error path; both 64-bit overflow paths (checked_add overflow at usize::MAX, u32::try_from failure at 7·10⁹) have tests

Verification

  • nix flake check — all checks green (clippy, fmt, taplo, audit, deny, 1993 nextest tests, doctests, wasm, no_std, fuzz-replay)
  • TDD: bug-probes confirmed RED against the old formula (ample(3) → 2, builder bt → "2"), GREEN after the port
  • Differential against keripy's executing code (AST-extracted ample from the local de59bc7d checkout): zero divergences over n = 0..=100,000; reproduce with:
    cd ~/Code/keripy && python3 - <<'PY'
    import ast
    from math import ceil
    src = open('src/keri/core/eventing.py').read()
    fn = next(n for n in ast.walk(ast.parse(src)) if isinstance(n, ast.FunctionDef) and n.name == 'ample')
    ns = {'ceil': ceil}
    exec(compile(ast.Module(body=[fn], type_ignores=[]), 'eventing.py', 'exec'), ns)
    def div_ceil(a, b): return -(-a // b)
    def rust_ample(n):
        if n == 0: return 0
        f1 = max((n - 1) // 3, 1); f2 = max(div_ceil(n - 1, 3), 1)
        return min(n, div_ceil(n + f1 + 1, 2), div_ceil(n + f2 + 1, 2))
    assert not [n for n in range(100001) if rust_ample(n) != ns['ample'](n)]
    print('parity confirmed')
    PY

Out of scope (noted, not touched)

majority() in builder/icp.rs:72 carries the same unwrap_or(u64::MAX) sentinel pattern — same class of cleanup, separate signature, deserves its own scoped issue.

Closes #147

🤖 Generated with Claude Code

…mula

ample() computed the simplified max(1, ceil(2n/3)), which diverges from
keripy's ample() at exactly n = 3: keripy returns 3, we returned 2. A
3-witness inception built without an explicit witness threshold emitted
"bt":"2" where keripy emits "bt":"3", byte-diverging from the oracle and
weakening the intended BFT margin.

Port the real formula (eventing.py:69-106 @ de59bc7d, weak defaults):
minimize m over both the floor and ceil candidates for the max fault
count f subject to n >= 3f+1 and (n+f+1)/2 <= m <= n-f.

BREAKING: ample(n) now returns Result<u32, SerderError> — the
unwrap_or(u32::MAX) sentinel conversion is replaced with a typed
SerderError::Validation on thresholds exceeding u32 (checked arithmetic
throughout, per the arithmetic-safety rules).

Tests: keripy test_eventing_v1 table (n = 0..13), exhaustive sweep +
proptest vs a verbatim keripy oracle over n = 0..=256, 64-bit overflow
error paths, and the flipped bug-probes (ample(3) == 3, builder bt "3").

Closes #147

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 21 skipped benchmarks1


Comparing fix/147-ample-keripy-parity (53b8e31) with main (e9df476)

Open in CodSpeed

Footnotes

  1. 21 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@joeldsouzax joeldsouzax merged commit 71a52f5 into main Jul 11, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(serder): ample(3) diverges from keripy — emits bt 2 where keripy computes 3

1 participant