Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions crates/consensus/src/consensus_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,9 @@ pub struct ConsensusConstants {

impl ConsensusConstants {
/// Committee size used when `TARI_DEVNET_COMMITTEE_SIZE` is unset, and the size [`Self::DEVNET`]
/// is const-evaluated at.
/// is built at.
pub const DEFAULT_DEVNET_COMMITTEE_SIZE: u32 = 7;
/// Const-evaluates [`Self::devnet`], so every shipped network's constants are checked at compile
/// time whether they are a const item or a const function of one argument.
/// Devnet at the default committee size.
pub const DEVNET: Self = Self::devnet(Self::DEFAULT_DEVNET_COMMITTEE_SIZE);
pub const ESMERALDA: Self = Self {
base_layer_confirmations: 100,
Expand Down Expand Up @@ -300,17 +299,24 @@ impl ConsensusConstants {
}
}

/// Forces [`ConsensusConstants::DEVNET`] to be evaluated, and with it `devnet`'s body. The other
/// networks are const items their constructors return, so they are evaluated wherever they are
/// built; `devnet` takes an argument and builds inline. The runtime reference in `From<Network>`
/// below is not enough on its own: rustc was observed not to evaluate `DEVNET` for it, the
/// initializer being a `const fn` call. This item does not depend on that — a const context
/// evaluates what it names. It pins `devnet` at the default committee size, which covers the whole
/// body: nothing in it varies with the argument beyond the field it sets.
const _: ConsensusConstants = ConsensusConstants::DEVNET;

impl From<Network> for ConsensusConstants {
fn from(network: Network) -> Self {
match network {
Network::MainNet => Self::mainnet(),
// Allow committee size to be overridden for LocalNet
Network::LocalNet => Self::devnet(
env::var("TARI_DEVNET_COMMITTEE_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(Self::DEFAULT_DEVNET_COMMITTEE_SIZE),
),
Network::LocalNet => env::var("TARI_DEVNET_COMMITTEE_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.map_or(Self::DEVNET, Self::devnet),
Network::Esmeralda => Self::esmeralda(),
Network::StageNet | Network::NextNet | Network::Igor => Self::testnet(),
}
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/src/fees/fee_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ impl WasmMeteringRate {
}
}

/// Whether this rate prices WASM execution at all. A rate that does not carries no compute
/// bound beyond the per-transaction hard cap, since there is nothing for a payment to fund.
pub fn prices_execution(&self) -> bool {
self.per_point_cost > 0
}

/// The WASM metering points that `fees_paid` microtari pre-fund: the inverse of the fee module's
/// charge (`points / divisor * per_point_cost`). `None` when WASM execution is not priced
/// (`per_point_cost == 0`) — payment cannot fund what is not charged, so no payment-derived
Expand Down
10 changes: 10 additions & 0 deletions crates/engine/src/runtime/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ pub enum RuntimeError {
TemplateNotFound { template_address: TemplateAddress },
#[error("Insufficient fees paid: required {required_fee}, paid {fees_paid}")]
InsufficientFeesPaid { required_fee: u64, fees_paid: u64 },
#[error(
"Native verification requiring {required_points} points exceeds the fee intent's {credit_points} points of \
compute credit: {consumed_points} already consumed. The credit is a flat allowance for sourcing a fee and \
does not rise with the fee paid — move this work to the main instructions."
)]
FeeIntentComputeExceeded {
required_points: u64,
consumed_points: u64,
credit_points: u64,
},
#[error(
"Insufficient fees to fund native verification requiring {required_points} points: {consumed_points} of \
{allowance} allowance points already consumed"
Expand Down
6 changes: 3 additions & 3 deletions crates/engine/src/runtime/impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ use crate::{
locking::{LockError, LockedSubstate},
pay_fee::PayFee,
scope::PushCallFrame,
tracker::{FinalizedState, StateTracker},
tracker::{ComputeAllowance, FinalizedState, StateTracker},
},
state_store::StateReader,
template::LoadedTemplate,
Expand Down Expand Up @@ -4009,8 +4009,8 @@ where
self.tracker.accumulated_native_points()
}

fn wasm_point_allowance(&self) -> Option<u64> {
self.tracker.wasm_point_allowance()
fn compute_allowance(&self) -> Option<ComputeAllowance> {
self.tracker.compute_allowance()
}

fn resolve_args(
Expand Down
4 changes: 2 additions & 2 deletions crates/engine/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ use tari_template_lib::{
stealth::StealthTransferStatement,
},
};
pub use tracker::{FinalizedState, StateTracker};
pub use tracker::{ComputeAllowance, ComputeFunding, FinalizedState, StateTracker};
pub use working_state::ChargeableState;

use crate::runtime::{locking::LockedSubstate, scope::PushCallFrame};
Expand Down Expand Up @@ -284,7 +284,7 @@ pub trait RuntimeInterface {
/// The maximum Wasmer metering points the transaction may consume given the fees paid so far,
/// used by `WasmProcess::invoke` to cap each call's allowance so unpaid compute cannot exceed
/// the grace. `None` means no payment-funded bound applies (only the per-transaction hard cap).
fn wasm_point_allowance(&self) -> Option<u64>;
fn compute_allowance(&self) -> Option<ComputeAllowance>;

fn resolve_args(
&self,
Expand Down
118 changes: 79 additions & 39 deletions crates/engine/src/runtime/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ impl<TStore> FinalizedState<TStore> {
}
}

/// The compute a transaction may still consume, and what authorizes it.
#[derive(Debug, Clone, Copy)]
pub struct ComputeAllowance {
/// Metering points, counting WASM execution and native verification together.
pub points: u64,
pub funding: ComputeFunding,
}

/// What is paying for the compute an allowance authorizes. Which one binds decides what a
/// transaction that exceeds it is told: a payment-funded allowance rises with the fee, the fee
/// intent's credit does not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComputeFunding {
/// The flat credit the fee intent runs on, sized to source a fee and nothing more.
FeeIntentCredit,
/// What the payment has left after the charges standing against it.
Payment,
}

#[derive(Debug)]
pub struct StateTracker<TStore> {
working_state: Option<WorkingState<TStore>>,
Expand Down Expand Up @@ -148,49 +167,61 @@ impl<TStore: StateReader> StateTracker<TStore> {
self.transaction_weight
}

/// The maximum WASM metering points this transaction may consume given the fees it has paid so
/// far, used by `WasmProcess::invoke` to cap each call's metering allowance. Returns `None` when
/// no payment-funded bound applies (WASM execution is not priced, or this is a dry run) — only
/// the per-transaction hard cap then constrains compute.
/// The compute this transaction may still consume, and what authorizes it. `None` when no bound
/// applies beyond the per-transaction hard cap — WASM execution is not priced, or this is a dry
/// run. Used by `WasmProcess::invoke` to cap each call's metering allowance, and by native
/// verification to pre-charge against the same figure.
///
/// Within the fee intent the allowance is the points the fees paid can cover plus
/// [`limits::FREE_COMPUTE_GRACE_POINTS`] of credit, so a transaction can run its fee-sourcing
/// instructions before it pays, while a transaction that never pays cannot consume more than the
/// grace. Past the fee checkpoint the credit no longer applies: the transaction has paid what its
/// fee intent charged, and the compute it may still run is funded by that payment alone.
/// The fee intent runs on a flat [`limits::FREE_COMPUTE_GRACE_POINTS`] of credit, whatever it
/// has paid. Sourcing a fee is the whole reason a transaction may run anything before paying,
/// and that is all the credit is sized for. Letting a payment raise this allowance would make
/// the fee intent the only place worth executing anything: a failure there leaves no checkpoint
/// to fall back to, so the transaction settles as a rejection that collects nothing, and the
/// work would be done and never paid for — repeatably, with the same funds. Work that needs
/// more than the credit belongs in the main intent, where the fee already paid funds it and a
/// failure still commits the fee intent.
///
/// Compute is funded by what the payment has *left*, not by the whole of it. A transaction that
/// cannot pay for the state it commits commits only its fee intent, and that state was priced
/// onto the charges when the checkpoint was taken — so the charges standing here already
/// include what the fallback costs, bar the compute being authorized. Funding compute out of the
/// full payment instead would let a transaction spend the whole of it on execution and leave its
/// own fee-intent commit unaffordable, which settles as a rejection that collects nothing: the
/// work would be done and never paid for.
/// Past the checkpoint the credit ends and compute is funded by what the payment has *left*,
/// not by the whole of it. The charges standing at the checkpoint are what the fallback commit
/// costs, so funding compute out of the full payment would let a transaction spend the lot on
/// execution and leave its own fee-intent commit unaffordable — a rejection that again collects
/// nothing.
///
/// This is measured against the charges *standing when it is asked*. Anything charged after the
/// last call — a host call inside the final invocation — is outside the figure, so it bounds the
/// unpaid work rather than reducing it to zero.
///
/// The exhaust burn is taken over whatever the charges come to, so the charges themselves can
/// only spend the payment net of it.
pub fn wasm_point_allowance(&self) -> Option<u64> {
/// That payment-funded figure is measured against the charges *standing when it is asked*.
/// Anything charged after the last call — a host call inside the final invocation — is outside
/// it, so it bounds the unpaid work rather than reducing it to zero. The exhaust burn is taken
/// over whatever the charges come to, so the charges themselves can only spend the payment net
/// of it.
pub fn compute_allowance(&self) -> Option<ComputeAllowance> {
let rate = self.wasm_metering_rate;
// The credit exists only so a transaction can source its fee before paying. Taking the fee
// checkpoint is precisely the point at which that need has been met, so the credit ends there.
let is_fee_intent = self.fee_checkpoint.is_none();
self.read_with(|state| {
if !rate.prices_execution() {
return None;
}
let fee_state = state.fee_state();
if is_fee_intent {
// The credit binds a dry run as it binds a real one. It is the same figure either
// way, so estimating against it costs no accuracy and is where a wallet finds out
// that the work has to move to the main instructions.
return Some(ComputeAllowance {
points: limits::FREE_COMPUTE_GRACE_POINTS,
funding: ComputeFunding::FeeIntentCredit,
});
}
// A dry run is metered at whatever `max_fee` the caller submitted, so past the
// checkpoint there is no payment to derive a bound from.
if fee_state.is_dry_run() {
return None;
}
let unspent = spendable_on_charges(fee_state.total_payments(), fee_state.burn_rate_bps())
.saturating_sub(fee_state.total_charges());
let funded = rate.points_funded_by(unspent)?;
if is_fee_intent {
Some(funded.saturating_add(limits::FREE_COMPUTE_GRACE_POINTS))
} else {
Some(funded)
}
Some(ComputeAllowance {
// `prices_execution` above is the only case that yields no figure, so nothing here
// may widen the allowance by failing to produce one.
points: rate.points_funded_by(unspent).unwrap_or(0),
funding: ComputeFunding::Payment,
})
})
}

Expand Down Expand Up @@ -373,11 +404,13 @@ impl<TStore: StateReader> StateTracker<TStore> {
self.read_with(|state| state.fee_state().accumulated_native_points())
}

/// Charges native verification work (priced in WASM-point equivalents) against the
/// payment-funded compute allowance, *before* the work is performed. Errors when the charge
/// would exceed the allowance, so a non-paying transaction traps here — having done none of the
/// priced crypto — rather than extracting it for free. No allowance applies (dry runs, unpriced
/// WASM execution) ⇒ the charge only accumulates, so dry-run fee estimates stay accurate.
/// Charges native verification work (priced in WASM-point equivalents) against the compute
/// allowance, *before* the work is performed. Errors when the charge would exceed the
/// allowance, so a transaction that cannot cover it traps here — having done none of the priced
/// crypto — rather than extracting it for free. Where no allowance applies — unpriced WASM
/// execution, or a dry run past the fee checkpoint — the charge only accumulates, so those
/// estimates stay accurate. A dry run's fee intent is bound by the credit as a real one is,
/// since the credit is the same figure either way.
pub fn charge_native_execution(&mut self, points: u64) -> Result<(), RuntimeError> {
// Hard per-transaction ceiling, independent of what the transaction pays: it bounds how far a block may
// overshoot the propose-time execution budget, which the validation budget has to leave room for.
Expand All @@ -388,15 +421,22 @@ impl<TStore: StateReader> StateTracker<TStore> {
max_points: limits::MAX_NATIVE_POINTS_PER_TRANSACTION,
});
}
if let Some(allowance) = self.wasm_point_allowance() {
if let Some(allowance) = self.compute_allowance() {
let consumed = self
.accumulated_wasm_points()
.saturating_add(self.accumulated_native_points());
if consumed.saturating_add(points) > allowance {
if consumed.saturating_add(points) > allowance.points {
if allowance.funding == ComputeFunding::FeeIntentCredit {
return Err(RuntimeError::FeeIntentComputeExceeded {
required_points: points,
consumed_points: consumed,
credit_points: allowance.points,
});
}
return Err(RuntimeError::InsufficientFeesForNativeExecution {
required_points: points,
consumed_points: consumed,
allowance,
allowance: allowance.points,
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/src/wasm/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ pub enum WasmExecutionError {
the paid fees cover. Increase the transaction fee."
)]
InsufficientFeesForCompute { consumed_points: u64 },
#[error(
"The fee intent exceeded its {credit_points} points of compute credit after consuming {consumed_points} WASM \
metering points. The credit is a flat allowance for sourcing a fee and does not rise with the fee paid — \
move this work to the main instructions."
)]
FeeIntentComputeExceeded { consumed_points: u64, credit_points: u64 },
#[error("Expected function {function} to return a pointer")]
ExpectedPointerReturn { function: String },
#[error("Memory access error: {0}")]
Expand Down
Loading
Loading