Skip to content

[Bug] Gas estimation excludes inclusive last_version and can miss trailing BlockEpilogue #19880

@fallintoplace

Description

@fallintoplace

🐛 Bug

Gas estimation treats a block's last_version as exclusive when scanning transactions for block_min_inclusion_price, even though the surrounding block APIs and storage logic treat last_version as inclusive.

Relevant code in api/src/context.rs:

match self.get_gas_prices_and_used(
    first,
    last - first,
    ledger_info.ledger_version.0,
    user_use_case_spread_factor.is_some(),
) {

But get_gas_prices_and_used() interprets that second argument as a count/limit:

fn get_gas_prices_and_used(
    &self,
    start_version: Version,
    limit: u64,
    ledger_version: Version,
    count_majority_use_case: bool,
) -> Result<(Vec<(u64, u64)>, Vec<BlockEndInfo>, Option<f32>)> {
    if start_version > ledger_version || limit == 0 {
        return Ok((vec![], vec![], None));
    }

    let limit = std::cmp::min(limit, ledger_version - start_version + 1);
    let txns = self.db.get_transaction_iterator(start_version, limit)?;
    let infos = self.db.get_transaction_info_iterator(start_version, limit)?;
    ...
}

The rest of the block stack treats last_version as inclusive:

// storage/aptosdb/src/db/aptosdb_internal.rs
Ok(next_block_info) => next_block_info.first_version() - 1,
// api/src/context.rs
(last_version - first_version + 1) as u16

That means the estimator currently scans [first, last) instead of [first, last] and drops last_version.

In normal committed blocks, last_version is typically the trailing BlockEpilogue. As a result, the estimator can miss BlockEndInfo::limit_reached() and incorrectly classify a full block as not full.

To reproduce

Code snippet to reproduce

let first = 100;
let last = 102; // inclusive block range [100, 102]

let current_limit = last - first;      // 2
let expected_limit = last - first + 1; // 3

// current logic reads 2 transactions starting at 100,
// so version 102 is skipped entirely

Behavioral path

} else if !block_end_infos.is_empty() {
    assert_eq!(1, block_end_infos.len());
    block_end_infos.first().unwrap().limit_reached()
}

If the skipped last_version is the block-ending BlockEpilogue, block_end_infos stays empty and full-block detection falls back to weaker heuristics.

Stack trace/error message

No runtime error; this is a logic bug in block range arithmetic.

Expected Behavior

Gas estimation should read the full inclusive block span, including last_version, so the block-ending BlockEpilogue is visible and limit_reached() is evaluated correctly.

System information

Please complete the following information:

  • Aptos Core Version: main at current HEAD
  • Rust Version: not checked
  • Computer OS: macOS

Additional context

Suggested fix:

let limit = last
    .checked_sub(first)
    .map(|delta| delta + 1)
    .unwrap_or(0);

match self.get_gas_prices_and_used(
    first,
    limit,
    ledger_info.ledger_version.0,
    user_use_case_spread_factor.is_some(),
) {
    ...
}

Suggested regression tests:

  • estimator reads the full inclusive block span
  • block-ending BlockEpilogue is observed by gas estimation
  • full-block detection respects limit_reached() from the trailing epilogue

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions