Add configurable fee rate bounds with decoupled simulation layout#13
Add configurable fee rate bounds with decoupled simulation layout#13laurenshareshian merged 15 commits intomainfrom
Conversation
Introduces BucketConfig to replace the hardcoded BUCKET_MIN constant, letting library consumers set their own minimum fee rate floor. The default is 1.0 sat/vB; users on Bitcoin Core 29.1+ can opt in to sub-1 sat/vB support with FeeEstimator(minFeeRate = 0.1). Bumps version to 0.3.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add upper bound validation: minFeeRate must be <= 22026 sat/vB (beyond which bucketMin exceeds BUCKET_MAX and array size goes negative) - Use ceil instead of round in BucketConfig so the lowest bucket never represents a fee rate below the user's configured minimum - Add tests for both edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
322567b to
0dd17ee
Compare
Moves BUCKET_MAX from a hardcoded constant into BucketConfig alongside bucketMin. The toArrayIndex/toBucketIndex helpers now live on BucketConfig since they depend on the configured max. Transactions with fee rates above maxFeeRate are folded into the highest bucket so their block weight is still counted in simulations, while fee estimates above the max are returned as null. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0dd17ee to
8ce8fe2
Compare
…ange fromMempoolTransactions now accepts minFeeRate/maxFeeRate so that snapshot bucketing matches the FeeEstimator config. Previously, custom fee rate bounds only took effect during estimation but not during snapshot creation, silently clamping high-fee transactions to the default bucket max. Also adds a BucketConfig init validation that the discretized bucket range is non-empty, catching edge cases where minFeeRate and maxFeeRate are too close together (e.g. 1.001 and 1.002) which would produce zero-length bucket arrays. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add input validation to BucketConfig.init (positive fee rates, min < max) so fromMempoolTransactions rejects invalid inputs instead of silently producing NaN-derived bucket indices - Fix README default maxFeeRate comment (22026.0 -> 22027.0) - Deduplicate DEFAULT_MIN/MAX_FEE_RATE by delegating FeeEstimator constants to BucketConfig - Add FeeEstimator.createSnapshot() to prevent bucket config drift between snapshot creation and estimation - Move BucketConfig construction after validation in FeeEstimator.init so invalid inputs produce clear error messages - Document asymmetric clamping in calculateBucketIndex (fold high, drop low) - Fix folding test to use separate slots for folded vs valid weight - Remove duplicate empty-bucket-range test from BucketCreatorTest Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @jvmoverloads to configure() to preserve old 4-arg binary signature - Move BucketConfig property computation after require checks (defensive hygiene) - Remove duplicate fee rate validation from FeeEstimator (BucketConfig is source of truth) - Deprecate MempoolSnapshot.fromMempoolTransactions() in favor of FeeEstimator.createSnapshot() - Fix KDoc maxFeeRate default (22026.0 -> 22027.0) - Document new minFeeRate/maxFeeRate params in configure() KDoc - Add inline comment explaining round() vs ceil/floor in calculateBucketIndex - Replace bare assert() with assertNotNull/assertTrue in tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e assertTrue - Drop @JvmStatic from deprecated fromMempoolTransactions (no need to expand Java API surface for a deprecated method) - Drop ReplaceWith to avoid suggesting throwaway FeeEstimator instances - Clarify in README that above-max transactions are folded into highest bucket - Replace bare assert() with assertTrue() in new tests for consistency Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep fromMempoolTransactions supported (no external Java callers exist), remove @jvmoverloads from configure/createSnapshot/fromMempoolTransactions to avoid unnecessary API surface, and document DEFAULT_MAX_FEE_RATE choice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ent, clarify minFeeRate KDoc Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ently - Clarify that estimates whose fee rate *exceeds* the bound are null (not "above"), matching the <= boundary check in prepareResultArray - Replace bare assert() with assertNotNull/assertTrue in FeeEstimatesCalculatorTest for consistency with other test cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Fee rate bounds (
Right now
A cleaner separation would keep the internal bucket structure fixed (or separately configured) and apply fee rate bounds as input/output filters. |
maxFeeRate was previously used to derive bucketMax, which sized the internal simulation arrays. This coupled a user-facing output filter to the internal state space, changing simulation dynamics (bucket folding, inflow distortion) when callers lowered maxFeeRate. Now: - Rename BucketConfig → BucketLayout (internal simulation layout only) - BucketLayout takes only minFeeRate; bucketMax is fixed at 1000 - maxFeeRate is passed separately to FeeEstimatesCalculator as a pure output filter in prepareResultArray - Remove maxFeeRate from MempoolSnapshot.fromMempoolTransactions - Remove minFeeRate < maxFeeRate validation (independent concerns) Default behavior is unchanged: bucketMax=1000, maxFeeRate=22027.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… fix nits - Make fromMempoolTransactions(minFeeRate) internal; public overload uses default layout only. Callers needing custom minFeeRate use createSnapshot. - Restore strict < in prepareResultArray to preserve pre-PR filter behavior. - Add assertNull to minFeeRate-above-maxFeeRate test (was a no-op loop). - Fix DEFAULT_MAX_FEE_RATE KDoc: "rounded up from exp(10)" not "~exp(10)". - Make BucketLayout.DEFAULT lazy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Deprecate MempoolSnapshot.fromMempoolTransactions() in favor of FeeEstimator.createSnapshot() to prevent bucket layout mismatches - Add minFeeRate validation in FeeEstimator.init for clearer errors - Change maxFeeRate filter from < to <= (include boundary value) - Document snapshot rollover behavior when changing minFeeRate - Fix DEFAULT_MAX_FEE_RATE KDoc to not conflate output filter with simulation ceiling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| return Array(feeRates.shape[0]) { blockTargetIndex -> | ||
| Array(feeRates.shape[1]) { probabilityIndex -> | ||
| feeRates[blockTargetIndex, probabilityIndex].takeIf { it < maxAllowedFeeRate } | ||
| feeRates[blockTargetIndex, probabilityIndex].takeIf { it < maxFeeRate } |
There was a problem hiding this comment.
The docs now say estimates are nulled only when they exceed maxFeeRate, but this strict < check also drops values exactly equal to the bound. That leaves the implementation out of sync with the README/KDoc and misses the exact-bound edge case in tests. Either switch this to <= or tighten the wording everywhere to say the bound is exclusive.
There was a problem hiding this comment.
Added a test. This change is intentional.
| * @param maxFeeRate New maximum fee rate in sat/vB (null to keep current) | ||
| * @return A new [FeeEstimator] instance with the specified settings | ||
| */ | ||
| public fun configure( |
There was a problem hiding this comment.
This still changes the published ABI for configure(): main exposed the 4-arg overload, but this PR only leaves the new 6-arg signature in lib.api. Existing compiled Kotlin/Java callers of the old method will hit NoSuchMethodError. If this release is meant to stay binary-compatible, it needs a delegating shim that preserves the old method shape.
There was a problem hiding this comment.
Acknowledged — this is intentionally not binary-compatible, which is why it's a 0.2.x → 0.3.0 bump. None of the three known consumers call configure(), and all recompile on dependency bump (this is a statically linked library, not a shared runtime dependency). Adding a delegating shim would preserve a method signature nobody calls.
sanket1729
left a comment
There was a problem hiding this comment.
Approving per request. I left two inline comments for follow-up on the remaining ABI and maxFeeRate-boundary issues.
- Pass BucketLayout directly instead of minFeeRate in internal fromMempoolTransactions to avoid redundant object allocation - Add user-friendly upper-bound validation for minFeeRate in FeeEstimator - Clarify that snapshots are layout-agnostic; minFeeRate only affects simulation array conversion, not snapshot creation - Document ceil-vs-round boundary edge case for nonstandard minFeeRate values - Soften deprecation message to reflect actual coupling - Document intentional rounding of DEFAULT_MAX_FEE_RATE - Remove unnecessary lazy from BucketLayout.DEFAULT Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…precation - Remove BucketLayout param from createFeeRateBuckets/calculateBucketIndex since bucketMax is fixed at 1000; clamp directly to SIMULATION_BUCKET_MAX - Delete internal fromMempoolTransactions overload that accepted BucketLayout - Move MAX_SIMULATABLE_FEE_RATE to BucketLayout companion (colocate with SIMULATION_BUCKET_MAX) - Remove redundant @InternalAugurApi from all internal classes and clean up @OptIn boilerplate — Kotlin internal visibility already prevents external access - Undeprecate fromMempoolTransactions since snapshots are layout-agnostic - Strengthen createSnapshot KDoc to explain layout-agnostic snapshot behavior - Add boundary edge-case test verifying estimates exactly equal to maxFeeRate are preserved by the <= filter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addressed: maxFeeRate is now a pure output filter — it no longer affects the simulation array. bucketMax is fixed at 1000 regardless of maxFeeRate. The filter is applied in |
Summary
BucketLayout(internal simulation array layout) to replace the hardcodedBUCKET_MINandBUCKET_MAXconstantsminFeeRatecontrols the simulation array lower bound — transactions below this threshold are excluded from the modeled state spacemaxFeeRateis a pure output filter — estimates whose fee rate exceeds this bound are returned as null, but the simulation always models the full fee rate space (buckets 0–1000) regardless of this valueminFeeRatedefault is 1.0 sat/vB; users on Bitcoin Core 29.1+ can opt in to sub-1 sat/vB support withFeeEstimator(minFeeRate = 0.1)maxFeeRatedefault is 22027.0 sat/vBminFeeRatevalidation (now centralized inBucketLayout) and unusedMAX_SIMULATABLE_FEE_RATE@InternalAugurApiopt-in annotation (redundant with Kotlininternalvisibility)Design
BucketLayoutis an internal-only class that takes onlyminFeeRate.bucketMaxis fixed at 1000 (the legacy simulation ceiling). This ensuresmaxFeeRatenever changes the modeled state space — no lossy bucket folding, no inflow distortion.FeeEstimatesCalculatortakesmaxFeeRateas a separate parameter and applies it inprepareResultArrayas a reporting filter.MempoolSnapshot.fromMempoolTransactionsis unchanged — it buckets all transactions regardless of fee rate bounds. TheminFeeRatefiltering happens later when snapshots are converted to the internal simulation array.minFeeRatevalidation is centralized inBucketLayout.init(positivity + simulation ceiling check).API compatibility note
This release changes JVM binary signatures for
FeeEstimatorconstructors andconfigure().FeeEstimatorgainsminFeeRateandmaxFeeRateparameters (with Kotlin defaults). These new defaults alter the synthetic JVM overloads. All changes are source-compatible — existing Kotlin and Java callers compile without modification — but callers compiled against 0.1.0 or 0.2.x must be recompiled; linking against old bytecode will produceNoSuchMethodErrorat runtime. The three known consumers (baywatch, bitcoin-augur-reference, bitcoin-augur-benchmarking) all recompile on dependency bump.One subtle behavioral change: the fee rate ceiling filter changed from
< exp(10)to<= 22027.0. An estimate at exactly bucket 1000 (~22026.47 sat/vB) was previously null, now passes. This is near-impossible to hit in practice.Test plan
BucketLayoutceil test:minFeeRate = 0.15yields bucket -189 (>= 0.15), not -190 (< 0.15)BucketLayouthas fixedbucketMax = 1000regardless of constructionBucketLayoutrejectsminFeeRatetoo high for simulation ceilingminFeeRateaccepts lower buckets inMempoolSnapshotF64ArraymaxFeeRatefilters estimates as output-only (does not affect simulation arrays)maxFeeRateare preservedminFeeRateandmaxFeeRateare independent — no cross-validation required🤖 Generated with Claude Code