Skip to content

Conversation

@snissn
Copy link
Contributor

@snissn snissn commented Nov 18, 2025

feat: add engine‑managed tipset gas reservations to ref‑fvm

This PR implements engine‑managed tipset‑scope gas reservations inside ref‑fvm, as described in a pending FIP proposal.

Summary

  • Add a reservation session ledger on the default executor keyed by ActorID.
  • Implement begin_reservation_session / end_reservation_session with affordability checks and invariants.
  • Rewrite preflight to assert coverage without pre‑deducting gas funds in reservation mode.
  • Enforce transfers against free balance balance − reserved_remaining.
  • Rewrite settlement to net‑charge gas and realize refunds via reservation release.
  • Add reservation telemetry and tests (unit + integration).

Changes

Executor: session lifecycle and preflight

  • fvm/src/executor/mod.rs

    • Add:
      • ReservationSession struct (re‑exported from default.rs) and ReservationError enum:
        • NotImplemented
        • InsufficientFundsAtBegin { sender }
        • SessionOpen
        • SessionClosed
        • NonZeroRemainder
        • PlanTooLarge
        • Overflow
        • ReservationInvariant(String)
  • fvm/src/executor/default.rs

    • Add ReservationSession { reservations: HashMap<ActorID, TokenAmount>, open: bool } and an Arc<Mutex<_>> on DefaultExecutor.
    • Implement:
      • begin_reservation_session(&mut self, plan: &[(Address, TokenAmount)]) -> Result<(), ReservationError>:
        • Empty plan = no‑op (do not enter reservation mode).
        • Enforce MAX_SENDERS (65,536) and track plan failures via telemetry.
        • Resolve senders via the state tree (robust or ID addresses → ActorID).
        • Aggregate Σ(plan) per actor and enforce reserved_total <= balance per sender.
        • Enforce single active session.
      • end_reservation_session(&mut self) -> Result<(), ReservationError>:
        • Require open == true and all reservation entries to be zero.
        • Clear the ledger and close the session; update telemetry.
    • Preflight:
      • Compute gas_cost = gas_fee_cap * gas_limit using big‑int; treat negative results as ReservationError::Overflow.
      • In reservation mode:
        • Assert coverage via reservation_assert_coverage(sender, &gas_cost); do not pre‑deduct funds.
        • On prevalidation failures (invalid sender, bad nonce, inclusion gas > limit), call reservation_prevalidation_decrement so the ledger can end at zero.
      • Legacy mode:
        • Preserve existing behaviour (check balance ≥ gas_cost, pre‑deduct from sender).

Transfer enforcement and settlement

  • fvm/src/call_manager/default.rs & fvm/src/call_manager/mod.rs

    • Thread Arc<Mutex<ReservationSession>> into the default call manager.
    • In transfer(from, to, value):
      • When the reservation session is open:
        • Compute reserved_remaining = reservations.get(from).unwrap_or(0).
        • Enforce value + reserved_remaining <= from.balance; otherwise return InsufficientFunds.
      • When no session is open:
        • Preserve existing value <= balance semantics.
  • fvm/src/executor/default.rs

    • Settlement (finish_message) in reservation mode:
      • Compute GasOutputs as today.
      • Define consumption = base_fee_burn + over_estimation_burn + miner_tip.
      • Deduct consumption from the sender’s actor balance.
      • Deposit burns and tip to the existing reward/burn actors.
      • Do not deposit refund to the sender; the “refund effect” is realized by releasing the reservation ledger.
      • Decrement reservations[sender] by gas_cost using reservation_prevalidation_decrement, update telemetry, and remove entries at zero.
    • Legacy mode settlement is unchanged.
    • Preserve the invariant:
      • base_fee_burn + over_estimation_burn + refund + miner_tip == gas_cost.

Telemetry

  • fvm/src/executor/telemetry.rs
    • Add ReservationTelemetry and helpers:
      • Track:
        • reservations_open
        • reservation_begin_failed
        • settle_basefee_burn
        • settle_tip_credit
        • settle_overburn
        • settle_refund_virtual
        • Per‑sender reservation totals and remaining amounts.
    • Expose snapshot() (for potential host export) and reset() under #[cfg(test)].

Kernel and test harness adjustments

  • fvm/src/kernel/default.rs

    • Plumb the updated CallManager type where necessary so that all value‑moving operations (SendOps, SELFDESTRUCT, etc.) route through the reservation‑aware transfer.
  • fvm/tests/dummy.rs

    • Update the dummy CallManager impl to accept the new ReservationSession argument in CallManager::new, keeping tests compiling against the updated trait.

