44using System ;
55using System . Collections . Generic ;
66using System . Text ;
7+ using System . Threading ;
78using Nethermind . Core ;
89using Nethermind . Core . Crypto ;
910using Nethermind . Core . Specs ;
1617namespace Nethermind . Evm . Test . CodeAnalysis ;
1718
1819/// <summary>
19- /// Randomized differential over the opcode alphabet the block analyzer treats specially: jump
20- /// targets, static and dynamic jumps, the fused glue pairs, constant folds, the peephole-consumed
21- /// shapes and the boundary ops that keep a block open. The hand-written differential cases cover
22- /// the shapes we thought of; a cross-client gas mismatch showed that the shapes we did not think
23- /// of are the ones that break, so this walks the space instead. Every generated program runs on
24- /// both interpreters twice - once with ample gas and once with a seed-derived budget that starves
25- /// it mid-run, which is what forces the metered fallback and the out-of-gas edges - and gas,
26- /// status and output must all match exactly. Gas alone cannot see a wrong value that does not
27- /// flip a branch, so the top of the stack is returned as output. A seed that fails prints its
28- /// bytecode, which is the reproduction.
20+ /// Randomized differential between the stream interpreter and the bytecode loop. Each program runs
21+ /// on both twice - ample gas, then a budget that starves it mid-run - and gas, status and output
22+ /// must match exactly. A failing seed prints its bytecode as the reproduction.
2923/// </summary>
30- [ TestFixture ]
24+ // Mutates process-wide StreamInterpreter statics, so it must not run alongside other EVM tests.
25+ [ TestFixture , NonParallelizable ]
3126public class StreamGasFuzzTests : VirtualMachineTestsBase
3227{
3328 protected override ulong BlockNumber => MainnetSpecProvider . ParisBlockNumber + 4 ;
@@ -41,29 +36,39 @@ public class StreamGasFuzzTests : VirtualMachineTestsBase
4136 public void StreamExecution_MatchesByteCodeLoop_OverRandomJumpHeavyPrograms ( )
4237 {
4338 List < string > failures = [ ] ;
39+ int completed = 0 ;
40+ int starved = 0 ;
4441 for ( int seed = 0 ; seed < Programs ; seed ++ )
4542 {
4643 byte [ ] code = Generate ( seed ) ;
4744
4845 ExecutionCapture baseline = RunFor ( code , useStream : false , AmpleGas ) ;
4946 ExecutionCapture streamed = RunFor ( code , useStream : true , AmpleGas ) ;
5047 Compare ( failures , seed , "ample" , code , baseline , streamed ) ;
48+ if ( baseline . StatusCode == Evm . StatusCode . Success ) completed ++ ;
5149
52- // A budget cut somewhere inside the run starves a block precharge, so the tail executes
53- // metered from raw code - the paths a run that always completes never crosses.
5450 if ( baseline . GasSpent > GasCostOf . Transaction )
5551 {
5652 ulong execGas = baseline . GasSpent - GasCostOf . Transaction ;
57- ulong budget = GasCostOf . Transaction + execGas * ( ulong ) ( seed % 97 ) / 97 ;
53+ ulong budget = GasCostOf . Transaction + execGas * ( ulong ) ( 1 + seed % 96 ) / 97 ;
5854 ExecutionCapture tightBaseline = RunFor ( code , useStream : false , budget ) ;
5955 ExecutionCapture tightStreamed = RunFor ( code , useStream : true , budget ) ;
6056 Compare ( failures , seed , $ "tight budget { budget } ", code , tightBaseline , tightStreamed ) ;
57+ if ( tightBaseline . StatusCode == Evm . StatusCode . Failure ) starved ++ ;
6158 }
6259
6360 if ( failures . Count >= 5 )
6461 break ;
6562 }
6663
64+ using ( Assert . EnterMultipleScope ( ) )
65+ {
66+ Assert . That ( completed , Is . GreaterThan ( Programs / 2 ) ,
67+ "most generated programs must run to completion, or the comparison is between two immediate halts" ) ;
68+ Assert . That ( starved , Is . GreaterThan ( Programs / 4 ) ,
69+ "the tight-budget pass must actually run out of gas, or the metered fallback is never crossed" ) ;
70+ }
71+
6772 if ( failures . Count > 0 )
6873 {
6974 StringBuilder message = new ( $ "{ failures . Count } run(s) diverged between the two interpreters:") ;
@@ -87,22 +92,18 @@ private static void Compare(List<string> failures, int seed, string label, byte[
8792 }
8893
8994 /// <summary>
90- /// Builds a program whose jump targets are always real JUMPDESTs, so the run exercises jump
91- /// accounting rather than invalid-destination failures, and whose backward jumps are all
92- /// conditional on a decrementing counter so it terminates. Weighted toward the constructs the
93- /// analyzer rewrites. Ends by returning the top of the stack, so a wrong value diverges even
94- /// when it never flips a branch or moves gas.
95+ /// Builds a program weighted toward the constructs the analyzer rewrites. Jump targets are
96+ /// always real JUMPDESTs and conditional branches decrement the word they test, so a backward
97+ /// jump makes progress towards leaving the loop instead of spinning until the gas runs out.
9598 /// </summary>
9699 private static byte [ ] Generate ( int seed )
97100 {
98101 Random random = new ( seed ) ;
99102 List < byte > code = [ ] ;
100103 List < int > jumpDests = [ ] ;
101104
102- // A leading counter lets generated loops decrement toward zero instead of spinning, and a
103- // dozen words under it give the deep DUP and SWAP forms something to reach - the first
104- // version of this generator only ever produced depth one and two, which is why it passed
105- // while a permutation-coalescing bug that only shows past that depth broke every real call.
105+ // A leading counter for loops to decrement, and a dozen words under it so the deep DUP and
106+ // SWAP forms have something to reach.
106107 code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) ( 1 + random . Next ( 3 ) ) ] ) ;
107108 for ( int i = 0 ; i < 12 ; i ++ )
108109 {
@@ -113,7 +114,7 @@ private static byte[] Generate(int seed)
113114 int slots = 6 + random . Next ( 18 ) ;
114115 for ( int i = 0 ; i < slots ; i ++ )
115116 {
116- switch ( random . Next ( 20 ) )
117+ switch ( random . Next ( 24 ) )
117118 {
118119 case 0 :
119120 jumpDests . Add ( code . Count ) ;
@@ -138,8 +139,7 @@ private static byte[] Generate(int seed)
138139 code . AddRange ( [ ( byte ) Instruction . DUP1 , ( byte ) Instruction . ISZERO ] ) ;
139140 break ;
140141 case 7 :
141- // A run of several permutation ops at mixed depths, the shape the coalescing pass
142- // rewrites and the shape the first generator never produced.
142+ // Permutation ops at mixed depths, the shape the coalescing pass rewrites.
143143 for ( int k = random . Next ( 2 , 6 ) ; k > 0 ; k -- )
144144 {
145145 code . Add ( random . Next ( 3 ) switch
@@ -155,37 +155,43 @@ private static byte[] Generate(int seed)
155155 code . AddRange ( [ ( byte ) Instruction . PUSH1 , 0x20 , ( byte ) Instruction . PUSH1 , 0x00 , ( byte ) Instruction . MSTORE ] ) ;
156156 break ;
157157 case 9 when jumpDests . Count > 0 :
158- // Static conditional jump: PUSH2 target + JUMPI , the fused shape.
158+ // Static conditional jump, the fused shape.
159159 int condDest = jumpDests [ random . Next ( jumpDests . Count ) ] ;
160- code . AddRange ( [ ( byte ) Instruction . DUP1 , ( byte ) Instruction . ISZERO , ( byte ) Instruction . PUSH2 , ( byte ) ( condDest >> 8 ) , ( byte ) condDest , ( byte ) Instruction . JUMPI ] ) ;
160+ code . AddRange ( [
161+ ( byte ) Instruction . PUSH1 , 0x01 , ( byte ) Instruction . SWAP1 , ( byte ) Instruction . SUB ,
162+ ( byte ) Instruction . DUP1 ,
163+ ( byte ) Instruction . PUSH2 , ( byte ) ( condDest >> 8 ) , ( byte ) condDest , ( byte ) Instruction . JUMPI ] ) ;
161164 break ;
162165 case 10 when jumpDests . Count > 0 :
163- // Dynamic conditional jump: the target arrives through the stack.
166+ // Dynamic conditional jump: the SWAP1 stops the static-jump fusion from
167+ // claiming the pair, which is the point of this arm.
164168 int dynDest = jumpDests [ random . Next ( jumpDests . Count ) ] ;
165- code . AddRange ( [ ( byte ) Instruction . PUSH1 , 0x00 , ( byte ) Instruction . PUSH2 , ( byte ) ( dynDest >> 8 ) , ( byte ) dynDest , ( byte ) Instruction . SWAP1 , ( byte ) Instruction . JUMPI ] ) ;
169+ code . AddRange ( [
170+ ( byte ) Instruction . PUSH2 , ( byte ) ( dynDest >> 8 ) , ( byte ) dynDest ,
171+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 2 ) ,
172+ ( byte ) Instruction . SWAP1 , ( byte ) Instruction . JUMPI ] ) ;
166173 break ;
167174 case 11 :
168- // AND alone and AND feeding ISZERO, the fused compare-to-zero pair.
169175 code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) Instruction . AND ] ) ;
170176 if ( random . Next ( 2 ) == 0 ) code . Add ( ( byte ) Instruction . ISZERO ) ;
171177 break ;
172178 case 12 :
173- // Division and modulo, with a zero divisor often enough to hit that fold.
179+ // Zero divisor often enough to hit that fold.
174180 code . AddRange ( [
175181 ( byte ) Instruction . PUSH1 , ( byte ) ( random . Next ( 4 ) == 0 ? 0 : random . Next ( 256 ) ) ,
176182 ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ,
177183 ( byte ) ( random . Next ( 2 ) == 0 ? Instruction . DIV : Instruction . MOD ) ] ) ;
178184 break ;
179185 case 13 :
180- // Shifts, sometimes past 255 so saturation folds and cores agree.
186+ // Sometimes past 255, so saturation folds and cores must agree.
181187 if ( random . Next ( 3 ) == 0 )
182188 code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) Instruction . PUSH2 , 0x01 , 0x00 ] ) ;
183189 else
184190 code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ] ) ;
185191 code . Add ( ( byte ) ( random . Next ( 2 ) == 0 ? Instruction . SHL : Instruction . SHR ) ) ;
186192 break ;
187193 case 14 :
188- // Wide constant pair feeding an operator: the fold path through the constant pool.
194+ // The fold path through the constant pool.
189195 random . NextBytes ( wide ) ;
190196 code . Add ( ( byte ) Instruction . PUSH32 ) ;
191197 code . AddRange ( wide ) ;
@@ -195,14 +201,13 @@ private static byte[] Generate(int seed)
195201 code . Add ( ( byte ) ( random . Next ( 3 ) switch { 0 => Instruction . ADD , 1 => Instruction . MUL , _ => Instruction . AND } ) ) ;
196202 break ;
197203 case 15 :
198- // Unconditional static jump to the very next instruction: the fused StaticJump
199- // shape, and a jump arrival on a marker whose gas the analyzer may have elided.
204+ // Fused StaticJump, arriving on a marker whose gas the analyzer may have elided.
200205 int target = code . Count + 4 ;
201206 code . AddRange ( [ ( byte ) Instruction . PUSH2 , ( byte ) ( target >> 8 ) , ( byte ) target , ( byte ) Instruction . JUMP , ( byte ) Instruction . JUMPDEST ] ) ;
202207 jumpDests . Add ( target ) ;
203208 break ;
204209 case 16 :
205- // Boundary ops that keep a block open - the widest behavioural change .
210+ // Boundary ops that keep a block open.
206211 switch ( random . Next ( 5 ) )
207212 {
208213 case 0 : code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 64 ) , ( byte ) Instruction . MLOAD ] ) ; break ;
@@ -214,12 +219,34 @@ private static byte[] Generate(int seed)
214219
215220 break ;
216221 case 17 :
217- // A table handler that consumes its successor and lands past it - adjacent to a
218- // random JUMPDEST from case 0 this is the elided-marker landing.
222+ // A handler that consumes its successor and lands past it; next to a JUMPDEST
223+ // from case 0 this is the elided-marker landing.
219224 code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) Instruction . EXTCODESIZE , ( byte ) Instruction . ISZERO ] ) ;
220225 break ;
226+ case 22 :
227+ // Constant shift feeding a subtraction, at saturating amounts too.
228+ code . AddRange ( [
229+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ,
230+ ( byte ) Instruction . PUSH1 , ( byte ) ( random . Next ( 4 ) == 0 ? 0xFF : random . Next ( 40 ) ) ,
231+ ( byte ) Instruction . SHL , ( byte ) Instruction . SUB ] ) ;
232+ break ;
233+ case 21 :
234+ code . AddRange ( [
235+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ,
236+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ,
237+ ( byte ) Instruction . SUB , ( byte ) Instruction . AND ] ) ;
238+ break ;
239+ case 19 :
240+ code . AddRange ( [ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) , ( byte ) ( ( byte ) Instruction . DUP1 + random . Next ( 8 ) ) ] ) ;
241+ break ;
242+ case 20 :
243+ code . AddRange ( [
244+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 256 ) ,
245+ ( byte ) Instruction . PUSH1 , ( byte ) random . Next ( 9 ) ,
246+ ( byte ) ( random . Next ( 4 ) switch { 0 => Instruction . SHL , 1 => Instruction . SHR , 2 => Instruction . ADD , _ => Instruction . DIV } ) ] ) ;
247+ break ;
221248 case 18 :
222- // Pushes of every width, so folds and pool references cross the PUSH8/PUSH9 seam.
249+ // Every push width, so folds and pool references cross the PUSH8/PUSH9 seam.
223250 int width = 3 + random . Next ( 30 ) ;
224251 code . Add ( ( byte ) ( ( byte ) Instruction . PUSH1 + width - 1 ) ) ;
225252 for ( int k = 0 ; k < width ; k ++ ) code . Add ( ( byte ) random . Next ( 256 ) ) ;
@@ -230,8 +257,8 @@ private static byte[] Generate(int seed)
230257 }
231258 }
232259
233- // Return the top of the stack so a wrong value is observable; an empty stack underflows
234- // identically on both interpreters, which is a comparison too .
260+ // Return the top of the stack, so a wrong value is observable even when it never flips a
261+ // branch or moves gas .
235262 code . AddRange ( [
236263 ( byte ) Instruction . PUSH1 , 0x00 , ( byte ) Instruction . MSTORE ,
237264 ( byte ) Instruction . PUSH1 , 0x20 , ( byte ) Instruction . PUSH1 , 0x00 , ( byte ) Instruction . RETURN ] ) ;
@@ -254,12 +281,15 @@ private ExecutionCapture RunFor(byte[] code, bool useStream, ulong gasLimit)
254281 {
255282 StreamInterpreter . BuildThreshold = 1 ;
256283 CodeInfo codeInfo = CodeInfoRepository . GetCachedCodeInfo ( Recipient , Spec ) ;
257- if ( ! System . Threading . SpinWait . SpinUntil ( ( ) => codeInfo . GetOrBuildStream ( ) is not null , TimeSpan . FromSeconds ( 5 ) ) )
258- Assert . Fail ( "the stream did not build within the timeout" ) ;
284+ if ( ! SpinWait . SpinUntil ( ( ) => codeInfo . GetOrBuildStream ( ) is not null , TimeSpan . FromSeconds ( 5 ) ) )
285+ Assert . Fail ( $ "the stream did not build within the timeout for code 0x { Convert . ToHexString ( code ) } ") ;
259286 }
260287
288+ long framesBefore = StreamInterpreter . FramesExecuted ;
261289 ExecutionCapture tracer = new ( ) ;
262290 _processor . Execute ( transaction , new BlockExecutionContext ( block . Header , SpecProvider . GetSpec ( block . Header ) ) , tracer ) ;
291+ if ( useStream && StreamInterpreter . FramesExecuted == framesBefore )
292+ Assert . Fail ( $ "the stream did not engage, so this comparison proved nothing, for code 0x{ Convert . ToHexString ( code ) } ") ;
263293 return tracer ;
264294 }
265295 finally
0 commit comments