|
3 | 3 |
|
4 | 4 | using FluentAssertions; |
5 | 5 | using Nethermind.Arbitrum.Arbos; |
| 6 | +using Nethermind.Arbitrum.Arbos.Storage; |
6 | 7 | using Nethermind.Arbitrum.Evm; |
7 | 8 | using Nethermind.Arbitrum.Execution; |
8 | 9 | using Nethermind.Arbitrum.Test.Infrastructure; |
9 | 10 | using Nethermind.Core; |
| 11 | +using Nethermind.Core.Test; |
10 | 12 | using Nethermind.Core.Test.Builders; |
11 | 13 | using Nethermind.Evm; |
12 | 14 | using Nethermind.Evm.State; |
13 | 15 | using Nethermind.Evm.Test; |
14 | 16 | using Nethermind.Evm.TransactionProcessing; |
| 17 | +using Nethermind.Int256; |
15 | 18 |
|
16 | 19 | namespace Nethermind.Arbitrum.Test.Execution; |
17 | 20 |
|
@@ -115,4 +118,179 @@ public void Execute_SufficientGas_ChargesPosterGasAsL1Calldata() |
115 | 118 |
|
116 | 119 | gas.Get(ResourceKind.L1Calldata).Should().BeGreaterThan(0UL, "expected L1Calldata > 0"); |
117 | 120 | } |
| 121 | + |
| 122 | + /// <summary> |
| 123 | + /// Tests multi-gas refund calculation for normal transactions. |
| 124 | + /// Mirrors Nitro's TestEndTxHookMultiGasRefundNormalTx from tx_processor_multigas_test.go. |
| 125 | + /// |
| 126 | + /// When multi-gas constraints make certain resources expensive (e.g., StorageGrowth weight=10), |
| 127 | + /// transactions that don't use those expensive resources should have multiDimensionalCost less than |
| 128 | + /// singleGasCost, resulting in a positive refund. |
| 129 | + /// </summary> |
| 130 | + [Test] |
| 131 | + public void MultiGasRefund_WhenExpensiveResourceNotUsed_ProducesPositiveRefund() |
| 132 | + { |
| 133 | + IWorldState worldState = TestWorldStateFactory.CreateForTest(); |
| 134 | + using IDisposable disposer = worldState.BeginScope(IWorldState.PreGenesis); |
| 135 | + _ = ArbOSInitialization.Create(worldState); |
| 136 | + |
| 137 | + PrecompileTestContextBuilder context = new PrecompileTestContextBuilder(worldState, ulong.MaxValue) |
| 138 | + .WithArbosState() |
| 139 | + .WithArbosVersion(ArbosVersion.Sixty) |
| 140 | + .WithReleaseSpec(); |
| 141 | + |
| 142 | + L2PricingState l2Pricing = context.ArbosState.L2PricingState; |
| 143 | + |
| 144 | + // Set up constraint that makes StorageGrowth expensive (weight=10) vs Computation (weight=1) |
| 145 | + // This mirrors Nitro's test setup where StorageGrowth is heavily constrained |
| 146 | + Dictionary<ResourceKind, ulong> weights = new() |
| 147 | + { |
| 148 | + { ResourceKind.Computation, 1 }, |
| 149 | + { ResourceKind.StorageGrowth, 10 }, |
| 150 | + }; |
| 151 | + |
| 152 | + // target=100000, window=10, backlog=200_000_000_000 (high backlog to elevate prices) |
| 153 | + l2Pricing.AddMultiGasConstraint(100000, 10, 200_000_000_000, weights); |
| 154 | + |
| 155 | + // Update pricing model to produce different per-resource fees |
| 156 | + l2Pricing.UpdatePricingModel(100); |
| 157 | + l2Pricing.CommitMultiGasFees(); |
| 158 | + |
| 159 | + UInt256 baseFee = l2Pricing.BaseFeeWeiStorage.Get(); |
| 160 | + baseFee.Should().BeGreaterThan(UInt256.Zero, "baseFee should be elevated due to high backlog"); |
| 161 | + |
| 162 | + // Simulate transaction that uses only Computation and StorageAccess (NOT StorageGrowth) |
| 163 | + // This means the expensive resource (StorageGrowth) is not used |
| 164 | + const ulong gasUsed = 1_000_000; |
| 165 | + MultiGas usedMultiGas = default; |
| 166 | + usedMultiGas.Increment(ResourceKind.Computation, gasUsed / 2); |
| 167 | + usedMultiGas.Increment(ResourceKind.StorageAccess, gasUsed / 2); |
| 168 | + |
| 169 | + // Calculate costs |
| 170 | + UInt256 singleGasCost = baseFee * gasUsed; |
| 171 | + UInt256 multiDimensionalCost = l2Pricing.MultiDimensionalPriceForRefund(usedMultiGas); |
| 172 | + |
| 173 | + // Since StorageGrowth (the expensive resource) wasn't used, multiDimensionalCost should be less |
| 174 | + multiDimensionalCost.Should().BeLessThan(singleGasCost, |
| 175 | + "multiDimensionalCost should be less than singleGasCost when expensive resources aren't used"); |
| 176 | + |
| 177 | + // Calculate refund |
| 178 | + UInt256 expectedRefund = singleGasCost - multiDimensionalCost; |
| 179 | + expectedRefund.Should().BeGreaterThan(UInt256.Zero, |
| 180 | + "refund should be positive when multiDimensionalCost < singleGasCost"); |
| 181 | + } |
| 182 | + |
| 183 | + /// <summary> |
| 184 | + /// Tests multi-gas refund calculation for retryable transactions. |
| 185 | + /// Mirrors Nitro's TestEndTxHookMultiGasRefundRetryableTx from tx_processor_multigas_test.go. |
| 186 | + /// |
| 187 | + /// For retryable transactions, the refund goes to RefundTo address (up to MaxRefund), |
| 188 | + /// with any excess going to the sender. |
| 189 | + /// </summary> |
| 190 | + [Test] |
| 191 | + public void MultiGasRefund_ForRetryableTx_RefundToAddressReceivesRefund() |
| 192 | + { |
| 193 | + IWorldState worldState = TestWorldStateFactory.CreateForTest(); |
| 194 | + using IDisposable disposer = worldState.BeginScope(IWorldState.PreGenesis); |
| 195 | + _ = ArbOSInitialization.Create(worldState); |
| 196 | + |
| 197 | + PrecompileTestContextBuilder context = new PrecompileTestContextBuilder(worldState, ulong.MaxValue) |
| 198 | + .WithArbosState() |
| 199 | + .WithArbosVersion(ArbosVersion.Sixty) |
| 200 | + .WithReleaseSpec(); |
| 201 | + |
| 202 | + L2PricingState l2Pricing = context.ArbosState.L2PricingState; |
| 203 | + |
| 204 | + // Set up constraint that makes StorageGrowth expensive |
| 205 | + Dictionary<ResourceKind, ulong> weights = new() |
| 206 | + { |
| 207 | + { ResourceKind.Computation, 1 }, |
| 208 | + { ResourceKind.StorageGrowth, 10 }, |
| 209 | + }; |
| 210 | + l2Pricing.AddMultiGasConstraint(100000, 10, 200_000_000_000, weights); |
| 211 | + |
| 212 | + // Update pricing model |
| 213 | + l2Pricing.UpdatePricingModel(100); |
| 214 | + l2Pricing.CommitMultiGasFees(); |
| 215 | + |
| 216 | + UInt256 baseFee = l2Pricing.BaseFeeWeiStorage.Get(); |
| 217 | + |
| 218 | + // Simulate retryable transaction gas usage (similar to normal tx) |
| 219 | + const ulong gasUsed = 1_000_000; |
| 220 | + MultiGas usedMultiGas = default; |
| 221 | + usedMultiGas.Increment(ResourceKind.Computation, gasUsed / 2); |
| 222 | + usedMultiGas.Increment(ResourceKind.StorageAccess, gasUsed / 2); |
| 223 | + |
| 224 | + // For retryables, gasFeeCap is used as the simple gas price |
| 225 | + UInt256 gasFeeCap = baseFee; |
| 226 | + UInt256 simpleGasCost = gasFeeCap * gasUsed; |
| 227 | + UInt256 multiDimensionalCost = l2Pricing.MultiDimensionalPriceForRefund(usedMultiGas); |
| 228 | + |
| 229 | + UInt256 expectedRefund = simpleGasCost - multiDimensionalCost; |
| 230 | + expectedRefund.Should().BeGreaterThan(UInt256.Zero, |
| 231 | + "retryable tx should have positive refund when expensive resources aren't used"); |
| 232 | + |
| 233 | + // Verify refund distribution logic: |
| 234 | + // If MaxRefund >= expectedRefund: entire refund goes to RefundTo |
| 235 | + // If MaxRefund < expectedRefund: MaxRefund to RefundTo, remainder to From |
| 236 | + UInt256 maxRefund = expectedRefund * 10; // Large MaxRefund |
| 237 | + UInt256 toRefundTo = UInt256.Min(expectedRefund, maxRefund); |
| 238 | + UInt256 toFrom = expectedRefund - toRefundTo; |
| 239 | + |
| 240 | + toRefundTo.Should().Be(expectedRefund, "with large MaxRefund, entire refund goes to RefundTo"); |
| 241 | + toFrom.Should().Be(UInt256.Zero, "with large MaxRefund, nothing goes to From"); |
| 242 | + |
| 243 | + // Test with small MaxRefund |
| 244 | + UInt256 smallMaxRefund = expectedRefund / 2; |
| 245 | + UInt256 toRefundToSmall = UInt256.Min(expectedRefund, smallMaxRefund); |
| 246 | + UInt256 toFromSmall = expectedRefund - toRefundToSmall; |
| 247 | + |
| 248 | + toRefundToSmall.Should().Be(smallMaxRefund, "with small MaxRefund, only MaxRefund goes to RefundTo"); |
| 249 | + toFromSmall.Should().Be(expectedRefund - smallMaxRefund, "remainder goes to From"); |
| 250 | + } |
| 251 | + |
| 252 | + /// <summary> |
| 253 | + /// Tests that when all gas goes to computation only, there's no refund |
| 254 | + /// (multiDimensionalCost equals singleGasCost). |
| 255 | + /// </summary> |
| 256 | + [Test] |
| 257 | + public void MultiGasRefund_WhenAllGasIsComputation_NoRefund() |
| 258 | + { |
| 259 | + IWorldState worldState = TestWorldStateFactory.CreateForTest(); |
| 260 | + using IDisposable disposer = worldState.BeginScope(IWorldState.PreGenesis); |
| 261 | + _ = ArbOSInitialization.Create(worldState); |
| 262 | + |
| 263 | + PrecompileTestContextBuilder context = new PrecompileTestContextBuilder(worldState, ulong.MaxValue) |
| 264 | + .WithArbosState() |
| 265 | + .WithArbosVersion(ArbosVersion.Sixty) |
| 266 | + .WithReleaseSpec(); |
| 267 | + |
| 268 | + L2PricingState l2Pricing = context.ArbosState.L2PricingState; |
| 269 | + |
| 270 | + // Set up constraint with only Computation (weight=1) |
| 271 | + Dictionary<ResourceKind, ulong> weights = new() |
| 272 | + { |
| 273 | + { ResourceKind.Computation, 1 }, |
| 274 | + }; |
| 275 | + l2Pricing.AddMultiGasConstraint(7_000_000, 60, 0, weights); |
| 276 | + |
| 277 | + // Use the initialized base fee from state (0.1 gwei = 100_000_000 wei) |
| 278 | + UInt256 baseFee = l2Pricing.BaseFeeWeiStorage.Get(); |
| 279 | + |
| 280 | + // Set multi-gas base fee for computation to match |
| 281 | + l2Pricing.SetNextBlockMultiGasBaseFee(ResourceKind.Computation, baseFee); |
| 282 | + l2Pricing.CommitMultiGasFees(); |
| 283 | + |
| 284 | + // All gas goes to Computation |
| 285 | + const ulong gasUsed = 100_000; |
| 286 | + MultiGas usedMultiGas = default; |
| 287 | + usedMultiGas.Increment(ResourceKind.Computation, gasUsed); |
| 288 | + |
| 289 | + UInt256 singleGasCost = baseFee * gasUsed; |
| 290 | + UInt256 multiDimensionalCost = l2Pricing.MultiDimensionalPriceForRefund(usedMultiGas); |
| 291 | + |
| 292 | + // When all gas is computation at same base fee, costs should be equal |
| 293 | + multiDimensionalCost.Should().Be(singleGasCost, |
| 294 | + "when all gas is computation at base fee, no refund should occur"); |
| 295 | + } |
118 | 296 | } |
0 commit comments