Tests

  • Unit tests in fvm/src/executor/default.rs:

    • Session lifecycle: empty plan, begin twice, end with non‑zero remainder, plan too large, unknown actors.
    • Preflight behaviour under reservations:
      • Coverage assertion, no balance deduction.
      • Under‑reserved ledger → ReservationError::Overflow.
    • Transfer enforcement:
      • transfer, send to existing actors, SELFDESTRUCT, and implicit sends must respect free balance.
    • Settlement invariants:
      • Net sender delta equals consumption.
      • Reservation ledger clears so end_reservation_session succeeds.
    • Gas output property test (with --features arb) continues to assert:
      • All components non‑negative.
      • base_fee_burn + over_estimation_burn + refund + miner_tip == gas_cost.
  • Integration tests:

    • testing/integration/tests/reservation_transfer_enforcement.rs:
      • Uses the integration tester to exercise reservation mode and confirm that:
        • Sends and actor creation fail when value > free = balance − reserved_remaining.
        • Failed transfers do not credit receivers.

Activation and host behaviour

  • ref‑fvm does not contain any network‑version logic for reservations.
  • Hosts (e.g., Lotus) control:
    • When to call begin_reservation_session / end_reservation_session.
    • How to treat ReservationError variants (legacy fallback, tipset invalid, node error) based on network version and feature flags.

Notes

  • This PR is designed to preserve receipts (ExitCode, GasUsed, events) and GasOutputs relative to pre‑reservation behaviour, while removing miner exposure to intra‑tipset underfunded messages when hosts enable reservations.

@snissn snissn self-assigned this Nov 18, 2025
@github-project-automation github-project-automation bot moved this to 📌 Triage in FilOz Nov 18, 2025
@snissn snissn force-pushed the multistage-execution branch from 770523f to 56c2695 Compare November 18, 2025 00:43
@snissn
Copy link
Contributor Author

snissn commented Nov 18, 2025

@codecov-commenter
Copy link

codecov-commenter commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 88.16926% with 137 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.80%. Comparing base (71c2f8c) to head (e467835).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
fvm/src/executor/default.rs 89.07% 115 Missing ⚠️
fvm/src/executor/telemetry.rs 78.12% 14 Missing ⚠️
fvm/src/call_manager/default.rs 77.14% 8 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2236      +/-   ##
==========================================
+ Coverage   77.58%   78.80%   +1.22%     
==========================================
  Files         147      148       +1     
  Lines       15789    16927    +1138     
