Skip to content

Commit 13cb3cb

Browse files
committed
test(invariants): add TIP-1016 state gas invariant tests and update GasPricing/BlockGasLimits for gas dimension split
1 parent 2c6fc13 commit 13cb3cb

3 files changed

Lines changed: 738 additions & 33 deletions

File tree

tips/ref-impls/test/invariants/BlockGasLimits.t.sol

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTran
2222
/// - Payment lane minimum: 470,000,000 (TEMPO-BLOCK5)
2323
/// - Max deployment fits in tx cap (TEMPO-BLOCK6)
2424
///
25+
/// TIP-1016 state gas changes:
26+
/// - Regular gas counts against tx/block limits; state gas is exempt
27+
/// - tx.gas > max_transaction_gas_limit is VALID when excess is state gas
28+
/// - Block gasUsed reflects regular gas only
29+
/// - Code deposit: 200 regular + 2,300 state per byte
30+
/// - CREATE base: 32,000 regular + 468,000 state
31+
/// - Account creation: 25,000 regular + 225,000 state
32+
///
2533
/// Block-level lane enforcement (BLOCK7, BLOCK12) and shared gas limit
2634
/// (BLOCK10) are tested in Rust (crates/consensus/src/lib.rs).
2735
contract BlockGasLimitsInvariantTest is InvariantBase {
@@ -63,21 +71,53 @@ contract BlockGasLimitsInvariantTest is InvariantBase {
6371
/// @dev TIP-1000: Account creation gas
6472
uint256 internal constant ACCOUNT_CREATION_GAS = 250_000;
6573

74+
/*//////////////////////////////////////////////////////////////
75+
TIP-1016 CONSTANTS
76+
//////////////////////////////////////////////////////////////*/
77+
78+
/// @dev TIP-1016: SSTORE regular gas component
79+
uint256 internal constant SSTORE_REGULAR_GAS = 20_000;
80+
81+
/// @dev TIP-1016: SSTORE state gas component
82+
uint256 internal constant SSTORE_STATE_GAS = 230_000;
83+
84+
/// @dev TIP-1016: Code deposit regular gas per byte
85+
uint256 internal constant CODE_DEPOSIT_REGULAR_PER_BYTE = 200;
86+
87+
/// @dev TIP-1016: Code deposit state gas per byte
88+
uint256 internal constant CODE_DEPOSIT_STATE_PER_BYTE = 2_300;
89+
90+
/// @dev TIP-1016: CREATE regular gas component
91+
uint256 internal constant CREATE_REGULAR_GAS = 32_000;
92+
93+
/// @dev TIP-1016: CREATE state gas component
94+
uint256 internal constant CREATE_STATE_GAS = 468_000;
95+
96+
/// @dev TIP-1016: Account creation regular gas component
97+
uint256 internal constant ACCOUNT_CREATION_REGULAR_GAS = 25_000;
98+
99+
/// @dev TIP-1016: Account creation state gas component
100+
uint256 internal constant ACCOUNT_CREATION_STATE_GAS = 225_000;
101+
66102
/*//////////////////////////////////////////////////////////////
67103
GHOST VARIABLES
68104
//////////////////////////////////////////////////////////////*/
69105

70106
/// @dev TEMPO-BLOCK3: Tx gas cap enforcement
71107
uint256 public ghost_txGasCapTests;
72108
uint256 public ghost_txAtCapSucceeded;
73-
uint256 public ghost_txOverCapRejected;
74-
uint256 public ghost_txOverCapViolations; // Over-cap tx was accepted
109+
uint256 public ghost_txOverCapWithStateGasSucceeded; // tx.gas > cap is valid when excess is state gas
75110

76111
/// @dev TEMPO-BLOCK6: Deployment fits in cap
77112
uint256 public ghost_deploymentTests;
78113
uint256 public ghost_maxDeploymentSucceeded;
79114
uint256 public ghost_maxDeploymentFailed; // Unexpected - would indicate cap too low
80115

116+
/// @dev TIP-1016: Block gasUsed reflects regular gas only
117+
/// @notice Verified at the block level in Rust (crates/consensus/src/lib.rs);
118+
/// this ghost tracks our Solidity-side accounting for consistency.
119+
uint256 public ghost_blockGasUsedRegularOnly;
120+
81121
/// @dev General tracking
82122
uint256 public ghost_validTxExecuted;
83123

@@ -105,14 +145,19 @@ contract BlockGasLimitsInvariantTest is InvariantBase {
105145
function invariant_globalInvariants() public view {
106146
_invariantTxGasCap();
107147
_invariantMaxDeploymentFits();
148+
_invariantBlockGasRegularOnly();
108149
}
109150

110-
/// @notice TEMPO-BLOCK3: Tx gas cap must be enforced at 30M
111-
/// @dev Violations occur if tx with gas > 30M is accepted
151+
/// @notice TEMPO-BLOCK3 + TIP-1016: Tx gas cap applies to regular gas only
152+
/// @dev Post-TIP-1016, tx.gas > TX_GAS_CAP is valid when excess is state gas.
153+
/// We verify that such transactions succeed rather than being rejected.
112154
function _invariantTxGasCap() internal view {
113-
assertEq(
114-
ghost_txOverCapViolations, 0, "TEMPO-BLOCK3: Transaction over 30M gas cap was accepted"
115-
);
155+
if (ghost_txGasCapTests > 0) {
156+
assertTrue(
157+
ghost_txOverCapWithStateGasSucceeded > 0 || ghost_txAtCapSucceeded > 0,
158+
"TEMPO-BLOCK3: No gas cap tests succeeded"
159+
);
160+
}
116161
}
117162

118163
/// @notice TEMPO-BLOCK6: Max contract deployment (24KB) must fit in tx cap
@@ -126,14 +171,25 @@ contract BlockGasLimitsInvariantTest is InvariantBase {
126171
}
127172
}
128173

174+
/// @notice TIP-1016: Block gasUsed must reflect regular gas only
175+
/// @dev State gas is exempt from block gas accounting. This ghost is a
176+
/// Solidity-side placeholder; actual block-level verification is
177+
/// performed in Rust (crates/consensus/src/lib.rs).
178+
function _invariantBlockGasRegularOnly() internal view {
179+
assertEq(
180+
ghost_blockGasUsedRegularOnly, 0,
181+
"TIP-1016: block.gasUsed included state gas"
182+
);
183+
}
184+
129185
/*//////////////////////////////////////////////////////////////
130186
HANDLERS
131187
//////////////////////////////////////////////////////////////*/
132188

133-
/// @notice Handler: Test tx gas cap enforcement (TEMPO-BLOCK3)
189+
/// @notice Handler: Test tx gas cap enforcement (TEMPO-BLOCK3 + TIP-1016)
134190
/// @param actorSeed Seed for selecting actor
135-
/// @param gasMultiplier Multiplier to test various gas levels
136-
function handler_txGasCapEnforcement(uint256 actorSeed, uint256 gasMultiplier) external {
191+
/// @param stateGasExtra Extra state gas above the cap (1 to 1M)
192+
function handler_txGasCapEnforcement(uint256 actorSeed, uint256 stateGasExtra) external {
137193
// Skip when not on Tempo (vmExec.executeTransaction not available)
138194
if (!isTempo) return;
139195

@@ -163,29 +219,29 @@ contract BlockGasLimitsInvariantTest is InvariantBase {
163219
// May fail for other reasons (balance, etc.) - not a violation
164220
}
165221

166-
// Test 2: Tx over the cap (should be rejected)
222+
// Test 2: Tx with gas ABOVE the cap where excess is state gas (should succeed)
223+
// Post-TIP-1016, tx.gas > TX_GAS_CAP is valid when the excess goes to
224+
// the state gas reservoir (e.g., an SSTORE needs SSTORE_STATE_GAS).
167225
nonce = uint64(vm.getNonce(sender));
168226

169-
// Gas amount over cap: 30M + 1 to 30M + 10M based on multiplier
170-
uint256 overAmount = bound(gasMultiplier, 1, 10_000_000);
227+
uint256 overAmount = bound(stateGasExtra, 1, 1_000_000);
171228
uint64 overCapGas = uint64(TX_GAS_CAP + overAmount);
172229

173230
bytes memory overCapTx = TxBuilder.buildLegacyCallWithGas(
174231
vmRlp, vm, address(feeToken), callData, nonce, overCapGas, privateKey
175232
);
176233

177234
try vmExec.executeTransaction(overCapTx) {
178-
// Over-cap tx was accepted - VIOLATION
179-
ghost_txOverCapViolations++;
235+
// Over-cap tx accepted — valid post-TIP-1016 (excess is state gas)
236+
ghost_txOverCapWithStateGasSucceeded++;
180237
ghost_protocolNonce[sender]++;
181-
} catch (bytes memory reason) {
182-
if (_isGasCapRevert(reason)) {
183-
ghost_txOverCapRejected++;
184-
}
238+
ghost_validTxExecuted++;
239+
} catch {
240+
// May fail for other reasons (balance, etc.) - not a violation
185241
}
186242
}
187243

188-
/// @notice Handler: Test max contract deployment fits in cap (TEMPO-BLOCK6)
244+
/// @notice Handler: Test max contract deployment fits in cap (TEMPO-BLOCK6 + TIP-1016)
189245
/// @param actorSeed Seed for selecting actor
190246
/// @param sizeFraction Fraction of max size to deploy (50-100%)
191247
function handler_maxDeploymentFits(uint256 actorSeed, uint256 sizeFraction) external {
@@ -206,13 +262,21 @@ contract BlockGasLimitsInvariantTest is InvariantBase {
206262
// Simple initcode: PUSH1 0x00 PUSH1 0x00 RETURN + padding
207263
bytes memory initcode = _createInitcodeOfSize(targetSize);
208264

209-
// Calculate required gas
210-
uint256 requiredGas = 53_000 // CREATE tx base
211-
+ CREATE_BASE_GAS + (initcode.length * CODE_DEPOSIT_PER_BYTE) + ACCOUNT_CREATION_GAS
212-
+ 100_000; // Buffer for memory expansion etc.
265+
// TIP-1016: Compute regular and state gas separately.
266+
// A 24KB contract needs ~7M regular gas but ~57M state gas.
267+
// tx.gas = regular + state can exceed TX_GAS_CAP because state gas
268+
// is exempt from the cap (goes to reservoir).
269+
uint256 requiredRegularGas = 53_000 // CREATE tx base
270+
+ CREATE_REGULAR_GAS + (initcode.length * CODE_DEPOSIT_REGULAR_PER_BYTE)
271+
+ ACCOUNT_CREATION_REGULAR_GAS + 100_000; // Buffer for memory expansion etc.
272+
273+
uint256 requiredStateGas = CREATE_STATE_GAS
274+
+ (initcode.length * CODE_DEPOSIT_STATE_PER_BYTE) + ACCOUNT_CREATION_STATE_GAS;
275+
276+
uint256 totalGas = requiredRegularGas + requiredStateGas;
213277

214-
// Should fit in TX_GAS_CAP
215-
uint64 gasLimit = uint64(requiredGas > TX_GAS_CAP ? TX_GAS_CAP : requiredGas);
278+
// totalGas can exceed TX_GAS_CAP — state gas is exempt from the cap
279+
uint64 gasLimit = uint64(totalGas);
216280

217281
uint64 nonce = uint64(vm.getNonce(sender));
218282
bytes memory createTx =

tips/ref-impls/test/invariants/GasPricing.t.sol

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { TxBuilder } from "../helpers/TxBuilder.sol";
1111
import { VmExecuteTransaction, VmRlp } from "tempo-std/StdVm.sol";
1212
import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTransactionLib.sol";
1313

14-
/// @title TIP-1000 Gas Pricing Invariant Tests
14+
/// @title TIP-1000 / TIP-1016 Gas Pricing Invariant Tests
1515
/// @notice Fuzz-based invariant tests for Tempo's state creation gas costs
1616
/// @dev Tests gas pricing invariants at the EVM opcode level using vmExec.executeTransaction()
1717
///
@@ -22,6 +22,11 @@ import { LegacyTransaction, LegacyTransactionLib } from "tempo-std/tx/LegacyTran
2222
/// - Account creation: 250,000 gas (part of TEMPO-GAS5)
2323
/// - Multiple new slots: 250,000 gas each (TEMPO-GAS8)
2424
///
25+
/// TIP-1016 splits gas into two dimensions:
26+
/// - Regular gas (20k for SSTORE new slot) — counts against tx/block limits
27+
/// - State gas (230k for SSTORE new slot) — exempt from limits but still charged
28+
/// Total gas per SSTORE remains 250k.
29+
///
2530
/// Protocol-level invariants (tx gas cap, intrinsic gas) are tested in Rust.
2631
contract GasPricingInvariantTest is InvariantBase {
2732

@@ -32,9 +37,19 @@ contract GasPricingInvariantTest is InvariantBase {
3237
TIP-1000 CONSTANTS
3338
//////////////////////////////////////////////////////////////*/
3439

35-
/// @dev SSTORE to new (zero) slot costs 250,000 gas
40+
/// @dev SSTORE to new (zero) slot costs 250,000 gas total
3641
uint256 internal constant SSTORE_SET_GAS = 250_000;
3742

43+
/*//////////////////////////////////////////////////////////////
44+
TIP-1016 CONSTANTS
45+
//////////////////////////////////////////////////////////////*/
46+
47+
/// @dev Regular gas for SSTORE new slot (counts against tx/block limits)
48+
uint256 internal constant SSTORE_REGULAR_GAS = 20_000;
49+
50+
/// @dev State gas for SSTORE new slot (exempt from limits, still charged)
51+
uint256 internal constant SSTORE_STATE_GAS = 230_000;
52+
3853
/// @dev CREATE base cost (excludes code deposit and account creation)
3954
uint256 internal constant CREATE_BASE_GAS = 500_000;
4055

@@ -85,6 +100,10 @@ contract GasPricingInvariantTest is InvariantBase {
85100
uint256 public ghost_multiSlotSufficientGasSucceeded;
86101
uint256 public ghost_multiSlotViolations; // All slots written with insufficient gas
87102

103+
/// @dev TIP-1016: State gas tracking (block vs receipt delta)
104+
uint256 public ghost_stateGasBlockDelta;
105+
uint256 public ghost_stateGasReceiptDelta;
106+
88107
/*//////////////////////////////////////////////////////////////
89108
SETUP
90109
//////////////////////////////////////////////////////////////*/
@@ -285,8 +304,8 @@ contract GasPricingInvariantTest is InvariantBase {
285304
bytes memory callData = abi.encodeCall(GasTestStorage.storeMultiple, (slots));
286305
uint64 nonce = uint64(vm.getNonce(sender));
287306

288-
// Test 1: Gas sufficient for ~1 slot only (should fail for N>1)
289-
uint64 lowGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_SET_GAS + GAS_TOLERANCE);
307+
// Test 1: Only enough regular gas for 1 SSTORE (insufficient total for any SSTORE)
308+
uint64 lowGas = uint64(BASE_TX_GAS + CALL_OVERHEAD + SSTORE_REGULAR_GAS + GAS_TOLERANCE);
290309
bytes memory lowGasTx = TxBuilder.buildLegacyCallWithGas(
291310
vmRlp, vm, address(storageContract), callData, nonce, lowGas, privateKey
292311
);
@@ -302,11 +321,10 @@ contract GasPricingInvariantTest is InvariantBase {
302321
}
303322
}
304323

305-
// Violation: all slots written with gas for only 1
306-
if (written == numSlots) {
324+
// Violation: any slot written with insufficient gas
325+
if (written > 0) {
307326
ghost_multiSlotViolations++;
308327
} else {
309-
// Partial write is expected (reverted mid-execution)
310328
ghost_multiSlotInsufficientGasFailed++;
311329
}
312330
ghost_protocolNonce[sender]++;

0 commit comments

Comments
 (0)