From a598d62395fc1a8da44635df1958dc82253f37e0 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Wed, 22 Jan 2025 10:11:16 -0600 Subject: [PATCH] feat: consider average tenure block fullness for tx fee estimations --- src/api/routes/core-node-rpc-proxy.ts | 48 ++++++++++++---- src/datastore/pg-store.ts | 79 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/api/routes/core-node-rpc-proxy.ts b/src/api/routes/core-node-rpc-proxy.ts index 37b3e46bf..4d36ae408 100644 --- a/src/api/routes/core-node-rpc-proxy.ts +++ b/src/api/routes/core-node-rpc-proxy.ts @@ -23,6 +23,12 @@ function getReqUrl(req: { url: string; hostname: string }): URL { // https://github.com/stacks-network/stacks-core/blob/20d5137438c7d169ea97dd2b6a4d51b8374a4751/stackslib/src/chainstate/stacks/db/blocks.rs#L338 const MINIMUM_TX_FEE_RATE_PER_BYTE = 1; +// https://github.com/stacks-network/stacks-core/blob/eb865279406d0700474748dc77df100cba6fa98e/stackslib/src/core/mod.rs#L212-L218 +const BLOCK_LIMIT_WRITE_LENGTH = 15_000_000; +const BLOCK_LIMIT_WRITE_COUNT = 15_000; +const BLOCK_LIMIT_READ_LENGTH = 100_000_000; +const BLOCK_LIMIT_READ_COUNT = 15_000; +const BLOCK_LIMIT_RUNTIME = 5_000_000_000; interface FeeEstimation { fee: number; @@ -128,6 +134,22 @@ export const CoreNodeRpcProxyRouter: FastifyPluginAsync< } } + /// Checks if we should modify all transaction fee estimations to always use the minimum fee. This + /// only happens if there is no fee market i.e. if the last N block tenures have not been full. We + /// use a threshold of 90% to determine if a block size dimension is full. + async function shouldUseTransactionMinimumFee(): Promise { + const tenureWindow = parseInt(process.env['STACKS_CORE_FEE_TENURE_FULLNESS_WINDOW'] ?? '5'); + const averageCosts = await fastify.db.getTenureAverageExecutionCosts(tenureWindow); + if (!averageCosts) return false; + return ( + averageCosts.read_count < BLOCK_LIMIT_READ_COUNT * 0.9 && + averageCosts.read_length < BLOCK_LIMIT_READ_LENGTH * 0.9 && + averageCosts.write_count < BLOCK_LIMIT_WRITE_COUNT * 0.9 && + averageCosts.write_length < BLOCK_LIMIT_WRITE_LENGTH * 0.9 && + averageCosts.runtime < BLOCK_LIMIT_RUNTIME * 0.9 + ); + } + const maxBodySize = 10_000_000; // 10 MB max POST body size fastify.addContentTypeParser( 'application/octet-stream', @@ -233,11 +255,7 @@ export const CoreNodeRpcProxyRouter: FastifyPluginAsync< const txId = responseBuffer.toString(); await logTxBroadcast(txId); await reply.send(responseBuffer); - } else if ( - getReqUrl(req).pathname === '/v2/fees/transaction' && - reply.statusCode === 200 && - feeEstimationModifier !== null - ) { + } else if (getReqUrl(req).pathname === '/v2/fees/transaction' && reply.statusCode === 200) { const reqBody = req.body as { estimated_len?: number; transaction_payload: string; @@ -248,13 +266,23 @@ export const CoreNodeRpcProxyRouter: FastifyPluginAsync< reqBody.transaction_payload.length / 2 ); const minFee = txSize * MINIMUM_TX_FEE_RATE_PER_BYTE; - const modifier = feeEstimationModifier; + const modifier = feeEstimationModifier ?? 1.0; const responseBuffer = await readRequestBody(response as ServerResponse); const responseJson = JSON.parse(responseBuffer.toString()) as FeeEstimateResponse; - responseJson.estimations.forEach(estimation => { - // max(min fee, estimate returned by node * configurable modifier) - estimation.fee = Math.max(minFee, Math.round(estimation.fee * modifier)); - }); + + if (await shouldUseTransactionMinimumFee()) { + // Tenures are not full i.e. there's no fee market. Return the minimum fee. + responseJson.estimations.forEach(estimation => { + estimation.fee = minFee; + }); + } else { + // Fall back to Stacks core's estimate, but modify it according to the ENV configured + // multiplier. + responseJson.estimations.forEach(estimation => { + // max(min fee, estimate returned by node * configurable modifier) + estimation.fee = Math.max(minFee, Math.round(estimation.fee * modifier)); + }); + } await reply.removeHeader('content-length').send(JSON.stringify(responseJson)); } else { await reply.send(response); diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index cd1563733..9809efb12 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -4533,4 +4533,83 @@ export class PgStore extends BasePgStore { `; return parseInt(result[0]?.count ?? '0'); } + + async getTenureAverageExecutionCosts(numTenures: number) { + return await this.sqlTransaction(async sql => { + // Get the last N+1 tenure change blocks so we can get a total of N tenures. + const tenureChanges = await sql<{ block_height: number }[]>` + WITH tenure_change_blocks AS ( + SELECT block_height + FROM txs + WHERE type_id = ${DbTxTypeId.TenureChange} + AND canonical = TRUE + AND microblock_canonical = TRUE + ORDER BY block_height DESC + LIMIT ${numTenures + 1} + ) + SELECT block_height FROM tenure_change_blocks ORDER BY block_height ASC + `; + if (tenureChanges.count != numTenures + 1) return undefined; + const firstTenureBlock = tenureChanges[0].block_height; + const currentTenureBlock = tenureChanges[numTenures].block_height; + + // Group blocks by tenure. + let tenureCond = sql``; + let low = firstTenureBlock; + let high = low; + for (let i = 1; i < tenureChanges.length; i++) { + high = tenureChanges[i].block_height; + tenureCond = sql`${tenureCond} WHEN block_height BETWEEN ${low} AND ${high} THEN ${i}`; + low = high + 1; + } + tenureCond = sql`${tenureCond} ELSE ${tenureChanges.length}`; + + // Sum and return each tenure's execution costs. + const result = await sql< + { + runtime: number; + read_count: number; + read_length: number; + write_count: number; + write_length: number; + }[] + >` + WITH grouped_blocks AS ( + SELECT + block_height, + execution_cost_runtime, + execution_cost_read_count, + execution_cost_read_length, + execution_cost_write_count, + execution_cost_write_length, + CASE + ${tenureCond} + END AS tenure_index + FROM blocks + WHERE block_height >= ${firstTenureBlock} + AND block_height < ${currentTenureBlock} + ORDER BY block_height DESC + ), + tenure_costs AS ( + SELECT + SUM(execution_cost_runtime) AS runtime, + SUM(execution_cost_read_count) AS read_count, + SUM(execution_cost_read_length) AS read_length, + SUM(execution_cost_write_count) AS write_count, + SUM(execution_cost_write_length) AS write_length + FROM grouped_blocks + GROUP BY tenure_index + ORDER BY tenure_index DESC + ) + SELECT + AVG(runtime) AS runtime, + AVG(read_count) AS read_count, + AVG(read_length) AS read_length, + AVG(write_count) AS write_count, + AVG(write_length) AS write_length + FROM tenure_costs + `; + return result[0]; + }); + } }