Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public final class xyz/block/augur/FeeEstimate {

public final class xyz/block/augur/FeeEstimator {
public static final field Companion Lxyz/block/augur/FeeEstimator$Companion;
public static final field MAX_BLOCK_TARGET D
public fun <init> ()V
public fun <init> (Ljava/util/List;)V
public fun <init> (Ljava/util/List;Ljava/util/List;)V
Expand Down
16 changes: 16 additions & 0 deletions lib/src/main/kotlin/xyz/block/augur/FeeEstimator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public class FeeEstimator @JvmOverloads public constructor(
require(blockTargets.isNotEmpty()) { "At least one block target must be provided" }
require(probabilities.all { it in 0.0..1.0 }) { "All probabilities must be between 0.0 and 1.0" }
require(blockTargets.all { it > 0 }) { "All block targets must be positive" }
// Same cost bound as numOfBlocks below: a target of a million blocks simulates a million blocks.
require(blockTargets.all { it <= MAX_BLOCK_TARGET }) {
"All block targets must be at most $MAX_BLOCK_TARGET, was ${blockTargets.filter { it > MAX_BLOCK_TARGET }}"
}
require(maxFeeRate > 0.0) { "maxFeeRate must be positive, was $maxFeeRate" }
bucketLayout = BucketLayout(minFeeRate)
feeEstimatesCalculator = FeeEstimatesCalculator(probabilities, blockTargets, bucketLayout, maxFeeRate)
Expand All @@ -93,6 +97,12 @@ public class FeeEstimator @JvmOverloads public constructor(
// If numOfBlocks is specified then it needs to be at least 3,
// since we can't simulate partial blocks being mined
require(numOfBlocks == null || numOfBlocks >= 3.0) { "numOfBlocks must be at least 3 if specified" }
// It also needs an upper bound: getExpectedBlocksMined evaluates a Poisson tail of
// 4 * numOfBlocks entries and then simulates that many blocks, so an unbounded value costs
// minutes of CPU per call -- and callers behind an HTTP service pass a request parameter through.
require(numOfBlocks == null || numOfBlocks <= MAX_BLOCK_TARGET) {
"numOfBlocks must be at most $MAX_BLOCK_TARGET if specified, was $numOfBlocks"
}

if (mempoolSnapshots.isEmpty()) {
return FeeEstimate(emptyMap(), Instant.now())
Expand Down Expand Up @@ -199,5 +209,11 @@ public class FeeEstimator @JvmOverloads public constructor(
* simulation ceiling (bucket 1000) pass the filter.
*/
public val DEFAULT_MAX_FEE_RATE: Double = FeeEstimatesCalculator.DEFAULT_MAX_FEE_RATE

/**
* Largest supported block target, one week of blocks. Simulation cost grows linearly with the
* target, so this bound keeps a caller-supplied `numOfBlocks` from costing minutes of CPU.
*/
public const val MAX_BLOCK_TARGET: Double = 1008.0
}
}
28 changes: 28 additions & 0 deletions lib/src/test/kotlin/xyz/block/augur/FeeEstimatorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,34 @@ class FeeEstimatorTest {
}
}

@Test
fun `test calculateEstimates throws if numOfBlocks exceeds the maximum block target`() {
// Simulation cost grows with the target, so an unbounded value burns minutes of CPU in a single
// call -- and callers behind an HTTP service pass a request parameter straight through.
val snapshots = TestUtils.createSnapshotSequence(blockCount = 5, snapshotsPerBlock = 3)
assertFailsWith<IllegalArgumentException> {
feeEstimator.calculateEstimates(snapshots, numOfBlocks = 1_000_000.0)
}
assertFailsWith<IllegalArgumentException> {
feeEstimator.calculateEstimates(snapshots, numOfBlocks = FeeEstimator.MAX_BLOCK_TARGET + 1)
}
// The bound itself is still accepted.
feeEstimator.calculateEstimates(snapshots, numOfBlocks = FeeEstimator.MAX_BLOCK_TARGET)
}

@Test
fun `test constructor throws if a block target exceeds the maximum`() {
// blockTargets reaches the same simulation cost as numOfBlocks, so it needs the same bound.
assertFailsWith<IllegalArgumentException> {
FeeEstimator(blockTargets = listOf(3.0, 1_000_000.0))
}
assertFailsWith<IllegalArgumentException> {
feeEstimator.configure(blockTargets = listOf(FeeEstimator.MAX_BLOCK_TARGET + 1))
}
// The bound itself is still accepted.
FeeEstimator(blockTargets = listOf(FeeEstimator.MAX_BLOCK_TARGET))
}

@Test
fun `test constructor throws if minFeeRate is zero or negative`() {
assertFailsWith<IllegalArgumentException> {
Expand Down
Loading