From 085d1f93a2eee179abd3b792d7878f1729c9723b Mon Sep 17 00:00:00 2001 From: Goran Rojovic Date: Tue, 9 Jan 2024 10:15:18 +0100 Subject: [PATCH] js tracer --- go.mod | 3 + go.sum | 21 + helper/common/common.go | 11 + jsonrpc/debug_endpoint.go | 130 +- jsonrpc/debug_endpoint_test.go | 168 +-- jsonrpc/eth_endpoint.go | 27 +- jsonrpc/eth_endpoint_test.go | 8 +- jsonrpc/helper.go | 12 +- jsonrpc/helper_test.go | 6 +- jsonrpc/types.go | 5 + server/server.go | 8 +- state/executor.go | 52 +- state/runtime/evm/evm_fuzz_test.go | 4 + state/runtime/evm/evm_test.go | 19 +- state/runtime/evm/opcodes.go | 6 +- state/runtime/evm/state.go | 70 +- .../precompiled/native_transfer_test.go | 4 + state/runtime/precompiled/precompiled.go | 18 +- state/runtime/runtime.go | 21 +- .../runtime/tracer/calltracer/call_tracer.go | 55 +- .../tracer/calltracer/call_tracer_test.go | 18 +- state/runtime/tracer/jstracer/bigint.go | 4 + state/runtime/tracer/jstracer/goja.go | 1303 +++++++++++++++++ .../{tracer.go => struct_tracer.go} | 41 +- .../{tracer_test.go => struct_tracer_test.go} | 123 +- state/runtime/tracer/types.go | 70 - state/runtime/types.go | 114 ++ 27 files changed, 2033 insertions(+), 288 deletions(-) create mode 100644 state/runtime/tracer/jstracer/bigint.go create mode 100644 state/runtime/tracer/jstracer/goja.go rename state/runtime/tracer/structtracer/{tracer.go => struct_tracer.go} (90%) rename state/runtime/tracer/structtracer/{tracer_test.go => struct_tracer_test.go} (87%) delete mode 100644 state/runtime/tracer/types.go create mode 100644 state/runtime/types.go diff --git a/go.mod b/go.mod index 11ac223a2b..be6e9e8830 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/btcsuite/btcd v0.22.1 github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 + github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d github.com/envoyproxy/protoc-gen-validate v1.0.2 github.com/golang/protobuf v1.5.3 github.com/google/uuid v1.4.0 @@ -217,4 +218,6 @@ require ( inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect lukechampine.com/blake3 v1.2.1 // indirect nhooyr.io/websocket v1.8.7 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect ) diff --git a/go.sum b/go.sum index b86fa08bd9..97c5af76f4 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,11 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= @@ -118,6 +121,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -132,6 +136,9 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etly github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= @@ -141,6 +148,11 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -192,6 +204,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -272,6 +286,7 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -337,6 +352,7 @@ github.com/huin/goupnp v1.1.0 h1:gEe0Dp/lZmPZiDFzJJaOfUpOvv2MKUkoBX8lDrn9vKU= github.com/huin/goupnp v1.1.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/boxo v0.8.1 h1:3DkKBCK+3rdEB5t77WDShUXXhktYwH99mkAsgajsKrU= @@ -383,11 +399,13 @@ github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoK github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= @@ -596,6 +614,7 @@ github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtD github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA= github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -869,6 +888,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -890,6 +910,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/helper/common/common.go b/helper/common/common.go index 6caf6b695e..42038df65d 100644 --- a/helper/common/common.go +++ b/helper/common/common.go @@ -384,3 +384,14 @@ func EncodeUint64ToBytes(value uint64) []byte { func EncodeBytesToUint64(b []byte) uint64 { return binary.BigEndian.Uint64(b) } + +// CopyBytes returns an exact copy of the provided bytes. +func CopyBytes(b []byte) (copiedBytes []byte) { + if b == nil { + return nil + } + copiedBytes = make([]byte, len(b)) + copy(copiedBytes, b) + + return +} diff --git a/jsonrpc/debug_endpoint.go b/jsonrpc/debug_endpoint.go index bc6694d989..54d3f811c5 100644 --- a/jsonrpc/debug_endpoint.go +++ b/jsonrpc/debug_endpoint.go @@ -2,13 +2,17 @@ package jsonrpc import ( "context" + "encoding/json" "errors" "fmt" + "math/big" + "strings" "time" "github.com/0xPolygon/polygon-edge/helper/hex" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/tracer/calltracer" + js "github.com/0xPolygon/polygon-edge/state/runtime/tracer/jstracer" "github.com/0xPolygon/polygon-edge/state/runtime/tracer/structtracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -16,7 +20,7 @@ import ( const callTracerName = "callTracer" var ( - defaultTraceTimeout = 5 * time.Second + defaultTraceTimeout = 5 * time.Minute // ErrExecutionTimeout indicates the execution was terminated due to timeout ErrExecutionTimeout = errors.New("execution timeout") @@ -24,6 +28,8 @@ var ( ErrTraceGenesisBlock = errors.New("genesis is not traceable") // ErrNoConfig is an error returns when config is empty ErrNoConfig = errors.New("missing config object") + + jsKeywords = []string{"function", "var", "let", "const", "if", "else", "for", "while", "switch", "return"} ) type debugBlockchainStore interface { @@ -43,13 +49,13 @@ type debugBlockchainStore interface { GetBlockByNumber(num uint64, full bool) (*types.Block, bool) // TraceBlock traces all transactions in the given block - TraceBlock(*types.Block, tracer.Tracer) ([]interface{}, error) + TraceBlock(*types.Block, runtime.Tracer) ([]interface{}, error) // TraceTxn traces a transaction in the block, associated with the given hash - TraceTxn(*types.Block, types.Hash, tracer.Tracer) (interface{}, error) + TraceTxn(*types.Block, types.Hash, runtime.Tracer) (interface{}, error) // TraceCall traces a single call at the point when the given header is mined - TraceCall(*types.Transaction, *types.Header, tracer.Tracer) (interface{}, error) + TraceCall(*types.Transaction, *types.Header, types.StateOverride, runtime.Tracer) (interface{}, error) } type debugTxPoolStore interface { @@ -79,14 +85,64 @@ func NewDebug(store debugStore, requestsPerSecond uint64) *Debug { } } +// BlockOverrides is a set of header fields to override. +type BlockOverrides struct { + Number *argBig + Difficulty *argBig + Time *argUint64 + GasLimit *argUint64 + Coinbase *types.Address + BaseFee *argBig +} + +// Apply overrides the given header fields into the given block context. +func (bo *BlockOverrides) Apply(header *types.Header) { + if bo == nil { + return + } + + if bo.Number != nil { + header.Number = bo.Number.ToInt().Uint64() + } + + if bo.Difficulty != nil { + header.Difficulty = bo.Difficulty.ToInt().Uint64() + } + + if bo.Time != nil { + header.Timestamp = uint64(*bo.Time) + } + + if bo.GasLimit != nil { + header.GasLimit = uint64(*bo.GasLimit) + } + + if bo.Coinbase != nil { + header.Miner = bo.Coinbase.Bytes() + } + + if bo.BaseFee != nil { + header.BaseFee = bo.BaseFee.ToInt().Uint64() + } +} + type TraceConfig struct { - EnableMemory bool `json:"enableMemory"` - DisableStack bool `json:"disableStack"` - DisableStorage bool `json:"disableStorage"` - EnableReturnData bool `json:"enableReturnData"` - DisableStructLogs bool `json:"disableStructLogs"` - Timeout *string `json:"timeout"` - Tracer string `json:"tracer"` + EnableMemory bool `json:"enableMemory"` + DisableStack bool `json:"disableStack"` + DisableStorage bool `json:"disableStorage"` + EnableReturnData bool `json:"enableReturnData"` + DisableStructLogs bool `json:"disableStructLogs"` + Timeout *string `json:"timeout"` + Tracer string `json:"tracer"` + TracerConfig json.RawMessage `json:"tracerConfig"` // Config specific to given tracer +} + +// TraceCallConfig is the config for traceCall API. It holds one more +// field to override the state for tracing. +type TraceCallConfig struct { + TraceConfig + StateOverrides *StateOverride `json:"stateOverrides"` + BlockOverrides *BlockOverrides `json:"blockOverrides"` } func (d *Debug) TraceBlockByNumber( @@ -157,7 +213,7 @@ func (d *Debug) TraceTransaction( return d.throttling.AttemptRequest( context.Background(), func() (interface{}, error) { - tx, block := GetTxAndBlockByTxHash(txHash, d.store) + tx, block, txIndex := GetTxAndBlockByTxHash(txHash, d.store) if tx == nil { return nil, fmt.Errorf("tx %s not found", txHash.String()) } @@ -166,7 +222,14 @@ func (d *Debug) TraceTransaction( return nil, ErrTraceGenesisBlock } - tracer, cancel, err := newTracer(config) + txCtx := &runtime.TracerContext{ + BlockHash: block.Hash(), + BlockNumber: new(big.Int).SetUint64(block.Number()), + TxIndex: txIndex, + TxHash: tx.Hash, + } + + tracer, cancel, err := newTracer(config, txCtx) if err != nil { return nil, err } @@ -181,7 +244,7 @@ func (d *Debug) TraceTransaction( func (d *Debug) TraceCall( arg *txnArgs, filter BlockNumberOrHash, - config *TraceConfig, + config *TraceCallConfig, ) (interface{}, error) { return d.throttling.AttemptRequest( context.Background(), @@ -191,6 +254,10 @@ func (d *Debug) TraceCall( return nil, ErrHeaderNotFound } + header = header.Copy() + header.BaseFee = 0 + config.BlockOverrides.Apply(header) + tx, err := DecodeTxn(arg, d.store, true) if err != nil { return nil, err @@ -201,14 +268,19 @@ func (d *Debug) TraceCall( tx.Gas = header.GasLimit } - tracer, cancel, err := newTracer(config) + tracer, cancel, err := newTracer(&config.TraceConfig, new(runtime.TracerContext)) if err != nil { return nil, err } defer cancel() - return d.store.TraceCall(tx, header, tracer) + var override types.StateOverride + if config.StateOverrides != nil { + override = config.StateOverrides.ToType() + } + + return d.store.TraceCall(tx, header, override, tracer) }, ) } @@ -221,7 +293,7 @@ func (d *Debug) traceBlock( return nil, ErrTraceGenesisBlock } - tracer, cancel, err := newTracer(config) + tracer, cancel, err := newTracer(config, nil) if err != nil { return nil, err } @@ -232,13 +304,14 @@ func (d *Debug) traceBlock( } // newTracer creates new tracer by config -func newTracer(config *TraceConfig) ( - tracer.Tracer, +func newTracer(config *TraceConfig, tracerCtx *runtime.TracerContext) ( + runtime.Tracer, context.CancelFunc, error, ) { var ( timeout = defaultTraceTimeout + tracer runtime.Tracer err error ) @@ -252,10 +325,10 @@ func newTracer(config *TraceConfig) ( } } - var tracer tracer.Tracer - if config.Tracer == callTracerName { tracer = &calltracer.CallTracer{} + } else if isJavaScriptCode(config.Tracer) { + tracer, err = js.NewJsTracer(config.Tracer, tracerCtx, config.TracerConfig) } else { tracer = structtracer.NewStructTracer(structtracer.Config{ EnableMemory: config.EnableMemory && !config.DisableStructLogs, @@ -279,3 +352,16 @@ func newTracer(config *TraceConfig) ( // cancellation of context is done by caller return tracer, cancel, nil } + +// isJavaScriptCode checks if the given string contains any JavaScript keywords. +// It iterates over a list of JavaScript keywords and checks if any of them are present in the string. +// If a keyword is found, it returns true; otherwise, it returns false. +func isJavaScriptCode(s string) bool { + for _, keyword := range jsKeywords { + if strings.Contains(s, keyword) { + return true + } + } + + return false +} diff --git a/jsonrpc/debug_endpoint_test.go b/jsonrpc/debug_endpoint_test.go index 1644d86ec5..0efbe3698b 100644 --- a/jsonrpc/debug_endpoint_test.go +++ b/jsonrpc/debug_endpoint_test.go @@ -6,11 +6,10 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/0xPolygon/polygon-edge/helper/hex" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/tracer/structtracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -21,9 +20,9 @@ type debugEndpointMockStore struct { readTxLookupFn func(types.Hash) (types.Hash, bool) getBlockByHashFn func(types.Hash, bool) (*types.Block, bool) getBlockByNumberFn func(uint64, bool) (*types.Block, bool) - traceBlockFn func(*types.Block, tracer.Tracer) ([]interface{}, error) - traceTxnFn func(*types.Block, types.Hash, tracer.Tracer) (interface{}, error) - traceCallFn func(*types.Transaction, *types.Header, tracer.Tracer) (interface{}, error) + traceBlockFn func(*types.Block, runtime.Tracer) ([]interface{}, error) + traceTxnFn func(*types.Block, types.Hash, runtime.Tracer) (interface{}, error) + traceCallFn func(*types.Transaction, *types.Header, runtime.Tracer) (interface{}, error) getNonceFn func(types.Address) uint64 getAccountFn func(types.Hash, types.Address) (*Account, error) } @@ -48,15 +47,16 @@ func (s *debugEndpointMockStore) GetBlockByNumber(num uint64, full bool) (*types return s.getBlockByNumberFn(num, full) } -func (s *debugEndpointMockStore) TraceBlock(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { +func (s *debugEndpointMockStore) TraceBlock(block *types.Block, tracer runtime.Tracer) ([]interface{}, error) { return s.traceBlockFn(block, tracer) } -func (s *debugEndpointMockStore) TraceTxn(block *types.Block, targetTx types.Hash, tracer tracer.Tracer) (interface{}, error) { +func (s *debugEndpointMockStore) TraceTxn(block *types.Block, targetTx types.Hash, tracer runtime.Tracer) (interface{}, error) { return s.traceTxnFn(block, targetTx, tracer) } -func (s *debugEndpointMockStore) TraceCall(tx *types.Transaction, parent *types.Header, tracer tracer.Tracer) (interface{}, error) { +func (s *debugEndpointMockStore) TraceCall(tx *types.Transaction, parent *types.Header, + override types.StateOverride, tracer runtime.Tracer) (interface{}, error) { return s.traceCallFn(tx, parent, tracer) } @@ -177,7 +177,7 @@ func TestDebugTraceConfigDecode(t *testing.T) { t.Run(test.input, func(t *testing.T) { result := TraceConfig{} - assert.NoError( + require.NoError( t, json.Unmarshal( []byte(test.input), @@ -185,7 +185,7 @@ func TestDebugTraceConfigDecode(t *testing.T) { ), ) - assert.Equal( + require.Equal( t, test.expected, result, @@ -214,13 +214,13 @@ func TestTraceBlockByNumber(t *testing.T) { return testLatestHeader }, getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { - assert.Equal(t, testLatestHeader.Number, num) - assert.True(t, full) + require.Equal(t, testLatestHeader.Number, num) + require.True(t, full) return testLatestBlock, true }, - traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { - assert.Equal(t, testLatestBlock, block) + traceBlockFn: func(block *types.Block, tracer runtime.Tracer) ([]interface{}, error) { + require.Equal(t, testLatestBlock, block) return testTraceResults, nil }, @@ -234,13 +234,13 @@ func TestTraceBlockByNumber(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { - assert.Equal(t, testHeader10.Number, num) - assert.True(t, full) + require.Equal(t, testHeader10.Number, num) + require.True(t, full) return testBlock10, true }, - traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { - assert.Equal(t, testBlock10, block) + traceBlockFn: func(block *types.Block, tracer runtime.Tracer) ([]interface{}, error) { + require.Equal(t, testBlock10, block) return testTraceResults, nil }, @@ -254,8 +254,8 @@ func TestTraceBlockByNumber(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { - assert.Equal(t, testGenesisHeader.Number, num) - assert.True(t, full) + require.Equal(t, testGenesisHeader.Number, num) + require.True(t, full) return testGenesisBlock, true }, @@ -269,8 +269,8 @@ func TestTraceBlockByNumber(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ getBlockByNumberFn: func(num uint64, full bool) (*types.Block, bool) { - assert.Equal(t, uint64(11), num) - assert.True(t, full) + require.Equal(t, uint64(11), num) + require.True(t, full) return nil, false }, @@ -290,12 +290,12 @@ func TestTraceBlockByNumber(t *testing.T) { res, err := endpoint.TraceBlockByNumber(test.blockNumber, test.config) - assert.Equal(t, test.result, res) + require.Equal(t, test.result, res) if test.err { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -318,13 +318,13 @@ func TestTraceBlockByHash(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testHeader10.Hash, hash) - assert.True(t, full) + require.Equal(t, testHeader10.Hash, hash) + require.True(t, full) return testBlock10, true }, - traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { - assert.Equal(t, testBlock10, block) + traceBlockFn: func(block *types.Block, tracer runtime.Tracer) ([]interface{}, error) { + require.Equal(t, testBlock10, block) return testTraceResults, nil }, @@ -338,8 +338,8 @@ func TestTraceBlockByHash(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testHash11, hash) - assert.True(t, full) + require.Equal(t, testHash11, hash) + require.True(t, full) return nil, false }, @@ -359,12 +359,12 @@ func TestTraceBlockByHash(t *testing.T) { res, err := endpoint.TraceBlockByHash(test.blockHash, test.config) - assert.Equal(t, test.result, res) + require.Equal(t, test.result, res) if test.err { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -389,8 +389,8 @@ func TestTraceBlock(t *testing.T) { input: blockHex, config: &TraceConfig{}, store: &debugEndpointMockStore{ - traceBlockFn: func(block *types.Block, tracer tracer.Tracer) ([]interface{}, error) { - assert.Equal(t, testLatestBlock, block) + traceBlockFn: func(block *types.Block, tracer runtime.Tracer) ([]interface{}, error) { + require.Equal(t, testLatestBlock, block) return testTraceResults, nil }, @@ -418,12 +418,12 @@ func TestTraceBlock(t *testing.T) { res, err := endpoint.TraceBlock(test.input, test.config) - assert.Equal(t, test.result, res) + require.Equal(t, test.result, res) if test.err { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -453,19 +453,19 @@ func TestTraceTransaction(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { - assert.Equal(t, testTxHash1, hash) + require.Equal(t, testTxHash1, hash) return testBlock10.Hash(), true }, getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testBlock10.Hash(), hash) - assert.True(t, full) + require.Equal(t, testBlock10.Hash(), hash) + require.True(t, full) return blockWithTx, true }, - traceTxnFn: func(block *types.Block, txHash types.Hash, tracer tracer.Tracer) (interface{}, error) { - assert.Equal(t, blockWithTx, block) - assert.Equal(t, testTxHash1, txHash) + traceTxnFn: func(block *types.Block, txHash types.Hash, tracer runtime.Tracer) (interface{}, error) { + require.Equal(t, blockWithTx, block) + require.Equal(t, testTxHash1, txHash) return testTraceResult, nil }, @@ -479,7 +479,7 @@ func TestTraceTransaction(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { - assert.Equal(t, testTxHash1, hash) + require.Equal(t, testTxHash1, hash) return types.ZeroHash, false }, @@ -493,13 +493,13 @@ func TestTraceTransaction(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { - assert.Equal(t, testTxHash1, hash) + require.Equal(t, testTxHash1, hash) return testBlock10.Hash(), true }, getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testBlock10.Hash(), hash) - assert.True(t, full) + require.Equal(t, testBlock10.Hash(), hash) + require.True(t, full) return nil, false }, @@ -513,13 +513,13 @@ func TestTraceTransaction(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { - assert.Equal(t, testTxHash1, hash) + require.Equal(t, testTxHash1, hash) return testBlock10.Hash(), true }, getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testBlock10.Hash(), hash) - assert.True(t, full) + require.Equal(t, testBlock10.Hash(), hash) + require.True(t, full) return testBlock10, true }, @@ -533,13 +533,13 @@ func TestTraceTransaction(t *testing.T) { config: &TraceConfig{}, store: &debugEndpointMockStore{ readTxLookupFn: func(hash types.Hash) (types.Hash, bool) { - assert.Equal(t, testTxHash1, hash) + require.Equal(t, testTxHash1, hash) return testBlock10.Hash(), true }, getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testBlock10.Hash(), hash) - assert.True(t, full) + require.Equal(t, testBlock10.Hash(), hash) + require.True(t, full) return &types.Block{ Header: testGenesisHeader, @@ -564,12 +564,12 @@ func TestTraceTransaction(t *testing.T) { res, err := endpoint.TraceTransaction(test.txHash, test.config) - assert.Equal(t, test.result, res) + require.Equal(t, test.result, res) if test.err { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -623,7 +623,7 @@ func TestTraceCall(t *testing.T) { name string arg *txnArgs filter BlockNumberOrHash - config *TraceConfig + config *TraceCallConfig store *debugEndpointMockStore result interface{} err bool @@ -634,16 +634,16 @@ func TestTraceCall(t *testing.T) { filter: BlockNumberOrHash{ BlockNumber: &blockNumber, }, - config: &TraceConfig{}, + config: &TraceCallConfig{}, store: &debugEndpointMockStore{ getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { - assert.Equal(t, testBlock10.Number(), num) + require.Equal(t, testBlock10.Number(), num) return testHeader10, true }, - traceCallFn: func(tx *types.Transaction, header *types.Header, tracer tracer.Tracer) (interface{}, error) { - assert.Equal(t, decodedTx, tx) - assert.Equal(t, testHeader10, header) + traceCallFn: func(tx *types.Transaction, header *types.Header, tracer runtime.Tracer) (interface{}, error) { + require.Equal(t, decodedTx, tx) + require.Equal(t, testHeader10, header) return testTraceResult, nil }, @@ -663,11 +663,11 @@ func TestTraceCall(t *testing.T) { filter: BlockNumberOrHash{ BlockHash: &testHeader10.Hash, }, - config: &TraceConfig{}, + config: &TraceCallConfig{}, store: &debugEndpointMockStore{ getBlockByHashFn: func(hash types.Hash, full bool) (*types.Block, bool) { - assert.Equal(t, testHeader10.Hash, hash) - assert.False(t, full) + require.Equal(t, testHeader10.Hash, hash) + require.False(t, full) return nil, false }, @@ -691,7 +691,7 @@ func TestTraceCall(t *testing.T) { Nonce: &nonce, }, filter: BlockNumberOrHash{}, - config: &TraceConfig{}, + config: &TraceCallConfig{}, store: &debugEndpointMockStore{ headerFn: func() *types.Header { return testLatestHeader @@ -715,12 +715,12 @@ func TestTraceCall(t *testing.T) { res, err := endpoint.TraceCall(test.arg, test.filter, test.config) - assert.Equal(t, test.result, res) + require.Equal(t, test.result, res) if test.err { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } }) } @@ -743,8 +743,8 @@ func Test_newTracer(t *testing.T) { cancel() }) - assert.NotNil(t, tracer) - assert.NoError(t, err) + require.NotNil(t, tracer) + require.NoError(t, err) }) t.Run("should return error if arg is nil", func(t *testing.T) { @@ -752,9 +752,9 @@ func Test_newTracer(t *testing.T) { tracer, cancel, err := newTracer(nil) - assert.Nil(t, tracer) - assert.Nil(t, cancel) - assert.ErrorIs(t, ErrNoConfig, err) + require.Nil(t, tracer) + require.Nil(t, cancel) + require.ErrorIs(t, ErrNoConfig, err) }) t.Run("GetResult should return errExecutionTimeout if timeout happens", func(t *testing.T) { @@ -773,14 +773,14 @@ func Test_newTracer(t *testing.T) { cancel() }) - assert.NoError(t, err) + require.NoError(t, err) // wait until timeout time.Sleep(100 * time.Millisecond) res, err := tracer.GetResult() - assert.Nil(t, res) - assert.Equal(t, ErrExecutionTimeout, err) + require.Nil(t, res) + require.Equal(t, ErrExecutionTimeout, err) }) t.Run("GetResult should not return if cancel is called beforre timeout", func(t *testing.T) { @@ -795,14 +795,14 @@ func Test_newTracer(t *testing.T) { Timeout: &timeout, }) - assert.NoError(t, err) + require.NoError(t, err) cancel() res, err := tracer.GetResult() - assert.NotNil(t, res) - assert.NoError(t, err) + require.NotNil(t, res) + require.NoError(t, err) }) t.Run("should disable everything if struct logs are disabled", func(t *testing.T) { @@ -820,12 +820,12 @@ func Test_newTracer(t *testing.T) { cancel() }) - assert.NoError(t, err) + require.NoError(t, err) st, ok := tracer.(*structtracer.StructTracer) require.True(t, ok) - assert.Equal(t, structtracer.Config{ + require.Equal(t, structtracer.Config{ EnableMemory: false, EnableStack: false, EnableStorage: false, diff --git a/jsonrpc/eth_endpoint.go b/jsonrpc/eth_endpoint.go index 00ac71a7b4..2146b2fa38 100644 --- a/jsonrpc/eth_endpoint.go +++ b/jsonrpc/eth_endpoint.go @@ -492,15 +492,15 @@ func (e *Eth) fillTransactionGasPrice(tx *types.Transaction) error { return nil } -type overrideAccount struct { +type OverrideAccount struct { Nonce *argUint64 `json:"nonce"` Code *argBytes `json:"code"` - Balance *argUint64 `json:"balance"` + Balance *argBig `json:"balance"` State *map[types.Hash]types.Hash `json:"state"` StateDiff *map[types.Hash]types.Hash `json:"stateDiff"` } -func (o *overrideAccount) ToType() types.OverrideAccount { +func (o *OverrideAccount) ToType() types.OverrideAccount { res := types.OverrideAccount{} if o.Nonce != nil { @@ -512,7 +512,7 @@ func (o *overrideAccount) ToType() types.OverrideAccount { } if o.Balance != nil { - res.Balance = new(big.Int).SetUint64(*(*uint64)(o.Balance)) + res.Balance = o.Balance.ToInt() } if o.State != nil { @@ -527,10 +527,20 @@ func (o *overrideAccount) ToType() types.OverrideAccount { } // StateOverride is the collection of overridden accounts. -type stateOverride map[types.Address]overrideAccount +type StateOverride map[types.Address]OverrideAccount + +func (s *StateOverride) ToType() types.StateOverride { + res := types.StateOverride{} + + for addr, o := range *s { + res[addr] = o.ToType() + } + + return res +} // Call executes a smart contract call using the transaction object data -func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *stateOverride) (interface{}, error) { +func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *StateOverride) (interface{}, error) { header, err := GetHeaderFromBlockNumberOrHash(filter, e.store) if err != nil { return nil, err @@ -553,10 +563,7 @@ func (e *Eth) Call(arg *txnArgs, filter BlockNumberOrHash, apiOverride *stateOve var override types.StateOverride if apiOverride != nil { - override = types.StateOverride{} - for addr, o := range *apiOverride { - override[addr] = o.ToType() - } + override = apiOverride.ToType() } // The return value of the execution is saved in the transition (returnValue field) diff --git a/jsonrpc/eth_endpoint_test.go b/jsonrpc/eth_endpoint_test.go index 05f531329f..f498095d01 100644 --- a/jsonrpc/eth_endpoint_test.go +++ b/jsonrpc/eth_endpoint_test.go @@ -381,14 +381,14 @@ func TestOverrideAccount_ToType(t *testing.T) { nonce := uint64(10) code := []byte("SC code") - balance := uint64(10000) + balance := new(big.Int).SetUint64(10000) state := map[types.Hash]types.Hash{types.StringToHash("1"): types.StringToHash("2")} stateDiff := map[types.Hash]types.Hash{types.StringToHash("3"): types.StringToHash("4")} - overrideAcc := &overrideAccount{ + overrideAcc := &OverrideAccount{ Nonce: toArgUint64Ptr(nonce), Code: toArgBytesPtr(code), - Balance: toArgUint64Ptr(balance), + Balance: argBigPtr(balance), State: &state, StateDiff: &stateDiff, } @@ -397,7 +397,7 @@ func TestOverrideAccount_ToType(t *testing.T) { require.NotNil(t, convertedAcc) require.Equal(t, nonce, *convertedAcc.Nonce) require.Equal(t, code, convertedAcc.Code) - require.Equal(t, new(big.Int).SetUint64(balance), convertedAcc.Balance) + require.Equal(t, balance, convertedAcc.Balance) require.Equal(t, state, convertedAcc.State) require.Equal(t, stateDiff, convertedAcc.StateDiff) } diff --git a/jsonrpc/helper.go b/jsonrpc/helper.go index 1ddf0696fe..31fb78e62c 100644 --- a/jsonrpc/helper.go +++ b/jsonrpc/helper.go @@ -79,22 +79,22 @@ type txLookupAndBlockGetter interface { } // GetTxAndBlockByTxHash returns the tx and the block including the tx by given tx hash -func GetTxAndBlockByTxHash(txHash types.Hash, store txLookupAndBlockGetter) (*types.Transaction, *types.Block) { +func GetTxAndBlockByTxHash(txHash types.Hash, store txLookupAndBlockGetter) (*types.Transaction, *types.Block, int) { blockHash, ok := store.ReadTxLookup(txHash) if !ok { - return nil, nil + return nil, nil, 0 } block, ok := store.GetBlockByHash(blockHash, true) if !ok { - return nil, nil + return nil, nil, 0 } - if txn, _ := types.FindTxByHash(block.Transactions, txHash); txn != nil { - return txn, block + if txn, index := types.FindTxByHash(block.Transactions, txHash); txn != nil { + return txn, block, index } - return nil, nil + return nil, nil, 0 } type blockGetter interface { diff --git a/jsonrpc/helper_test.go b/jsonrpc/helper_test.go index 23c3f62cee..b71dbfe75f 100644 --- a/jsonrpc/helper_test.go +++ b/jsonrpc/helper_test.go @@ -28,7 +28,9 @@ func createTestTransaction(hash types.Hash) *types.Transaction { func createTestHeader(height uint64, setterFn func(h *types.Header)) *types.Header { h := &types.Header{ - Number: height, + Number: height, + Miner: []byte{}, + ExtraData: []byte{}, } if setterFn != nil { @@ -409,7 +411,7 @@ func TestGetTxAndBlockByTxHash(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - tx, block := GetTxAndBlockByTxHash(test.txHash, test.store) + tx, block, _ := GetTxAndBlockByTxHash(test.txHash, test.store) assert.Equal(t, test.tx, tx) assert.Equal(t, test.block, block) diff --git a/jsonrpc/types.go b/jsonrpc/types.go index b230d52c1f..b49942f15b 100644 --- a/jsonrpc/types.go +++ b/jsonrpc/types.go @@ -271,6 +271,11 @@ func argBigPtr(b *big.Int) *argBig { return &v } +// ToInt converts b to a big.Int. +func (a *argBig) ToInt() *big.Int { + return (*big.Int)(a) +} + func (a *argBig) UnmarshalText(input []byte) error { buf, err := decodeToHex(input) if err != nil { diff --git a/server/server.go b/server/server.go index 457301e3e4..9c033a0be0 100644 --- a/server/server.go +++ b/server/server.go @@ -35,7 +35,6 @@ import ( itrie "github.com/0xPolygon/polygon-edge/state/immutable-trie" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/addresslist" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/txpool" "github.com/0xPolygon/polygon-edge/types" "github.com/0xPolygon/polygon-edge/validate" @@ -716,7 +715,7 @@ func (j *jsonRPCHub) ApplyTxn( // TraceBlock traces all transactions in the given block and returns all results func (j *jsonRPCHub) TraceBlock( block *types.Block, - tracer tracer.Tracer, + tracer runtime.Tracer, ) ([]interface{}, error) { if block.Number() == 0 { return nil, errors.New("genesis block can't have transaction") @@ -760,7 +759,7 @@ func (j *jsonRPCHub) TraceBlock( func (j *jsonRPCHub) TraceTxn( block *types.Block, targetTxHash types.Hash, - tracer tracer.Tracer, + tracer runtime.Tracer, ) (interface{}, error) { if block.Number() == 0 { return nil, errors.New("genesis block can't have transaction") @@ -812,7 +811,8 @@ func (j *jsonRPCHub) TraceTxn( func (j *jsonRPCHub) TraceCall( tx *types.Transaction, parentHeader *types.Header, - tracer tracer.Tracer, + override types.StateOverride, + tracer runtime.Tracer, ) (interface{}, error) { blockCreator, err := j.GetConsensus().GetBlockCreator(parentHeader) if err != nil { diff --git a/state/executor.go b/state/executor.go index 286e712917..51e5458aac 100644 --- a/state/executor.go +++ b/state/executor.go @@ -15,7 +15,6 @@ import ( "github.com/0xPolygon/polygon-edge/state/runtime/addresslist" "github.com/0xPolygon/polygon-edge/state/runtime/evm" "github.com/0xPolygon/polygon-edge/state/runtime/precompiled" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -668,7 +667,11 @@ func (t *Transition) Create2( address := crypto.CreateAddress(caller, t.state.GetNonce(caller)) contract := runtime.NewContractCreation(1, caller, caller, address, value, gas, code) - return t.applyCreate(contract, t) + t.captureStart(contract, evm.CREATE) + result := t.applyCreate(contract, t) + t.captureEnd(result) + + return result } func (t *Transition) Call2( @@ -680,7 +683,11 @@ func (t *Transition) Call2( ) *runtime.ExecutionResult { c := runtime.NewContractCall(1, caller, caller, to, value, gas, t.state.GetCode(to), input) - return t.applyCall(c, runtime.Call, t) + t.captureStart(c, runtime.Call) + result := t.applyCall(c, runtime.Call, t) + t.captureEnd(result) + + return result } func (t *Transition) run(contract *runtime.Contract, host runtime.Host) *runtime.ExecutionResult { @@ -905,7 +912,7 @@ func (t *Transition) applyCreate(c *runtime.Contract, host runtime.Host) *runtim return result } - if t.config.EIP158 && len(result.ReturnValue) > SpuriousDragonMaxCodeSize { + if t.config.EIP158 && len(result.ReturnValue) > TxPoolMaxInitCodeSize { // Contract size exceeds 'SpuriousDragon' size limit if err := t.state.RevertToSnapshot(snapshot); err != nil { return &runtime.ExecutionResult{ @@ -1092,7 +1099,7 @@ func (t *Transition) SetNonPayable(nonPayable bool) { } // SetTracer sets tracer to the context in order to enable it -func (t *Transition) SetTracer(tracer tracer.Tracer) { +func (t *Transition) SetTracer(tracer runtime.Tracer) { t.ctx.Tracer = tracer } @@ -1105,6 +1112,10 @@ func (t *Transition) GetRefund() uint64 { return t.state.GetRefund() } +func (t *Transition) ActivePrecompiles() []types.Address { + return t.precompiles.ActivePrecompiles(&t.config) +} + func TransactionGasCost(msg *types.Transaction, isHomestead, isIstanbul bool) (uint64, error) { cost := uint64(0) @@ -1207,6 +1218,35 @@ func checkAndProcessStateTx(msg *types.Transaction) error { return nil } +// captureCallStart calls CallStart in Tracer if context has the tracer +func (t *Transition) captureStart(c *runtime.Contract, callType runtime.CallType) { + if t.ctx.Tracer == nil { + return + } + + t.ctx.Tracer.CaptureStart( + c.Caller, + c.Address, + int(callType), + c.Input, + c.Gas, + c.Value, + t, + ) +} + +func (t *Transition) captureEnd(result *runtime.ExecutionResult) { + if t.ctx.Tracer == nil { + return + } + + t.ctx.Tracer.CaptureEnd( + result.ReturnValue, + result.GasUsed, + result.Err, + ) +} + // captureCallStart calls CallStart in Tracer if context has the tracer func (t *Transition) captureCallStart(c *runtime.Contract, callType runtime.CallType) { if t.ctx.Tracer == nil { @@ -1221,6 +1261,7 @@ func (t *Transition) captureCallStart(c *runtime.Contract, callType runtime.Call c.Gas, c.Value, c.Input, + t, ) } @@ -1232,6 +1273,7 @@ func (t *Transition) captureCallEnd(c *runtime.Contract, result *runtime.Executi t.ctx.Tracer.CallEnd( c.Depth, + result.GasUsed, result.ReturnValue, result.Err, ) diff --git a/state/runtime/evm/evm_fuzz_test.go b/state/runtime/evm/evm_fuzz_test.go index c7c1889d4b..1a13e40256 100644 --- a/state/runtime/evm/evm_fuzz_test.go +++ b/state/runtime/evm/evm_fuzz_test.go @@ -142,6 +142,10 @@ func (m *mockHostF) GetRefund() uint64 { return m.refund } +func (m *mockHostF) ActivePrecompiles() []types.Address { + return nil +} + func FuzzTestEVM(f *testing.F) { seed := []byte{ PUSH1, 0x01, PUSH1, 0x02, ADD, diff --git a/state/runtime/evm/evm_test.go b/state/runtime/evm/evm_test.go index e5ad31c878..385fa27b90 100644 --- a/state/runtime/evm/evm_test.go +++ b/state/runtime/evm/evm_test.go @@ -6,7 +6,6 @@ import ( "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/state/runtime" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" "github.com/stretchr/testify/assert" ) @@ -23,6 +22,8 @@ func newMockContract(value *big.Int, gas uint64, code []byte) *runtime.Contract ) } +var _ runtime.Host = (*mockHost)(nil) + // mockHost is a struct which meets the requirements of runtime.Host interface but throws panic in each methods // we don't test all opcodes in this test type mockHost struct { @@ -114,6 +115,10 @@ func (m *mockHost) GetRefund() uint64 { panic("Not implemented in tests") //nolint:gocritic } +func (m *mockHost) ActivePrecompiles() []types.Address { + panic("Not implemented in tests") //nolint:gocritic +} + func TestRun(t *testing.T) { t.Parallel() @@ -215,8 +220,8 @@ func (m *mockTracer) CaptureState( opCode int, contractAddress types.Address, sp int, - _host tracer.RuntimeHost, - _state tracer.VMState, + _host runtime.Host, + _state runtime.VMState, ) { m.calls = append(m.calls, mockCall{ name: "CaptureState", @@ -233,13 +238,13 @@ func (m *mockTracer) CaptureState( func (m *mockTracer) ExecuteState( contractAddress types.Address, ip uint64, - opcode string, + opcode int, availableGas uint64, cost uint64, lastReturnData []byte, depth int, err error, - _host tracer.RuntimeHost, + _host runtime.Host, ) { m.calls = append(m.calls, mockCall{ name: "ExecuteState", @@ -293,7 +298,7 @@ func TestRunWithTracer(t *testing.T) { args: map[string]interface{}{ "contractAddress": contractAddress, "ip": uint64(0), - "opcode": opCodeToString[PUSH1], + "opcode": OpCodeToString[PUSH1], "availableGas": uint64(5000), "cost": uint64(3), "lastReturnData": []byte{}, @@ -338,7 +343,7 @@ func TestRunWithTracer(t *testing.T) { args: map[string]interface{}{ "contractAddress": contractAddress, "ip": uint64(0), - "opcode": opCodeToString[POP], + "opcode": OpCodeToString[POP], "availableGas": uint64(5000), "cost": uint64(2), "lastReturnData": []byte{}, diff --git a/state/runtime/evm/opcodes.go b/state/runtime/evm/opcodes.go index c511d1c3fa..e41620efa4 100644 --- a/state/runtime/evm/opcodes.go +++ b/state/runtime/evm/opcodes.go @@ -264,7 +264,7 @@ const ( SELFDESTRUCT = 0xFF ) -var opCodeToString = map[OpCode]string{ +var OpCodeToString = map[OpCode]string{ STOP: "STOP", ADD: "ADD", MUL: "MUL", @@ -347,7 +347,7 @@ func opCodesToString(from, to OpCode, str string) { } for i := from; i <= to; i++ { - opCodeToString[i] = fmt.Sprintf("%s%d", str, c) + OpCodeToString[i] = fmt.Sprintf("%s%d", str, c) c++ } } @@ -364,5 +364,5 @@ func init() { } func (op OpCode) String() string { - return opCodeToString[op] + return OpCodeToString[op] } diff --git a/state/runtime/evm/state.go b/state/runtime/evm/state.go index 9a849cecb4..0810975143 100644 --- a/state/runtime/evm/state.go +++ b/state/runtime/evm/state.go @@ -2,6 +2,7 @@ package evm import ( "errors" + "fmt" "math/big" "strings" @@ -119,6 +120,10 @@ func (c *state) Halt() { c.stop = true } +func (c *state) GetContract() *runtime.Contract { + return c.msg +} + func (c *state) exit(err error) { if err == nil { return @@ -217,15 +222,28 @@ func (c *state) Run() ([]byte, error) { var ( vmerr error - op OpCode + op OpCode // curent opcode ok bool + + cost uint64 + + // copies used by tracer + ipCopy uint64 // needed for the deferred tracer (program counter) + gasCopy uint64 // for tracer to log gas remaining before execution + debug = c.host.GetTracer() != nil ) + if debug { + defer func() { + if vmerr != nil { + c.captureStateBre(op, ipCopy, gasCopy, cost, c.returnData) + } + }() + } + for !c.stop { + gasCopy, cost, ipCopy = c.gas, 0, uint64(c.ip) op, ok = c.CurrentOpCode() - gasCopy, ipCopy := c.gas, uint64(c.ip) - - c.captureState(int(op)) if !ok { c.Halt() @@ -233,34 +251,40 @@ func (c *state) Run() ([]byte, error) { break } + if debug { + fmt.Println("[GOJA] CaptureState called with op code: " + OpCodeToString[op] + " and depth: " + fmt.Sprint(c.msg.Depth)) + } + inst := dispatchTable[op] if inst.inst == nil { c.exit(errOpCodeNotFound) - c.captureExecution(op.String(), uint64(c.ip), gasCopy, 0) break } // check if the depth of the stack is enough for the instruction if c.sp < inst.stack { + cost = inst.gas c.exit(&runtime.StackUnderflowError{StackLen: c.sp, Required: inst.stack}) - c.captureExecution(op.String(), uint64(c.ip), gasCopy, inst.gas) break } // consume the gas of the instruction if !c.consumeGas(inst.gas) { + cost = inst.gas c.exit(errOutOfGas) - c.captureExecution(op.String(), uint64(c.ip), gasCopy, inst.gas) break } // execute the instruction - inst.inst(c) + cost = gasCopy - inst.gas + if debug { + c.captureStateBre(op, ipCopy, gasCopy, cost, c.returnData) + } - c.captureExecution(op.String(), ipCopy, gasCopy, gasCopy-c.gas) + inst.inst(c) // check if stack size exceeds the max size if c.sp > stackSize { @@ -373,6 +397,26 @@ func (c *state) CurrentOpCode() (OpCode, bool) { return OpCode(c.code[c.ip]), true } +func (c *state) captureStateBre( + opCode OpCode, + ip, gas, cost uint64, + returnData []byte) { + tracer := c.host.GetTracer() + tracer.CaptureStateBre( + int(opCode), + c.msg.Depth, + ip, gas, cost, + returnData, + &runtime.ScopeContext{ + Memory: c.memory, + Stack: c.stack, + Sp: c.sp, + }, + c.host, + c, c.err, + ) +} + func (c *state) captureState(opCode int) { tracer := c.host.GetTracer() if tracer == nil { @@ -391,10 +435,10 @@ func (c *state) captureState(opCode int) { } func (c *state) captureExecution( - opCode string, + opCode OpCode, ip uint64, gas uint64, - consumedGas uint64, + cost uint64, ) { tracer := c.host.GetTracer() if tracer == nil { @@ -404,9 +448,9 @@ func (c *state) captureExecution( tracer.ExecuteState( c.msg.Address, ip, - opCode, + int(opCode), gas, - consumedGas, + cost, c.returnData, c.msg.Depth, c.err, diff --git a/state/runtime/precompiled/native_transfer_test.go b/state/runtime/precompiled/native_transfer_test.go index a6309f6d42..0cb5c125aa 100644 --- a/state/runtime/precompiled/native_transfer_test.go +++ b/state/runtime/precompiled/native_transfer_test.go @@ -197,3 +197,7 @@ func (d dummyHost) GetTracer() runtime.VMTracer { func (d dummyHost) GetRefund() uint64 { return 0 } + +func (d dummyHost) ActivePrecompiles() []types.Address { + return nil +} diff --git a/state/runtime/precompiled/precompiled.go b/state/runtime/precompiled/precompiled.go index 8b33d5eb80..58cf7fd538 100644 --- a/state/runtime/precompiled/precompiled.go +++ b/state/runtime/precompiled/precompiled.go @@ -74,7 +74,7 @@ func (p *Precompiled) setupContracts() { p.register(contracts.NativeTransferPrecompile.String(), &nativeTransfer{}) // Console precompile - // p.register(contracts.ConsolePrecompile.String(), &console{}) + p.register(contracts.ConsolePrecompile.String(), &console{}) // BLS aggregated signatures verification precompile p.register(contracts.BLSAggSigsVerificationPrecompile.String(), &blsAggSignsVerification{}) @@ -96,6 +96,22 @@ var ( nine = types.StringToAddress("9") ) +// ActivePrecompiles returns a list of active precompiled contract addresses based on the provided configuration. +// It checks if each contract address in the precompiled contracts map can be run with the given configuration. +// If a contract address can be run, it is added to the list of active precompiles. +// The list of active precompiled contract addresses is returned. +func (p *Precompiled) ActivePrecompiles(config *chain.ForksInTime) []types.Address { + var precompiles []types.Address + + for addr := range p.contracts { + if p.CanRun(&runtime.Contract{CodeAddress: addr}, nil, config) { + precompiles = append(precompiles, addr) + } + } + + return precompiles +} + // CanRun implements the runtime interface func (p *Precompiled) CanRun(c *runtime.Contract, _ runtime.Host, config *chain.ForksInTime) bool { if _, ok := p.contracts[c.CodeAddress]; !ok { diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 248e9f3fd1..815302eddc 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -6,7 +6,6 @@ import ( "math/big" "github.com/0xPolygon/polygon-edge/chain" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -20,7 +19,7 @@ type TxContext struct { GasLimit int64 ChainID int64 Difficulty types.Hash - Tracer tracer.Tracer + Tracer Tracer NonPayable bool BaseFee *big.Int BurnContract types.Address @@ -80,6 +79,7 @@ type Host interface { Transfer(from types.Address, to types.Address, amount *big.Int) error GetTracer() VMTracer GetRefund() uint64 + ActivePrecompiles() []types.Address } type VMTracer interface { @@ -89,19 +89,28 @@ type VMTracer interface { opCode int, contractAddress types.Address, sp int, - host tracer.RuntimeHost, - state tracer.VMState, + host Host, + state VMState, ) ExecuteState( contractAddress types.Address, ip uint64, - opcode string, + opcode int, availableGas uint64, cost uint64, lastReturnData []byte, depth int, err error, - host tracer.RuntimeHost, + host Host, + ) + CaptureStateBre( + opCode, depth int, + ip, gas, cost uint64, + returnData []byte, + scope *ScopeContext, + host Host, + state VMState, + err error, ) } diff --git a/state/runtime/tracer/calltracer/call_tracer.go b/state/runtime/tracer/calltracer/call_tracer.go index 23e6e1adc9..ca319a47b8 100644 --- a/state/runtime/tracer/calltracer/call_tracer.go +++ b/state/runtime/tracer/calltracer/call_tracer.go @@ -5,21 +5,10 @@ import ( "sync" "github.com/0xPolygon/polygon-edge/helper/hex" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" + "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/types" ) -var ( - callTypes = map[int]string{ - 0: "CALL", - 1: "CALLCODE", - 2: "DELEGATECALL", - 3: "STATICCALL", - 4: "CREATE", - 5: "CREATE2", - } -) - type Call struct { Type string `json:"type"` From string `json:"from"` @@ -84,12 +73,12 @@ func (c *CallTracer) TxEnd(gasLeft uint64) { } func (c *CallTracer) CallStart(depth int, from, to types.Address, callType int, - gas uint64, value *big.Int, input []byte) { + gas uint64, value *big.Int, input []byte, host runtime.Host) { if c.cancelled() { return } - typ, ok := callTypes[callType] + typ, ok := runtime.CallTypes[callType] if !ok { typ = "UNKNOWN" } @@ -122,16 +111,15 @@ func (c *CallTracer) CallStart(depth int, from, to types.Address, callType int, } } -func (c *CallTracer) CallEnd(depth int, output []byte, err error) { +func (c *CallTracer) CallEnd(depth int, totalGasUsed uint64, output []byte, err error) { c.activeCall.Output = hex.EncodeToHex(output) - gasUsed := uint64(0) - + gasUsedByCall := uint64(0) if c.activeCall.startGas > c.activeAvailableGas { - gasUsed = c.activeCall.startGas - c.activeAvailableGas + gasUsedByCall = c.activeCall.startGas - c.activeAvailableGas } - c.activeCall.GasUsed = hex.EncodeUint64(gasUsed) + c.activeCall.GasUsed = hex.EncodeUint64(gasUsedByCall) c.activeGas = 0 if depth > 1 { @@ -144,14 +132,37 @@ func (c *CallTracer) CallEnd(depth int, output []byte, err error) { } func (c *CallTracer) CaptureState(memory []byte, stack []*big.Int, opCode int, - contractAddress types.Address, sp int, host tracer.RuntimeHost, state tracer.VMState) { + contractAddress types.Address, sp int, host runtime.Host, state runtime.VMState) { if c.cancelled() { state.Halt() } } -func (c *CallTracer) ExecuteState(contractAddress types.Address, ip uint64, opcode string, - availableGas uint64, cost uint64, lastReturnData []byte, depth int, err error, host tracer.RuntimeHost) { +func (c *CallTracer) ExecuteState(contractAddress types.Address, ip uint64, opcode int, + availableGas uint64, cost uint64, lastReturnData []byte, depth int, err error, host runtime.Host) { c.activeGas += cost c.activeAvailableGas = availableGas } + +func (t *CallTracer) CaptureStateBre( + opCode, depth int, + ip, gas, cost uint64, + returnData []byte, + scope *runtime.ScopeContext, + host runtime.Host, + state runtime.VMState, + err error, +) { + +} + +func (t *CallTracer) CaptureStart( + from, to types.Address, + callType int, + input []byte, + gas uint64, + value *big.Int, + host runtime.Host) { +} + +func (t *CallTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {} diff --git a/state/runtime/tracer/calltracer/call_tracer_test.go b/state/runtime/tracer/calltracer/call_tracer_test.go index 80b493ac77..5a286e45e5 100644 --- a/state/runtime/tracer/calltracer/call_tracer_test.go +++ b/state/runtime/tracer/calltracer/call_tracer_test.go @@ -58,7 +58,7 @@ func TestCallTracer_CallStart(t *testing.T) { input = []byte("input") ) - c.CallStart(depth, from, to, callType, gas, value, input) + c.CallStart(depth, from, to, callType, gas, value, input, nil) expectedCall := &Call{ Type: "CALL", @@ -105,7 +105,7 @@ func TestCallTracer_CallStart(t *testing.T) { } c.activeCall = parentCall - c.CallStart(depth, from, to, callType, gas, value, input) + c.CallStart(depth, from, to, callType, gas, value, input, nil) expectedCall := &Call{ Type: "CALL", @@ -138,7 +138,7 @@ func TestCallTracer_CallStart(t *testing.T) { gas = uint64(100000) ) - c.CallStart(depth, from, to, callType, gas, nil, nil) + c.CallStart(depth, from, to, callType, gas, nil, nil, nil) expectedCall := &Call{ Type: "CALL", @@ -171,7 +171,7 @@ func TestCallTracer_CallStart(t *testing.T) { input = []byte("input") ) - c.CallStart(depth, from, to, callType, gas, value, input) + c.CallStart(depth, from, to, callType, gas, value, input, nil) expectedCall := &Call{ Type: "UNKNOWN", @@ -205,7 +205,7 @@ func TestCallTracer_CallEnd(t *testing.T) { startGas: 1000, } - tracer.CallEnd(1, output, nil) + tracer.CallEnd(1, 2000, output, nil) require.Equal(t, uint64(0), tracer.activeGas) require.Equal(t, hex.EncodeToHex(output), tracer.activeCall.Output) @@ -221,7 +221,7 @@ func TestCallTracer_CallEnd(t *testing.T) { startGas: 1000, } - tracer.CallEnd(1, output, err) + tracer.CallEnd(1, 1000, output, err) require.Equal(t, uint64(0), tracer.activeGas) require.Equal(t, hex.EncodeToHex(output), tracer.activeCall.Output) @@ -239,7 +239,7 @@ func TestCallTracer_CallEnd(t *testing.T) { startGas: 2000, } - tracer.CallEnd(1, output, nil) + tracer.CallEnd(1, 2000, output, nil) require.Equal(t, uint64(0), tracer.activeGas) require.Equal(t, hex.EncodeToHex(output), tracer.activeCall.Output) @@ -255,7 +255,7 @@ func TestCallTracer_CallEnd(t *testing.T) { startGas: 2000, } - tracer.CallEnd(1, output, err) + tracer.CallEnd(1, 2000, output, err) require.Equal(t, uint64(0), tracer.activeGas) require.Equal(t, hex.EncodeToHex(output), tracer.activeCall.Output) @@ -278,7 +278,7 @@ func TestCallTracer_CallEnd(t *testing.T) { }, } - tracer.CallEnd(2, output, nil) + tracer.CallEnd(2, 1000, output, nil) require.Equal(t, uint64(0), tracer.activeGas) require.Equal(t, hex.EncodeToHex(output), tracer.activeCall.Output) diff --git a/state/runtime/tracer/jstracer/bigint.go b/state/runtime/tracer/jstracer/bigint.go new file mode 100644 index 0000000000..f585d714aa --- /dev/null +++ b/state/runtime/tracer/jstracer/bigint.go @@ -0,0 +1,4 @@ +package js + +// bigIntegerJS is the minified version of https://github.com/peterolson/BigInteger.js. +const bigIntegerJS = `var bigInt=function(undefined){"use strict";var BASE=1e7,LOG_BASE=7,MAX_INT=9007199254740992,MAX_INT_ARR=smallToArray(MAX_INT),LOG_MAX_INT=Math.log(MAX_INT);function Integer(v,radix){if(typeof v==="undefined")return Integer[0];if(typeof radix!=="undefined")return+radix===10?parseValue(v):parseBase(v,radix);return parseValue(v)}function BigInteger(value,sign){this.value=value;this.sign=sign;this.isSmall=false}BigInteger.prototype=Object.create(Integer.prototype);function SmallInteger(value){this.value=value;this.sign=value<0;this.isSmall=true}SmallInteger.prototype=Object.create(Integer.prototype);function isPrecise(n){return-MAX_INT0)return Math.floor(n);return Math.ceil(n)}function add(a,b){var l_a=a.length,l_b=b.length,r=new Array(l_a),carry=0,base=BASE,sum,i;for(i=0;i=base?1:0;r[i]=sum-carry*base}while(i0)r.push(carry);return r}function addAny(a,b){if(a.length>=b.length)return add(a,b);return add(b,a)}function addSmall(a,carry){var l=a.length,r=new Array(l),base=BASE,sum,i;for(i=0;i0){r[i++]=carry%base;carry=Math.floor(carry/base)}return r}BigInteger.prototype.add=function(v){var n=parseValue(v);if(this.sign!==n.sign){return this.subtract(n.negate())}var a=this.value,b=n.value;if(n.isSmall){return new BigInteger(addSmall(a,Math.abs(b)),this.sign)}return new BigInteger(addAny(a,b),this.sign)};BigInteger.prototype.plus=BigInteger.prototype.add;SmallInteger.prototype.add=function(v){var n=parseValue(v);var a=this.value;if(a<0!==n.sign){return this.subtract(n.negate())}var b=n.value;if(n.isSmall){if(isPrecise(a+b))return new SmallInteger(a+b);b=smallToArray(Math.abs(b))}return new BigInteger(addSmall(b,Math.abs(a)),a<0)};SmallInteger.prototype.plus=SmallInteger.prototype.add;function subtract(a,b){var a_l=a.length,b_l=b.length,r=new Array(a_l),borrow=0,base=BASE,i,difference;for(i=0;i=0){value=subtract(a,b)}else{value=subtract(b,a);sign=!sign}value=arrayToSmall(value);if(typeof value==="number"){if(sign)value=-value;return new SmallInteger(value)}return new BigInteger(value,sign)}function subtractSmall(a,b,sign){var l=a.length,r=new Array(l),carry=-b,base=BASE,i,difference;for(i=0;i=0)};SmallInteger.prototype.minus=SmallInteger.prototype.subtract;BigInteger.prototype.negate=function(){return new BigInteger(this.value,!this.sign)};SmallInteger.prototype.negate=function(){var sign=this.sign;var small=new SmallInteger(-this.value);small.sign=!sign;return small};BigInteger.prototype.abs=function(){return new BigInteger(this.value,false)};SmallInteger.prototype.abs=function(){return new SmallInteger(Math.abs(this.value))};function multiplyLong(a,b){var a_l=a.length,b_l=b.length,l=a_l+b_l,r=createArray(l),base=BASE,product,carry,i,a_i,b_j;for(i=0;i0){r[i++]=carry%base;carry=Math.floor(carry/base)}return r}function shiftLeft(x,n){var r=[];while(n-- >0)r.push(0);return r.concat(x)}function multiplyKaratsuba(x,y){var n=Math.max(x.length,y.length);if(n<=30)return multiplyLong(x,y);n=Math.ceil(n/2);var b=x.slice(n),a=x.slice(0,n),d=y.slice(n),c=y.slice(0,n);var ac=multiplyKaratsuba(a,c),bd=multiplyKaratsuba(b,d),abcd=multiplyKaratsuba(addAny(a,b),addAny(c,d));var product=addAny(addAny(ac,shiftLeft(subtract(subtract(abcd,ac),bd),n)),shiftLeft(bd,2*n));trim(product);return product}function useKaratsuba(l1,l2){return-.012*l1-.012*l2+15e-6*l1*l2>0}BigInteger.prototype.multiply=function(v){var n=parseValue(v),a=this.value,b=n.value,sign=this.sign!==n.sign,abs;if(n.isSmall){if(b===0)return Integer[0];if(b===1)return this;if(b===-1)return this.negate();abs=Math.abs(b);if(abs=0;shift--){quotientDigit=base-1;if(remainder[shift+b_l]!==divisorMostSignificantDigit){quotientDigit=Math.floor((remainder[shift+b_l]*base+remainder[shift+b_l-1])/divisorMostSignificantDigit)}carry=0;borrow=0;l=divisor.length;for(i=0;ib_l){highx=(highx+1)*base}guess=Math.ceil(highx/highy);do{check=multiplySmall(b,guess);if(compareAbs(check,part)<=0)break;guess--}while(guess);result.push(guess);part=subtract(part,check)}result.reverse();return[arrayToSmall(result),arrayToSmall(part)]}function divModSmall(value,lambda){var length=value.length,quotient=createArray(length),base=BASE,i,q,remainder,divisor;remainder=0;for(i=length-1;i>=0;--i){divisor=remainder*base+value[i];q=truncate(divisor/lambda);remainder=divisor-q*lambda;quotient[i]=q|0}return[quotient,remainder|0]}function divModAny(self,v){var value,n=parseValue(v);var a=self.value,b=n.value;var quotient;if(b===0)throw new Error("Cannot divide by zero");if(self.isSmall){if(n.isSmall){return[new SmallInteger(truncate(a/b)),new SmallInteger(a%b)]}return[Integer[0],self]}if(n.isSmall){if(b===1)return[self,Integer[0]];if(b==-1)return[self.negate(),Integer[0]];var abs=Math.abs(b);if(absb.length?1:-1}for(var i=a.length-1;i>=0;i--){if(a[i]!==b[i])return a[i]>b[i]?1:-1}return 0}BigInteger.prototype.compareAbs=function(v){var n=parseValue(v),a=this.value,b=n.value;if(n.isSmall)return 1;return compareAbs(a,b)};SmallInteger.prototype.compareAbs=function(v){var n=parseValue(v),a=Math.abs(this.value),b=n.value;if(n.isSmall){b=Math.abs(b);return a===b?0:a>b?1:-1}return-1};BigInteger.prototype.compare=function(v){if(v===Infinity){return-1}if(v===-Infinity){return 1}var n=parseValue(v),a=this.value,b=n.value;if(this.sign!==n.sign){return n.sign?1:-1}if(n.isSmall){return this.sign?-1:1}return compareAbs(a,b)*(this.sign?-1:1)};BigInteger.prototype.compareTo=BigInteger.prototype.compare;SmallInteger.prototype.compare=function(v){if(v===Infinity){return-1}if(v===-Infinity){return 1}var n=parseValue(v),a=this.value,b=n.value;if(n.isSmall){return a==b?0:a>b?1:-1}if(a<0!==n.sign){return a<0?-1:1}return a<0?1:-1};SmallInteger.prototype.compareTo=SmallInteger.prototype.compare;BigInteger.prototype.equals=function(v){return this.compare(v)===0};SmallInteger.prototype.eq=SmallInteger.prototype.equals=BigInteger.prototype.eq=BigInteger.prototype.equals;BigInteger.prototype.notEquals=function(v){return this.compare(v)!==0};SmallInteger.prototype.neq=SmallInteger.prototype.notEquals=BigInteger.prototype.neq=BigInteger.prototype.notEquals;BigInteger.prototype.greater=function(v){return this.compare(v)>0};SmallInteger.prototype.gt=SmallInteger.prototype.greater=BigInteger.prototype.gt=BigInteger.prototype.greater;BigInteger.prototype.lesser=function(v){return this.compare(v)<0};SmallInteger.prototype.lt=SmallInteger.prototype.lesser=BigInteger.prototype.lt=BigInteger.prototype.lesser;BigInteger.prototype.greaterOrEquals=function(v){return this.compare(v)>=0};SmallInteger.prototype.geq=SmallInteger.prototype.greaterOrEquals=BigInteger.prototype.geq=BigInteger.prototype.greaterOrEquals;BigInteger.prototype.lesserOrEquals=function(v){return this.compare(v)<=0};SmallInteger.prototype.leq=SmallInteger.prototype.lesserOrEquals=BigInteger.prototype.leq=BigInteger.prototype.lesserOrEquals;BigInteger.prototype.isEven=function(){return(this.value[0]&1)===0};SmallInteger.prototype.isEven=function(){return(this.value&1)===0};BigInteger.prototype.isOdd=function(){return(this.value[0]&1)===1};SmallInteger.prototype.isOdd=function(){return(this.value&1)===1};BigInteger.prototype.isPositive=function(){return!this.sign};SmallInteger.prototype.isPositive=function(){return this.value>0};BigInteger.prototype.isNegative=function(){return this.sign};SmallInteger.prototype.isNegative=function(){return this.value<0};BigInteger.prototype.isUnit=function(){return false};SmallInteger.prototype.isUnit=function(){return Math.abs(this.value)===1};BigInteger.prototype.isZero=function(){return false};SmallInteger.prototype.isZero=function(){return this.value===0};BigInteger.prototype.isDivisibleBy=function(v){var n=parseValue(v);var value=n.value;if(value===0)return false;if(value===1)return true;if(value===2)return this.isEven();return this.mod(n).equals(Integer[0])};SmallInteger.prototype.isDivisibleBy=BigInteger.prototype.isDivisibleBy;function isBasicPrime(v){var n=v.abs();if(n.isUnit())return false;if(n.equals(2)||n.equals(3)||n.equals(5))return true;if(n.isEven()||n.isDivisibleBy(3)||n.isDivisibleBy(5))return false;if(n.lesser(25))return true}BigInteger.prototype.isPrime=function(){var isPrime=isBasicPrime(this);if(isPrime!==undefined)return isPrime;var n=this.abs(),nPrev=n.prev();var a=[2,3,5,7,11,13,17,19],b=nPrev,d,t,i,x;while(b.isEven())b=b.divide(2);for(i=0;i-MAX_INT)return new SmallInteger(value-1);return new BigInteger(MAX_INT_ARR,true)};var powersOfTwo=[1];while(2*powersOfTwo[powersOfTwo.length-1]<=BASE)powersOfTwo.push(2*powersOfTwo[powersOfTwo.length-1]);var powers2Length=powersOfTwo.length,highestPower2=powersOfTwo[powers2Length-1];function shift_isSmall(n){return(typeof n==="number"||typeof n==="string")&&+Math.abs(n)<=BASE||n instanceof BigInteger&&n.value.length<=1}BigInteger.prototype.shiftLeft=function(n){if(!shift_isSmall(n)){throw new Error(String(n)+" is too large for shifting.")}n=+n;if(n<0)return this.shiftRight(-n);var result=this;while(n>=powers2Length){result=result.multiply(highestPower2);n-=powers2Length-1}return result.multiply(powersOfTwo[n])};SmallInteger.prototype.shiftLeft=BigInteger.prototype.shiftLeft;BigInteger.prototype.shiftRight=function(n){var remQuo;if(!shift_isSmall(n)){throw new Error(String(n)+" is too large for shifting.")}n=+n;if(n<0)return this.shiftLeft(-n);var result=this;while(n>=powers2Length){if(result.isZero())return result;remQuo=divModAny(result,highestPower2);result=remQuo[1].isNegative()?remQuo[0].prev():remQuo[0];n-=powers2Length-1}remQuo=divModAny(result,powersOfTwo[n]);return remQuo[1].isNegative()?remQuo[0].prev():remQuo[0]};SmallInteger.prototype.shiftRight=BigInteger.prototype.shiftRight;function bitwise(x,y,fn){y=parseValue(y);var xSign=x.isNegative(),ySign=y.isNegative();var xRem=xSign?x.not():x,yRem=ySign?y.not():y;var xDigit=0,yDigit=0;var xDivMod=null,yDivMod=null;var result=[];while(!xRem.isZero()||!yRem.isZero()){xDivMod=divModAny(xRem,highestPower2);xDigit=xDivMod[1].toJSNumber();if(xSign){xDigit=highestPower2-1-xDigit}yDivMod=divModAny(yRem,highestPower2);yDigit=yDivMod[1].toJSNumber();if(ySign){yDigit=highestPower2-1-yDigit}xRem=xDivMod[0];yRem=yDivMod[0];result.push(fn(xDigit,yDigit))}var sum=fn(xSign?1:0,ySign?1:0)!==0?bigInt(-1):bigInt(0);for(var i=result.length-1;i>=0;i-=1){sum=sum.multiply(highestPower2).add(bigInt(result[i]))}return sum}BigInteger.prototype.not=function(){return this.negate().prev()};SmallInteger.prototype.not=BigInteger.prototype.not;BigInteger.prototype.and=function(n){return bitwise(this,n,function(a,b){return a&b})};SmallInteger.prototype.and=BigInteger.prototype.and;BigInteger.prototype.or=function(n){return bitwise(this,n,function(a,b){return a|b})};SmallInteger.prototype.or=BigInteger.prototype.or;BigInteger.prototype.xor=function(n){return bitwise(this,n,function(a,b){return a^b})};SmallInteger.prototype.xor=BigInteger.prototype.xor;var LOBMASK_I=1<<30,LOBMASK_BI=(BASE&-BASE)*(BASE&-BASE)|LOBMASK_I;function roughLOB(n){var v=n.value,x=typeof v==="number"?v|LOBMASK_I:v[0]+v[1]*BASE|LOBMASK_BI;return x&-x}function max(a,b){a=parseValue(a);b=parseValue(b);return a.greater(b)?a:b}function min(a,b){a=parseValue(a);b=parseValue(b);return a.lesser(b)?a:b}function gcd(a,b){a=parseValue(a).abs();b=parseValue(b).abs();if(a.equals(b))return a;if(a.isZero())return b;if(b.isZero())return a;var c=Integer[1],d,t;while(a.isEven()&&b.isEven()){d=Math.min(roughLOB(a),roughLOB(b));a=a.divide(d);b=b.divide(d);c=c.multiply(d)}while(a.isEven()){a=a.divide(roughLOB(a))}do{while(b.isEven()){b=b.divide(roughLOB(b))}if(a.greater(b)){t=b;b=a;a=t}b=b.subtract(a)}while(!b.isZero());return c.isUnit()?a:a.multiply(c)}function lcm(a,b){a=parseValue(a).abs();b=parseValue(b).abs();return a.divide(gcd(a,b)).multiply(b)}function randBetween(a,b){a=parseValue(a);b=parseValue(b);var low=min(a,b),high=max(a,b);var range=high.subtract(low).add(1);if(range.isSmall)return low.add(Math.floor(Math.random()*range));var length=range.value.length-1;var result=[],restricted=true;for(var i=length;i>=0;i--){var top=restricted?range.value[i]:BASE;var digit=truncate(Math.random()*top);result.unshift(digit);if(digit=absBase){if(c==="1"&&absBase===1)continue;throw new Error(c+" is not a valid digit in base "+base+".")}else if(c.charCodeAt(0)-87>=absBase){throw new Error(c+" is not a valid digit in base "+base+".")}}}if(2<=base&&base<=36){if(length<=LOG_MAX_INT/Math.log(base)){var result=parseInt(text,base);if(isNaN(result)){throw new Error(c+" is not a valid digit in base "+base+".")}return new SmallInteger(parseInt(text,base))}}base=parseValue(base);var digits=[];var isNegative=text[0]==="-";for(i=isNegative?1:0;i");digits.push(parseValue(text.slice(start+1,i)))}else throw new Error(c+" is not a valid character")}return parseBaseFromArray(digits,base,isNegative)};function parseBaseFromArray(digits,base,isNegative){var val=Integer[0],pow=Integer[1],i;for(i=digits.length-1;i>=0;i--){val=val.add(digits[i].times(pow));pow=pow.times(base)}return isNegative?val.negate():val}function stringify(digit){var v=digit.value;if(typeof v==="number")v=[v];if(v.length===1&&v[0]<=35){return"0123456789abcdefghijklmnopqrstuvwxyz".charAt(v[0])}return"<"+v+">"}function toBase(n,base){base=bigInt(base);if(base.isZero()){if(n.isZero())return"0";throw new Error("Cannot convert nonzero numbers to base 0.")}if(base.equals(-1)){if(n.isZero())return"0";if(n.isNegative())return new Array(1-n).join("10");return"1"+new Array(+n).join("01")}var minusSign="";if(n.isNegative()&&base.isPositive()){minusSign="-";n=n.abs()}if(base.equals(1)){if(n.isZero())return"0";return minusSign+new Array(+n+1).join(1)}var out=[];var left=n,divmod;while(left.isNegative()||left.compareAbs(base)>=0){divmod=left.divmod(base);left=divmod.quotient;var digit=divmod.remainder;if(digit.isNegative()){digit=base.minus(digit).abs();left=left.next()}out.push(stringify(digit))}out.push(stringify(left));return minusSign+out.reverse().join("")}BigInteger.prototype.toString=function(radix){if(radix===undefined)radix=10;if(radix!==10)return toBase(this,radix);var v=this.value,l=v.length,str=String(v[--l]),zeros="0000000",digit;while(--l>=0){digit=String(v[l]);str+=zeros.slice(digit.length)+digit}var sign=this.sign?"-":"";return sign+str};SmallInteger.prototype.toString=function(radix){if(radix===undefined)radix=10;if(radix!=10)return toBase(this,radix);return String(this.value)};BigInteger.prototype.toJSON=SmallInteger.prototype.toJSON=function(){return this.toString()};BigInteger.prototype.valueOf=function(){return+this.toString()};BigInteger.prototype.toJSNumber=BigInteger.prototype.valueOf;SmallInteger.prototype.valueOf=function(){return this.value};SmallInteger.prototype.toJSNumber=SmallInteger.prototype.valueOf;function parseStringValue(v){if(isPrecise(+v)){var x=+v;if(x===truncate(x))return new SmallInteger(x);throw"Invalid integer: "+v}var sign=v[0]==="-";if(sign)v=v.slice(1);var split=v.split(/e/i);if(split.length>2)throw new Error("Invalid integer: "+split.join("e"));if(split.length===2){var exp=split[1];if(exp[0]==="+")exp=exp.slice(1);exp=+exp;if(exp!==truncate(exp)||!isPrecise(exp))throw new Error("Invalid integer: "+exp+" is not a valid exponent.");var text=split[0];var decimalPlace=text.indexOf(".");if(decimalPlace>=0){exp-=text.length-decimalPlace-1;text=text.slice(0,decimalPlace)+text.slice(decimalPlace+1)}if(exp<0)throw new Error("Cannot include negative exponent part for integers");text+=new Array(exp+1).join("0");v=text}var isValid=/^([0-9][0-9]*)$/.test(v);if(!isValid)throw new Error("Invalid integer: "+v);var r=[],max=v.length,l=LOG_BASE,min=max-l;while(max>0){r.push(+v.slice(min,max));min-=l;if(min<0)min=0;max-=l}trim(r);return new BigInteger(r,sign)}function parseNumberValue(v){if(isPrecise(v)){if(v!==truncate(v))throw new Error(v+" is not an integer.");return new SmallInteger(v)}return parseStringValue(v.toString())}function parseValue(v){if(typeof v==="number"){return parseNumberValue(v)}if(typeof v==="string"){return parseStringValue(v)}return v}for(var i=0;i<1e3;i++){Integer[i]=new SmallInteger(i);if(i>0)Integer[-i]=new SmallInteger(-i)}Integer.one=Integer[1];Integer.zero=Integer[0];Integer.minusOne=Integer[-1];Integer.max=max;Integer.min=min;Integer.gcd=gcd;Integer.lcm=lcm;Integer.isInstance=function(x){return x instanceof BigInteger||x instanceof SmallInteger};Integer.randBetween=randBetween;Integer.fromArray=function(digits,base,isNegative){return parseBaseFromArray(digits.map(parseValue),parseValue(base||10),isNegative)};return Integer}();if(typeof module!=="undefined"&&module.hasOwnProperty("exports")){module.exports=bigInt}if(typeof define==="function"&&define.amd){define("big-integer",[],function(){return bigInt})}; bigInt` diff --git a/state/runtime/tracer/jstracer/goja.go b/state/runtime/tracer/jstracer/goja.go new file mode 100644 index 0000000000..8984f4a32c --- /dev/null +++ b/state/runtime/tracer/jstracer/goja.go @@ -0,0 +1,1303 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package js + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/0xPolygon/polygon-edge/crypto" + "github.com/0xPolygon/polygon-edge/helper/common" + "github.com/0xPolygon/polygon-edge/helper/hex" + "github.com/0xPolygon/polygon-edge/state/runtime" + "github.com/0xPolygon/polygon-edge/state/runtime/evm" + "github.com/0xPolygon/polygon-edge/types" + "github.com/dop251/goja" +) + +const ( + memoryPadLimit = 1024 * 1024 +) + +// bigIntProgram is compiled once and the exported function mostly invoked to convert +// hex strings into big ints. +var bigIntProgram = goja.MustCompile("bigInt", bigIntegerJS, false) + +type toBigFn = func(gr *goja.Runtime, val string) (goja.Value, error) +type toBufFn = func(gr *goja.Runtime, val []byte) (goja.Value, error) +type fromBufFn = func(gr *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) + +func toBuf(gr *goja.Runtime, bufType goja.Value, val []byte) (goja.Value, error) { + // bufType is usually Uint8Array. This is equivalent to `new Uint8Array(val)` in JS. + return gr.New(bufType, gr.ToValue(gr.NewArrayBuffer(val))) +} + +func fromBuf(gr *goja.Runtime, bufType goja.Value, buf goja.Value, allowString bool) ([]byte, error) { + obj := buf.ToObject(gr) + switch obj.ClassName() { + case "String": + if !allowString { + break + } + return types.StringToBytes(obj.String()), nil + + case "Array": + var b []byte + if err := gr.ExportTo(buf, &b); err != nil { + return nil, err + } + return b, nil + + case "Object": + if !obj.Get("constructor").SameAs(bufType) { + break + } + b := obj.Export().([]byte) + return b, nil + } + return nil, errors.New("invalid buffer type") +} + +// JSTracer is an implementation of the Tracer interface which evaluates +// JS functions on the relevant EVM hooks. It uses Goja as its JS engine. +type JSTracer struct { + gojaRuntime *goja.Runtime + toBig toBigFn // Converts a hex string into a JS bigint + toBuf toBufFn // Converts a []byte into a JS buffer + fromBuf fromBufFn // Converts an array, hex string or Uint8Array to a []byte + ctx map[string]goja.Value // KV-bag passed to JS in `result` + activePrecompiles []types.Address // List of active precompiles at current block + traceStep bool // True if tracer object exposes a `step()` method + traceFrame bool // True if tracer object exposes the `enter()` and `exit()` methods + gasLimit uint64 // Amount of gas bought for the whole tx + obj *goja.Object // Trace object + + // Methods exposed by tracer + result goja.Callable + fault goja.Callable + step goja.Callable + enter goja.Callable + exit goja.Callable + + // Underlying structs being passed into JS + log *steplog + frame *callframe + frameResult *callframeResult + + // Goja-wrapping of types prepared for JS consumption + logValue goja.Value + dbValue goja.Value + frameValue goja.Value + frameResultValue goja.Value + + cancelLock sync.RWMutex + reason error + stop bool +} + +// NewJsTracer instantiates a new JS tracer instance. code is a +// Javascript snippet which evaluates to an expression returning +// an object with certain methods: +// +// The methods `result` and `fault` are required to be present. +// The methods `step`, `enter`, and `exit` are optional, but note that +// `enter` and `exit` always go together. +func NewJsTracer(code string, ctx *runtime.TracerContext, cfg json.RawMessage) (*JSTracer, error) { + gr := goja.New() + // By default field names are exported to JS as is, i.e. capitalized. + gr.SetFieldNameMapper(goja.UncapFieldNameMapper()) + t := &JSTracer{ + gojaRuntime: gr, + ctx: make(map[string]goja.Value), + } + + t.setTypeConverters() + t.setBuiltinFunctions() + + if ctx == nil { + ctx = new(runtime.TracerContext) + } + + if ctx.BlockHash != (types.Hash{}) { + blockHash, err := t.toBuf(gr, ctx.BlockHash.Bytes()) + if err != nil { + return nil, err + } + t.ctx["blockHash"] = blockHash + if ctx.TxHash != (types.Hash{}) { + t.ctx["txIndex"] = gr.ToValue(ctx.TxIndex) + txHash, err := t.toBuf(gr, ctx.TxHash.Bytes()) + if err != nil { + return nil, err + } + t.ctx["txHash"] = txHash + } + } + + ret, err := gr.RunString("(" + code + ")") + if err != nil { + return nil, err + } + // Check tracer's interface for required and optional methods. + obj := ret.ToObject(gr) + + result, ok := goja.AssertFunction(obj.Get("result")) + if !ok { + return nil, errors.New("trace object must expose a function result()") + } + + fault, ok := goja.AssertFunction(obj.Get("fault")) + if !ok { + return nil, errors.New("trace object must expose a function fault()") + } + + step, ok := goja.AssertFunction(obj.Get("step")) + t.traceStep = ok + enter, hasEnter := goja.AssertFunction(obj.Get("enter")) + exit, hasExit := goja.AssertFunction(obj.Get("exit")) + + if hasEnter != hasExit { + return nil, errors.New("trace object must expose either both or none of enter() and exit()") + } + + t.traceFrame = hasEnter + t.obj = obj + t.step = step + t.enter = enter + t.exit = exit + t.result = result + t.fault = fault + + // Pass in config + if setup, ok := goja.AssertFunction(obj.Get("setup")); ok { + cfgStr := "{}" + + if cfg != nil { + cfgStr = string(cfg) + } + + if _, err := setup(obj, gr.ToValue(cfgStr)); err != nil { + return nil, err + } + } + + // Setup objects carrying data to JS. These are created once and re-used. + t.log = &steplog{ + vm: gr, + op: &opObj{vm: gr}, + memory: &memoryObj{vm: gr, toBig: t.toBig, toBuf: t.toBuf}, + stack: &stackObj{vm: gr, toBig: t.toBig}, + contract: &contractObj{gr: gr, toBig: t.toBig, toBuf: t.toBuf}, + } + + t.frame = &callframe{vm: gr, toBig: t.toBig, toBuf: t.toBuf} + t.frameResult = &callframeResult{vm: gr, toBuf: t.toBuf} + t.frameValue = t.frame.setupObject() + t.frameResultValue = t.frameResult.setupObject() + t.logValue = t.log.setupObject() + + return t, nil +} + +func (t *JSTracer) Cancel(err error) { + t.cancelLock.Lock() + defer t.cancelLock.Unlock() + + t.reason = err + t.stop = true +} + +func (t *JSTracer) cancelled() bool { + t.cancelLock.RLock() + defer t.cancelLock.RUnlock() + + return t.stop +} + +func (t *JSTracer) Clear() { +} + +// TxStart implements the Tracer interface and is invoked at the beginning of +// transaction processing. +func (t *JSTracer) TxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +// TxEnd implements the Tracer interface and is invoked at the end of +// transaction processing. +func (t *JSTracer) TxEnd(restGas uint64) { + t.ctx["gasUsed"] = t.gojaRuntime.ToValue(t.gasLimit - restGas) +} + +func (t *JSTracer) CaptureStart( + from, to types.Address, + callType int, + input []byte, + gas uint64, + value *big.Int, + host runtime.Host) { + t.captureStart(from, to, callType, gas, value, input, host) +} + +func (t *JSTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { + t.captureEnd(output, err) +} + +func (t *JSTracer) CallStart( + depth int, // begins from 1 + from, to types.Address, + callType int, + gas uint64, + value *big.Int, + input []byte, + host runtime.Host) { + t.captureEnter(callType, from, to, input, gas, value) +} + +// captureStart implements the Tracer interface to initialize the tracing operation. +func (t *JSTracer) captureStart( + from, to types.Address, + callType int, + gas uint64, + value *big.Int, + input []byte, + host runtime.Host) { + if t.cancelled() { + return + } + + db := &dbObj{host: host, vm: t.gojaRuntime, toBig: t.toBig, toBuf: t.toBuf, fromBuf: t.fromBuf} + t.dbValue = db.setupObject() + + if runtime.CallTypes[callType] == "CREATE" { + t.ctx["type"] = t.gojaRuntime.ToValue("CREATE") + } else { + t.ctx["type"] = t.gojaRuntime.ToValue("CALL") + } + + fromVal, err := t.toBuf(t.gojaRuntime, from.Bytes()) + if err != nil { + t.Cancel(err) + + return + } + + t.ctx["from"] = fromVal + + toVal, err := t.toBuf(t.gojaRuntime, to.Bytes()) + if err != nil { + return + } + + t.ctx["to"] = toVal + + inputVal, err := t.toBuf(t.gojaRuntime, input) + if err != nil { + t.Cancel(err) + + return + } + + t.ctx["input"] = inputVal + t.ctx["gas"] = t.gojaRuntime.ToValue(t.gasLimit) + + gasPriceBig, err := t.toBig(t.gojaRuntime, host.GetTxContext().GasPrice.String()) + if err != nil { + t.Cancel(err) + + return + } + + t.ctx["gasPrice"] = gasPriceBig + + valueBig, err := t.toBig(t.gojaRuntime, value.String()) + if err != nil { + t.Cancel(err) + + return + } + + t.ctx["value"] = valueBig + t.ctx["block"] = t.gojaRuntime.ToValue(uint64(host.GetTxContext().Number)) + + t.activePrecompiles = host.ActivePrecompiles() +} + +// captureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *JSTracer) captureEnter(typ int, from types.Address, to types.Address, input []byte, gas uint64, value *big.Int) { + if !t.traceFrame { + return + } + + if t.reason != nil { + return + } + + t.frame.typ = runtime.CallTypes[typ] + t.frame.from = from + t.frame.to = to + t.frame.input = common.CopyBytes(input) + t.frame.gas = uint(gas) + t.frame.value = nil + if value != nil { + t.frame.value = new(big.Int).SetBytes(value.Bytes()) + } + + if _, err := t.enter(t.obj, t.frameValue); err != nil { + t.onError("enter", err) + } +} + +func (t *JSTracer) CallEnd(depth int, gasUsed uint64, output []byte, err error) { + t.captureExit(output, gasUsed, err) +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *JSTracer) captureEnd(output []byte, err error) { + if err != nil { + t.ctx["error"] = t.gojaRuntime.ToValue(err.Error()) + } + + outputVal, err := t.toBuf(t.gojaRuntime, output) + if err != nil { + t.reason = err + + return + } + + t.ctx["output"] = outputVal +} + +// captureExit is called when EVM exits a scope, even if the scope didn't execute any code. +func (t *JSTracer) captureExit(output []byte, gasUsed uint64, err error) { + if !t.traceFrame { + return + } + + t.frameResult.gasUsed = uint(gasUsed) + t.frameResult.output = common.CopyBytes(output) + t.frameResult.err = err + + if _, err := t.exit(t.obj, t.frameResultValue); err != nil { + t.onError("exit", err) + } +} + +func (t *JSTracer) CaptureStateBre( + opCode, depth int, + ip, gas, cost uint64, + returnData []byte, + scope *runtime.ScopeContext, + host runtime.Host, + state runtime.VMState, + err error, +) { + // fmt.Println("[GOJA] CaptureState called with op code: " + evm.OpCodeToString[evm.OpCode(opCode)] + " and depth: " + fmt.Sprint(depth)) + + if t.cancelled() { + state.Halt() + + return + } + + if err != nil { + t.log.err = err + // Other log fields have been already set as part of the last CaptureState. + if _, err := t.fault(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("fault", err) + } + + return + } + + if !t.traceStep { + return + } + + contract := state.GetContract() + + log := t.log + log.op.op = opCode + log.memory.memory = scope.Memory + log.stack.stack = scope.Stack + log.contract.contract = contract + log.pc = ip + log.gas = gas + log.cost = cost + log.refund = host.GetRefund() + log.depth = depth + log.err = err + + if _, err := t.step(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("step", err) + } +} + +// CaptureState implements the Tracer interface to trace a single step of VM execution. +func (t *JSTracer) CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host runtime.Host, + state runtime.VMState) { + if t.cancelled() { + state.Halt() + + return + } + + if !t.traceStep { + return + } + + contract := state.GetContract() + + log := t.log + log.memory.memory = memory + log.stack.stack = stack + log.contract.contract = contract + log.pc = 0 + log.refund = host.GetRefund() +} + +// ExecuteState implements the Tracer interface to trace an execution fault +func (t *JSTracer) ExecuteState( + contractAddress types.Address, + ip uint64, + opCode int, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + host runtime.Host) { + if err != nil { + t.log.err = err + // Other log fields have been already set as part of the last CaptureState. + if _, err := t.fault(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("fault", err) + } + } else { + log := t.log + log.gas = availableGas + log.cost = cost + log.op.op = opCode + log.depth = depth + + if _, err := t.step(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("step", err) + } + } +} + +// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error +func (t *JSTracer) GetResult() (interface{}, error) { + ctx := t.gojaRuntime.ToValue(t.ctx) + res, err := t.result(t.obj, ctx, t.dbValue) + if err != nil { + return nil, wrapError("result", err) + } + encoded, err := json.Marshal(res) + if err != nil { + return nil, err + } + return json.RawMessage(encoded), t.reason +} + +// onError is called anytime the running JS code is interrupted +// and returns an error. It in turn pings the EVM to cancel its +// execution. +func (t *JSTracer) onError(context string, err error) { + // `env` is set on CaptureStart which comes before any JS execution. + // So it should be non-nil. + t.Cancel(wrapError(context, err)) +} + +func wrapError(context string, err error) error { + return fmt.Errorf("%v in server-side tracer function '%v'", err, context) +} + +// setBuiltinFunctions injects Go functions which are available to tracers into the environment. +// It depends on type converters having been set up. +func (t *JSTracer) setBuiltinFunctions() { + vm := t.gojaRuntime + // TODO: load console from goja-nodejs + vm.Set("toHex", func(v goja.Value) string { + b, err := t.fromBuf(vm, v, false) + if err != nil { + vm.Interrupt(err) + + return "" + } + + return hex.EncodeToHex(b) + }) + + vm.Set("toWord", func(v goja.Value) goja.Value { + // TODO: add test with []byte len < 32 or > 32 + b, err := t.fromBuf(vm, v, true) + if err != nil { + vm.Interrupt(err) + + return nil + } + + b = types.BytesToHash(b).Bytes() + + res, err := t.toBuf(vm, b) + if err != nil { + vm.Interrupt(err) + + return nil + } + + return res + }) + vm.Set("toAddress", func(v goja.Value) goja.Value { + a, err := t.fromBuf(vm, v, true) + if err != nil { + vm.Interrupt(err) + + return nil + } + + a = types.BytesToAddress(a).Bytes() + + res, err := t.toBuf(vm, a) + if err != nil { + vm.Interrupt(err) + + return nil + } + + return res + }) + vm.Set("toContract", func(from goja.Value, nonce uint) goja.Value { + a, err := t.fromBuf(vm, from, true) + if err != nil { + vm.Interrupt(err) + + return nil + } + + addr := types.BytesToAddress(a) + b := crypto.CreateAddress(addr, uint64(nonce)).Bytes() + + res, err := t.toBuf(vm, b) + if err != nil { + vm.Interrupt(err) + + return nil + } + + return res + }) + vm.Set("toContract2", func(from goja.Value, salt string, initcode goja.Value) goja.Value { + a, err := t.fromBuf(vm, from, true) + if err != nil { + vm.Interrupt(err) + + return nil + } + + addr := types.BytesToAddress(a) + + code, err := t.fromBuf(vm, initcode, true) + if err != nil { + vm.Interrupt(err) + + return nil + } + + code = common.CopyBytes(code) + codeHash := crypto.Keccak256(code) + b := crypto.CreateAddress2(addr, types.StringToHash(salt), codeHash).Bytes() + + res, err := t.toBuf(vm, b) + if err != nil { + vm.Interrupt(err) + + return nil + } + + return res + }) + vm.Set("isPrecompiled", func(v goja.Value) bool { + a, err := t.fromBuf(vm, v, true) + if err != nil { + vm.Interrupt(err) + + return false + } + + addr := types.BytesToAddress(a) + for _, p := range t.activePrecompiles { + if p == addr { + return true + } + } + + return false + }) + vm.Set("slice", func(slice goja.Value, start, end int64) goja.Value { + b, err := t.fromBuf(vm, slice, false) + if err != nil { + vm.Interrupt(err) + + return nil + } + + if start < 0 || start > end || end > int64(len(b)) { + vm.Interrupt(fmt.Sprintf("Tracer accessed out of bound memory: available %d, offset %d, size %d", len(b), start, end-start)) + + return nil + } + + res, err := t.toBuf(vm, b[start:end]) + if err != nil { + vm.Interrupt(err) + + return nil + } + + return res + }) +} + +// setTypeConverters sets up utilities for converting Go types into those +// suitable for JS consumption. +func (t *JSTracer) setTypeConverters() error { + // Inject bigint logic. + // TODO: To be replaced after goja adds support for native JS bigint. + toBigCode, err := t.gojaRuntime.RunProgram(bigIntProgram) + if err != nil { + return err + } + // Used to create JS bigint objects from go. + toBigFn, ok := goja.AssertFunction(toBigCode) + if !ok { + return errors.New("failed to bind bigInt func") + } + + toBigWrapper := func(vm *goja.Runtime, val string) (goja.Value, error) { + return toBigFn(goja.Undefined(), vm.ToValue(val)) + } + + t.toBig = toBigWrapper + // NOTE: We need this workaround to create JS buffers because + // goja doesn't at the moment expose constructors for typed arrays. + // + // Cache uint8ArrayType once to be used every time for less overhead. + uint8ArrayType := t.gojaRuntime.Get("Uint8Array") + + toBufWrapper := func(vm *goja.Runtime, val []byte) (goja.Value, error) { + return toBuf(vm, uint8ArrayType, val) + } + + t.toBuf = toBufWrapper + fromBufWrapper := func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) { + return fromBuf(vm, uint8ArrayType, buf, allowString) + } + + t.fromBuf = fromBufWrapper + + return nil +} + +type opObj struct { + vm *goja.Runtime + op int +} + +func (o *opObj) ToNumber() int { + return o.op +} + +func (o *opObj) ToString() string { + return evm.OpCodeToString[evm.OpCode(o.op)] +} + +func (o *opObj) IsPush() bool { + return evm.PUSH0 <= o.op && o.op <= evm.PUSH32 +} + +func (o *opObj) setupObject() *goja.Object { + obj := o.vm.NewObject() + + obj.Set("toNumber", o.vm.ToValue(o.ToNumber)) + obj.Set("toString", o.vm.ToValue(o.ToString)) + obj.Set("isPush", o.vm.ToValue(o.IsPush)) + + return obj +} + +type memoryObj struct { + memory memory + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn +} + +func (mo *memoryObj) Slice(begin, end int64) goja.Value { + b, err := mo.slice(begin, end) + if err != nil { + mo.vm.Interrupt(err) + + return nil + } + + res, err := mo.toBuf(mo.vm, b) + if err != nil { + mo.vm.Interrupt(err) + + return nil + } + + return res +} + +// slice returns the requested range of memory as a byte slice. +func (mo *memoryObj) slice(begin, end int64) ([]byte, error) { + if end == begin { + return []byte{}, nil + } + + if end < begin || begin < 0 { + return nil, fmt.Errorf("tracer accessed out of bound memory: offset %d, end %d", begin, end) + } + + slice, err := getMemoryCopyPadded(mo.memory, begin, end-begin) + if err != nil { + return nil, err + } + + return slice, nil +} + +func (mo *memoryObj) GetUint(addr int64) goja.Value { + value, err := mo.getUint(addr) + if err != nil { + mo.vm.Interrupt(err) + + return nil + } + + res, err := mo.toBig(mo.vm, value.String()) + if err != nil { + mo.vm.Interrupt(err) + + return nil + } + + return res +} + +// getUint returns the 32 bytes at the specified address interpreted as a uint. +func (mo *memoryObj) getUint(addr int64) (*big.Int, error) { + if mo.memory.Len() < int(addr)+32 || addr < 0 { + return nil, fmt.Errorf("tracer accessed out of bound memory: available %d, offset %d, size %d", mo.memory.Len(), addr, 32) + } + + return new(big.Int).SetBytes(mo.memory.GetPtr(addr, 32)), nil +} + +func (mo *memoryObj) Length() int { + return mo.memory.Len() +} + +func (m *memoryObj) setupObject() *goja.Object { + o := m.vm.NewObject() + + o.Set("slice", m.vm.ToValue(m.Slice)) + o.Set("getUint", m.vm.ToValue(m.GetUint)) + o.Set("length", m.vm.ToValue(m.Length)) + + return o +} + +type stackObj struct { + stack []*big.Int + vm *goja.Runtime + toBig toBigFn +} + +func (s *stackObj) Peek(idx int) goja.Value { + value, err := s.peek(idx) + if err != nil { + s.vm.Interrupt(err) + + return nil + } + + res, err := s.toBig(s.vm, value.String()) + if err != nil { + s.vm.Interrupt(err) + + return nil + } + + return res +} + +// peek returns the nth-from-the-top element of the stack. +func (s *stackObj) peek(idx int) (*big.Int, error) { + if len(s.stack) <= idx || idx < 0 { + return nil, fmt.Errorf("tracer accessed out of bound stack: size %d, index %d", len(s.stack), idx) + } + + return s.stack[len(s.stack)-idx-1], nil +} + +func (s *stackObj) Length() int { + return len(s.stack) +} + +func (s *stackObj) setupObject() *goja.Object { + o := s.vm.NewObject() + o.Set("peek", s.vm.ToValue(s.Peek)) + o.Set("length", s.vm.ToValue(s.Length)) + return o +} + +type dbObj struct { + host runtime.Host + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + fromBuf fromBufFn +} + +func (do *dbObj) GetBalance(addrSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + addr := types.BytesToAddress(a) + value := do.host.GetBalance(addr) + + res, err := do.toBig(do.vm, value.String()) + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + return res +} + +func (do *dbObj) GetNonce(addrSlice goja.Value) uint64 { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + do.vm.Interrupt(err) + + return 0 + } + + addr := types.BytesToAddress(a) + + return do.host.GetNonce(addr) +} + +func (do *dbObj) GetCode(addrSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + addr := types.BytesToAddress(a) + code := do.host.GetCode(addr) + + res, err := do.toBuf(do.vm, code) + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + return res +} + +func (do *dbObj) GetState(addrSlice goja.Value, hashSlice goja.Value) goja.Value { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + addr := types.BytesToAddress(a) + h, err := do.fromBuf(do.vm, hashSlice, false) + + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + hash := types.BytesToHash(h) + state := do.host.GetStorage(addr, hash).Bytes() + res, err := do.toBuf(do.vm, state) + + if err != nil { + do.vm.Interrupt(err) + + return nil + } + + return res +} + +func (do *dbObj) Exists(addrSlice goja.Value) bool { + a, err := do.fromBuf(do.vm, addrSlice, false) + if err != nil { + do.vm.Interrupt(err) + + return false + } + + addr := types.BytesToAddress(a) + + return do.host.AccountExists(addr) +} + +func (do *dbObj) setupObject() *goja.Object { + o := do.vm.NewObject() + + o.Set("getBalance", do.vm.ToValue(do.GetBalance)) + o.Set("getNonce", do.vm.ToValue(do.GetNonce)) + o.Set("getCode", do.vm.ToValue(do.GetCode)) + o.Set("getState", do.vm.ToValue(do.GetState)) + o.Set("exists", do.vm.ToValue(do.Exists)) + + return o +} + +type contractObj struct { + contract *runtime.Contract + gr *goja.Runtime + toBig toBigFn + toBuf toBufFn +} + +func (co *contractObj) GetCaller() goja.Value { + caller := co.contract.Caller.Bytes() + + res, err := co.toBuf(co.gr, caller) + if err != nil { + co.gr.Interrupt(err) + + return nil + } + + return res +} + +func (co *contractObj) GetAddress() goja.Value { + addr := co.contract.Address.Bytes() + + res, err := co.toBuf(co.gr, addr) + if err != nil { + co.gr.Interrupt(err) + + return nil + } + + return res +} + +func (co *contractObj) GetValue() goja.Value { + res, err := co.toBig(co.gr, co.contract.Value.String()) + if err != nil { + co.gr.Interrupt(err) + + return nil + } + + return res +} + +func (co *contractObj) GetInput() goja.Value { + input := common.CopyBytes(co.contract.Input) + + res, err := co.toBuf(co.gr, input) + if err != nil { + co.gr.Interrupt(err) + + return nil + } + + return res +} + +func (c *contractObj) setupObject() *goja.Object { + o := c.gr.NewObject() + + o.Set("getCaller", c.gr.ToValue(c.GetCaller)) + o.Set("getAddress", c.gr.ToValue(c.GetAddress)) + o.Set("getValue", c.gr.ToValue(c.GetValue)) + o.Set("getInput", c.gr.ToValue(c.GetInput)) + + return o +} + +type callframe struct { + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + + typ string + from types.Address + to types.Address + input []byte + gas uint + value *big.Int +} + +func (f *callframe) GetType() string { + return f.typ +} + +func (f *callframe) GetFrom() goja.Value { + from := f.from.Bytes() + + res, err := f.toBuf(f.vm, from) + if err != nil { + f.vm.Interrupt(err) + + return nil + } + + return res +} + +func (f *callframe) GetTo() goja.Value { + to := f.to.Bytes() + res, err := f.toBuf(f.vm, to) + if err != nil { + f.vm.Interrupt(err) + + return nil + } + + return res +} + +func (f *callframe) GetInput() goja.Value { + input := f.input + + res, err := f.toBuf(f.vm, input) + if err != nil { + f.vm.Interrupt(err) + + return nil + } + + return res +} + +func (f *callframe) GetGas() uint { + return f.gas +} + +func (f *callframe) GetValue() goja.Value { + if f.value == nil { + return goja.Undefined() + } + + res, err := f.toBig(f.vm, f.value.String()) + if err != nil { + f.vm.Interrupt(err) + + return nil + } + + return res +} + +func (f *callframe) setupObject() *goja.Object { + o := f.vm.NewObject() + + o.Set("getType", f.vm.ToValue(f.GetType)) + o.Set("getFrom", f.vm.ToValue(f.GetFrom)) + o.Set("getTo", f.vm.ToValue(f.GetTo)) + o.Set("getInput", f.vm.ToValue(f.GetInput)) + o.Set("getGas", f.vm.ToValue(f.GetGas)) + o.Set("getValue", f.vm.ToValue(f.GetValue)) + + return o +} + +type callframeResult struct { + vm *goja.Runtime + toBuf toBufFn + + gasUsed uint + output []byte + err error +} + +func (r *callframeResult) GetGasUsed() uint { + return r.gasUsed +} + +func (r *callframeResult) GetOutput() goja.Value { + res, err := r.toBuf(r.vm, r.output) + if err != nil { + r.vm.Interrupt(err) + + return nil + } + + return res +} + +func (r *callframeResult) GetError() goja.Value { + if r.err != nil { + return r.vm.ToValue(r.err.Error()) + } + + return goja.Undefined() +} + +func (r *callframeResult) setupObject() *goja.Object { + o := r.vm.NewObject() + + o.Set("getGasUsed", r.vm.ToValue(r.GetGasUsed)) + o.Set("getOutput", r.vm.ToValue(r.GetOutput)) + o.Set("getError", r.vm.ToValue(r.GetError)) + + return o +} + +type steplog struct { + vm *goja.Runtime + + op *opObj + memory *memoryObj + stack *stackObj + contract *contractObj + + pc uint64 + gas uint64 + cost uint64 + depth int + refund uint64 + err error +} + +func (l *steplog) GetPC() uint64 { return l.pc } +func (l *steplog) GetGas() uint64 { return l.gas } +func (l *steplog) GetCost() uint64 { return l.cost } +func (l *steplog) GetDepth() int { return l.depth } +func (l *steplog) GetRefund() uint64 { return l.refund } + +func (l *steplog) GetError() goja.Value { + if l.err != nil { + return l.vm.ToValue(l.err.Error()) + } + return goja.Undefined() +} + +func (l *steplog) setupObject() *goja.Object { + o := l.vm.NewObject() + + // Setup basic fields. + o.Set("getPC", l.vm.ToValue(l.GetPC)) + o.Set("getGas", l.vm.ToValue(l.GetGas)) + o.Set("getCost", l.vm.ToValue(l.GetCost)) + o.Set("getDepth", l.vm.ToValue(l.GetDepth)) + o.Set("getRefund", l.vm.ToValue(l.GetRefund)) + o.Set("getError", l.vm.ToValue(l.GetError)) + + // Setup nested objects. + o.Set("op", l.op.setupObject()) + o.Set("stack", l.stack.setupObject()) + o.Set("memory", l.memory.setupObject()) + o.Set("contract", l.contract.setupObject()) + + return o +} + +// getMemoryCopyPadded returns offset + size as a new slice. +// It zero-pads the slice if it extends beyond memory bounds. +func getMemoryCopyPadded(m memory, offset, size int64) ([]byte, error) { + if offset < 0 || size < 0 { + return nil, errors.New("offset or size must not be negative") + } + + if int(offset+size) < m.Len() { // slice fully inside memory + return m.GetCopy(offset, size), nil + } + + paddingNeeded := int(offset+size) - m.Len() + if paddingNeeded > memoryPadLimit { + return nil, fmt.Errorf("reached limit for padding memory slice: %d", paddingNeeded) + } + + cpy := make([]byte, size) + if overlap := int64(m.Len()) - offset; overlap > 0 { + copy(cpy, m.GetPtr(offset, overlap)) + } + + return cpy, nil +} + +type memory []byte + +// GetCopy returns offset + size as a new slice +func (m memory) GetCopy(offset, size int64) (cpy []byte) { + if size == 0 { + return nil + } + + if len(m) > int(offset) { + cpy = make([]byte, size) + copy(cpy, m[offset:offset+size]) + + return + } + + return +} + +// GetPtr returns the offset + size +func (m memory) GetPtr(offset, size int64) []byte { + if size == 0 { + return nil + } + + if len(m) > int(offset) { + return m[offset : offset+size] + } + + return nil +} + +// Len returns the length of the backing slice +func (m memory) Len() int { + return len(m) +} diff --git a/state/runtime/tracer/structtracer/tracer.go b/state/runtime/tracer/structtracer/struct_tracer.go similarity index 90% rename from state/runtime/tracer/structtracer/tracer.go rename to state/runtime/tracer/structtracer/struct_tracer.go index 259eaf5567..8d5cac0899 100644 --- a/state/runtime/tracer/structtracer/tracer.go +++ b/state/runtime/tracer/structtracer/struct_tracer.go @@ -9,7 +9,6 @@ import ( "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/evm" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -113,11 +112,13 @@ func (t *StructTracer) CallStart( gas uint64, value *big.Int, input []byte, + host runtime.Host, ) { } func (t *StructTracer) CallEnd( depth int, + gasUsed uint64, output []byte, err error, ) { @@ -133,8 +134,8 @@ func (t *StructTracer) CaptureState( opCode int, contractAddress types.Address, sp int, - host tracer.RuntimeHost, - state tracer.VMState, + host runtime.Host, + state runtime.VMState, ) { if t.cancelled() { state.Halt() @@ -199,7 +200,7 @@ func (t *StructTracer) captureStorage( opCode int, contractAddress types.Address, sp int, - host tracer.RuntimeHost, + host runtime.Host, ) { if !t.Config.EnableStorage { return @@ -237,16 +238,39 @@ func (t *StructTracer) captureStorage( } } +func (t *StructTracer) CaptureStateBre( + opCode, depth int, + ip, gas, cost uint64, + returnData []byte, + scope *runtime.ScopeContext, + host runtime.Host, + state runtime.VMState, + err error, +) { + +} + +func (t *StructTracer) CaptureStart( + from, to types.Address, + callType int, + input []byte, + gas uint64, + value *big.Int, + host runtime.Host) { +} + +func (t *StructTracer) CaptureEnd(output []byte, gasUsed uint64, err error) {} + func (t *StructTracer) ExecuteState( contractAddress types.Address, ip uint64, - opCode string, + opCode int, availableGas uint64, cost uint64, lastReturnData []byte, depth int, err error, - host tracer.RuntimeHost, + host runtime.Host, ) { var ( errStr string @@ -254,7 +278,8 @@ func (t *StructTracer) ExecuteState( stack []string returnData string storage map[string]string - isCallOp bool = opCode == evm.OpCode(evm.CALL).String() || opCode == evm.OpCode(evm.STATICCALL).String() + opc = evm.OpCodeToString[evm.OpCode(opCode)] + isCallOp bool = opc == evm.OpCode(evm.CALL).String() || opc == evm.OpCode(evm.STATICCALL).String() ) if t.Config.EnableMemory { @@ -311,7 +336,7 @@ func (t *StructTracer) ExecuteState( t.logs, StructLog{ Pc: ip, - Op: opCode, + Op: opc, Gas: availableGas, GasCost: cost, Memory: memory, diff --git a/state/runtime/tracer/structtracer/tracer_test.go b/state/runtime/tracer/structtracer/struct_tracer_test.go similarity index 87% rename from state/runtime/tracer/structtracer/tracer_test.go rename to state/runtime/tracer/structtracer/struct_tracer_test.go index 07b1d2ad5f..cfbdeff806 100644 --- a/state/runtime/tracer/structtracer/tracer_test.go +++ b/state/runtime/tracer/structtracer/struct_tracer_test.go @@ -8,10 +8,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/0xPolygon/polygon-edge/chain" "github.com/0xPolygon/polygon-edge/helper/hex" "github.com/0xPolygon/polygon-edge/state/runtime" "github.com/0xPolygon/polygon-edge/state/runtime/evm" - "github.com/0xPolygon/polygon-edge/state/runtime/tracer" "github.com/0xPolygon/polygon-edge/types" ) @@ -22,6 +22,8 @@ var ( testEmptyConfig = Config{} ) +var _ runtime.VMState = (*mockState)(nil) + type mockState struct { halted bool } @@ -30,11 +32,107 @@ func (m *mockState) Halt() { m.halted = true } +func (m *mockState) GetContract() *runtime.Contract { + return nil +} + +var _ runtime.Host = (*mockHost)(nil) + type mockHost struct { getRefundFn func() uint64 getStorageFunc func(types.Address, types.Hash) types.Hash } +// AccountExists implements runtime.Host. +func (*mockHost) AccountExists(addr types.Address) bool { + panic("unimplemented") +} + +// ActivePrecompiles implements runtime.Host. +func (*mockHost) ActivePrecompiles() []types.Address { + panic("unimplemented") +} + +// Callx implements runtime.Host. +func (*mockHost) Callx(*runtime.Contract, runtime.Host) *runtime.ExecutionResult { + panic("unimplemented") +} + +// EmitLog implements runtime.Host. +func (*mockHost) EmitLog(addr types.Address, topics []types.Hash, data []byte) { + panic("unimplemented") +} + +// Empty implements runtime.Host. +func (*mockHost) Empty(addr types.Address) bool { + panic("unimplemented") +} + +// GetBalance implements runtime.Host. +func (*mockHost) GetBalance(addr types.Address) *big.Int { + panic("unimplemented") +} + +// GetBlockHash implements runtime.Host. +func (*mockHost) GetBlockHash(number int64) types.Hash { + panic("unimplemented") +} + +// GetCode implements runtime.Host. +func (*mockHost) GetCode(addr types.Address) []byte { + panic("unimplemented") +} + +// GetCodeHash implements runtime.Host. +func (*mockHost) GetCodeHash(addr types.Address) types.Hash { + panic("unimplemented") +} + +// GetCodeSize implements runtime.Host. +func (*mockHost) GetCodeSize(addr types.Address) int { + panic("unimplemented") +} + +// GetNonce implements runtime.Host. +func (*mockHost) GetNonce(addr types.Address) uint64 { + panic("unimplemented") +} + +// GetTracer implements runtime.Host. +func (*mockHost) GetTracer() runtime.VMTracer { + panic("unimplemented") +} + +// GetTxContext implements runtime.Host. +func (*mockHost) GetTxContext() runtime.TxContext { + panic("unimplemented") +} + +// Selfdestruct implements runtime.Host. +func (*mockHost) Selfdestruct(addr types.Address, beneficiary types.Address) { + panic("unimplemented") +} + +// SetNonPayable implements runtime.Host. +func (*mockHost) SetNonPayable(nonPayable bool) { + panic("unimplemented") +} + +// SetState implements runtime.Host. +func (*mockHost) SetState(addr types.Address, key types.Hash, value types.Hash) { + panic("unimplemented") +} + +// SetStorage implements runtime.Host. +func (*mockHost) SetStorage(addr types.Address, key types.Hash, value types.Hash, config *chain.ForksInTime) runtime.StorageStatus { + panic("unimplemented") +} + +// Transfer implements runtime.Host. +func (*mockHost) Transfer(from types.Address, to types.Address, amount *big.Int) error { + panic("unimplemented") +} + func (m *mockHost) GetRefund() uint64 { return m.getRefundFn() } @@ -243,6 +341,7 @@ func TestStructTracerCallStart(t *testing.T) { 1024, new(big.Int).SetUint64(10000), []byte("input"), + nil, ) // make sure the method updates nothing @@ -308,7 +407,7 @@ func TestStructTracerCallEnd(t *testing.T) { tracer := NewStructTracer(testEmptyConfig) - tracer.CallEnd(test.depth, test.output, test.err) + tracer.CallEnd(test.depth, 1000, test.output, test.err) assert.Equal( t, @@ -344,12 +443,12 @@ func TestStructTracerCaptureState(t *testing.T) { opCode int contractAddress types.Address sp int - host tracer.RuntimeHost - vmState tracer.VMState + host runtime.Host + vmState runtime.VMState // expected state expectedTracer *StructTracer - expectedVMState tracer.VMState + expectedVMState runtime.VMState }{ { name: "should capture memory", @@ -610,7 +709,7 @@ func TestStructTracerExecuteState(t *testing.T) { var ( contractAddress = types.StringToAddress("1") ip = uint64(2) - opCode = "ADD" + opCode = evm.ADD availableGas = uint64(1000) cost = uint64(100) lastReturnData = []byte("return data") @@ -642,13 +741,13 @@ func TestStructTracerExecuteState(t *testing.T) { // input contractAddress types.Address ip uint64 - opCode string + opCode int availableGas uint64 cost uint64 lastReturnData []byte depth int err error - host tracer.RuntimeHost + host runtime.Host // expected result expected []StructLog @@ -696,7 +795,7 @@ func TestStructTracerExecuteState(t *testing.T) { expected: []StructLog{ { Pc: ip, - Op: opCode, + Op: evm.OpCodeToString[evm.OpCode(opCode)], Gas: availableGas, GasCost: cost, Memory: nil, @@ -734,7 +833,7 @@ func TestStructTracerExecuteState(t *testing.T) { expected: []StructLog{ { Pc: ip, - Op: opCode, + Op: evm.OpCodeToString[evm.OpCode(opCode)], Gas: availableGas, GasCost: cost, Memory: []string{hex.EncodeToString(memory[0])}, @@ -775,7 +874,7 @@ func TestStructTracerExecuteState(t *testing.T) { expected: []StructLog{ { Pc: ip, - Op: opCode, + Op: evm.OpCodeToString[evm.OpCode(opCode)], Gas: availableGas, GasCost: cost, Memory: nil, @@ -838,7 +937,7 @@ func TestStructTracerExecuteState(t *testing.T) { expected: []StructLog{ { Pc: ip, - Op: opCode, + Op: evm.OpCodeToString[evm.OpCode(opCode)], Gas: availableGas, GasCost: cost, Memory: nil, diff --git a/state/runtime/tracer/types.go b/state/runtime/tracer/types.go deleted file mode 100644 index 902d8d8156..0000000000 --- a/state/runtime/tracer/types.go +++ /dev/null @@ -1,70 +0,0 @@ -package tracer - -import ( - "math/big" - - "github.com/0xPolygon/polygon-edge/types" -) - -// RuntimeHost is the interface defining the methods for accessing state by tracer -type RuntimeHost interface { - // GetRefund returns refunded value - GetRefund() uint64 - // GetStorage access the storage slot at the given address and slot hash - GetStorage(types.Address, types.Hash) types.Hash -} - -type VMState interface { - // Halt tells VM to terminate its process - Halt() -} - -type Tracer interface { - // Cancel tells termination of execution and tracing - Cancel(error) - // Clear clears the tracked data - Clear() - // GetResult returns a result based on tracked data - GetResult() (interface{}, error) - - // Tx-level - TxStart(gasLimit uint64) - TxEnd(gasLeft uint64) - - // Call-level - CallStart( - depth int, // begins from 1 - from, to types.Address, - callType int, - gas uint64, - value *big.Int, - input []byte, - ) - CallEnd( - depth int, // begins from 1 - output []byte, - err error, - ) - - // Op-level - CaptureState( - memory []byte, - stack []*big.Int, - opCode int, - contractAddress types.Address, - sp int, - host RuntimeHost, - state VMState, - ) - ExecuteState( - contractAddress types.Address, - ip uint64, - opcode string, - availableGas uint64, - cost uint64, - lastReturnData []byte, - depth int, - err error, - host RuntimeHost, - ) -} diff --git a/state/runtime/types.go b/state/runtime/types.go new file mode 100644 index 0000000000..d336be269e --- /dev/null +++ b/state/runtime/types.go @@ -0,0 +1,114 @@ +package runtime + +import ( + "math/big" + + "github.com/0xPolygon/polygon-edge/types" +) + +var ( + CallTypes = map[int]string{ + 0: "CALL", + 1: "CALLCODE", + 2: "DELEGATECALL", + 3: "STATICCALL", + 4: "CREATE", + 5: "CREATE2", + } +) + +// TracerContext contains some contextual infos for a transaction execution that is not +// available from within the EVM object. +type TracerContext struct { + BlockHash types.Hash // Hash of the block the tx is contained within (zero if dangling tx or call) + BlockNumber *big.Int // Number of the block the tx is contained within (zero if dangling tx or call) + TxIndex int // Index of the transaction within a block (zero if dangling tx or call) + TxHash types.Hash // Hash of the transaction being traced (zero if dangling call) +} + +// ScopeContext contains the things that are per-call, such as stack and memory, +// but not transients like pc and gas +type ScopeContext struct { + Memory []byte + Stack []*big.Int + Sp int + ContractAddr types.Address +} + +type VMState interface { + // Halt tells VM to terminate its process + Halt() + GetContract() *Contract +} + +type Tracer interface { + // Cancel tells termination of execution and tracing + Cancel(error) + // Clear clears the tracked data + Clear() + // GetResult returns a result based on tracked data + GetResult() (interface{}, error) + + // Tx-level + TxStart(gasLimit uint64) + TxEnd(gasLeft uint64) + + // Top call level + CaptureStart( + from, to types.Address, + callType int, + input []byte, + gas uint64, + value *big.Int, + host Host) + + CaptureEnd(output []byte, gasUsed uint64, err error) + + // Call-level + CallStart( + depth int, // begins from 1 + from, to types.Address, + callType int, + gas uint64, + value *big.Int, + input []byte, + host Host, + ) + CallEnd( + depth int, // begins from 1 + gasUsed uint64, + output []byte, + err error, + ) + + // Op-level + CaptureState( + memory []byte, + stack []*big.Int, + opCode int, + contractAddress types.Address, + sp int, + host Host, + state VMState, + ) + ExecuteState( + contractAddress types.Address, + ip uint64, + opcode int, + availableGas uint64, + cost uint64, + lastReturnData []byte, + depth int, + err error, + host Host, + ) + CaptureStateBre( + opCode, depth int, + ip, gas, cost uint64, + returnData []byte, + scope *ScopeContext, + host Host, + state VMState, + err error, + ) +}