==========================================
+ Hits        12250    13340    +1090     
- Misses       3539     3587      +48     
Files with missing lines Coverage Δ
fvm/src/call_manager/mod.rs 83.33% <ø> (ø)
fvm/src/executor/mod.rs 93.93% <ø> (+72.72%) ⬆️
fvm/src/kernel/default.rs 82.26% <100.00%> (+0.02%) ⬆️
fvm/src/call_manager/default.rs 89.53% <77.14%> (-0.52%) ⬇️
fvm/src/executor/telemetry.rs 78.12% <78.12%> (ø)
fvm/src/executor/default.rs 87.94% <89.07%> (+10.22%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@snissn snissn marked this pull request as ready for review November 20, 2025 22:30
Copilot AI review requested due to automatic review settings November 20, 2025 22:30
Copilot finished reviewing on behalf of snissn November 20, 2025 22:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements engine-managed tipset-scope gas reservations in ref-fvm to prevent miners from being exposed to intra-tipset underfunded messages. The implementation adds a reservation session ledger that tracks per-actor gas commitments throughout tipset execution, with settlement realizing refunds through reservation release rather than direct balance transfers.

Key changes:

  • Add ReservationSession ledger and lifecycle methods (begin_reservation_session/end_reservation_session) with affordability checks
  • Modify preflight to assert coverage without pre-deducting funds in reservation mode
  • Enforce transfer restrictions against free balance (balance - reserved_remaining)
  • Update settlement to net-charge gas costs and realize refunds via reservation release
  • Add reservation telemetry tracking and comprehensive test coverage

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
fvm/src/executor/mod.rs Defines ReservationError enum and re-exports ReservationSession struct for public API
fvm/src/executor/default.rs Core implementation of reservation session lifecycle, preflight validation, settlement logic, and extensive unit tests
fvm/src/executor/telemetry.rs New telemetry module for tracking reservation metrics using global singleton with per-sender gauges
fvm/src/call_manager/mod.rs Updates CallManager::new trait signature to accept ReservationSession parameter
fvm/src/call_manager/default.rs Implements reservation-aware transfer enforcement by checking free balance against reserved amounts
fvm/src/kernel/default.rs Minor formatting change to self-destruct error handling to route through reservation-aware transfer
fvm/tests/dummy.rs Updates dummy CallManager implementation to accept new ReservationSession argument
testing/integration/tests/reservation_transfer_enforcement.rs New integration tests verifying reservation enforcement for sends and actor creation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to +57
pub fn reservation_begin_succeeded(reservations: &HashMap<ActorID, TokenAmount>) {
let mut m = metrics()
.lock()
.expect("reservation telemetry mutex poisoned");
m.reservations_open = m.reservations_open.saturating_add(1);
m.reservation_total_per_sender = reservations.clone();
m.reserved_remaining_per_sender = reservations.clone();
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with global telemetry state

The telemetry module uses a process-global OnceLock<Mutex<ReservationTelemetry>> singleton. When multiple reservation sessions are opened concurrently (e.g., in a multi-threaded test environment or if the host processes multiple tipsets in parallel), line 55-56 will overwrite reservation_total_per_sender and reserved_remaining_per_sender with the latest session's data, losing information about concurrent sessions.

This could lead to:

  1. Incorrect gauge values when sessions overlap
  2. Incomplete telemetry data
  3. Difficulty debugging issues in multi-executor scenarios

Consider either:

  • Documenting that only one session should be active globally at a time
  • Using per-executor telemetry instead of a global singleton
  • Aggregating data from multiple concurrent sessions instead of replacing

Copilot uses AI. Check for mistakes.
Comment on lines +741 to +807
let mut session = self
.reservation_session
.lock()
.expect("reservation session mutex poisoned");

if session.open {
telemetry::reservation_begin_failed();
return Err(ReservationError::SessionOpen);
}

// Aggregate per-actor reservations.
let mut reservations: HashMap<ActorID, TokenAmount> = HashMap::new();

for (addr, amount) in plan {
// Resolve address to ActorID via the state tree.
let sender_id = self
.state_tree()
.lookup_id(addr)
.map_err(|e| {
ReservationError::ReservationInvariant(format!(
"failed to lookup actor {addr}: {e}"
))
})?
.ok_or_else(|| {
ReservationError::ReservationInvariant(format!(
"failed to resolve address {addr} to actor ID"
))
})?;

if amount.is_zero() {
continue;
}

reservations
.entry(sender_id)
.and_modify(|total| *total += amount.clone())
.or_insert_with(|| amount.clone());
}

// Check affordability per sender: Σ(plan) ≤ actor.balance.
for (actor_id, reserved) in &reservations {
let actor_state = self
.state_tree()
.get_actor(*actor_id)
.map_err(|e| {
ReservationError::ReservationInvariant(format!(
"failed to load actor {actor_id}: {e}"
))
})?
.ok_or_else(|| {
ReservationError::ReservationInvariant(format!(
"reservation plan includes unknown actor {actor_id}"
))
})?;

if &actor_state.balance < reserved {
telemetry::reservation_begin_failed();
return Err(ReservationError::InsufficientFundsAtBegin { sender: *actor_id });
}
}

telemetry::reservation_begin_succeeded(&reservations);

session.reservations = reservations;
session.open = true;
Ok(())
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding mutex while performing I/O operations

The code acquires the reservation_session mutex on line 741-744 and holds it throughout the entire reservation plan validation, including:

  • State tree lookups (line 757-768)
  • Actor state loads (line 783-794)

These operations could potentially be slow (especially if the state tree is backed by a slow blockstore), and holding the mutex during these I/O operations could:

  1. Block other threads that need to check reservation status during transfers
  2. Increase contention in multi-threaded scenarios

Consider refactoring to:

  1. Release the mutex before performing state tree operations
  2. Re-acquire it only when updating session.reservations and session.open
  3. Add a check after re-acquiring to ensure the session wasn't opened by another thread in the meantime

Copilot uses AI. Check for mistakes.
"failed to resolve address {addr} to actor ID"
))
})?;

Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for negative reservation amounts

The code checks if amount.is_zero() on line 770 and skips zero entries, but it doesn't validate that amount is positive. If the plan contains negative amounts, they will be:

  1. Added to the reservations map (line 774-777), potentially creating negative reservation totals
  2. Compared against actor balance on line 796, which could pass the check if the negative value is less than the balance

Negative reservations don't make semantic sense and could lead to undefined behavior. Consider adding validation:

if amount.is_negative() {
    telemetry::reservation_begin_failed();
    return Err(ReservationError::ReservationInvariant(
        format!("negative reservation amount for {addr}: {amount}")
    ));
}
if amount.is_zero() {
    continue;
}
Suggested change
if amount.is_negative() {
telemetry::reservation_begin_failed();
return Err(ReservationError::ReservationInvariant(
format!("negative reservation amount for {addr}: {amount}")
));
}

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
#[derive(Default)]
pub struct ReservationSession {
pub reservations: HashMap<ActorID, TokenAmount>,
pub open: bool,
}
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for public ReservationSession struct

The ReservationSession struct is exported publicly (line 30, re-exported from line 10 in mod.rs) but lacks any documentation comments explaining:

  • Its purpose and role in the reservation system
  • The meaning of the reservations and open fields
  • The invariants that should be maintained (e.g., reservations should sum to <= actor balance)
  • Thread-safety expectations (it's wrapped in Arc<Mutex<>> by the executor)

Consider adding a doc comment like:

/// Tracks the gas reservation ledger for a tipset-scope session.
///
/// The ledger maintains per-actor reservation amounts that are decremented
/// as messages are processed. All entries must reach zero before the session
/// can be closed.
#[derive(Default)]
pub struct ReservationSession { ... }

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📌 Triage

Development

Successfully merging this pull request may close these issues.

3 participants