@@ -645,3 +645,292 @@ func TestMapOpcodeToCallType(t *testing.T) {
645645 })
646646 }
647647}
648+
649+ func TestFrameAggregator_PrecompileFrame (t * testing.T ) {
650+ // Test that precompile calls (CALL to 0x01-0x11, 0x100) emit synthetic frames
651+ // with the correct gas split: parent CALL retains overhead (100), precompile
652+ // frame gets the remaining execution gas.
653+ aggregator := NewFrameAggregator ()
654+
655+ precompileAddr := "0x0000000000000000000000000000000000000001" // ecrecover
656+
657+ // Root frame opcodes
658+ aggregator .ProcessStructlog (& execution.StructLog {
659+ Op : "PUSH1" ,
660+ Depth : 1 ,
661+ Gas : 10000 ,
662+ }, 0 , 0 , []uint32 {0 }, 3 , 3 , nil , nil )
663+
664+ // CALL to precompile: gasSelf=3100 (100 overhead + 3000 precompile execution).
665+ // With precompile gas extraction:
666+ // effectiveGasSelf = 100 (overhead only)
667+ // precompileGas = 3000 (execution gas)
668+ aggregator .ProcessStructlog (& execution.StructLog {
669+ Op : "CALL" ,
670+ Depth : 1 ,
671+ Gas : 9997 ,
672+ }, 1 , 0 , []uint32 {0 }, 3100 , 100 , & precompileAddr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
673+
674+ // Synthetic precompile frame (gas = precompileGas = 3000)
675+ aggregator .ProcessStructlog (& execution.StructLog {
676+ Op : "" ,
677+ Depth : 2 ,
678+ }, 1 , 1 , []uint32 {0 , 1 }, 3000 , 3000 , & precompileAddr , & execution.StructLog {Op : "CALL" , Depth : 1 })
679+
680+ // Back to root frame
681+ aggregator .ProcessStructlog (& execution.StructLog {
682+ Op : "STOP" ,
683+ Depth : 1 ,
684+ Gas : 6897 ,
685+ }, 2 , 0 , []uint32 {0 }, 0 , 0 , nil , & execution.StructLog {Op : "" , Depth : 2 })
686+
687+ trace := & execution.TraceTransaction {
688+ Gas : 10000 ,
689+ Failed : false ,
690+ }
691+
692+ frames := aggregator .Finalize (trace , 5000 )
693+
694+ // Should have 2 summary rows: root + precompile synthetic frame
695+ assert .Equal (t , 2 , countSummaryRows (frames ))
696+
697+ rootFrame := getSummaryRow (frames , 0 )
698+ precompileFrame := getSummaryRow (frames , 1 )
699+
700+ require .NotNil (t , rootFrame , "root frame should exist" )
701+ require .NotNil (t , precompileFrame , "precompile frame should exist" )
702+
703+ // Root frame: 3 real opcodes (PUSH1, CALL, STOP)
704+ assert .Equal (t , uint64 (3 ), rootFrame .OpcodeCount )
705+
706+ // Precompile frame: 0 opcodes (synthetic), gas > 0
707+ assert .Equal (t , uint64 (0 ), precompileFrame .OpcodeCount )
708+ assert .Equal (t , "CALL" , precompileFrame .CallType )
709+ require .NotNil (t , precompileFrame .TargetAddress )
710+ assert .Equal (t , precompileAddr , * precompileFrame .TargetAddress )
711+
712+ // Precompile frame gas_cumulative should reflect precompile execution gas
713+ assert .Equal (t , uint64 (3000 ), precompileFrame .GasCumulative )
714+
715+ // Verify parent CALL opcode row has only overhead gas (100)
716+ callRow := getOpcodeRow (frames , 0 , "CALL" )
717+ require .NotNil (t , callRow )
718+ assert .Equal (t , uint64 (100 ), callRow .Gas , "parent CALL gas should only include overhead" )
719+ }
720+
721+ func TestFrameAggregator_PrecompileGasSplitInvariant (t * testing.T ) {
722+ // Verify the gas split invariant:
723+ // SUM(parent CALL overhead) + SUM(precompile frame gas) == SUM(original CALL gasSelf)
724+ aggregator := NewFrameAggregator ()
725+
726+ precompileAddr := "0x0000000000000000000000000000000000000002" // sha256
727+
728+ originalGasSelf := uint64 (5100 ) // 100 overhead + 5000 precompile
729+ overhead := uint64 (100 )
730+ precompileGas := originalGasSelf - overhead
731+
732+ aggregator .ProcessStructlog (& execution.StructLog {
733+ Op : "PUSH1" ,
734+ Depth : 1 ,
735+ Gas : 20000 ,
736+ }, 0 , 0 , []uint32 {0 }, 3 , 3 , nil , nil )
737+
738+ // CALL with effectiveGasSelf = overhead
739+ aggregator .ProcessStructlog (& execution.StructLog {
740+ Op : "CALL" ,
741+ Depth : 1 ,
742+ Gas : 19997 ,
743+ }, 1 , 0 , []uint32 {0 }, originalGasSelf , overhead , & precompileAddr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
744+
745+ // Synthetic precompile frame
746+ aggregator .ProcessStructlog (& execution.StructLog {
747+ Op : "" ,
748+ Depth : 2 ,
749+ }, 1 , 1 , []uint32 {0 , 1 }, precompileGas , precompileGas , & precompileAddr , & execution.StructLog {Op : "CALL" , Depth : 1 })
750+
751+ aggregator .ProcessStructlog (& execution.StructLog {
752+ Op : "STOP" ,
753+ Depth : 1 ,
754+ Gas : 14897 ,
755+ }, 2 , 0 , []uint32 {0 }, 0 , 0 , nil , & execution.StructLog {Op : "" , Depth : 2 })
756+
757+ trace := & execution.TraceTransaction {Gas : 20000 , Failed : false }
758+ frames := aggregator .Finalize (trace , 10000 )
759+
760+ // Verify invariant: CALL opcode gas + precompile frame gas == original gasSelf
761+ callRow := getOpcodeRow (frames , 0 , "CALL" )
762+ precompileFrame := getSummaryRow (frames , 1 )
763+
764+ require .NotNil (t , callRow )
765+ require .NotNil (t , precompileFrame )
766+
767+ assert .Equal (t , originalGasSelf , callRow .Gas + precompileFrame .GasCumulative ,
768+ "gas split invariant: CALL overhead + precompile gas == original gasSelf" )
769+ }
770+
771+ func TestFrameAggregator_EOAFrameUnchanged (t * testing.T ) {
772+ // Verify that EOA calls still produce synthetic frames with gas=0
773+ // (unchanged behavior after precompile frame changes).
774+ aggregator := NewFrameAggregator ()
775+
776+ eoaAddr := "0x1234567890123456789012345678901234567890"
777+
778+ aggregator .ProcessStructlog (& execution.StructLog {
779+ Op : "PUSH1" ,
780+ Depth : 1 ,
781+ Gas : 10000 ,
782+ }, 0 , 0 , []uint32 {0 }, 3 , 3 , nil , nil )
783+
784+ // CALL to EOA: gasSelf=100, no precompile gas extraction
785+ aggregator .ProcessStructlog (& execution.StructLog {
786+ Op : "CALL" ,
787+ Depth : 1 ,
788+ Gas : 9997 ,
789+ }, 1 , 0 , []uint32 {0 }, 100 , 100 , & eoaAddr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
790+
791+ // Synthetic EOA frame (gas = 0)
792+ aggregator .ProcessStructlog (& execution.StructLog {
793+ Op : "" ,
794+ Depth : 2 ,
795+ }, 1 , 1 , []uint32 {0 , 1 }, 0 , 0 , & eoaAddr , & execution.StructLog {Op : "CALL" , Depth : 1 })
796+
797+ aggregator .ProcessStructlog (& execution.StructLog {
798+ Op : "STOP" ,
799+ Depth : 1 ,
800+ Gas : 9897 ,
801+ }, 2 , 0 , []uint32 {0 }, 0 , 0 , nil , & execution.StructLog {Op : "" , Depth : 2 })
802+
803+ trace := & execution.TraceTransaction {Gas : 10000 , Failed : false }
804+ frames := aggregator .Finalize (trace , 5000 )
805+
806+ assert .Equal (t , 2 , countSummaryRows (frames ))
807+
808+ eoaFrame := getSummaryRow (frames , 1 )
809+ require .NotNil (t , eoaFrame )
810+
811+ // EOA frame: gas = 0, gas_cumulative = 0
812+ assert .Equal (t , uint64 (0 ), eoaFrame .Gas )
813+ assert .Equal (t , uint64 (0 ), eoaFrame .GasCumulative )
814+ assert .Equal (t , uint64 (0 ), eoaFrame .OpcodeCount )
815+ require .NotNil (t , eoaFrame .TargetAddress )
816+ assert .Equal (t , eoaAddr , * eoaFrame .TargetAddress )
817+ }
818+
819+ func TestFrameAggregator_MultiplePrecompileCalls (t * testing.T ) {
820+ // Test transaction with multiple precompile calls producing correct
821+ // number of synthetic frames, each with correct gas.
822+ aggregator := NewFrameAggregator ()
823+
824+ ecrecoverAddr := "0x0000000000000000000000000000000000000001"
825+ sha256Addr := "0x0000000000000000000000000000000000000002"
826+
827+ aggregator .ProcessStructlog (& execution.StructLog {
828+ Op : "PUSH1" ,
829+ Depth : 1 ,
830+ Gas : 50000 ,
831+ }, 0 , 0 , []uint32 {0 }, 3 , 3 , nil , nil )
832+
833+ // First precompile call: ecrecover (gas = 3100 = 100 + 3000)
834+ aggregator .ProcessStructlog (& execution.StructLog {
835+ Op : "CALL" ,
836+ Depth : 1 ,
837+ Gas : 49997 ,
838+ }, 1 , 0 , []uint32 {0 }, 3100 , 100 , & ecrecoverAddr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
839+
840+ // Synthetic frame for ecrecover
841+ aggregator .ProcessStructlog (& execution.StructLog {
842+ Op : "" ,
843+ Depth : 2 ,
844+ }, 1 , 1 , []uint32 {0 , 1 }, 3000 , 3000 , & ecrecoverAddr , & execution.StructLog {Op : "CALL" , Depth : 1 })
845+
846+ // Some opcodes between the two precompile calls
847+ aggregator .ProcessStructlog (& execution.StructLog {
848+ Op : "PUSH1" ,
849+ Depth : 1 ,
850+ Gas : 46897 ,
851+ }, 2 , 0 , []uint32 {0 }, 3 , 3 , nil , & execution.StructLog {Op : "" , Depth : 2 })
852+
853+ // Second precompile call: sha256 (gas = 1100 = 100 + 1000)
854+ aggregator .ProcessStructlog (& execution.StructLog {
855+ Op : "STATICCALL" ,
856+ Depth : 1 ,
857+ Gas : 46894 ,
858+ }, 3 , 0 , []uint32 {0 }, 1100 , 100 , & sha256Addr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
859+
860+ // Synthetic frame for sha256
861+ aggregator .ProcessStructlog (& execution.StructLog {
862+ Op : "" ,
863+ Depth : 2 ,
864+ }, 3 , 2 , []uint32 {0 , 2 }, 1000 , 1000 , & sha256Addr , & execution.StructLog {Op : "STATICCALL" , Depth : 1 })
865+
866+ aggregator .ProcessStructlog (& execution.StructLog {
867+ Op : "STOP" ,
868+ Depth : 1 ,
869+ Gas : 45794 ,
870+ }, 4 , 0 , []uint32 {0 }, 0 , 0 , nil , & execution.StructLog {Op : "" , Depth : 2 })
871+
872+ trace := & execution.TraceTransaction {Gas : 50000 , Failed : false }
873+ frames := aggregator .Finalize (trace , 30000 )
874+
875+ // Should have 3 summary rows: root + ecrecover + sha256
876+ assert .Equal (t , 3 , countSummaryRows (frames ))
877+
878+ ecrecoverFrame := getSummaryRow (frames , 1 )
879+ sha256Frame := getSummaryRow (frames , 2 )
880+
881+ require .NotNil (t , ecrecoverFrame )
882+ require .NotNil (t , sha256Frame )
883+
884+ assert .Equal (t , uint64 (3000 ), ecrecoverFrame .GasCumulative )
885+ require .NotNil (t , ecrecoverFrame .TargetAddress )
886+ assert .Equal (t , ecrecoverAddr , * ecrecoverFrame .TargetAddress )
887+
888+ assert .Equal (t , uint64 (1000 ), sha256Frame .GasCumulative )
889+ require .NotNil (t , sha256Frame .TargetAddress )
890+ assert .Equal (t , sha256Addr , * sha256Frame .TargetAddress )
891+ }
892+
893+ func TestFrameAggregator_PrecompileGasSelfLessThanOverhead (t * testing.T ) {
894+ // Edge case: gasSelf <= overhead (100). No gas split occurs —
895+ // precompileGas stays 0, effectiveGasSelf stays at gasSelf.
896+ aggregator := NewFrameAggregator ()
897+
898+ precompileAddr := "0x0000000000000000000000000000000000000004" // identity
899+
900+ aggregator .ProcessStructlog (& execution.StructLog {
901+ Op : "PUSH1" ,
902+ Depth : 1 ,
903+ Gas : 10000 ,
904+ }, 0 , 0 , []uint32 {0 }, 3 , 3 , nil , nil )
905+
906+ // CALL to precompile with gasSelf=50 (less than overhead=100)
907+ // This shouldn't split — effectiveGasSelf stays 50
908+ aggregator .ProcessStructlog (& execution.StructLog {
909+ Op : "CALL" ,
910+ Depth : 1 ,
911+ Gas : 9997 ,
912+ }, 1 , 0 , []uint32 {0 }, 50 , 50 , & precompileAddr , & execution.StructLog {Op : "PUSH1" , Depth : 1 })
913+
914+ // Synthetic frame with gas=0 (no precompile gas extracted)
915+ aggregator .ProcessStructlog (& execution.StructLog {
916+ Op : "" ,
917+ Depth : 2 ,
918+ }, 1 , 1 , []uint32 {0 , 1 }, 0 , 0 , & precompileAddr , & execution.StructLog {Op : "CALL" , Depth : 1 })
919+
920+ aggregator .ProcessStructlog (& execution.StructLog {
921+ Op : "STOP" ,
922+ Depth : 1 ,
923+ Gas : 9947 ,
924+ }, 2 , 0 , []uint32 {0 }, 0 , 0 , nil , & execution.StructLog {Op : "" , Depth : 2 })
925+
926+ trace := & execution.TraceTransaction {Gas : 10000 , Failed : false }
927+ frames := aggregator .Finalize (trace , 5000 )
928+
929+ callRow := getOpcodeRow (frames , 0 , "CALL" )
930+ require .NotNil (t , callRow )
931+ assert .Equal (t , uint64 (50 ), callRow .Gas , "CALL gas should remain 50 when gasSelf <= overhead" )
932+
933+ precompileFrame := getSummaryRow (frames , 1 )
934+ require .NotNil (t , precompileFrame )
935+ assert .Equal (t , uint64 (0 ), precompileFrame .GasCumulative , "precompile frame gas should be 0" )
936+ }
0 commit comments