Skip to content

Commit 058acbf

Browse files
author
Aaron Blankstein
authored
Merge pull request #2872 from blockstack/feat/fee-rate-rpc
Fee estimation: RPC interface for estimating transaction costs and fees
2 parents c0b7404 + cbc9a8a commit 058acbf

22 files changed

+791
-97
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
- tests::neon_integrations::mining_transactions_is_fair
5353
steps:
5454
- uses: actions/checkout@v2
55-
- name: Download math result for job 1
55+
- name: Download docker image
5656
uses: actions/download-artifact@v2
5757
with:
5858
name: integration-image.tar

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1212
- FeeEstimator and CostEstimator interfaces. These can be controlled
1313
via node configuration options. See the `README.md` for more
1414
information on the configuration.
15+
- New fee rate estimation endpoint `/v2/fees/transaction` (#2872). See
16+
`docs/rpc/openapi.yaml` for more information.
1517

1618
## Changed
1719

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"cost_scalar_change_by_byte": 0.00476837158203125,
3+
"estimated_cost": {
4+
"read_count": 19,
5+
"read_length": 4814,
6+
"runtime": 7175000,
7+
"write_count": 2,
8+
"write_length": 1020
9+
},
10+
"estimated_cost_scalar": 14,
11+
"estimations": [
12+
{
13+
"fee": 17,
14+
"fee_rate": 1.2410714285714286
15+
},
16+
{
17+
"fee": 125,
18+
"fee_rate": 8.958333333333332
19+
},
20+
{
21+
"fee": 140,
22+
"fee_rate": 10
23+
}
24+
]
25+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"description": "POST response for estimated fee",
4+
"title": "TransactionFeeEstimateResponse",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": ["estimated_cost", "estimated_cost_scalar", "estimated_fee_rates", "estimated_fees"],
8+
"properties": {
9+
"estimated_cost_scalar": {
10+
"type": "integer"
11+
},
12+
"cost_scalar_change_by_byte": {
13+
"type": "number"
14+
},
15+
"estimated_cost": {
16+
"type": "object",
17+
"additionalProperties": false,
18+
"required": ["read_count", "write_count", "read_length", "write_length", "runtime"],
19+
"properties": {
20+
"read_count": { "type": "integer" },
21+
"read_length": { "type": "integer" },
22+
"runtime": { "type": "integer" },
23+
"write_count": { "type": "integer" },
24+
"write_length": { "type": "integer" }
25+
}
26+
},
27+
"estimations": {
28+
"type": "array",
29+
"items": {
30+
"type": "object",
31+
"properties": {
32+
"fee_rate": {
33+
"type": "number"
34+
},
35+
"fee": {
36+
"type": "number"
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"estimated_len": 350,
3+
"transaction_payload": "021af942874ce525e87f21bbe8c121b12fac831d02f4086765742d696e666f0b7570646174652d696e666f00000000"
4+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"description": "POST request for estimated fee",
4+
"title": "TransactionFeeEstimateRequest",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": ["transaction_payload"],
8+
"properties": {
9+
"transaction_payload": {
10+
"type": "string"
11+
},
12+
"estimated_len": {
13+
"type": "integer"
14+
}
15+
}
16+
}

docs/rpc/openapi.yaml

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,96 @@ paths:
276276
example:
277277
$ref: ./api/core-node/get-account-data.example.json
278278

279+
/v2/fees/transaction:
280+
post:
281+
summary: Get approximate fees for the given transaction
282+
tags:
283+
- Fees
284+
description: |
285+
Get an estimated fee for the supplied transaction. This
286+
estimates the execution cost of the transaction, the current
287+
fee rate of the network, and returns estimates for fee
288+
amounts.
289+
290+
* `transaction_payload` is a hex-encoded serialization of
291+
the TransactionPayload for the transaction.
292+
* `estimated_len` is an optional argument that provides the
293+
endpoint with an estimation of the final length (in bytes)
294+
of the transaction, including any post-conditions and
295+
signatures
296+
297+
If the node cannot provide an estimate for the transaction
298+
(e.g., if the node has never seen a contract-call for the
299+
given contract and function) or if estimation is not
300+
configured on this node, a 400 response is returned.
301+
The 400 response will be a JSON error containing a `reason`
302+
field which can be one of the following:
303+
304+
* `DatabaseError` - this Stacks node has had an internal
305+
database error while trying to estimate the costs of the
306+
supplied transaction.
307+
* `NoEstimateAvailable` - this Stacks node has not seen this
308+
kind of contract-call before, and it cannot provide an
309+
estimate yet.
310+
* `CostEstimationDisabled` - this Stacks node does not perform
311+
fee or cost estimation, and it cannot respond on this
312+
endpoint.
313+
314+
The 200 response contains the following data:
315+
316+
* `estimated_cost` - the estimated multi-dimensional cost of
317+
executing the Clarity VM on the provided transaction.
318+
* `estimated_cost_scalar` - a unitless integer that the Stacks
319+
node uses to compare how much of the block limit is consumed
320+
by different transactions. This value incorporates the
321+
estimated length of the transaction and the estimated
322+
execution cost of the transaction. The range of this integer
323+
may vary between different Stacks nodes. In order to compute
324+
an estimate of total fee amount for the transaction, this
325+
value is multiplied by the same Stacks node's estimated fee
326+
rate.
327+
* `cost_scalar_change_by_byte` - a float value that indicates how
328+
much the `estimated_cost_scalar` value would increase for every
329+
additional byte in the final transaction.
330+
* `estimations` - an array of estimated fee rates and total fees to
331+
pay in microSTX for the transaction. This array provides a range of
332+
estimates (default: 3) that may be used. Each element of the array
333+
contains the following fields:
334+
* `fee_rate` - the estimated value for the current fee
335+
rates in the network
336+
* `fee` - the estimated value for the total fee in
337+
microSTX that the given transaction should pay. These
338+
values are the result of computing:
339+
`fee_rate` x `estimated_cost_scalar`.
340+
If the estimated fees are less than the minimum relay
341+
fee `(1 ustx x estimated_len)`, then that minimum relay
342+
fee will be returned here instead.
343+
344+
345+
Note: If the final transaction's byte size is larger than
346+
supplied to `estimated_len`, then applications should increase
347+
this fee amount by:
348+
349+
`fee_rate` x `cost_scalar_change_by_byte` x (`final_size` - `estimated_size`)
350+
351+
operationId: post_fee_transaction
352+
requestBody:
353+
content:
354+
application/json:
355+
schema:
356+
$ref: ./api/core-node/post-fee-transaction.schema.json
357+
example:
358+
$ref: ./api/core-node/post-fee-transaction.example.json
359+
responses:
360+
200:
361+
description: Estimated fees for the transaction
362+
content:
363+
application/json:
364+
schema:
365+
$ref: ./api/core-node/post-fee-transaction-response.schema.json
366+
example:
367+
$ref: ./api/core-node/post-fee-transaction-response.example.json
368+
279369
/v2/fees/transfer:
280370
get:
281371
summary: Get estimated fee
@@ -378,4 +468,4 @@ paths:
378468
in: query
379469
schema:
380470
type: string
381-
description: The Stacks chain tip to query from
471+
description: The Stacks chain tip to query from

src/cost_estimates/fee_scalar.rs

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use core::BLOCK_LIMIT_MAINNET;
2020
use chainstate::stacks::db::StacksEpochReceipt;
2121
use chainstate::stacks::events::TransactionOrigin;
2222

23+
use crate::util::db::set_wal_mode;
2324
use crate::util::db::sql_pragma;
2425
use crate::util::db::table_exists;
2526

@@ -31,15 +32,15 @@ const SINGLETON_ROW_ID: i64 = 1;
3132
const CREATE_TABLE: &'static str = "
3233
CREATE TABLE scalar_fee_estimator (
3334
estimate_key NUMBER PRIMARY KEY,
34-
fast NUMBER NOT NULL,
35-
medium NUMBER NOT NULL,
36-
slow NUMBER NOT NULL
35+
high NUMBER NOT NULL,
36+
middle NUMBER NOT NULL,
37+
low NUMBER NOT NULL
3738
)";
3839

3940
/// This struct estimates fee rates by translating a transaction's `ExecutionCost`
4041
/// into a scalar using `ExecutionCost::proportion_dot_product` and computing
4142
/// the subsequent fee rate using the actual paid fee. The 5th, 50th and 95th
42-
/// percentile fee rates for each block are used as the slow, medium, and fast
43+
/// percentile fee rates for each block are used as the low, middle, and high
4344
/// estimates. Estimates are updated via exponential decay windowing.
4445
pub struct ScalarFeeRateEstimator<M: CostMetric> {
4546
db: Connection,
@@ -53,11 +54,16 @@ pub struct ScalarFeeRateEstimator<M: CostMetric> {
5354
impl<M: CostMetric> ScalarFeeRateEstimator<M> {
5455
/// Open a fee rate estimator at the given db path. Creates if not existent.
5556
pub fn open(p: &Path, metric: M) -> Result<Self, SqliteError> {
56-
let db = Connection::open_with_flags(p, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)
57-
.or_else(|e| {
57+
let db = match Connection::open_with_flags(p, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE) {
58+
Ok(db) => {
59+
set_wal_mode(&db)?;
60+
Ok(db)
61+
}
62+
Err(e) => {
5863
if let SqliteError::SqliteFailure(ref internal, _) = e {
5964
if let rusqlite::ErrorCode::CannotOpen = internal.code {
6065
let mut db = Connection::open(p)?;
66+
set_wal_mode(&db)?;
6167
let tx = tx_begin_immediate_sqlite(&mut db)?;
6268
Self::instantiate_db(&tx)?;
6369
tx.commit()?;
@@ -68,7 +74,9 @@ impl<M: CostMetric> ScalarFeeRateEstimator<M> {
6874
} else {
6975
Err(e)
7076
}
71-
})?;
77+
}
78+
}?;
79+
7280
Ok(Self {
7381
db,
7482
metric,
@@ -84,7 +92,6 @@ impl<M: CostMetric> ScalarFeeRateEstimator<M> {
8492

8593
fn instantiate_db(tx: &SqlTransaction) -> Result<(), SqliteError> {
8694
if !Self::db_already_instantiated(tx)? {
87-
tx.pragma_update(None, "journal_mode", &"WAL".to_string())?;
8895
tx.execute(CREATE_TABLE, rusqlite::NO_PARAMS)?;
8996
}
9097

@@ -103,18 +110,18 @@ impl<M: CostMetric> ScalarFeeRateEstimator<M> {
103110
// because of integer math, we can end up with some edge effects
104111
// when the estimate is < decay_rate_fraction.1, so just saturate
105112
// on the low end at a rate of "1"
106-
next_computed.fast = if next_computed.fast >= 1f64 {
107-
next_computed.fast
113+
next_computed.high = if next_computed.high >= 1f64 {
114+
next_computed.high
108115
} else {
109116
1f64
110117
};
111-
next_computed.medium = if next_computed.medium >= 1f64 {
112-
next_computed.medium
118+
next_computed.middle = if next_computed.middle >= 1f64 {
119+
next_computed.middle
113120
} else {
114121
1f64
115122
};
116-
next_computed.slow = if next_computed.slow >= 1f64 {
117-
next_computed.slow
123+
next_computed.low = if next_computed.low >= 1f64 {
124+
next_computed.low
118125
} else {
119126
1f64
120127
};
@@ -129,25 +136,25 @@ impl<M: CostMetric> ScalarFeeRateEstimator<M> {
129136
};
130137

131138
debug!("Updating fee rate estimate for new block";
132-
"new_measure_fast" => new_measure.fast,
133-
"new_measure_medium" => new_measure.medium,
134-
"new_measure_slow" => new_measure.slow,
135-
"new_estimate_fast" => next_estimate.fast,
136-
"new_estimate_medium" => next_estimate.medium,
137-
"new_estimate_slow" => next_estimate.slow);
139+
"new_measure_high" => new_measure.high,
140+
"new_measure_middle" => new_measure.middle,
141+
"new_measure_low" => new_measure.low,
142+
"new_estimate_high" => next_estimate.high,
143+
"new_estimate_middle" => next_estimate.middle,
144+
"new_estimate_low" => next_estimate.low);
138145

139146
let sql = "INSERT OR REPLACE INTO scalar_fee_estimator
140-
(estimate_key, fast, medium, slow) VALUES (?, ?, ?, ?)";
147+
(estimate_key, high, middle, low) VALUES (?, ?, ?, ?)";
141148

142149
let tx = tx_begin_immediate_sqlite(&mut self.db).expect("SQLite failure");
143150

144151
tx.execute(
145152
sql,
146153
rusqlite::params![
147154
SINGLETON_ROW_ID,
148-
next_estimate.fast,
149-
next_estimate.medium,
150-
next_estimate.slow,
155+
next_estimate.high,
156+
next_estimate.middle,
157+
next_estimate.low,
151158
],
152159
)
153160
.expect("SQLite failure");
@@ -207,13 +214,13 @@ impl<M: CostMetric> FeeEstimator for ScalarFeeRateEstimator<M> {
207214
let measures_len = all_fee_rates.len();
208215
if measures_len > 0 {
209216
// use 5th, 50th, and 95th percentiles from block
210-
let fastest_index = measures_len - cmp::max(1, measures_len / 20);
217+
let highest_index = measures_len - cmp::max(1, measures_len / 20);
211218
let median_index = measures_len / 2;
212-
let slowest_index = measures_len / 20;
219+
let lowest_index = measures_len / 20;
213220
let block_estimate = FeeRateEstimate {
214-
fast: all_fee_rates[fastest_index],
215-
medium: all_fee_rates[median_index],
216-
slow: all_fee_rates[slowest_index],
221+
high: all_fee_rates[highest_index],
222+
middle: all_fee_rates[median_index],
223+
low: all_fee_rates[lowest_index],
217224
};
218225

219226
self.update_estimate(block_estimate);
@@ -223,17 +230,17 @@ impl<M: CostMetric> FeeEstimator for ScalarFeeRateEstimator<M> {
223230
}
224231

225232
fn get_rate_estimates(&self) -> Result<FeeRateEstimate, EstimatorError> {
226-
let sql = "SELECT fast, medium, slow FROM scalar_fee_estimator WHERE estimate_key = ?";
233+
let sql = "SELECT high, middle, low FROM scalar_fee_estimator WHERE estimate_key = ?";
227234
self.db
228235
.query_row(sql, &[SINGLETON_ROW_ID], |row| {
229-
let fast: f64 = row.get(0)?;
230-
let medium: f64 = row.get(1)?;
231-
let slow: f64 = row.get(2)?;
232-
Ok((fast, medium, slow))
236+
let high: f64 = row.get(0)?;
237+
let middle: f64 = row.get(1)?;
238+
let low: f64 = row.get(2)?;
239+
Ok((high, middle, low))
233240
})
234241
.optional()
235242
.expect("SQLite failure")
236-
.map(|(fast, medium, slow)| FeeRateEstimate { fast, medium, slow })
243+
.map(|(high, middle, low)| FeeRateEstimate { high, middle, low })
237244
.ok_or_else(|| EstimatorError::NoEstimateAvailable)
238245
}
239246
}

0 commit comments

Comments
 (0)