Skip to content

CheckTx gas_wanted under-reports the data-contract registration cost (Paid execution event) #3806

@QuantumExplorer

Description

@QuantumExplorer

Summary

In validate_fees_of_event_v0, the Paid execution-event arm advertises a FeeResult that excludes additional_fixed_fee_cost, even though block execution charges it. Because CheckTx turns that returned FeeResult into gas_wanted, the advertised gas for DataContractCreate and DataContractUpdate under-reports the data-contract registration cost by exactly that fixed amount.

This is mempool/observability only — block execution re-derives and books the real fee independently, and gas_wanted does not gate block inclusion — so there is no consensus impact and no funds are lost or mis-booked. But CheckTx gas_wanted (and the fee surfaced to clients via CheckTx) is wrong by the registration cost, which can skew mempool gas accounting/admission and any client that trusts the advertised fee.

This is the same bug that was just fixed for the sibling PaidFromAddressInputs arm in #3793 (commit 23e0c01631); this issue tracks the symmetric fix for the Paid arm, which is a shipped, non-shielded path and so was intentionally left out of that PR's scope.

Affected transitions

The Paid event carries additional_fixed_fee_cost: Some(registration_cost) for:

  • DataContractCreatepackages/rs-drive-abci/src/execution/types/execution_event/mod.rs (DataContractCreateAction arm): registration_cost = data_contract_ref().registration_cost(platform_version), then ExecutionEvent::Paid { …, additional_fixed_fee_cost: Some(registration_cost), … }.
  • DataContractUpdate — same file, DataContractUpdateAction arm (identical pattern).

All other Paid constructions pass additional_fixed_fee_cost: None, so they are unaffected.

Root cause

packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/validate_fees_of_event/v0/mod.rs, the ExecutionEvent::Paid { .. } arm (~L102–164):

let mut required_balance = estimated_fee_result.total_base_fee();
if let Some(additional_fee_cost) = additional_fee_cost {
    required_balance = required_balance.saturating_add(*additional_fee_cost);
}

if balance_after_principal_operation >= required_balance {
    Ok(ConsensusValidationResult::new_with_data(
        estimated_fee_result,        // <-- metered-only; the fixed cost is NOT folded in
    ))
} else {
    // …error branch also returns the metered-only `estimated_fee_result`…
}

The additional_fee_cost is used to compute required_balance (the funding check) but is not folded into the returned FeeResult. The returned value is the metered estimate only.

Meanwhile block execution does charge itpackages/rs-drive-abci/src/execution/platform_events/state_transition_processing/execute_event/v0/mod.rs (the Paid booking, ~L69–73):

if let Some(additional_fixed_fee_cost) = additional_fixed_fee_cost {
    individual_fee_result.processing_fee = individual_fee_result
        .processing_fee
        .saturating_add(additional_fixed_fee_cost);
}
let balance_change = individual_fee_result.into_balance_change(identity.id);

And CheckTx uses the validation result for gas_wantedpackages/rs-drive-abci/src/execution/check_tx/v0/mod.rs (~L186–188):

let (estimated_fee_result, errors) = validation_result.into_data_and_errors()?;
…
check_tx_result.fee_result = Some(estimated_fee_result);   // -> gas_wanted

Net: for DataContractCreate/DataContractUpdate, execution charges metered + registration_cost, but CheckTx advertises metered only.

Impact

Proposed fix

Mirror the fix applied to PaidFromAddressInputs in #3793: fold additional_fixed_fee_cost into the advertised processing_fee so the returned FeeResult matches what execution books. The required_balance value is unchanged.

// Fold the fixed cost into the advertised fee so CheckTx gas_wanted matches what
// execution charges (execution adds it to processing_fee). Advisory only — the booked
// fee is re-derived independently.
if let Some(additional_fee_cost) = additional_fee_cost {
    estimated_fee_result.processing_fee = estimated_fee_result
        .processing_fee
        .saturating_add(*additional_fee_cost);
}
let required_balance = estimated_fee_result.total_base_fee();
// …then return `estimated_fee_result` in both the sufficient- and insufficient-balance branches.

Add a regression test analogous to validate_fees_of_event_v0_paid_from_address_inputs_advertises_additional_fixed_fee_cost (added in #3793): a Paid event with empty operations (metered = 0) and additional_fixed_fee_cost = X, asserting the advertised processing_fee == X.

References

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