Skip to content

Commit

Permalink
feat: consider average tenure block fullness for tx fee estimations
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Jan 22, 2025
1 parent 26c53dc commit a598d62
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 10 deletions.
48 changes: 38 additions & 10 deletions src/api/routes/core-node-rpc-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<boolean> {
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',
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions src/datastore/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
});
}
}

0 comments on commit a598d62

Please sign in to comment.