diff --git a/.gitignore b/.gitignore index 1f732223..62dfce57 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ vendor/ playground.go /tmp-swagger-gen/ /github.com/ +.DS_Store diff --git a/app/ante/eth.go b/app/ante/eth.go index b354e489..2a9d11b1 100644 --- a/app/ante/eth.go +++ b/app/ante/eth.go @@ -17,6 +17,41 @@ import ( evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" ) +// EthTxPayloadVerificationDecorator validates base tx payload and check some limitations +type EthTxPayloadVerificationDecorator struct { +} + +func NewEthTxPayloadVerificationDecorator() EthTxPayloadVerificationDecorator { + return EthTxPayloadVerificationDecorator{} +} + +// AnteHandle validates msgs count, some signature protection and applied limitations +func (tpvd EthTxPayloadVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + if len(tx.GetMsgs()) > 1 { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidType, "evm transactions only operates with 1 msg") + } + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evmtypes.MsgEthereumTx)(nil)) + } + + ethTx := msgEthTx.AsTransaction() + // EIP-155 only allowed + if !ethTx.Protected() { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrNotSupported, "legacy pre-eip-155 transactions not supported") + } + + // forbid EIP-2930 update with access list + if len(ethTx.AccessList()) > 0 { + return ctx, sdkerrors.Wrapf(sdkerrors.ErrNotSupported, "eip-2930 transactions not supported") + } + } + + return next(ctx, tx, simulate) +} + // EthSigVerificationDecorator validates an ethereum signatures type EthSigVerificationDecorator struct { evmKeeper EVMKeeper diff --git a/app/ante/handler_options.go b/app/ante/handler_options.go index 2c122484..a55f9589 100644 --- a/app/ante/handler_options.go +++ b/app/ante/handler_options.go @@ -48,6 +48,8 @@ func newEthAnteHandler(options HandlerOptions) sdk.AnteHandler { NewEthSetUpContextDecorator(options.EvmKeeper), // outermost AnteDecorator. SetUpContext must be called first NewEthMempoolFeeDecorator(options.EvmKeeper), // Check eth effective gas price against minimal-gas-prices NewEthValidateBasicDecorator(options.EvmKeeper), + // TODO: UNCOMMENT THIS FOR PROD!!!!!! + // NewEthTxPayloadVerificationDecorator(), NewEthSigVerificationDecorator(options.EvmKeeper), NewEthAccountVerificationDecorator(options.AccountKeeper, options.EvmKeeper), NewEthGasConsumeDecorator(options.EvmKeeper, options.MaxTxGasWanted), diff --git a/app/app.go b/app/app.go index b1ec71b7..3fe367cc 100644 --- a/app/app.go +++ b/app/app.go @@ -192,6 +192,10 @@ var ( } ) +type EVMLKeeperApp interface { + GetEVMKeeper() *evmkeeper.Keeper +} + type NewApp struct { *baseapp.BaseApp @@ -751,6 +755,10 @@ func (app *NewApp) GetPotKeeper() potkeeper.Keeper { return app.potKeeper } +func (app *NewApp) GetEVMKeeper() *evmkeeper.Keeper { + return app.evmKeeper +} + // RegisterSwaggerAPI registers swagger route with API Server func RegisterSwaggerAPI(_ client.Context, rtr *mux.Router) { statikFS, err := fs.New() diff --git a/go.mod b/go.mod index 2d2878b2..9d31e1cb 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,17 @@ require ( github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/ibc-go/v3 v3.0.0 github.com/davecgh/go-spew v1.1.1 + github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf github.com/ethereum/go-ethereum v1.10.26 github.com/gogo/protobuf v1.3.3 github.com/golang/protobuf v1.5.2 github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/improbable-eng/grpc-web v0.15.0 github.com/ipfs/go-cid v0.1.0 github.com/pkg/errors v0.9.1 github.com/rakyll/statik v0.1.7 github.com/regen-network/cosmos-proto v0.3.1 - github.com/rs/cors v1.8.2 github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.6.0 github.com/spf13/pflag v1.0.5 @@ -58,6 +57,7 @@ require ( github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/iavl v0.19.4 // indirect github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/creachadair/taskgroup v0.3.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/deckarep/golang-set v1.8.0 // indirect @@ -66,10 +66,12 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.2 // indirect github.com/dgraph-io/ristretto v0.0.3 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect github.com/go-kit/kit v0.12.0 // indirect @@ -77,18 +79,22 @@ require ( github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/gateway v1.1.0 // indirect + github.com/golang-jwt/jwt/v4 v4.3.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.0 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -114,6 +120,7 @@ require ( github.com/minio/highwayhash v1.0.2 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect @@ -134,7 +141,9 @@ require ( github.com/prometheus/tsdb v0.7.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rjeczalik/notify v0.9.1 // indirect + github.com/rs/cors v1.8.2 // indirect github.com/rs/zerolog v1.27.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/afero v1.9.2 // indirect @@ -146,6 +155,8 @@ require ( github.com/tidwall/btree v1.5.0 // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect + github.com/urfave/cli/v2 v2.10.2 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/zondax/hid v0.9.1 // indirect github.com/zondax/ledger-go v0.14.1 // indirect go.etcd.io/bbolt v1.3.6 // indirect diff --git a/go.sum b/go.sum index f3ad16d0..80a95e03 100644 --- a/go.sum +++ b/go.sum @@ -188,7 +188,6 @@ github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76 h1:DdzS1m6o/pCqeZ github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76/go.mod h1:0mkLWIoZuQ7uBoospo5Q9zIpqq6rYCPJDSUdeCJvPM8= github.com/cosmos/ledger-cosmos-go v0.12.2 h1:/XYaBlE2BJxtvpkHiBm97gFGSGmYGKunKyF3nNqAXZA= github.com/cosmos/ledger-cosmos-go v0.12.2/go.mod h1:ZcqYgnfNJ6lAXe4HPtWgarNEY+B74i+2/8MhZw4ziiI= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 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.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -196,6 +195,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= @@ -224,10 +224,15 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf h1:Yt+4K30SdjOkRoRRm3vYNQgR+/ZIy0RmeUDZo7Y8zeQ= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -256,6 +261,7 @@ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8S github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= @@ -299,6 +305,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +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-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -315,6 +323,7 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -427,6 +436,7 @@ github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoP github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -523,10 +533,12 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/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/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= @@ -582,9 +594,11 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -621,7 +635,6 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/neilotoole/errgroup v0.1.5/go.mod h1:Q2nLGf+594h0CLBs/Mbg6qOr7GtqDK7C2S41udRnToE= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -750,7 +763,6 @@ github.com/rs/xhandler v0.0.0-20160618193221-ed27b6fd6521/go.mod h1:RvLn4FgxWubr github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 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= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -858,15 +870,16 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= +github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/vmihailenco/msgpack/v5 v5.1.4/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1281,8 +1294,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/rpc/apis.go b/rpc/apis.go index df3a02e4..ca599896 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -5,11 +5,12 @@ package rpc import ( "fmt" - rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + "github.com/tendermint/tendermint/node" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" + storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/ethereum/go-ethereum/rpc" "github.com/stratosnet/stratos-chain/rpc/backend" @@ -22,6 +23,8 @@ import ( "github.com/stratosnet/stratos-chain/rpc/namespaces/ethereum/txpool" "github.com/stratosnet/stratos-chain/rpc/namespaces/ethereum/web3" "github.com/stratosnet/stratos-chain/rpc/types" + + evmkeeper "github.com/stratosnet/stratos-chain/x/evm/keeper" ) // RPC namespaces and API version @@ -44,16 +47,16 @@ const ( ) // APICreator creates the JSON-RPC API implementations. -type APICreator = func(*server.Context, client.Context, *rpcclient.WSClient) []rpc.API +type APICreator = func(*server.Context, *node.Node, *evmkeeper.Keeper, storetypes.MultiStore, client.Context) []rpc.API // apiCreators defines the JSON-RPC API namespaces. var apiCreators map[string]APICreator func init() { apiCreators = map[string]APICreator{ - EthNamespace: func(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc.API { + EthNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { nonceLock := new(types.AddrLocker) - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: EthNamespace, @@ -64,12 +67,12 @@ func init() { { Namespace: EthNamespace, Version: apiVersion, - Service: filters.NewPublicAPI(ctx.Logger, clientCtx, tmWSClient, evmBackend), + Service: filters.NewPublicAPI(ctx.Logger, clientCtx, tmNode.EventBus(), evmBackend), Public: true, }, } }, - Web3Namespace: func(*server.Context, client.Context, *rpcclient.WSClient) []rpc.API { + Web3Namespace: func(*server.Context, *node.Node, *evmkeeper.Keeper, storetypes.MultiStore, client.Context) []rpc.API { return []rpc.API{ { Namespace: Web3Namespace, @@ -79,18 +82,19 @@ func init() { }, } }, - NetNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { + NetNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: NetNamespace, Version: apiVersion, - Service: net.NewPublicAPI(clientCtx), + Service: net.NewPublicAPI(evmBackend), Public: true, }, } }, - PersonalNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + PersonalNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: PersonalNamespace, @@ -100,18 +104,19 @@ func init() { }, } }, - TxPoolNamespace: func(ctx *server.Context, _ client.Context, _ *rpcclient.WSClient) []rpc.API { + TxPoolNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: TxPoolNamespace, Version: apiVersion, - Service: txpool.NewPublicAPI(ctx.Logger), + Service: txpool.NewPublicAPI(ctx.Logger, clientCtx, evmBackend), Public: true, }, } }, - DebugNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + DebugNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: DebugNamespace, @@ -121,8 +126,8 @@ func init() { }, } }, - MinerNamespace: func(ctx *server.Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { - evmBackend := backend.NewBackend(ctx, ctx.Logger, clientCtx) + MinerNamespace: func(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context) []rpc.API { + evmBackend := backend.NewBackend(ctx, tmNode, evmKeeper, ms, ctx.Logger, clientCtx) return []rpc.API{ { Namespace: MinerNamespace, @@ -136,12 +141,12 @@ func init() { } // GetRPCAPIs returns the list of all APIs -func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, selectedAPIs []string) []rpc.API { +func GetRPCAPIs(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context, selectedAPIs []string) []rpc.API { var apis []rpc.API for _, ns := range selectedAPIs { if creator, ok := apiCreators[ns]; ok { - apis = append(apis, creator(ctx, clientCtx, tmWSClient)...) + apis = append(apis, creator(ctx, tmNode, evmKeeper, ms, clientCtx)...) } else { ctx.Logger.Error("invalid namespace value", "namespace", ns) } diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 7b9aee17..1b6080a0 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -2,11 +2,17 @@ package backend import ( "context" + "fmt" "math/big" "time" + cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/tendermint/tendermint/store" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" @@ -18,15 +24,20 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/stratosnet/stratos-chain/rpc/types" "github.com/stratosnet/stratos-chain/server/config" + evmkeeper "github.com/stratosnet/stratos-chain/x/evm/keeper" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" ) // BackendI implements the Cosmos and EVM backend. type BackendI interface { // nolint: revive CosmosBackend EVMBackend + TMBackend } // CosmosBackend implements the functionality shared within cosmos namespaces @@ -37,6 +48,9 @@ type CosmosBackend interface { // GetAccounts() // SignDirect() // SignAmino() + GetEVMKeeper() *evmkeeper.Keeper + GetSdkContext() sdk.Context + GetSdkContextWithHeader(header *tmtypes.Header) (sdk.Context, error) } // EVMBackend implements the functionality shared within ethereum namespaces @@ -47,30 +61,32 @@ type EVMBackend interface { RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether. - + RPCFilterCap() int32 RPCMinGasPrice() int64 - SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) + SuggestGasTipCap() (*big.Int, error) + RPCLogsCap() int32 + RPCBlockRangeCap() int32 // Blockchain API BlockNumber() (hexutil.Uint64, error) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpctypes.ResultBlock, error) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) - GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) - GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) - BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error) - BlockByHash(blockHash common.Hash) (*ethtypes.Block, error) - CurrentHeader() *ethtypes.Header - HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) - HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) + GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (*types.Block, error) + GetBlockByHash(hash common.Hash, fullTx bool) (*types.Block, error) + CurrentHeader() *types.Header + HeaderByNumber(blockNum types.BlockNumber) (*types.Header, error) + HeaderByHash(blockHash common.Hash) (*types.Header, error) PendingTransactions() ([]*sdk.Tx, error) - GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) + GetTransactionCount(address common.Address, blockNum types.BlockNumber) (hexutil.Uint64, error) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) GetCoinbase() (sdk.AccAddress, error) - GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) - GetTxByEthHash(txHash common.Hash) (*tmrpctypes.ResultTx, error) + GetTransactionByHash(txHash common.Hash) (*types.Transaction, error) + GetTxByHash(txHash common.Hash) (*tmrpctypes.ResultTx, error) GetTxByTxIndex(height int64, txIndex uint) (*tmrpctypes.ResultTx, error) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error) - BaseFee(height int64) (*big.Int, error) + BaseFee() (*big.Int, error) + GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) + BlockBloom(height *int64) (ethtypes.Bloom, error) // Fee API FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*types.FeeHistoryResult, error) @@ -81,32 +97,107 @@ type EVMBackend interface { GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) ChainConfig() *params.ChainConfig SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) - GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx +} + +type TMBackend interface { + // tendermint helpers + GetNode() *node.Node + GetBlockStore() *store.BlockStore + GetMempool() mempool.Mempool + GetConsensusReactor() *cs.Reactor + GetSwitch() *p2p.Switch } var _ BackendI = (*Backend)(nil) // Backend implements the BackendI interface type Backend struct { - ctx context.Context - clientCtx client.Context - queryClient *types.QueryClient // gRPC query client - logger log.Logger - cfg config.Config + ctx context.Context + clientCtx client.Context + tmNode *node.Node // directly tendermint access, new impl + evmkeeper *evmkeeper.Keeper + ms storetypes.MultiStore + logger log.Logger + cfg config.Config } // NewBackend creates a new Backend instance for cosmos and ethereum namespaces -func NewBackend(ctx *server.Context, logger log.Logger, clientCtx client.Context) *Backend { +func NewBackend(ctx *server.Context, tmNode *node.Node, evmkeeper *evmkeeper.Keeper, ms storetypes.MultiStore, logger log.Logger, clientCtx client.Context) *Backend { appConf, err := config.GetConfig(ctx.Viper) if err != nil { panic(err) } return &Backend{ - ctx: context.Background(), - clientCtx: clientCtx, - queryClient: types.NewQueryClient(clientCtx), - logger: logger.With("module", "backend"), - cfg: appConf, + ctx: context.Background(), + clientCtx: clientCtx, + tmNode: tmNode, + evmkeeper: evmkeeper, + ms: ms, + logger: logger.With("module", "backend"), + cfg: appConf, + } +} + +func (b *Backend) GetEVMKeeper() *evmkeeper.Keeper { + return b.evmkeeper +} + +func (b *Backend) copySdkContext(ms storetypes.MultiStore, header *tmtypes.Header) sdk.Context { + sdkCtx := sdk.NewContext(ms, tmproto.Header{}, true, b.logger) + if header != nil { + return sdkCtx.WithHeaderHash( + header.Hash(), + ).WithBlockHeader( + types.FormatTmHeaderToProto(header), + ).WithBlockHeight( + header.Height, + ).WithProposer( + sdk.ConsAddress(header.ProposerAddress), + ) + } + return sdkCtx +} + +func (b *Backend) GetSdkContext() sdk.Context { + return b.copySdkContext(b.ms.CacheMultiStore(), nil) +} + +func (b *Backend) GetSdkContextWithHeader(header *tmtypes.Header) (sdk.Context, error) { + if header == nil { + return b.GetSdkContext(), nil + } + latestHeight := b.GetBlockStore().Height() + if latestHeight == 0 { + return sdk.Context{}, fmt.Errorf("block store not loaded") } + if latestHeight == header.Height { + return b.copySdkContext(b.ms.CacheMultiStore(), header), nil + } + + cms, err := b.ms.CacheMultiStoreWithVersion(header.Height) + if err != nil { + return sdk.Context{}, err + } + return b.copySdkContext(cms, header), nil +} + +func (b *Backend) GetNode() *node.Node { + return b.tmNode +} + +func (b *Backend) GetBlockStore() *store.BlockStore { + return b.tmNode.BlockStore() +} + +func (b *Backend) GetMempool() mempool.Mempool { + return b.tmNode.Mempool() +} + +func (b *Backend) GetConsensusReactor() *cs.Reactor { + return b.tmNode.ConsensusReactor() +} + +func (b *Backend) GetSwitch() *p2p.Switch { + return b.tmNode.Switch() } diff --git a/rpc/backend/evm_backend.go b/rpc/backend/evm_backend.go index 98e0c611..978284e4 100644 --- a/rpc/backend/evm_backend.go +++ b/rpc/backend/evm_backend.go @@ -1,16 +1,12 @@ package backend import ( - "bytes" "encoding/json" "fmt" "math/big" - "strconv" "time" "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" @@ -20,46 +16,36 @@ import ( "github.com/ethereum/go-ethereum/rpc" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" - tmtypes "github.com/tendermint/tendermint/types" + tmjsonrpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/stratosnet/stratos-chain/rpc/types" stratos "github.com/stratosnet/stratos-chain/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" + tmrpccore "github.com/tendermint/tendermint/rpc/core" ) -var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom) - // BlockNumber returns the current block number in abci app state. // Because abci app state could lag behind from tendermint latest block, it's more stable // for the client to use the latest block number in abci app state than tendermint rpc. func (b *Backend) BlockNumber() (hexutil.Uint64, error) { - // do any grpc query, ignore the response and use the returned block height - var header metadata.MD - _, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}, grpc.Header(&header)) + res, err := tmrpccore.Block(nil, nil) if err != nil { return hexutil.Uint64(0), err } - blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) - if headerLen := len(blockHeightHeader); headerLen != 1 { - return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1) - } - - height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse block height: %w", err) + if res.Block == nil { + return hexutil.Uint64(0), errors.Errorf("block store not loaded") } - return hexutil.Uint64(height), nil + return hexutil.Uint64(res.Block.Height), nil } // GetBlockByNumber returns the block identified by number. -func (b *Backend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) { +func (b *Backend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (*types.Block, error) { resBlock, err := b.GetTendermintBlockByNumber(blockNum) if err != nil { return nil, err @@ -70,119 +56,63 @@ func (b *Backend) GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map return nil, nil } - res, err := b.EthBlockFromTendermint(resBlock.Block, fullTx) + res, err := types.EthBlockFromTendermint(b.clientCtx.TxConfig.TxDecoder(), resBlock.Block, fullTx) if err != nil { b.logger.Debug("EthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) - return nil, err + return nil, nil } - return res, nil -} - -// GetBlockByHash returns the block identified by hash. -func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { - resBlock, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) + // override dynamicly miner address + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) if err != nil { - b.logger.Debug("BlockByHash block not found", "hash", hash.Hex(), "error", err.Error()) + b.logger.Debug("GetSdkContextWithHeader context", "height", blockNum, "error", err.Error()) return nil, err } - if resBlock == nil || resBlock.Block == nil { - b.logger.Debug("BlockByHash block not found", "hash", hash.Hex()) - return nil, nil - } - - return b.EthBlockFromTendermint(resBlock.Block, fullTx) -} - -// BlockByNumber returns the block identified by number. -func (b *Backend) BlockByNumber(blockNum types.BlockNumber) (*ethtypes.Block, error) { - height := blockNum.Int64() - - switch blockNum { - case types.EthLatestBlockNumber: - currentBlockNumber, _ := b.BlockNumber() - if currentBlockNumber > 0 { - height = int64(currentBlockNumber) - } - case types.EthPendingBlockNumber: - currentBlockNumber, _ := b.BlockNumber() - if currentBlockNumber > 0 { - height = int64(currentBlockNumber) - } - case types.EthEarliestBlockNumber: - height = 1 - default: - if blockNum < 0 { - return nil, errors.Errorf("incorrect block height: %d", height) - } - } - - resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + validator, err := b.evmkeeper.GetCoinbaseAddress(sdkCtx) if err != nil { - b.logger.Debug("HeaderByNumber failed", "height", height) + b.logger.Debug("GetCoinbaseAddress no validator", "height", blockNum, "error", err.Error()) return nil, err } + res.Miner = validator - if resBlock == nil || resBlock.Block == nil { - return nil, errors.Errorf("block not found for height %d", height) - } - - return b.EthBlockFromTm(resBlock.Block) + return res, nil } -// BlockByHash returns the block identified by hash. -func (b *Backend) BlockByHash(hash common.Hash) (*ethtypes.Block, error) { - resBlock, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) +// GetBlockByHash returns the block identified by hash. +func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (*types.Block, error) { + resBlock, err := tmrpccore.BlockByHash(nil, hash.Bytes()) if err != nil { - b.logger.Debug("HeaderByHash failed", "hash", hash.Hex()) + b.logger.Debug("BlockByHash block not found", "hash", hash.Hex(), "error", err.Error()) return nil, err } if resBlock == nil || resBlock.Block == nil { - return nil, errors.Errorf("block not found for hash %s", hash) + b.logger.Debug("BlockByHash block not found", "hash", hash.Hex()) + return nil, nil } - return b.EthBlockFromTm(resBlock.Block) -} - -func (b *Backend) EthBlockFromTm(block *tmtypes.Block) (*ethtypes.Block, error) { - height := block.Height - bloom, err := b.BlockBloom(&height) + res, err := types.EthBlockFromTendermint(b.clientCtx.TxConfig.TxDecoder(), resBlock.Block, fullTx) if err != nil { - b.logger.Debug("HeaderByNumber BlockBloom failed", "height", height) + b.logger.Debug("EthBlockFromTendermint failed", "hash", hash.Hex(), "error", err.Error()) + return nil, nil } - baseFee, err := b.BaseFee(height) + // override dynamicly miner address + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) if err != nil { - b.logger.Debug("HeaderByNumber BaseFee failed", "height", height, "error", err.Error()) + b.logger.Debug("GetSdkContextWithHeader context", "hash", hash.Hex(), "error", err.Error()) return nil, err } - ethHeader := types.EthHeaderFromTendermint(block.Header, bloom, baseFee) - - var txs []*ethtypes.Transaction - for _, txBz := range block.Txs { - tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) - if err != nil { - b.logger.Debug("failed to decode transaction in block", "height", height, "error", err.Error()) - continue - } - - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - tx := ethMsg.AsTransaction() - txs = append(txs, tx) - } + validator, err := b.evmkeeper.GetCoinbaseAddress(sdkCtx) + if err != nil { + b.logger.Debug("GetCoinbaseAddress no validator", "hash", hash.Hex(), "error", err.Error()) + return nil, err } + res.Miner = validator - // TODO: add tx receipts - ethBlock := ethtypes.NewBlock(ethHeader, txs, nil, nil, nil) - return ethBlock, nil + return res, nil } // GetTendermintBlockByNumber returns a Tendermint format block by block number @@ -210,9 +140,9 @@ func (b *Backend) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpc } } - resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + resBlock, err := tmrpccore.Block(nil, &height) if err != nil { - if resBlock, err = b.clientCtx.Client.Block(b.ctx, nil); err != nil { + if resBlock, err = tmrpccore.Block(nil, nil); err != nil { b.logger.Debug("tendermint client failed to get latest block", "height", height, "error", err.Error()) return nil, nil } @@ -228,7 +158,7 @@ func (b *Backend) GetTendermintBlockByNumber(blockNum types.BlockNumber) (*tmrpc // GetTendermintBlockByHash returns a Tendermint format block by block number func (b *Backend) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { - resBlock, err := b.clientCtx.Client.BlockByHash(b.ctx, blockHash.Bytes()) + resBlock, err := tmrpccore.BlockByHash(nil, blockHash.Bytes()) if err != nil { b.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error()) } @@ -243,148 +173,21 @@ func (b *Backend) GetTendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.R // BlockBloom query block bloom filter from block results func (b *Backend) BlockBloom(height *int64) (ethtypes.Bloom, error) { - result, err := b.clientCtx.Client.BlockResults(b.ctx, height) + result, err := tmrpccore.BlockResults(nil, height) if err != nil { return ethtypes.Bloom{}, err } - for _, event := range result.EndBlockEvents { - if event.Type != evmtypes.EventTypeBlockBloom { - continue - } - - for _, attr := range event.Attributes { - if bytes.Equal(attr.Key, bAttributeKeyEthereumBloom) { - return ethtypes.BytesToBloom(attr.Value), nil - } - } - } - return ethtypes.Bloom{}, errors.New("block bloom event is not found") -} - -// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum block from a given Tendermint block and its block result. -func (b *Backend) EthBlockFromTendermint( - block *tmtypes.Block, - fullTx bool, -) (map[string]interface{}, error) { - ethRPCTxs := []interface{}{} - - ctx := types.ContextWithHeight(block.Height) - - baseFee, err := b.BaseFee(block.Height) - if err != nil { - return nil, err - } - - resBlockResult, err := b.clientCtx.Client.BlockResults(ctx, &block.Height) - if err != nil { - return nil, err - } - - txResults := resBlockResult.TxsResults - txIndex := uint64(0) - - for i, txBz := range block.Txs { - tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) - if err != nil { - b.logger.Debug("failed to decode transaction in block", "height", block.Height, "error", err.Error()) - continue - } - - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - tx := ethMsg.AsTransaction() - - // check tx exists on EVM by cross checking with blockResults - if txResults[i].Code != 0 { - b.logger.Debug("invalid tx result code", "hash", tx.Hash().Hex()) - continue - } - - if !fullTx { - hash := tx.Hash() - ethRPCTxs = append(ethRPCTxs, hash) - continue - } - - rpcTx, err := types.NewRPCTransaction( - tx, - common.BytesToHash(block.Hash()), - uint64(block.Height), - txIndex, - baseFee, - ) - if err != nil { - b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) - continue - } - ethRPCTxs = append(ethRPCTxs, rpcTx) - txIndex++ - } - } - - bloom, err := b.BlockBloom(&block.Height) - if err != nil { - b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) - } - - req := &evmtypes.QueryValidatorAccountRequest{ - ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), - } - - res, err := b.queryClient.ValidatorAccount(ctx, req) - if err != nil { - b.logger.Debug( - "failed to query validator operator address", - "height", block.Height, - "cons-address", req.ConsAddress, - "error", err.Error(), - ) - return nil, err - } - - addr, err := sdk.AccAddressFromBech32(res.AccountAddress) - if err != nil { - return nil, err - } - - validatorAddr := common.BytesToAddress(addr) - - gasLimit, err := types.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) - if err != nil { - b.logger.Error("failed to query consensus params", "error", err.Error()) - } - - gasUsed := uint64(0) - - for _, txsResult := range txResults { - // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 - if txsResult.GetCode() == 11 && txsResult.GetLog() == "no block gas left to run tx: out of gas" { - // block gas limit has exceeded, other txs must have failed with same reason. - break - } - gasUsed += uint64(txsResult.GetGasUsed()) - } - - formattedBlock := types.FormatBlock( - block.Header, block.Size(), - gasLimit, new(big.Int).SetUint64(gasUsed), - ethRPCTxs, bloom, validatorAddr, baseFee, - ) - return formattedBlock, nil + return types.GetBlockBloom(result) } // CurrentHeader returns the latest block header -func (b *Backend) CurrentHeader() *ethtypes.Header { +func (b *Backend) CurrentHeader() *types.Header { header, _ := b.HeaderByNumber(types.EthLatestBlockNumber) return header } // HeaderByNumber returns the block header identified by height. -func (b *Backend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) { +func (b *Backend) HeaderByNumber(blockNum types.BlockNumber) (*types.Header, error) { height := blockNum.Int64() switch blockNum { @@ -406,30 +209,37 @@ func (b *Backend) HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, } } - resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + resBlock, err := tmrpccore.Block(nil, &height) if err != nil { b.logger.Debug("HeaderByNumber failed") return nil, err } - bloom, err := b.BlockBloom(&resBlock.Block.Height) + ethHeader, err := types.EthHeaderFromTendermint(resBlock.Block.Header) if err != nil { - b.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height) + b.logger.Debug("HeaderByNumber EthHeaderFromTendermint failed", "height", resBlock.Block.Height, "error", err.Error()) + return nil, err } - baseFee, err := b.BaseFee(resBlock.Block.Height) + // override dynamicly miner address + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) if err != nil { - b.logger.Debug("HeaderByNumber BaseFee failed", "height", resBlock.Block.Height, "error", err.Error()) + b.logger.Debug("GetSdkContextWithHeader context", "height", blockNum, "error", err.Error()) return nil, err } - ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + validator, err := b.evmkeeper.GetCoinbaseAddress(sdkCtx) + if err != nil { + b.logger.Debug("EthBlockFromTendermint no validator", "height", blockNum, "error", err.Error()) + return nil, err + } + ethHeader.Coinbase = validator return ethHeader, nil } // HeaderByHash returns the block header identified by hash. -func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { - resBlock, err := b.clientCtx.Client.BlockByHash(b.ctx, blockHash.Bytes()) +func (b *Backend) HeaderByHash(blockHash common.Hash) (*types.Header, error) { + resBlock, err := tmrpccore.BlockByHash(nil, blockHash.Bytes()) if err != nil { b.logger.Debug("HeaderByHash failed", "hash", blockHash.Hex()) return nil, err @@ -439,31 +249,35 @@ func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) } - bloom, err := b.BlockBloom(&resBlock.Block.Height) + ethHeader, err := types.EthHeaderFromTendermint(resBlock.Block.Header) if err != nil { - b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) + b.logger.Debug("HeaderByHash EthHeaderFromTendermint failed", "height", resBlock.Block.Height, "error", err.Error()) + return nil, err } - baseFee, err := b.BaseFee(resBlock.Block.Height) + // override dynamicly miner address + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) if err != nil { - b.logger.Debug("HeaderByHash BaseFee failed", "height", resBlock.Block.Height, "error", err.Error()) + b.logger.Debug("GetSdkContextWithHeader context", "hash", blockHash.Hex(), "error", err.Error()) return nil, err } - ethHeader := types.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) + validator, err := b.evmkeeper.GetCoinbaseAddress(sdkCtx) + if err != nil { + b.logger.Debug("EthBlockFromTendermint no validator", "hash", blockHash.Hex(), "error", err.Error()) + return nil, err + } + ethHeader.Coinbase = validator return ethHeader, nil } // PendingTransactions returns the transactions that are in the transaction pool // and have a from address that is one of the accounts this node manages. func (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { - res, err := b.clientCtx.Client.UnconfirmedTxs(b.ctx, nil) - if err != nil { - return nil, err - } + txs := b.GetMempool().ReapMaxTxs(100) - result := make([]*sdk.Tx, 0, len(res.Txs)) - for _, txBz := range res.Txs { + result := make([]*sdk.Tx, 0, len(txs)) + for _, txBz := range txs { tx, err := b.clientCtx.TxConfig.TxDecoder()(txBz) if err != nil { return nil, err @@ -477,7 +291,7 @@ func (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { // GetLogsByHeight returns all the logs from all the ethereum transactions in a block. func (b *Backend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) { // NOTE: we query the state in case the tx result logs are not persisted after an upgrade. - blockRes, err := b.clientCtx.Client.BlockResults(b.ctx, height) + blockRes, err := tmrpccore.BlockResults(nil, height) if err != nil { return nil, err } @@ -497,7 +311,7 @@ func (b *Backend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) { // GetLogs returns all the logs from all the ethereum transactions in a block. func (b *Backend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) { - block, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) + block, err := tmrpccore.BlockByHash(nil, hash.Bytes()) if err != nil { return nil, err } @@ -537,12 +351,7 @@ func (b *Backend) BloomStatus() (uint64, uint64) { // GetCoinbase is the address that staking rewards will be send to (alias for Etherbase). func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { - node, err := b.clientCtx.GetNode() - if err != nil { - return nil, err - } - - status, err := node.Status(b.ctx) + status, err := tmrpccore.Status(nil) if err != nil { return nil, err } @@ -551,7 +360,8 @@ func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { ConsAddress: sdk.ConsAddress(status.ValidatorInfo.Address).String(), } - res, err := b.queryClient.ValidatorAccount(b.ctx, req) + ctx := b.GetSdkContext() + res, err := b.GetEVMKeeper().ValidatorAccount(sdk.WrapSDKContext(ctx), req) if err != nil { return nil, err } @@ -561,115 +371,51 @@ func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { } // GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash -func (b *Backend) GetTransactionByHash(txHash common.Hash) (*types.RPCTransaction, error) { - res, err := b.GetTxByEthHash(txHash) - hexTx := txHash.Hex() +func (b *Backend) GetTransactionByHash(txHash common.Hash) (*types.Transaction, error) { + res, err := b.GetTxByHash(txHash) if err != nil { - // try to find tx in mempool - txs, err := b.PendingTransactions() + // TODO: Get chain id value from genesis + tx, err := types.GetPendingTx(b.clientCtx.TxConfig.TxDecoder(), b.GetMempool(), txHash) if err != nil { - b.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) + b.logger.Debug("tx not found", "hash", txHash, "error", err.Error()) return nil, nil } - - for _, tx := range txs { - msg, err := evmtypes.UnwrapEthereumMsg(tx, txHash) - if err != nil { - // not ethereum tx - continue - } - - if msg.Hash == hexTx { - rpctx, err := types.NewTransactionFromMsg( - msg, - common.Hash{}, - uint64(0), - uint64(0), - b.ChainConfig().ChainID, - ) - if err != nil { - return nil, err - } - return rpctx, nil - } - } - - b.logger.Debug("tx not found", "hash", hexTx) - return nil, nil - } - - if res.TxResult.Code != 0 { - return nil, errors.New("invalid ethereum tx") - } - - msgIndex, attrs := types.FindTxAttributes(res.TxResult.Events, hexTx) - if msgIndex < 0 { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) + return tx, nil } - tx, err := b.clientCtx.TxConfig.TxDecoder()(res.Tx) - if err != nil { + block := b.GetBlockStore().LoadBlock(res.Height) + if block == nil { + b.logger.Debug("eth_getTransactionByHash", "hash", txHash, "block not found") return nil, err } - // the `msgIndex` is inferred from tx events, should be within the bound. - msg, ok := tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - return nil, errors.New("invalid ethereum tx") - } - - block, err := b.clientCtx.Client.Block(b.ctx, &res.Height) - if err != nil { - b.logger.Debug("block not found", "height", res.Height, "error", err.Error()) - return nil, err - } + blockHash := common.BytesToHash(block.Hash()) + blockHeight := uint64(res.Height) + txIndex := uint64(res.Index) - // Try to find txIndex from events - found := false - txIndex, err := types.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) - if err == nil { - found = true - } else { - // Fallback to find tx index by iterating all valid eth transactions - blockRes, err := b.clientCtx.Client.BlockResults(b.ctx, &block.Block.Height) - if err != nil { - return nil, nil - } - msgs := b.GetEthereumMsgsFromTendermintBlock(block, blockRes) - for i := range msgs { - if msgs[i].Hash == hexTx { - txIndex = uint64(i) - found = true - break - } - } - } - if !found { - return nil, errors.New("can't find index of ethereum tx") - } - - return types.NewTransactionFromMsg( - msg, - common.BytesToHash(block.BlockID.Hash.Bytes()), - uint64(res.Height), - txIndex, - b.ChainConfig().ChainID, + return types.TmTxToEthTx( + b.clientCtx.TxConfig.TxDecoder(), + res.Tx, + &blockHash, + &blockHeight, + &txIndex, ) } -// GetTxByEthHash uses `/tx_query` to find transaction by ethereum tx hash -// TODO: Don't need to convert once hashing is fixed on Tendermint -// https://github.com/tendermint/tendermint/issues/6539 -func (b *Backend) GetTxByEthHash(hash common.Hash) (*tmrpctypes.ResultTx, error) { - query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) - resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") +func (b *Backend) GetTxByHash(hash common.Hash) (*tmrpctypes.ResultTx, error) { + resTx, err := tmrpccore.Tx(nil, hash.Bytes(), false) if err != nil { - return nil, err - } - if len(resTxs.Txs) == 0 { - return nil, errors.Errorf("ethereum tx not found for hash %s", hash.Hex()) + query := fmt.Sprintf("%s.%s='%s'", evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyEthereumTxHash, hash.Hex()) + resTxs, err := tmrpccore.TxSearch(new(tmjsonrpctypes.Context), query, false, nil, nil, "") + if err != nil { + return nil, err + } + if len(resTxs.Txs) == 0 { + return nil, errors.Errorf("ethereum tx not found for hash %s", hash.Hex()) + } + return resTxs.Txs[0], nil } - return resTxs.Txs[0], nil + return resTx, nil } // GetTxByTxIndex uses `/tx_query` to find transaction by tx index of valid ethereum txs @@ -678,7 +424,7 @@ func (b *Backend) GetTxByTxIndex(height int64, index uint) (*tmrpctypes.ResultTx height, evmtypes.TypeMsgEthereumTx, evmtypes.AttributeKeyTxIndex, index, ) - resTxs, err := b.clientCtx.Client.TxSearch(b.ctx, query, false, nil, nil, "") + resTxs, err := tmrpccore.TxSearch(new(tmjsonrpctypes.Context), query, false, nil, nil, "") if err != nil { return nil, err } @@ -722,7 +468,8 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e } // Query params to use the EVM denomination - res, err := b.queryClient.QueryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + sdkCtx := b.GetSdkContext() + res, err := b.GetEVMKeeper().Params(sdk.WrapSDKContext(sdkCtx), &evmtypes.QueryParamsRequest{}) if err != nil { b.logger.Error("failed to query evm params", "error", err.Error()) return common.Hash{}, err @@ -743,7 +490,13 @@ func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, e return common.Hash{}, err } - txHash := msg.AsTransaction().Hash() + ethTx := msg.AsTransaction() + if !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted. + return common.Hash{}, errors.New("legacy pre-eip-155 transactions not supported") + } + + txHash := ethTx.Hash() // Broadcast transaction in sync mode (default) // NOTE: If error is encountered on the node, the broadcast will not return an error @@ -778,10 +531,23 @@ func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *ty GasCap: b.RPCGasCap(), } - // From ContextWithHeight: if the provided height is 0, - // it will return an empty context and the gRPC query will use + resBlock, err := b.GetTendermintBlockByNumber(blockNr) + if err != nil { + return 0, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return 0, nil + } + + // it will return an empty context and the sdk.Context will use // the latest block height for querying. - res, err := b.queryClient.EstimateGas(types.ContextWithHeight(blockNr.Int64()), &req) + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return 0, err + } + res, err := b.GetEVMKeeper().EstimateGas(sdk.WrapSDKContext(sdkCtx), &req) if err != nil { return 0, err } @@ -789,26 +555,21 @@ func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *ty } // GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (b *Backend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) { +func (b *Backend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (hexutil.Uint64, error) { // Get nonce (sequence) from account from := sdk.AccAddress(address.Bytes()) accRet := b.clientCtx.AccountRetriever err := accRet.EnsureExists(b.clientCtx, from) if err != nil { - // account doesn't exist yet, return 0 - n := hexutil.Uint64(0) - return &n, nil + return hexutil.Uint64(0), nil } - includePending := blockNum == types.EthPendingBlockNumber - nonce, err := b.getAccountNonce(address, includePending, blockNum.Int64(), b.logger) + nonce, err := b.getAccountNonce(address, blockNum) if err != nil { - return nil, err + return hexutil.Uint64(0), err } - - n := hexutil.Uint64(nonce) - return &n, nil + return hexutil.Uint64(nonce), nil } // RPCGasCap is the global gas cap for eth-call variants. @@ -850,7 +611,8 @@ func (b *Backend) RPCBlockRangeCap() int32 { // the node config. If set value is 0, it will default to 20. func (b *Backend) RPCMinGasPrice() int64 { - evmParams, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + sdkCtx := b.GetSdkContext() + evmParams, err := b.GetEVMKeeper().Params(sdk.WrapSDKContext(sdkCtx), &evmtypes.QueryParamsRequest{}) if err != nil { return stratos.DefaultGasPrice } @@ -866,7 +628,8 @@ func (b *Backend) RPCMinGasPrice() int64 { // ChainConfig returns the latest ethereum chain configuration func (b *Backend) ChainConfig() *params.ChainConfig { - params, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + sdkCtx := b.GetSdkContext() + params, err := b.GetEVMKeeper().Params(sdk.WrapSDKContext(sdkCtx), &evmtypes.QueryParamsRequest{}) if err != nil { return nil } @@ -877,13 +640,15 @@ func (b *Backend) ChainConfig() *params.ChainConfig { // SuggestGasTipCap returns the suggested tip cap // Although we don't support tx prioritization yet, but we return a positive value to help client to // mitigate the base fee changes. -func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { - if baseFee == nil { +func (b *Backend) SuggestGasTipCap() (*big.Int, error) { + baseFee, err := b.BaseFee() + if err != nil { // london hardfork not enabled or feemarket not enabled return big.NewInt(0), nil } - params, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}) + sdkCtx := b.GetSdkContext() + params, err := b.GetEVMKeeper().Params(sdk.WrapSDKContext(sdkCtx), &evmtypes.QueryParamsRequest{}) if err != nil { return nil, err } @@ -909,9 +674,23 @@ func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { // If the base fee is not enabled globally, the query returns nil. // If the London hard fork is not activated at the current height, the query will // return nil. -func (b *Backend) BaseFee(height int64) (*big.Int, error) { +func (b *Backend) BaseFee() (*big.Int, error) { + resBlock, err := b.GetTendermintBlockByNumber(types.EthLatestBlockNumber) + if err != nil { + return nil, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + sdkCtx, err := b.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return nil, err + } // return BaseFee if London hard fork is activated and feemarket is enabled - res, err := b.queryClient.BaseFee(types.ContextWithHeight(height), &evmtypes.QueryBaseFeeRequest{}) + res, err := b.GetEVMKeeper().BaseFee(sdk.WrapSDKContext(sdkCtx), nil) if err != nil { return nil, err } @@ -980,14 +759,14 @@ func (b *Backend) FeeHistory( } // tendermint block result - tendermintBlockResult, err := b.clientCtx.Client.BlockResults(b.ctx, &tendermintblock.Block.Height) + tendermintBlockResult, err := tmrpccore.BlockResults(nil, &tendermintblock.Block.Height) if tendermintBlockResult == nil { b.logger.Debug("block result not found", "height", tendermintblock.Block.Height, "error", err.Error()) return nil, err } oneFeeHistory := types.OneFeeHistory{} - err = b.processBlock(tendermintblock, ðBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) + err = b.processBlock(tendermintblock, ethBlock, rewardPercentiles, tendermintBlockResult, &oneFeeHistory) if err != nil { return nil, err } @@ -1017,36 +796,3 @@ func (b *Backend) FeeHistory( return &feeHistory, nil } - -// GetEthereumMsgsFromTendermintBlock returns all real MsgEthereumTxs from a Tendermint block. -// It also ensures consistency over the correct txs indexes across RPC endpoints -func (b *Backend) GetEthereumMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx { - var result []*evmtypes.MsgEthereumTx - - txResults := blockRes.TxsResults - - for i, tx := range block.Block.Txs { - // check tx exists on EVM by cross checking with blockResults - if txResults[i].Code != 0 { - b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) - continue - } - - tx, err := b.clientCtx.TxConfig.TxDecoder()(tx) - if err != nil { - b.logger.Debug("failed to decode transaction in block", "height", block.Block.Height, "error", err.Error()) - continue - } - - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - result = append(result, ethMsg) - } - } - - return result -} diff --git a/rpc/backend/utils.go b/rpc/backend/utils.go index b2ac112c..81bc8d28 100644 --- a/rpc/backend/utils.go +++ b/rpc/backend/utils.go @@ -8,16 +8,14 @@ import ( "math/big" "sort" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stratosnet/stratos-chain/rpc/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" ) @@ -50,13 +48,18 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac return args, errors.New("latest header is nil") } + baseFee, err := b.BaseFee() + if err != nil { + return args, err + } + // If user specifies both maxPriorityfee and maxFee, then we do not // need to consult the chain for defaults. It's definitely a London tx. if args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil { // In this clause, user left some fields unspecified. - if head.BaseFee != nil && args.GasPrice == nil { + if baseFee != nil && args.GasPrice == nil { if args.MaxPriorityFeePerGas == nil { - tip, err := b.SuggestGasTipCap(head.BaseFee) + tip, err := b.SuggestGasTipCap() if err != nil { return args, err } @@ -66,7 +69,7 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac if args.MaxFeePerGas == nil { gasFeeCap := new(big.Int).Add( (*big.Int)(args.MaxPriorityFeePerGas), - new(big.Int).Mul(head.BaseFee, big.NewInt(2)), + new(big.Int).Mul(baseFee, big.NewInt(2)), ) args.MaxFeePerGas = (*hexutil.Big)(gasFeeCap) } @@ -81,15 +84,15 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac } if args.GasPrice == nil { - price, err := b.SuggestGasTipCap(head.BaseFee) + price, err := b.SuggestGasTipCap() if err != nil { return args, err } - if head.BaseFee != nil { + if baseFee != nil { // The legacy tx gas price suggestion should not add 2x base fee // because all fees are consumed, so it would result in a spiral // upwards. - price.Add(price, head.BaseFee) + price.Add(price, baseFee) } args.GasPrice = (*hexutil.Big)(price) } @@ -107,7 +110,10 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac if args.Nonce == nil { // get the nonce from the account retriever // ignore error in case tge account doesn't exist yet - nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) + nonce, err := b.getAccountNonce(*args.From, types.EthPendingBlockNumber) + if err != nil { + return args, err + } args.Nonce = (*hexutil.Uint64)(&nonce) } @@ -170,64 +176,47 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac // If the pending value is true, it will iterate over the mempool (pending) // txs in order to compute and return the pending tx sequence. // Todo: include the ability to specify a blockNumber -func (b *Backend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) { - queryClient := authtypes.NewQueryClient(b.clientCtx) - res, err := queryClient.Account(types.ContextWithHeight(height), &authtypes.QueryAccountRequest{Address: sdk.AccAddress(accAddr.Bytes()).String()}) - if err != nil { - return 0, err +func (b *Backend) getAccountNonce(address common.Address, height types.BlockNumber) (uint64, error) { + var ( + pendingNonce uint64 + ) + if height == types.EthPendingBlockNumber { + pendingNonce = types.GetPendingTxCountByAddress(b.clientCtx.TxConfig.TxDecoder(), b.GetMempool(), address) } - var acc authtypes.AccountI - if err := b.clientCtx.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { - return 0, err + req := evmtypes.QueryCosmosAccountRequest{ + Address: address.Hex(), } - nonce := acc.GetSequence() + block, err := b.GetTendermintBlockByNumber(height) + if err != nil { + return 0, err + } - if !pending { - return nonce, nil + if block.Block == nil { + return 0, fmt.Errorf("failed to get block for %d height", height) } - // the account retriever doesn't include the uncommitted transactions on the nonce so we need to - // to manually add them. - pendingTxs, err := b.PendingTransactions() + sdkCtx, err := b.GetSdkContextWithHeader(&block.Block.Header) if err != nil { - logger.Error("failed to fetch pending transactions", "error", err.Error()) - return nonce, nil + return 0, err } - - // add the uncommitted txs to the nonce counter - // only supports `MsgEthereumTx` style tx - for _, tx := range pendingTxs { - for _, msg := range (*tx).GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - // not ethereum tx - break - } - - sender, err := ethMsg.GetSender(b.ChainConfig().ChainID) - if err != nil { - continue - } - if sender == accAddr { - nonce++ - } - } + acc, err := b.GetEVMKeeper().CosmosAccount(sdk.WrapSDKContext(sdkCtx), &req) + if err != nil { + return pendingNonce, err } - - return nonce, nil + return acc.GetSequence() + pendingNonce, nil } // output: targetOneFeeHistory func (b *Backend) processBlock( tendermintBlock *tmrpctypes.ResultBlock, - ethBlock *map[string]interface{}, + ethBlock *types.Block, rewardPercentiles []float64, tendermintBlockResult *tmrpctypes.ResultBlockResults, targetOneFeeHistory *types.OneFeeHistory, ) error { blockHeight := tendermintBlock.Block.Height - blockBaseFee, err := b.BaseFee(blockHeight) + blockBaseFee, err := b.BaseFee() if err != nil { return err } @@ -236,15 +225,9 @@ func (b *Backend) processBlock( targetOneFeeHistory.BaseFee = blockBaseFee // set gas used ratio - gasLimitUint64, ok := (*ethBlock)["gasLimit"].(hexutil.Uint64) - if !ok { - return fmt.Errorf("invalid gas limit type: %T", (*ethBlock)["gasLimit"]) - } + gasLimitUint64 := hexutil.Uint64(ethBlock.GasLimit.ToInt().Uint64()) - gasUsedBig, ok := (*ethBlock)["gasUsed"].(*hexutil.Big) - if !ok { - return fmt.Errorf("invalid gas used type: %T", (*ethBlock)["gasUsed"]) - } + gasUsedBig := ethBlock.GasUsed gasusedfloat, _ := new(big.Float).SetInt(gasUsedBig.ToInt()).Float64() diff --git a/rpc/ethereum/pubsub/pubsub.go b/rpc/ethereum/pubsub/pubsub.go deleted file mode 100644 index 4ac7f34c..00000000 --- a/rpc/ethereum/pubsub/pubsub.go +++ /dev/null @@ -1,143 +0,0 @@ -package pubsub - -import ( - "sync" - "sync/atomic" - - "github.com/pkg/errors" - - coretypes "github.com/tendermint/tendermint/rpc/core/types" -) - -type UnsubscribeFunc func() - -type EventBus interface { - AddTopic(name string, src <-chan coretypes.ResultEvent) error - RemoveTopic(name string) - Subscribe(name string) (<-chan coretypes.ResultEvent, UnsubscribeFunc, error) - Topics() []string -} - -type memEventBus struct { - topics map[string]<-chan coretypes.ResultEvent - topicsMux *sync.RWMutex - subscribers map[string]map[uint64]chan<- coretypes.ResultEvent - subscribersMux *sync.RWMutex - currentUniqueID uint64 -} - -func NewEventBus() EventBus { - return &memEventBus{ - topics: make(map[string]<-chan coretypes.ResultEvent), - topicsMux: new(sync.RWMutex), - subscribers: make(map[string]map[uint64]chan<- coretypes.ResultEvent), - subscribersMux: new(sync.RWMutex), - } -} - -func (m *memEventBus) GenUniqueID() uint64 { - return atomic.AddUint64(&m.currentUniqueID, 1) -} - -func (m *memEventBus) Topics() (topics []string) { - m.topicsMux.RLock() - defer m.topicsMux.RUnlock() - - topics = make([]string, 0, len(m.topics)) - for topicName := range m.topics { - topics = append(topics, topicName) - } - - return topics -} - -func (m *memEventBus) AddTopic(name string, src <-chan coretypes.ResultEvent) error { - m.topicsMux.RLock() - _, ok := m.topics[name] - m.topicsMux.RUnlock() - - if ok { - return errors.New("topic already registered") - } - - m.topicsMux.Lock() - m.topics[name] = src - m.topicsMux.Unlock() - - go m.publishTopic(name, src) - - return nil -} - -func (m *memEventBus) RemoveTopic(name string) { - m.topicsMux.Lock() - delete(m.topics, name) - m.topicsMux.Unlock() -} - -func (m *memEventBus) Subscribe(name string) (<-chan coretypes.ResultEvent, UnsubscribeFunc, error) { - m.topicsMux.RLock() - _, ok := m.topics[name] - m.topicsMux.RUnlock() - - if !ok { - return nil, nil, errors.Errorf("topic not found: %s", name) - } - - ch := make(chan coretypes.ResultEvent) - m.subscribersMux.Lock() - defer m.subscribersMux.Unlock() - - id := m.GenUniqueID() - if _, ok := m.subscribers[name]; !ok { - m.subscribers[name] = make(map[uint64]chan<- coretypes.ResultEvent) - } - m.subscribers[name][id] = ch - - unsubscribe := func() { - m.subscribersMux.Lock() - defer m.subscribersMux.Unlock() - delete(m.subscribers[name], id) - } - - return ch, unsubscribe, nil -} - -func (m *memEventBus) publishTopic(name string, src <-chan coretypes.ResultEvent) { - for { - msg, ok := <-src - if !ok { - m.closeAllSubscribers(name) - m.topicsMux.Lock() - delete(m.topics, name) - m.topicsMux.Unlock() - return - } - m.publishAllSubscribers(name, msg) - } -} - -func (m *memEventBus) closeAllSubscribers(name string) { - m.subscribersMux.Lock() - defer m.subscribersMux.Unlock() - - subsribers := m.subscribers[name] - delete(m.subscribers, name) - - for _, sub := range subsribers { - close(sub) - } -} - -func (m *memEventBus) publishAllSubscribers(name string, msg coretypes.ResultEvent) { - m.subscribersMux.RLock() - subsribers := m.subscribers[name] - m.subscribersMux.RUnlock() - - for _, sub := range subsribers { - select { - case sub <- msg: - default: - } - } -} diff --git a/rpc/namespaces/ethereum/debug/api.go b/rpc/namespaces/ethereum/debug/api.go index 363f12ab..93c4908d 100644 --- a/rpc/namespaces/ethereum/debug/api.go +++ b/rpc/namespaces/ethereum/debug/api.go @@ -2,10 +2,12 @@ package debug import ( "bytes" + "context" "encoding/json" "errors" "fmt" "io" + "math/big" "os" "runtime" "runtime/debug" @@ -14,14 +16,19 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/eth/tracers" stderrors "github.com/pkg/errors" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/tendermint/tendermint/libs/log" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" + ethtypes "github.com/ethereum/go-ethereum/core/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -29,9 +36,33 @@ import ( "github.com/stratosnet/stratos-chain/rpc/backend" rpctypes "github.com/stratosnet/stratos-chain/rpc/types" + jstracers "github.com/stratosnet/stratos-chain/x/evm/tracers/js" + nativetracers "github.com/stratosnet/stratos-chain/x/evm/tracers/native" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" + tmrpccore "github.com/tendermint/tendermint/rpc/core" +) + +const ( + // defaultTraceTimeout is the amount of time a single transaction can execute + // by default before being forcefully aborted. + defaultTraceTimeout = 5 * time.Second + + // NOTE: Commented for now. Maybe even not needed as cosmos could obtain state by version + // If we have transaction, we could just take snapshot from previous height, whih should be enough + // defaultTraceReexec is the number of blocks the tracer is willing to go back + // and reexecute to produce missing historical state necessary to run a specific + // trace. + // defaultTraceReexec = uint64(128) ) +// initialize tracers +// NOTE: Required action as some go-ethereum modules incapsulated, so some tracer copy pasted +// so should be periodically checked in order to update/extend functionality +func init() { + jstracers.InitTracer() + nativetracers.InitTracer() +} + // HandlerT keeps track of the cpu profiler and trace execution type HandlerT struct { cpuFilename string @@ -43,133 +74,125 @@ type HandlerT struct { // API is the collection of tracing APIs exposed over the private debugging endpoint. type API struct { - ctx *server.Context - logger log.Logger - backend backend.EVMBackend - clientCtx client.Context - queryClient *rpctypes.QueryClient - handler *HandlerT + ctx *server.Context + logger log.Logger + backend backend.BackendI + clientCtx client.Context + handler *HandlerT } // NewAPI creates a new API definition for the tracing methods of the Ethereum service. func NewAPI( ctx *server.Context, - backend backend.EVMBackend, + backend backend.BackendI, clientCtx client.Context, ) *API { return &API{ - ctx: ctx, - logger: ctx.Logger.With("module", "debug"), - backend: backend, - clientCtx: clientCtx, - queryClient: rpctypes.NewQueryClient(clientCtx), - handler: new(HandlerT), + ctx: ctx, + logger: ctx.Logger.With("module", "debug"), + backend: backend, + clientCtx: clientCtx, + handler: new(HandlerT), } } // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. -func (a *API) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { +func (a *API) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (interface{}, error) { + // Assemble the structured logger or the JavaScript tracer + var ( + tracer tracers.Tracer + err error + timeout = defaultTraceTimeout + ) a.logger.Debug("debug_traceTransaction", "hash", hash) + // Get transaction by hash - transaction, err := a.backend.GetTxByEthHash(hash) + resultTx, err := a.backend.GetTxByHash(hash) if err != nil { - a.logger.Debug("tx not found", "hash", hash) + a.logger.Debug("debug_traceTransaction", "tx not found", "hash", hash) return nil, err } - // check if block number is 0 - if transaction.Height == 0 { - return nil, errors.New("genesis is not traceable") - } - - blk, err := a.backend.GetTendermintBlockByNumber(rpctypes.BlockNumber(transaction.Height)) + tx, err := a.clientCtx.TxConfig.TxDecoder()(resultTx.Tx) if err != nil { - a.logger.Debug("block not found", "height", transaction.Height) + a.logger.Debug("tx not found", "hash", hash) return nil, err } - msgIndex, _ := rpctypes.FindTxAttributes(transaction.TxResult.Events, hash.Hex()) - if msgIndex < 0 { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hash.Hex()) + if len(tx.GetMsgs()) == 0 { + return nil, fmt.Errorf("empty msg") } - // check tx index is not out of bound - if uint32(len(blk.Block.Txs)) < transaction.Index { - a.logger.Debug("tx index out of bounds", "index", transaction.Index, "hash", hash.String(), "height", blk.Block.Height) - return nil, fmt.Errorf("transaction not included in block %v", blk.Block.Height) + ethMsg, ok := tx.GetMsgs()[0].(*evmtypes.MsgEthereumTx) + if !ok { + a.logger.Debug("debug_traceTransaction", "invalid transaction type", "type", fmt.Sprintf("%T", tx)) + return logger.NewStructLogger(nil).GetResult() } - var predecessors []*evmtypes.MsgEthereumTx - for _, txBz := range blk.Block.Txs[:transaction.Index] { - tx, err := a.clientCtx.TxConfig.TxDecoder()(txBz) - if err != nil { - a.logger.Debug("failed to decode transaction in block", "height", blk.Block.Height, "error", err.Error()) - continue - } - for _, msg := range tx.GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - - predecessors = append(predecessors, ethMsg) - } + parentHeight := resultTx.Height - 1 + parentBlock, err := tmrpccore.Block(nil, &parentHeight) + if err != nil || parentBlock.Block == nil { + a.logger.Debug("debug_traceTransaction", "block not found", "height", resultTx.Height) + return nil, err } - - tx, err := a.clientCtx.TxConfig.TxDecoder()(transaction.Tx) - if err != nil { - a.logger.Debug("tx not found", "hash", hash) + currentBlock, err := tmrpccore.Block(nil, &resultTx.Height) + if err != nil || parentBlock.Block == nil { + a.logger.Debug("debug_traceTransaction", "block not found", "height", resultTx.Height) return nil, err } - // add predecessor messages in current cosmos tx - for i := 0; i < msgIndex; i++ { - ethMsg, ok := tx.GetMsgs()[i].(*evmtypes.MsgEthereumTx) - if !ok { - continue - } - predecessors = append(predecessors, ethMsg) + txctx := &tracers.Context{ + BlockHash: common.BytesToHash(currentBlock.Block.Hash()), + TxIndex: int(resultTx.Index), + TxHash: ethMsg.AsTransaction().Hash(), } - ethMessage, ok := tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - a.logger.Debug("invalid transaction type", "type", fmt.Sprintf("%T", tx)) - return nil, fmt.Errorf("invalid transaction type %T", tx) + // Default tracer is the struct logger + tracer = logger.NewStructLogger(config.Config) + if config.Tracer != nil { + tracer, err = tracers.New(*config.Tracer, txctx, config.TracerConfig) + if err != nil { + return nil, err + } } - - traceTxRequest := evmtypes.QueryTraceTxRequest{ - Msg: ethMessage, - Predecessors: predecessors, - BlockNumber: blk.Block.Height, - BlockTime: blk.Block.Time, - BlockHash: common.Bytes2Hex(blk.BlockID.Hash), + // Define a meaningful timeout of a single transaction trace + if config.Timeout != nil { + if timeout, err = time.ParseDuration(*config.Timeout); err != nil { + return nil, err + } } - if config != nil { - traceTxRequest.TraceConfig = config - } + deadlineCtx, cancel := context.WithTimeout(ctx, timeout) + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + } + }() + defer cancel() - // minus one to get the context of block beginning - contextHeight := transaction.Height - 1 - if contextHeight < 1 { - // 0 is a special value in `ContextWithHeight` - contextHeight = 1 + sdkCtx, err := a.backend.GetSdkContextWithHeader(&parentBlock.Block.Header) + if err != nil { + return nil, fmt.Errorf("Failed to load state at height: %d\n", parentHeight) } - traceResult, err := a.queryClient.TraceTx(rpctypes.ContextWithHeight(contextHeight), &traceTxRequest) + + keeper := a.backend.GetEVMKeeper() + + cfg, err := keeper.EVMConfig(sdkCtx) if err != nil { return nil, err } + signer := ethtypes.MakeSigner(cfg.ChainConfig, big.NewInt(parentBlock.Block.Height)) - // Response format is unknown due to custom tracer config param - // More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered - var decodedResult interface{} - err = json.Unmarshal(traceResult.Data, &decodedResult) + msg, err := ethMsg.AsMessage(signer, cfg.BaseFee) if err != nil { return nil, err } - - return decodedResult, nil + if _, err := keeper.ApplyMessage(sdkCtx, msg, tracer, false); err != nil { + return nil, fmt.Errorf("tracing failed: %w", err) + } + return tracer.GetResult() } // TraceBlockByNumber returns the structured logs created during the execution of @@ -246,7 +269,6 @@ func (a *API) traceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConf // 0 is a special value for `ContextWithHeight`. contextHeight = 1 } - ctxWithHeight := rpctypes.ContextWithHeight(int64(contextHeight)) traceBlockRequest := &evmtypes.QueryTraceBlockRequest{ Txs: txsMessages, @@ -256,7 +278,12 @@ func (a *API) traceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConf BlockHash: common.Bytes2Hex(block.BlockID.Hash), } - res, err := a.queryClient.TraceBlock(ctxWithHeight, traceBlockRequest) + sdkCtx, err := a.backend.GetSdkContextWithHeader(&block.Block.Header) + if err != nil { + return nil, err + } + + res, err := a.backend.GetEVMKeeper().TraceBlock(sdk.WrapSDKContext(sdkCtx), traceBlockRequest) if err != nil { return nil, err } @@ -465,7 +492,7 @@ func (a *API) GetHeaderRlp(number uint64) (hexutil.Bytes, error) { // GetBlockRlp retrieves the RLP encoded for of a single block. func (a *API) GetBlockRlp(number uint64) (hexutil.Bytes, error) { - block, err := a.backend.BlockByNumber(rpctypes.BlockNumber(number)) + block, err := a.backend.GetBlockByNumber(rpctypes.BlockNumber(number), true) if err != nil { return nil, err } @@ -475,7 +502,7 @@ func (a *API) GetBlockRlp(number uint64) (hexutil.Bytes, error) { // PrintBlock retrieves a block and returns its pretty printed form. func (a *API) PrintBlock(number uint64) (string, error) { - block, err := a.backend.BlockByNumber(rpctypes.BlockNumber(number)) + block, err := a.backend.GetBlockByNumber(rpctypes.BlockNumber(number), true) if err != nil { return "", err } diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 44b315f4..05243e5e 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -7,6 +7,7 @@ import ( "math" "math/big" + abci "github.com/tendermint/tendermint/abci/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -20,7 +21,6 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -35,19 +35,21 @@ import ( "github.com/stratosnet/stratos-chain/crypto/hd" "github.com/stratosnet/stratos-chain/ethereum/eip712" "github.com/stratosnet/stratos-chain/rpc/backend" + "github.com/stratosnet/stratos-chain/rpc/types" rpctypes "github.com/stratosnet/stratos-chain/rpc/types" stratos "github.com/stratosnet/stratos-chain/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" + mempl "github.com/tendermint/tendermint/mempool" + tmrpccore "github.com/tendermint/tendermint/rpc/core" ) // PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. type PublicAPI struct { ctx context.Context clientCtx client.Context - queryClient *rpctypes.QueryClient chainIDEpoch *big.Int logger log.Logger - backend backend.EVMBackend + backend backend.BackendI nonceLock *rpctypes.AddrLocker signer ethtypes.Signer } @@ -56,7 +58,7 @@ type PublicAPI struct { func NewPublicAPI( logger log.Logger, clientCtx client.Context, - backend backend.EVMBackend, + backend backend.BackendI, nonceLock *rpctypes.AddrLocker, ) *PublicAPI { algos, _ := clientCtx.Keyring.SupportedAlgorithms() @@ -88,7 +90,6 @@ func NewPublicAPI( api := &PublicAPI{ ctx: context.Background(), clientCtx: clientCtx, - queryClient: rpctypes.NewQueryClient(clientCtx), chainIDEpoch: cfg.ChainID, logger: logger.With("client", "json-rpc"), backend: backend, @@ -104,10 +105,6 @@ func (e *PublicAPI) ClientCtx() client.Context { return e.clientCtx } -func (e *PublicAPI) QueryClient() *rpctypes.QueryClient { - return e.queryClient -} - func (e *PublicAPI) Ctx() context.Context { return e.ctx } @@ -121,18 +118,9 @@ func (e *PublicAPI) ProtocolVersion() hexutil.Uint { // ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config. func (e *PublicAPI) ChainId() (*hexutil.Big, error) { // nolint e.logger.Debug("eth_chainId") - // if current block is at or past the EIP-155 replay-protection fork block, return chainID from config - bn, err := e.backend.BlockNumber() - if err != nil { - e.logger.Debug("failed to fetch latest block number", "error", err.Error()) - return (*hexutil.Big)(e.chainIDEpoch), nil - } - - if config := e.backend.ChainConfig(); config.IsEIP155(new(big.Int).SetUint64(uint64(bn))) { - return (*hexutil.Big)(config.ChainID), nil - } - - return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block") + ctx := e.backend.GetSdkContext() + params := e.backend.GetEVMKeeper().GetParams(ctx) + return (*hexutil.Big)(params.ChainConfig.ChainID.BigInt()), nil } // Syncing returns false in case the node is currently not syncing with the network. It can be up to date or has not @@ -145,7 +133,11 @@ func (e *PublicAPI) ChainId() (*hexutil.Big, error) { // nolint func (e *PublicAPI) Syncing() (interface{}, error) { e.logger.Debug("eth_syncing") - status, err := e.clientCtx.Client.Status(e.ctx) + if !e.backend.GetConsensusReactor().WaitSync() { + return false, nil + } + + status, err := tmrpccore.Status(nil) if err != nil { return false, err } @@ -157,9 +149,9 @@ func (e *PublicAPI) Syncing() (interface{}, error) { return map[string]interface{}{ "startingBlock": hexutil.Uint64(status.SyncInfo.EarliestBlockHeight), "currentBlock": hexutil.Uint64(status.SyncInfo.LatestBlockHeight), - // "highestBlock": nil, // NA - // "pulledStates": nil, // NA - // "knownStates": nil, // NA + "highestBlock": nil, // NA + "pulledStates": nil, // NA + "knownStates": nil, // NA }, nil } @@ -194,12 +186,16 @@ func (e *PublicAPI) GasPrice() (*hexutil.Big, error) { result *big.Int err error ) - if head := e.backend.CurrentHeader(); head.BaseFee != nil { - result, err = e.backend.SuggestGasTipCap(head.BaseFee) + baseFee, err := e.backend.BaseFee() + if err != nil { + return nil, err + } + if baseFee != nil { + result, err = e.backend.SuggestGasTipCap() if err != nil { return nil, err } - result = result.Add(result, head.BaseFee) + result = result.Add(result, baseFee) } else { result = big.NewInt(e.backend.RPCMinGasPrice()) } @@ -210,8 +206,7 @@ func (e *PublicAPI) GasPrice() (*hexutil.Big, error) { // MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions. func (e *PublicAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { e.logger.Debug("eth_maxPriorityFeePerGas") - head := e.backend.CurrentHeader() - tipcap, err := e.backend.SuggestGasTipCap(head.BaseFee) + tipcap, err := e.backend.SuggestGasTipCap() if err != nil { return nil, err } @@ -257,21 +252,23 @@ func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.Bl return nil, err } - req := &evmtypes.QueryBalanceRequest{ - Address: address.String(), - } - - res, err := e.queryClient.Balance(rpctypes.ContextWithHeight(blockNum.Int64()), req) + resBlock, err := e.backend.GetTendermintBlockByNumber(blockNum) if err != nil { return nil, err } - val, ok := sdk.NewIntFromString(res.Balance) - if !ok { - return nil, errors.New("invalid balance") + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + sdkCtx, err := e.backend.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return nil, err } + balance := e.backend.GetEVMKeeper().GetBalance(sdkCtx, address) - return (*hexutil.Big)(val.BigInt()), nil + return (*hexutil.Big)(balance), nil } // GetStorageAt returns the contract storage at the given address, block number, and key. @@ -283,26 +280,30 @@ func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHa return nil, err } - req := &evmtypes.QueryStorageRequest{ - Address: address.String(), - Key: key, + resBlock, err := e.backend.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil } - res, err := e.queryClient.Storage(rpctypes.ContextWithHeight(blockNum.Int64()), req) + sdkCtx, err := e.backend.GetSdkContextWithHeader(&resBlock.Block.Header) if err != nil { return nil, err } - - value := common.HexToHash(res.Value) - return value.Bytes(), nil + state := e.backend.GetEVMKeeper().GetState(sdkCtx, address, common.HexToHash(key)) + return state.Bytes(), nil } // GetTransactionCount returns the number of transactions at the given address up to the given block number. -func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) { +func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Uint64, error) { e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) blockNum, err := e.getBlockNumber(blockNrOrHash) if err != nil { - return nil, err + return 0, err } return e.backend.GetTransactionCount(address, blockNum) } @@ -311,7 +312,7 @@ func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rp func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) - block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) + block, err := tmrpccore.BlockByHash(nil, hash.Bytes()) if err != nil { e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) return nil @@ -322,20 +323,14 @@ func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Ui return nil } - blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) - if err != nil { - return nil - } - - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - n := hexutil.Uint(len(ethMsgs)) + n := hexutil.Uint(len(block.Block.Txs)) return &n } // GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. func (e *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64()) - block, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) + block, err := tmrpccore.Block(nil, blockNum.TmHeight()) if err != nil { e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) return nil @@ -346,13 +341,7 @@ func (e *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumb return nil } - blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) - if err != nil { - return nil - } - - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - n := hexutil.Uint(len(ethMsgs)) + n := hexutil.Uint(len(block.Block.Txs)) return &n } @@ -379,7 +368,21 @@ func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.Block Address: address.String(), } - res, err := e.queryClient.Code(rpctypes.ContextWithHeight(blockNum.Int64()), req) + resBlock, err := e.backend.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + sdkCtx, err := e.backend.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return nil, err + } + res, err := e.backend.GetEVMKeeper().Code(sdk.WrapSDKContext(sdkCtx), req) if err != nil { return nil, err } @@ -392,7 +395,7 @@ func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, err e.logger.Debug("eth_getTransactionLogs", "hash", txHash) hexTx := txHash.Hex() - res, err := e.backend.GetTxByEthHash(txHash) + res, err := e.backend.GetTxByHash(txHash) if err != nil { e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) return nil, nil @@ -508,36 +511,53 @@ func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) return common.Hash{}, err } + sdkCtx := e.backend.GetSdkContext() // Query params to use the EVM denomination - res, err := e.queryClient.QueryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{}) - if err != nil { - e.logger.Error("failed to query evm params", "error", err.Error()) - return common.Hash{}, err - } + params := e.backend.GetEVMKeeper().GetParams(sdkCtx) - cosmosTx, err := ethereumTx.BuildTx(e.clientCtx.TxConfig.NewTxBuilder(), res.Params.EvmDenom) + cosmosTx, err := ethereumTx.BuildTx(e.clientCtx.TxConfig.NewTxBuilder(), params.EvmDenom) if err != nil { e.logger.Error("failed to build cosmos tx", "error", err.Error()) return common.Hash{}, err } // Encode transaction by default Tx encoder - txBytes, err := e.clientCtx.TxConfig.TxEncoder()(cosmosTx) + packet, err := e.clientCtx.TxConfig.TxEncoder()(cosmosTx) if err != nil { e.logger.Error("failed to encode eth tx using default encoder", "error", err.Error()) return common.Hash{}, err } - txHash := ethereumTx.AsTransaction().Hash() - - syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync) - rsp, err := syncCtx.BroadcastTx(txBytes) - if rsp != nil && rsp.Code != 0 { - err = sdkerrors.ABCIError(rsp.Codespace, rsp.Code, rsp.RawLog) + ethTx := ethereumTx.AsTransaction() + if !ethTx.Protected() { + // Ensure only eip155 signed transactions are submitted. + return common.Hash{}, errors.New("legacy pre-eip-155 transactions not supported") } - if err != nil { - e.logger.Error("failed to broadcast tx", "error", err.Error()) - return txHash, err + txHash := ethTx.Hash() + + mempool := e.backend.GetMempool() + if e.clientCtx.BroadcastMode == "async" { + e.logger.Info("Use async mode to propagate tx", txHash) + err = mempool.CheckTx(packet, nil, mempl.TxInfo{}) + if err != nil { + return common.Hash{}, err + } + } else { + e.logger.Info("Use sync mode to propagate tx", txHash) + resCh := make(chan *abci.Response, 1) + err = mempool.CheckTx(packet, func(res *abci.Response) { + resCh <- res + }, mempl.TxInfo{}) + if err != nil { + e.logger.Error("failed to send eth tx packet to mempool", "error", err.Error()) + return common.Hash{}, err + } + res := <-resCh + resBrodTx := res.GetCheckTx() + if resBrodTx.Code != 0 { + e.logger.Error("exec failed on check tx", "error", resBrodTx.Log) + return common.Hash{}, fmt.Errorf(resBrodTx.Log) + } } return txHash, nil @@ -658,12 +678,27 @@ func (e *PublicAPI) doCall( GasCap: e.backend.RPCGasCap(), } - // From ContextWithHeight: if the provided height is 0, + resBlock, err := e.backend.GetTendermintBlockByNumber(blockNr) + if err != nil { + return nil, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + sdkCtx, err := e.backend.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return nil, err + } + // it will return an empty context and the gRPC query will use // the latest block height for querying. - ctx := rpctypes.ContextWithHeight(blockNr.Int64()) timeout := e.backend.RPCEVMTimeout() + ctx := sdk.WrapSDKContext(sdkCtx) + // Setup context so it may be canceled the call has completed // or, in case of unmetered gas, setup a context with a timeout. var cancel context.CancelFunc @@ -677,7 +712,7 @@ func (e *PublicAPI) doCall( // this makes sure resources are cleaned up. defer cancel() - res, err := e.queryClient.EthCall(ctx, &req) + res, err := e.backend.GetEVMKeeper().EthCall(ctx, &req) if err != nil { return nil, err } @@ -699,77 +734,50 @@ func (e *PublicAPI) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional * } // GetBlockByHash returns the block identified by hash. -func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { +func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (*types.Block, error) { e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) return e.backend.GetBlockByHash(hash, fullTx) } // GetBlockByNumber returns the block identified by number. -func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { +func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (*types.Block, error) { e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) return e.backend.GetBlockByNumber(ethBlockNum, fullTx) } // GetTransactionByHash returns the transaction identified by hash. -func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) { +func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.Transaction, error) { e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex()) return e.backend.GetTransactionByHash(hash) } // getTransactionByBlockAndIndex is the common code shared by `GetTransactionByBlockNumberAndIndex` and `GetTransactionByBlockHashAndIndex`. -func (e *PublicAPI) getTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { - var msg *evmtypes.MsgEthereumTx - // try /tx_search first - res, err := e.backend.GetTxByTxIndex(block.Block.Height, uint(idx)) - if err == nil { - tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) - if err != nil { - e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) - return nil, nil - } - // find msg index in events - msgIndex := rpctypes.FindTxAttributesByIndex(res.TxResult.Events, uint64(idx)) - if msgIndex < 0 { - e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) - return nil, nil - } - var ok bool - // msgIndex is inferred from tx events, should be within bound. - msg, ok = tx.GetMsgs()[msgIndex].(*evmtypes.MsgEthereumTx) - if !ok { - e.logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx) - return nil, nil - } - } else { - blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &block.Block.Height) - if err != nil { - return nil, nil - } +func (e *PublicAPI) getTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.Transaction, error) { + // return if index out of bounds + if uint64(idx) >= uint64(len(block.Block.Txs)) { + return nil, nil + } - i := int(idx) - ethMsgs := e.backend.GetEthereumMsgsFromTendermintBlock(block, blockRes) - if i >= len(ethMsgs) { - e.logger.Debug("block txs index out of bound", "index", i) - return nil, nil - } + tx := block.Block.Txs[idx] - msg = ethMsgs[i] - } + blockHash := common.BytesToHash(block.Block.Hash()) + blockHeight := uint64(block.Block.Height) + txIndex := uint64(idx) - return rpctypes.NewTransactionFromMsg( - msg, - common.BytesToHash(block.Block.Hash()), - uint64(block.Block.Height), - uint64(idx), - e.chainIDEpoch, + return rpctypes.TmTxToEthTx( + e.clientCtx.TxConfig.TxDecoder(), + tx, + &blockHash, + &blockHeight, + &txIndex, ) } // GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. -func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { +func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.Transaction, error) { e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) - block, err := e.clientCtx.Client.BlockByHash(e.ctx, hash.Bytes()) + block, err := tmrpccore.BlockByHash(nil, hash.Bytes()) if err != nil { e.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) return nil, nil @@ -784,10 +792,10 @@ func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexu } // GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. -func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { +func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.Transaction, error) { e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) - block, err := e.clientCtx.Client.Block(e.ctx, blockNum.TmHeight()) + block, err := tmrpccore.Block(nil, blockNum.TmHeight()) if err != nil { e.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) return nil, nil @@ -802,150 +810,113 @@ func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockN } // GetTransactionReceipt returns the transaction receipt identified by hash. -func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { - hexTx := hash.Hex() - e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) - - res, err := e.backend.GetTxByEthHash(hash) +func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (*rpctypes.TransactionReceipt, error) { + e.logger.Debug("eth_getTransactionReceipt", "hash", hash) + res, err := e.backend.GetTxByHash(hash) if err != nil { - e.logger.Debug("tx not found", "hash", hexTx, "error", err.Error()) return nil, nil } - msgIndex, attrs := rpctypes.FindTxAttributes(res.TxResult.Events, hexTx) - if msgIndex < 0 { - return nil, fmt.Errorf("ethereum tx not found in msgs: %s", hexTx) - } - - resBlock, err := e.clientCtx.Client.Block(e.ctx, &res.Height) - if err != nil { - e.logger.Debug("block not found", "height", res.Height, "error", err.Error()) + block := e.backend.GetBlockStore().LoadBlock(res.Height) + if block == nil { + e.logger.Debug("eth_getTransactionReceipt", "hash", hash, "block not found") return nil, nil } - tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) + blockResults, err := tmrpccore.BlockResults(nil, &block.Height) if err != nil { - e.logger.Debug("decoding failed", "error", err.Error()) - return nil, fmt.Errorf("failed to decode tx: %w", err) + e.logger.Debug("eth_getTransactionReceipt", "hash", hash, "block not found") + return nil, nil } - // the `msgIndex` is inferred from tx events, should be within the bound. - msg := tx.GetMsgs()[msgIndex] - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - e.logger.Debug(fmt.Sprintf("invalid tx type: %T", msg)) - return nil, fmt.Errorf("invalid tx type: %T", msg) - } + blockHash := common.BytesToHash(block.Hash()) + blockHeight := uint64(res.Height) + txIndex := uint64(res.Index) - txData, err := evmtypes.UnpackTxData(ethMsg.Data) + rpcTx, err := rpctypes.TmTxToEthTx( + e.clientCtx.TxConfig.TxDecoder(), + res.Tx, + &blockHash, + &blockHeight, + &txIndex, + ) if err != nil { - e.logger.Error("failed to unpack tx data", "error", err.Error()) return nil, err } - cumulativeGasUsed := uint64(0) - blockRes, err := e.clientCtx.Client.BlockResults(e.ctx, &res.Height) - if err != nil { - e.logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error()) - return nil, nil + cumulativeGasUsed := uint64(res.TxResult.GasUsed) + if *rpcTx.TransactionIndex != 0 { + cumulativeGasUsed += rpctypes.GetBlockCumulativeGas(blockResults, int(*rpcTx.TransactionIndex)) } - for i := 0; i < int(res.Index) && i < len(blockRes.TxsResults); i++ { - cumulativeGasUsed += uint64(blockRes.TxsResults[i].GasUsed) - } - cumulativeGasUsed += rpctypes.AccumulativeGasUsedOfMsg(res.TxResult.Events, msgIndex) + _, attrs := rpctypes.FindTxAttributes(res.TxResult.Events, hash.Hex()) - var gasUsed uint64 - if len(tx.GetMsgs()) == 1 { - // backward compatibility - gasUsed = uint64(res.TxResult.GasUsed) + var ( + contractAddress *common.Address + bloom = ethtypes.BytesToBloom(make([]byte, 6)) + logs = make([]*ethtypes.Log, 0) + ) + // Set status codes based on tx result + status := ethtypes.ReceiptStatusSuccessful + if res.TxResult.GetCode() == 1 { + status = ethtypes.ReceiptStatusFailed } else { - gasUsed, err = rpctypes.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxGasUsed) - if err != nil { - return nil, err + // Get the transaction result from the log + _, found := attrs[evmtypes.AttributeKeyEthereumTxFailed] + if found { + status = ethtypes.ReceiptStatusFailed } - } - // Get the transaction result from the log - _, found := attrs[evmtypes.AttributeKeyEthereumTxFailed] - var status hexutil.Uint - if found { - status = hexutil.Uint(ethtypes.ReceiptStatusFailed) - } else { - status = hexutil.Uint(ethtypes.ReceiptStatusSuccessful) - } - - from, err := ethMsg.GetSender(e.chainIDEpoch) - if err != nil { - return nil, err - } - - // parse tx logs from events - logs, err := backend.TxLogsFromEvents(res.TxResult.Events, msgIndex) - if err != nil { - e.logger.Debug("logs not found", "hash", hexTx, "error", err.Error()) - } - - // Try to find txIndex from events - found = false - txIndex, err := rpctypes.GetUint64Attribute(attrs, evmtypes.AttributeKeyTxIndex) - if err == nil { - found = true - } else { - // Fallback to find tx index by iterating all valid eth transactions - msgs := e.backend.GetEthereumMsgsFromTendermintBlock(resBlock, blockRes) - for i := range msgs { - if msgs[i].Hash == hexTx { - txIndex = uint64(i) - found = true - break + if status == ethtypes.ReceiptStatusSuccessful { + // parse tx logs from events + logs, err = backend.TxLogsFromEvents(res.TxResult.Events, 0) + if err != nil { + e.logger.Debug("logs not found", "hash", hash, "error", err.Error()) + } + if logs == nil { + logs = make([]*ethtypes.Log, 0) + } + if rpcTx.To == nil { + // TODO: Rewrite on more optimal way in order to get a contract address + tx, err := e.clientCtx.TxConfig.TxDecoder()(res.Tx) + if err != nil { + e.logger.Debug("decoding failed", "error", err.Error()) + return nil, fmt.Errorf("failed to decode tx: %w", err) + } + + // the `msgIndex` is inferred from tx events, should be within the bound. + msg := tx.GetMsgs()[0] + ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) + if !ok { + e.logger.Debug(fmt.Sprintf("invalid tx type: %T", msg)) + return nil, fmt.Errorf("invalid tx type: %T", msg) + } + + txData, err := evmtypes.UnpackTxData(ethMsg.Data) + if err != nil { + e.logger.Error("failed to unpack tx data", "error", err.Error()) + return nil, err + } + contractAddress = new(common.Address) + *contractAddress = crypto.CreateAddress(rpcTx.From, txData.GetNonce()) } + bloom = ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)) } } - if !found { - return nil, errors.New("can't find index of ethereum tx") - } - - receipt := map[string]interface{}{ - // Consensus fields: These fields are defined by the Yellow Paper - "status": status, - "cumulativeGasUsed": hexutil.Uint64(cumulativeGasUsed), - "logsBloom": ethtypes.BytesToBloom(ethtypes.LogsBloom(logs)), - "logs": logs, - - // Implementation fields: These fields are added by geth when processing a transaction. - // They are stored in the chain database. - "transactionHash": hash, - "contractAddress": nil, - "gasUsed": hexutil.Uint64(gasUsed), - "type": hexutil.Uint(txData.TxType()), - // Inclusion information: These fields provide information about the inclusion of the - // transaction corresponding to this receipt. - "blockHash": common.BytesToHash(resBlock.Block.Header.Hash()).Hex(), - "blockNumber": hexutil.Uint64(res.Height), - "transactionIndex": hexutil.Uint64(txIndex), - - // sender and receiver (contract or EOA) addreses - "from": from, - "to": txData.GetTo(), - } - - if logs == nil { - receipt["logs"] = [][]*ethtypes.Log{} - } - - // If the ContractAddress is 20 0x0 bytes, assume it is not a contract creation - if txData.GetTo() == nil { - receipt["contractAddress"] = crypto.CreateAddress(from, txData.GetNonce()) - } - - if dynamicTx, ok := txData.(*evmtypes.DynamicFeeTx); ok { - baseFee, err := e.backend.BaseFee(res.Height) - if err != nil { - return nil, err - } - receipt["effectiveGasPrice"] = hexutil.Big(*dynamicTx.GetEffectiveGasPrice(baseFee)) + receipt := &rpctypes.TransactionReceipt{ + Status: hexutil.Uint64(status), + CumulativeGasUsed: hexutil.Uint64(cumulativeGasUsed), + LogsBloom: bloom, + Logs: logs, + TransactionHash: rpcTx.Hash, + ContractAddress: contractAddress, + GasUsed: hexutil.Uint64(res.TxResult.GasUsed), + BlockHash: *rpcTx.BlockHash, + BlockNumber: *rpcTx.BlockNumber, + TransactionIndex: *rpcTx.TransactionIndex, + From: rpcTx.From, + To: rpcTx.To, } return receipt, nil @@ -953,36 +924,19 @@ func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interfac // GetPendingTransactions returns the transactions that are in the transaction pool // and have a from address that is one of the accounts this node manages. -func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) { +func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.Transaction, error) { e.logger.Debug("eth_getPendingTransactions") - txs, err := e.backend.PendingTransactions() - if err != nil { - return nil, err - } + txs := e.backend.GetMempool().ReapMaxTxs(100) - result := make([]*rpctypes.RPCTransaction, 0, len(txs)) + result := make([]*rpctypes.Transaction, 0, len(txs)) for _, tx := range txs { - for _, msg := range (*tx).GetMsgs() { - ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) - if !ok { - // not valid ethereum tx - break - } - - rpctx, err := rpctypes.NewTransactionFromMsg( - ethMsg, - common.Hash{}, - uint64(0), - uint64(0), - e.chainIDEpoch, - ) - if err != nil { - return nil, err - } - - result = append(result, rpctx) + rpctx, err := rpctypes.TmTxToEthTx(e.clientCtx.TxConfig.TxDecoder(), tx, nil, nil, nil) + if err != nil { + return nil, err } + + result = append(result, rpctx) } return result, nil @@ -1008,7 +962,6 @@ func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, block } height := blockNum.Int64() - ctx := rpctypes.ContextWithHeight(height) // if the height is equal to zero, meaning the query condition of the block is either "pending" or "latest" if height == 0 { @@ -1024,14 +977,27 @@ func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, block height = int64(bn) } - clientCtx := e.clientCtx.WithHeight(height) - // query storage proofs storageProofs := make([]rpctypes.StorageResult, len(storageKeys)) + resBlock, err := e.backend.GetTendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + // return if requested block height is greater than the current one or chain not synced + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + sdkCtx, err := e.backend.GetSdkContextWithHeader(&resBlock.Block.Header) + if err != nil { + return nil, err + } + for i, key := range storageKeys { hexKey := common.HexToHash(key) - valueBz, proof, err := e.queryClient.GetProof(clientCtx, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) + valueBz, proof, err := types.GetProof(height, evmtypes.StoreKey, evmtypes.StateKey(address, hexKey.Bytes())) if err != nil { return nil, err } @@ -1050,18 +1016,11 @@ func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, block } // query EVM account - req := &evmtypes.QueryAccountRequest{ - Address: address.String(), - } - - res, err := e.queryClient.Account(ctx, req) - if err != nil { - return nil, err - } + acc := e.backend.GetEVMKeeper().GetAccountOrEmpty(sdkCtx, address) // query account proofs accountKey := authtypes.AddressStoreKey(sdk.AccAddress(address.Bytes())) - _, proof, err := e.queryClient.GetProof(clientCtx, authtypes.StoreKey, accountKey) + _, proof, err := types.GetProof(height, authtypes.StoreKey, accountKey) if err != nil { return nil, err } @@ -1072,17 +1031,12 @@ func (e *PublicAPI) GetProof(address common.Address, storageKeys []string, block accProofStr = proof.String() } - balance, ok := sdk.NewIntFromString(res.Balance) - if !ok { - return nil, errors.New("invalid balance") - } - return &rpctypes.AccountResult{ Address: address, AccountProof: []string{accProofStr}, - Balance: (*hexutil.Big)(balance.BigInt()), - CodeHash: common.HexToHash(res.CodeHash), - Nonce: hexutil.Uint64(res.Nonce), + Balance: (*hexutil.Big)(acc.Balance), + CodeHash: common.BytesToHash(acc.CodeHash), + Nonce: hexutil.Uint64(acc.Nonce), StorageHash: common.Hash{}, // NOTE: stratos doesn't have a storage hash. TODO: implement? StorageProof: storageProofs, }, nil diff --git a/rpc/namespaces/ethereum/eth/filters/api.go b/rpc/namespaces/ethereum/eth/filters/api.go index 11f6867c..ccadf761 100644 --- a/rpc/namespaces/ethereum/eth/filters/api.go +++ b/rpc/namespaces/ethereum/eth/filters/api.go @@ -8,7 +8,6 @@ import ( "github.com/tendermint/tendermint/libs/log" coretypes "github.com/tendermint/tendermint/rpc/core/types" - rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" tmtypes "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/client" @@ -18,26 +17,11 @@ import ( "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/rpc" + "github.com/stratosnet/stratos-chain/rpc/backend" "github.com/stratosnet/stratos-chain/rpc/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" ) -// Backend defines the methods requided by the PublicFilterAPI backend -type Backend interface { - GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error) - HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error) - HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) - GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error) - GetLogsByNumber(blockNum types.BlockNumber) ([][]*ethtypes.Log, error) - BlockBloom(height *int64) (ethtypes.Bloom, error) - - BloomStatus() (uint64, uint64) - - RPCFilterCap() int32 - RPCLogsCap() int32 - RPCBlockRangeCap() int32 -} - // consider a filter inactive if it has not been polled for within deadline var deadline = 5 * time.Minute @@ -57,21 +41,21 @@ type filter struct { type PublicFilterAPI struct { logger log.Logger clientCtx client.Context - backend Backend + backend backend.BackendI events *EventSystem filtersMu sync.Mutex filters map[rpc.ID]*filter } // NewPublicAPI returns a new PublicFilterAPI instance. -func NewPublicAPI(logger log.Logger, clientCtx client.Context, tmWSClient *rpcclient.WSClient, backend Backend) *PublicFilterAPI { +func NewPublicAPI(logger log.Logger, clientCtx client.Context, eventBus *tmtypes.EventBus, b backend.BackendI) *PublicFilterAPI { logger = logger.With("api", "filter") api := &PublicFilterAPI{ logger: logger, clientCtx: clientCtx, - backend: backend, + backend: b, filters: make(map[rpc.ID]*filter), - events: NewEventSystem(logger, tmWSClient), + events: NewEventSystem(clientCtx, logger, eventBus), } go api.timeoutLoop() @@ -109,9 +93,6 @@ func (api *PublicFilterAPI) timeoutLoop() { // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newPendingTransactionFilter func (api *PublicFilterAPI) NewPendingTransactionFilter() rpc.ID { - api.filtersMu.Lock() - defer api.filtersMu.Unlock() - if len(api.filters) >= int(api.backend.RPCFilterCap()) { return rpc.ID("error creating pending tx filter: max limit reached") } @@ -121,42 +102,32 @@ func (api *PublicFilterAPI) NewPendingTransactionFilter() rpc.ID { // wrap error on the ID return rpc.ID(fmt.Sprintf("error creating pending tx filter: %s", err.Error())) } - + api.filtersMu.Lock() api.filters[pendingTxSub.ID()] = &filter{typ: filters.PendingTransactionsSubscription, deadline: time.NewTimer(deadline), hashes: make([]common.Hash, 0), s: pendingTxSub} + api.filtersMu.Unlock() go func(txsCh <-chan coretypes.ResultEvent, errCh <-chan error) { defer cancelSubs() for { select { - case ev, ok := <-txsCh: - if !ok { - api.filtersMu.Lock() - delete(api.filters, pendingTxSub.ID()) - api.filtersMu.Unlock() - return - } - + case ev := <-txsCh: data, ok := ev.Data.(tmtypes.EventDataTx) if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventTx) + pendingTxSub.err <- err + return } - tx, err := api.clientCtx.TxConfig.TxDecoder()(data.Tx) + txHash, err := types.GetTxHash(api.clientCtx.TxConfig.TxDecoder(), tmtypes.Tx(data.Tx)) if err != nil { - api.logger.Debug("fail to decode tx", "error", err.Error()) - continue + pendingTxSub.err <- err + return } api.filtersMu.Lock() if f, found := api.filters[pendingTxSub.ID()]; found { - for _, msg := range tx.GetMsgs() { - ethTx, ok := msg.(*evmtypes.MsgEthereumTx) - if ok { - f.hashes = append(f.hashes, common.HexToHash(ethTx.Hash)) - } - } + f.hashes = append(f.hashes, txHash) } api.filtersMu.Unlock() case <-errCh: @@ -195,31 +166,25 @@ func (api *PublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Su for { select { - case ev, ok := <-txsCh: - if !ok { - api.filtersMu.Lock() - delete(api.filters, pendingTxSub.ID()) - api.filtersMu.Unlock() - return - } - + case ev := <-txsCh: data, ok := ev.Data.(tmtypes.EventDataTx) if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventTx) + pendingTxSub.err <- err + return } - tx, err := api.clientCtx.TxConfig.TxDecoder()(data.Tx) + txHash, err := types.GetTxHash(api.clientCtx.TxConfig.TxDecoder(), tmtypes.Tx(data.Tx)) if err != nil { - api.logger.Debug("fail to decode tx", "error", err.Error()) - continue + pendingTxSub.err <- err + return } - for _, msg := range tx.GetMsgs() { - ethTx, ok := msg.(*evmtypes.MsgEthereumTx) - if ok { - _ = notifier.Notify(rpcSub.ID, common.HexToHash(ethTx.Hash)) - } + // To keep the original behaviour, send a single tx hash in one notification. + // TODO(rjl493456442) Send a batch of tx hashes in one notification + err = notifier.Notify(rpcSub.ID, txHash) + if err != nil { + return } case <-rpcSub.Err(): pendingTxSub.Unsubscribe(api.events) @@ -239,58 +204,62 @@ func (api *PublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Su // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter func (api *PublicFilterAPI) NewBlockFilter() rpc.ID { - api.filtersMu.Lock() - defer api.filtersMu.Unlock() - - if len(api.filters) >= int(api.backend.RPCFilterCap()) { - return rpc.ID("error creating block filter: max limit reached") - } - - headerSub, cancelSubs, err := api.events.SubscribeNewHeads() + headersSub, cancelSubs, err := api.events.SubscribeNewHeads() if err != nil { // wrap error on the ID return rpc.ID(fmt.Sprintf("error creating block filter: %s", err.Error())) } - api.filters[headerSub.ID()] = &filter{typ: filters.BlocksSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: headerSub} + api.filtersMu.Lock() + api.filters[headersSub.ID()] = &filter{typ: filters.BlocksSubscription, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: headersSub} + api.filtersMu.Unlock() go func(headersCh <-chan coretypes.ResultEvent, errCh <-chan error) { defer cancelSubs() for { select { - case ev, ok := <-headersCh: + case ev := <-headersCh: + data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) if !ok { - api.filtersMu.Lock() - delete(api.filters, headerSub.ID()) - api.filtersMu.Unlock() + err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventNewBlockHeader) + headersSub.err <- err return } - - data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) - if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + header, err := types.EthHeaderFromTendermint(data.Header) + if err != nil { + headersSub.err <- err + return + } + // override dynamicly miner address + sdkCtx, err := api.backend.GetSdkContextWithHeader(&data.Header) + if err != nil { + headersSub.err <- err + return } - baseFee := types.BaseFeeFromEvents(data.ResultBeginBlock.Events) + validator, err := api.backend.GetEVMKeeper().GetCoinbaseAddress(sdkCtx) + if err != nil { + headersSub.err <- err + return + } + header.Coinbase = validator - header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee) api.filtersMu.Lock() - if f, found := api.filters[headerSub.ID()]; found { - f.hashes = append(f.hashes, header.Hash()) + if f, found := api.filters[headersSub.ID()]; found { + f.hashes = append(f.hashes, header.Hash) } api.filtersMu.Unlock() case <-errCh: api.filtersMu.Lock() - delete(api.filters, headerSub.ID()) + delete(api.filters, headersSub.ID()) api.filtersMu.Unlock() return } } - }(headerSub.eventCh, headerSub.Err()) + }(headersSub.eventCh, headersSub.Err()) - return headerSub.ID() + return headersSub.ID() } // NewHeads send a notification each time a new (header) block is appended to the chain. @@ -313,23 +282,38 @@ func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, er for { select { - case ev, ok := <-headersCh: + case ev := <-headersCh: + data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) if !ok { - headersSub.Unsubscribe(api.events) + err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventNewBlockHeader) + headersSub.err <- err return } - data, ok := ev.Data.(tmtypes.EventDataNewBlockHeader) - if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + header, err := types.EthHeaderFromTendermint(data.Header) + if err != nil { + headersSub.err <- err + return + } + // override dynamicly miner address + sdkCtx, err := api.backend.GetSdkContextWithHeader(&data.Header) + if err != nil { + headersSub.err <- err + return } - baseFee := types.BaseFeeFromEvents(data.ResultBeginBlock.Events) + validator, err := api.backend.GetEVMKeeper().GetCoinbaseAddress(sdkCtx) + if err != nil { + headersSub.err <- err + return + } + header.Coinbase = validator - // TODO: fetch bloom from events - header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee) - _ = notifier.Notify(rpcSub.ID, header) + err = notifier.Notify(rpcSub.ID, header) + if err != nil { + headersSub.err <- err + return + } case <-rpcSub.Err(): headersSub.Unsubscribe(api.events) return @@ -363,36 +347,38 @@ func (api *PublicFilterAPI) Logs(ctx context.Context, crit filters.FilterCriteri for { select { - case ev, ok := <-logsCh: - if !ok { - logsSub.Unsubscribe(api.events) - return - } - - // filter only events from EVM module txs - _, isMsgEthereumTx := ev.Events[evmtypes.TypeMsgEthereumTx] - + case ev := <-logsCh: + _, isMsgEthereumTx := ev.Events[fmt.Sprintf("%s.%s", evmtypes.EventTypeEthereumTx, evmtypes.AttributeKeyEthereumTxHash)] + api.logger.Debug("\x1b[32m------ logs tx type is evm from sub: %t\x1b[0m\n", isMsgEthereumTx) if !isMsgEthereumTx { - // ignore transaction as it's not from the evm module - return + continue } // get transaction result data dataTx, ok := ev.Data.(tmtypes.EventDataTx) if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + err = fmt.Errorf("invalid event data %T, expected %s", ev.Data, tmtypes.EventTx) + logsSub.err <- err + return } + api.logger.Debug("\x1b[32m------ logs tx dataTx: %+v\x1b[0m\n", dataTx) txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data) if err != nil { + logsSub.err <- err return } - - logs := FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) - - for _, log := range logs { - _ = notifier.Notify(rpcSub.ID, log) + api.logger.Debug("\x1b[32m------ logs tx response: %+v\x1b[0m\n", txResponse) + api.logger.Debug("\x1b[32m------ logs crit: %+v\x1b[0m\n", crit) + + matchedLogs := FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) + api.logger.Debug("\x1b[32m------ logs matchedLogs: %+v\x1b[0m\n", matchedLogs) + for _, log := range matchedLogs { + err = notifier.Notify(rpcSub.ID, log) + if err != nil { + logsSub.err <- err + return + } } case <-rpcSub.Err(): // client send an unsubscribe request logsSub.Unsubscribe(api.events) @@ -421,9 +407,6 @@ func (api *PublicFilterAPI) Logs(ctx context.Context, crit filters.FilterCriteri // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, error) { - api.filtersMu.Lock() - defer api.filtersMu.Unlock() - if len(api.filters) >= int(api.backend.RPCFilterCap()) { return rpc.ID(""), fmt.Errorf("error creating filter: max limit reached") } @@ -440,28 +423,31 @@ func (api *PublicFilterAPI) NewFilter(criteria filters.FilterCriteria) (rpc.ID, filterID = logsSub.ID() + api.filtersMu.Lock() api.filters[filterID] = &filter{typ: filters.LogsSubscription, crit: criteria, deadline: time.NewTimer(deadline), hashes: []common.Hash{}, s: logsSub} + api.filtersMu.Unlock() go func(eventCh <-chan coretypes.ResultEvent) { defer cancelSubs() for { select { - case ev, ok := <-eventCh: - if !ok { - api.filtersMu.Lock() - delete(api.filters, filterID) - api.filtersMu.Unlock() - return + case ev := <-eventCh: + _, isMsgEthereumTx := ev.Events[fmt.Sprintf("%s.%s", evmtypes.EventTypeEthereumTx, evmtypes.AttributeKeyEthereumTxHash)] + if !isMsgEthereumTx { + continue } + dataTx, ok := ev.Data.(tmtypes.EventDataTx) if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue + err = fmt.Errorf("invalid event data %T, expected EventDataTx", ev.Data) + logsSub.err <- err + return } txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data) if err != nil { + logsSub.err <- err return } @@ -612,3 +598,8 @@ func (api *PublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) { return nil, fmt.Errorf("invalid filter %s type %d", id, f.typ) } } + +// Syncing provides information when this nodes starts synchronising with the OneLedger network and when it's finished. +func (api *PublicFilterAPI) Syncing() (*rpc.Subscription, error) { + return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported +} diff --git a/rpc/namespaces/ethereum/eth/filters/filter_system.go b/rpc/namespaces/ethereum/eth/filters/filter_system.go index a8b0ec72..6cb8e06c 100644 --- a/rpc/namespaces/ethereum/eth/filters/filter_system.go +++ b/rpc/namespaces/ethereum/eth/filters/filter_system.go @@ -6,13 +6,12 @@ import ( "sync" "time" + "github.com/cosmos/cosmos-sdk/client" "github.com/pkg/errors" - - tmjson "github.com/tendermint/tendermint/libs/json" "github.com/tendermint/tendermint/libs/log" + tmpubsub "github.com/tendermint/tendermint/libs/pubsub" tmquery "github.com/tendermint/tendermint/libs/pubsub/query" coretypes "github.com/tendermint/tendermint/rpc/core/types" - rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" tmtypes "github.com/tendermint/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -22,34 +21,48 @@ import ( "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/rpc" - "github.com/stratosnet/stratos-chain/rpc/ethereum/pubsub" + "github.com/stratosnet/stratos-chain/rpc/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" ) var ( - txEvents = tmtypes.QueryForEvent(tmtypes.EventTx).String() - evmEvents = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", tmtypes.EventTypeKey, tmtypes.EventTx, sdk.EventTypeMessage, sdk.AttributeKeyModule, evmtypes.ModuleName)).String() + // base tm tx event + txEvents = tmtypes.QueryForEvent(tmtypes.EventTx).String() + // listen only evm txs + evmEvents = tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s.%s='%s'", tmtypes.EventTypeKey, tmtypes.EventTx, sdk.EventTypeMessage, sdk.AttributeKeyModule, evmtypes.ModuleName)).String() + // specify to listen a blocks instead of heads as it contain more details about the block header including current block hash headerEvents = tmtypes.QueryForEvent(tmtypes.EventNewBlockHeader).String() + // for tendermint subscribe channel capacity to make it buffered + tmChannelCapacity = 100 ) // EventSystem creates subscriptions, processes events and broadcasts them to the // subscription which match the subscription criteria using the Tendermint's RPC client. type EventSystem struct { - logger log.Logger - ctx context.Context - tmWSClient *rpcclient.WSClient - - // light client mode - lightMode bool + logger log.Logger + ctx context.Context + clientCtx client.Context index filterIndex topicChans map[string]chan<- coretypes.ResultEvent indexMux *sync.RWMutex + // Subscriptions + txsSub *Subscription // Subscription for new transaction event + logsSub *Subscription // Subscription for new log event + pendingLogsSub *Subscription // Subscription for pending log event + chainSub *Subscription // Subscription for new chain event + + // Unidirectional channels to receive Tendermint ResultEvents + txsCh <-chan coretypes.ResultEvent // Channel to receive new pending transactions event + logsCh <-chan coretypes.ResultEvent // Channel to receive new log event + pendingLogsCh <-chan coretypes.ResultEvent // Channel to receive new pending log event + chainCh <-chan coretypes.ResultEvent // Channel to receive new chain event + // Channels install chan *Subscription // install filter for event notification uninstall chan *Subscription // remove filter for event notification - eventBus pubsub.EventBus + eventBus *tmtypes.EventBus } // NewEventSystem creates a new manager that listens for event on the given mux, @@ -58,27 +71,29 @@ type EventSystem struct { // // The returned manager has a loop that needs to be stopped with the Stop function // or by stopping the given mux. -func NewEventSystem(logger log.Logger, tmWSClient *rpcclient.WSClient) *EventSystem { +func NewEventSystem(clientCtx client.Context, logger log.Logger, eventBus *tmtypes.EventBus) *EventSystem { index := make(filterIndex) for i := filters.UnknownSubscription; i < filters.LastIndexSubscription; i++ { index[i] = make(map[rpc.ID]*Subscription) } es := &EventSystem{ - logger: logger, - ctx: context.Background(), - tmWSClient: tmWSClient, - lightMode: false, - index: index, - topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), - indexMux: new(sync.RWMutex), - install: make(chan *Subscription), - uninstall: make(chan *Subscription), - eventBus: pubsub.NewEventBus(), + logger: logger, + ctx: context.Background(), + clientCtx: clientCtx, + index: index, + topicChans: make(map[string]chan<- coretypes.ResultEvent, len(index)), + indexMux: new(sync.RWMutex), + install: make(chan *Subscription), + uninstall: make(chan *Subscription), + eventBus: eventBus, + txsCh: make(<-chan coretypes.ResultEvent), + logsCh: make(<-chan coretypes.ResultEvent), + pendingLogsCh: make(<-chan coretypes.ResultEvent), + chainCh: make(<-chan coretypes.ResultEvent), } go es.eventLoop() - go es.consumeEvents() return es } @@ -89,64 +104,128 @@ func (es *EventSystem) WithContext(ctx context.Context) { } // subscribe performs a new event subscription to a given Tendermint event. -// The subscription creates a unidirectional receive event channel to receive the ResultEvent. -func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, pubsub.UnsubscribeFunc, error) { +// The subscription creates a unidirectional receive event channel to receive the ResultEvent. By +// default, the subscription timeouts (i.e is canceled) after 5 minutes. This function returns an +// error if the subscription fails (eg: if the identifier is already subscribed) or if the filter +// type is invalid. +func (es *EventSystem) subscribe(sub *Subscription) (*Subscription, context.CancelFunc, error) { var ( err error cancelFn context.CancelFunc + eventCh <-chan coretypes.ResultEvent ) - ctx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - existingSubs := es.eventBus.Topics() - for _, topic := range existingSubs { - if topic == sub.event { - eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) - if err != nil { - err := errors.Wrapf(err, "failed to subscribe to topic: %s", sub.event) - return nil, nil, err - } - - sub.eventCh = eventCh - return sub, unsubFn, nil - } - } + es.ctx, cancelFn = context.WithTimeout(context.Background(), deadline) switch sub.typ { - case filters.LogsSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) - case filters.BlocksSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) - case filters.PendingTransactionsSubscription: - err = es.tmWSClient.Subscribe(ctx, sub.event) + case + filters.PendingTransactionsSubscription, + filters.PendingLogsSubscription, + filters.MinedAndPendingLogsSubscription, + filters.LogsSubscription, + filters.BlocksSubscription: + + eventCh, err = es.createEventBusSubscription(string(sub.id), sub.event) default: err = fmt.Errorf("invalid filter subscription type %d", sub.typ) } if err != nil { sub.err <- err - return nil, nil, err + return nil, cancelFn, err } // wrap events in a go routine to prevent blocking - es.install <- sub - <-sub.installed + go func() { + es.install <- sub + <-sub.installed + }() + + sub.eventCh = eventCh + return sub, cancelFn, nil +} - eventCh, unsubFn, err := es.eventBus.Subscribe(sub.event) +func (es *EventSystem) createEventBusSubscription(subscriber, query string) (out <-chan coretypes.ResultEvent, err error) { + q, err := tmquery.New(query) if err != nil { - return nil, nil, errors.Wrapf(err, "failed to subscribe to topic after installed: %s", sub.event) + return nil, errors.Wrap(err, "failed to parse query") } - sub.eventCh = eventCh - return sub, unsubFn, nil + var sub tmtypes.Subscription + if tmChannelCapacity > 0 { + sub, err = es.eventBus.Subscribe(es.ctx, subscriber, q, tmChannelCapacity) + } else { + sub, err = es.eventBus.SubscribeUnbuffered(es.ctx, subscriber, q) + } + + if err != nil { + return nil, errors.Wrap(err, "failed to subscribe") + } + + outc := make(chan coretypes.ResultEvent, tmChannelCapacity) + go es.eventsRoutine(sub, subscriber, q, outc) + + return outc, nil +} + +func (es *EventSystem) eventsRoutine( + sub tmtypes.Subscription, + subscriber string, + q tmpubsub.Query, + outc chan<- coretypes.ResultEvent) { + for { + select { + case msg := <-sub.Out(): + result := coretypes.ResultEvent{Query: q.String(), Data: msg.Data(), Events: msg.Events()} + if cap(outc) == 0 { + outc <- result + } else { + select { + case outc <- result: + default: + // es.logger.Error("wanted to publish ResultEvent, but out channel is full", "result", result, "query", result.Query) + } + } + case <-sub.Cancelled(): + if sub.Err() == tmpubsub.ErrUnsubscribed { + return + } + + es.logger.Error("subscription was cancelled, resubscribing...", "err", sub.Err(), "query", q.String()) + sub = es.resubscribe(subscriber, q) + if sub == nil { // client was stopped + return + } + case <-es.eventBus.Quit(): + return + } + } +} + +// Try to resubscribe with exponential backoff. +func (es *EventSystem) resubscribe(subscriber string, q tmpubsub.Query) tmtypes.Subscription { + attempts := 0 + for { + if !es.eventBus.IsRunning() { + return nil + } + + sub, err := es.eventBus.Subscribe(context.Background(), subscriber, q) + if err == nil { + return sub + } + + attempts++ + time.Sleep((10 << uint(attempts)) * time.Millisecond) // 10ms -> 20ms -> 40ms + } } // SubscribeLogs creates a subscription that will write all logs matching the // given criteria to the given logs channel. Default value for the from and to // block is "latest". If the fromBlock > toBlock an error is returned. -func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { +func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) { var from, to rpc.BlockNumber + es.logger.Debug("\x1b[32m------ SubscribeLogs crit: %+v\x1b[0m\n", crit) if crit.FromBlock == nil { from = rpc.LatestBlockNumber } else { @@ -159,21 +238,44 @@ func (es *EventSystem) SubscribeLogs(crit filters.FilterCriteria) (*Subscription } switch { + // only interested in pending logs + case from == rpc.PendingBlockNumber && to == rpc.PendingBlockNumber: + return es.subscribePendingLogs(crit) + // only interested in new mined logs, mined logs within a specific block range, or // logs from a specific block number to new mined blocks case (from == rpc.LatestBlockNumber && to == rpc.LatestBlockNumber), - (from >= 0 && to >= 0 && to >= from), - (from >= 0 && to == rpc.LatestBlockNumber): + (from >= 0 && to >= 0 && to >= from): return es.subscribeLogs(crit) + // interested in mined logs from a specific block number, new logs and pending logs + case from >= rpc.LatestBlockNumber && (to == rpc.PendingBlockNumber || to == rpc.LatestBlockNumber): + return es.subscribeMinedPendingLogs(crit) + default: return nil, nil, fmt.Errorf("invalid from and to block combination: from > to (%d > %d)", from, to) } } +// subscribePendingLogs creates a subscription that writes transaction hashes for +// transactions that enter the transaction pool. +func (es *EventSystem) subscribePendingLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) { + sub := &Subscription{ + id: rpc.NewID(), + typ: filters.PendingLogsSubscription, + event: evmEvents, + logsCrit: crit, + created: time.Now().UTC(), + logs: make(chan []*ethtypes.Log), + installed: make(chan struct{}, 1), + err: make(chan error, 1), + } + return es.subscribe(sub) +} + // subscribeLogs creates a subscription that will write all logs matching the // given criteria to the given logs channel. -func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription, pubsub.UnsubscribeFunc, error) { +func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) { sub := &Subscription{ id: rpc.NewID(), typ: filters.LogsSubscription, @@ -184,17 +286,34 @@ func (es *EventSystem) subscribeLogs(crit filters.FilterCriteria) (*Subscription installed: make(chan struct{}, 1), err: make(chan error, 1), } + es.logger.Debug("\x1b[32m------ logs subscribe: %+v\x1b[0m\n", sub) + return es.subscribe(sub) +} + +// subscribeMinedPendingLogs creates a subscription that returned mined and +// pending logs that match the given criteria. +func (es *EventSystem) subscribeMinedPendingLogs(crit filters.FilterCriteria) (*Subscription, context.CancelFunc, error) { + sub := &Subscription{ + id: rpc.NewID(), + typ: filters.MinedAndPendingLogsSubscription, + event: evmEvents, + logsCrit: crit, + created: time.Now().UTC(), + logs: make(chan []*ethtypes.Log), + installed: make(chan struct{}, 1), + err: make(chan error, 1), + } return es.subscribe(sub) } // SubscribeNewHeads subscribes to new block headers events. -func (es EventSystem) SubscribeNewHeads() (*Subscription, pubsub.UnsubscribeFunc, error) { +func (es EventSystem) SubscribeNewHeads() (*Subscription, context.CancelFunc, error) { sub := &Subscription{ id: rpc.NewID(), typ: filters.BlocksSubscription, event: headerEvents, created: time.Now().UTC(), - headers: make(chan *ethtypes.Header), + headers: make(chan *types.Header), installed: make(chan struct{}, 1), err: make(chan error, 1), } @@ -202,7 +321,7 @@ func (es EventSystem) SubscribeNewHeads() (*Subscription, pubsub.UnsubscribeFunc } // SubscribePendingTxs subscribes to new pending transactions events from the mempool. -func (es EventSystem) SubscribePendingTxs() (*Subscription, pubsub.UnsubscribeFunc, error) { +func (es EventSystem) SubscribePendingTxs() (*Subscription, context.CancelFunc, error) { sub := &Subscription{ id: rpc.NewID(), typ: filters.PendingTransactionsSubscription, @@ -217,88 +336,138 @@ func (es EventSystem) SubscribePendingTxs() (*Subscription, pubsub.UnsubscribeFu type filterIndex map[filters.Type]map[rpc.ID]*Subscription -// eventLoop (un)installs filters and processes mux events. -func (es *EventSystem) eventLoop() { - for { - select { - case f := <-es.install: - es.indexMux.Lock() - es.index[f.typ][f.id] = f - ch := make(chan coretypes.ResultEvent) - es.topicChans[f.event] = ch - if err := es.eventBus.AddTopic(f.event, ch); err != nil { - es.logger.Error("failed to add event topic to event bus", "topic", f.event, "error", err.Error()) - } - es.indexMux.Unlock() - close(f.installed) - case f := <-es.uninstall: - es.indexMux.Lock() - delete(es.index[f.typ], f.id) - - var channelInUse bool - for _, sub := range es.index[f.typ] { - if sub.event == f.event { - channelInUse = true - break - } - } +func (es *EventSystem) handleLogs(ev coretypes.ResultEvent) { + data, _ := ev.Data.(tmtypes.EventDataTx) + es.logger.Debug("\x1b[32m------ logs data: %+v\x1b[0m\n", data) + // logReceipt := onetypes.GetTxEthLogs(&data.TxResult.Result, data.Index) + resultData, err := evmtypes.DecodeTransactionLogs(data.TxResult.Result.Data) + if err != nil { + return + } - // remove topic only when channel is not used by other subscriptions - if !channelInUse { - if err := es.tmWSClient.Unsubscribe(es.ctx, f.event); err != nil { - es.logger.Error("failed to unsubscribe from query", "query", f.event, "error", err.Error()) - } + if len(resultData.Logs) == 0 { + return + } - ch, ok := es.topicChans[f.event] - if ok { - es.eventBus.RemoveTopic(f.event) - close(ch) - delete(es.topicChans, f.event) - } - } + for _, f := range es.index[filters.LogsSubscription] { + matchedLogs := FilterLogs(evmtypes.LogsToEthereum(resultData.Logs), f.logsCrit.FromBlock, f.logsCrit.ToBlock, f.logsCrit.Addresses, f.logsCrit.Topics) + if len(matchedLogs) > 0 { + f.logs <- matchedLogs + } + } +} - es.indexMux.Unlock() - close(f.err) +func (es *EventSystem) handleTxsEvent(ev coretypes.ResultEvent) { + data, _ := ev.Data.(tmtypes.EventDataTx) + for _, f := range es.index[filters.PendingTransactionsSubscription] { + // NOTE: In previous version, data.Tx return types.Tx, but right now just bytes, + // so we need manually covert in order to get sha256 hash fot tx payload. + txHash, err := types.GetTxHash(es.clientCtx.TxConfig.TxDecoder(), tmtypes.Tx(data.Tx)) + if err != nil { + continue } + f.hashes <- []common.Hash{txHash} } } -func (es *EventSystem) consumeEvents() { - for { - for rpcResp := range es.tmWSClient.ResponsesCh { - var ev coretypes.ResultEvent - - if rpcResp.Error != nil { - time.Sleep(5 * time.Second) - continue - } else if err := tmjson.Unmarshal(rpcResp.Result, &ev); err != nil { - es.logger.Error("failed to JSON unmarshal ResponsesCh result event", "error", err.Error()) - continue - } +func (es *EventSystem) handleChainEvent(ev coretypes.ResultEvent) { + data, _ := ev.Data.(tmtypes.EventDataNewBlockHeader) + for _, f := range es.index[filters.BlocksSubscription] { + header, err := types.EthHeaderFromTendermint(data.Header) + if err != nil { + continue + } + f.headers <- header + } +} - if len(ev.Query) == 0 { - // skip empty responses - continue - } +// eventLoop (un)installs filters and processes mux events. +func (es *EventSystem) eventLoop() { + var ( + err error + cancelPendingTxsSubs, cancelLogsSubs, cancelPendingLogsSubs, cancelHeaderSubs context.CancelFunc + ) + + // Subscribe events + es.txsSub, cancelPendingTxsSubs, err = es.SubscribePendingTxs() + if err != nil { + panic(fmt.Errorf("failed to subscribe pending txs: %w", err)) + } + + defer cancelPendingTxsSubs() + + es.logsSub, cancelLogsSubs, err = es.SubscribeLogs(filters.FilterCriteria{}) + if err != nil { + panic(fmt.Errorf("failed to subscribe logs: %w", err)) + } + + defer cancelLogsSubs() + + es.pendingLogsSub, cancelPendingLogsSubs, err = es.subscribePendingLogs(filters.FilterCriteria{}) + if err != nil { + panic(fmt.Errorf("failed to subscribe pending logs: %w", err)) + } + + defer cancelPendingLogsSubs() + + es.chainSub, cancelHeaderSubs, err = es.SubscribeNewHeads() + if err != nil { + panic(fmt.Errorf("failed to subscribe headers: %w", err)) + } + + defer cancelHeaderSubs() - es.indexMux.RLock() - ch, ok := es.topicChans[ev.Query] - es.indexMux.RUnlock() - if !ok { - es.logger.Debug("channel for subscription not found", "topic", ev.Query) - es.logger.Debug("list of available channels", "channels", es.eventBus.Topics()) - continue + // Ensure all subscriptions get cleaned up + defer func() { + es.txsSub.Unsubscribe(es) + es.logsSub.Unsubscribe(es) + es.pendingLogsSub.Unsubscribe(es) + es.chainSub.Unsubscribe(es) + }() + + for { + select { + case txEvent := <-es.txsSub.eventCh: + es.logger.Debug("\x1b[32m------ tx event trigger from event loop: %+v\x1b[0m\n", txEvent) + es.handleTxsEvent(txEvent) + case headerEv := <-es.chainSub.eventCh: + es.logger.Debug("\x1b[32m------ header event trigger from event loop: %+v\x1b[0m\n", headerEv) + es.handleChainEvent(headerEv) + case logsEv := <-es.logsSub.eventCh: + es.logger.Debug("\x1b[32m------ logs event trigger from event loop: %+v\x1b[0m\n", logsEv) + es.handleLogs(logsEv) + case logsEv := <-es.pendingLogsSub.eventCh: + es.logger.Debug("\x1b[32m------ pending logs event trigger from event loop: %+v\x1b[0m\n", logsEv) + es.handleLogs(logsEv) + + case f := <-es.install: + if f.typ == filters.MinedAndPendingLogsSubscription { + // the type are logs and pending logs subscriptions + es.index[filters.LogsSubscription][f.id] = f + es.index[filters.PendingLogsSubscription][f.id] = f + } else { + es.index[f.typ][f.id] = f } + close(f.installed) - // gracefully handle lagging subscribers - t := time.NewTimer(time.Second) - select { - case <-t.C: - es.logger.Debug("dropped event during lagging subscription", "topic", ev.Query) - case ch <- ev: + case f := <-es.uninstall: + if f.typ == filters.MinedAndPendingLogsSubscription { + // the type are logs and pending logs subscriptions + delete(es.index[filters.LogsSubscription], f.id) + delete(es.index[filters.PendingLogsSubscription], f.id) + } else { + delete(es.index[f.typ], f.id) } + close(f.err) + // System stopped + case <-es.txsSub.Err(): + return + case <-es.logsSub.Err(): + return + case <-es.pendingLogsSub.Err(): + return + case <-es.chainSub.Err(): + return } - - time.Sleep(time.Second) } } diff --git a/rpc/namespaces/ethereum/eth/filters/filters.go b/rpc/namespaces/ethereum/eth/filters/filters.go index 1b6cb647..98faa945 100644 --- a/rpc/namespaces/ethereum/eth/filters/filters.go +++ b/rpc/namespaces/ethereum/eth/filters/filters.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/filters" + "github.com/stratosnet/stratos-chain/rpc/backend" "github.com/stratosnet/stratos-chain/rpc/types" ) @@ -26,7 +27,7 @@ type BloomIV struct { // Filter can be used to retrieve and filter logs. type Filter struct { logger log.Logger - backend Backend + backend backend.BackendI criteria filters.FilterCriteria bloomFilters [][]BloomIV // Filter the system is matching for @@ -34,14 +35,14 @@ type Filter struct { // NewBlockFilter creates a new filter which directly inspects the contents of // a block to figure out whether it is interesting or not. -func NewBlockFilter(logger log.Logger, backend Backend, criteria filters.FilterCriteria) *Filter { +func NewBlockFilter(logger log.Logger, b backend.BackendI, criteria filters.FilterCriteria) *Filter { // Create a generic filter and convert it into a block filter - return newFilter(logger, backend, criteria, nil) + return newFilter(logger, b, criteria, nil) } // NewRangeFilter creates a new filter which uses a bloom filter on blocks to // figure out whether a particular block is interesting or not. -func NewRangeFilter(logger log.Logger, backend Backend, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { +func NewRangeFilter(logger log.Logger, b backend.BackendI, begin, end int64, addresses []common.Address, topics [][]common.Hash) *Filter { // Flatten the address and topic filter clauses into a single bloombits filter // system. Since the bloombits are not positional, nil topics are permitted, // which get flattened into a nil byte slice. @@ -70,14 +71,14 @@ func NewRangeFilter(logger log.Logger, backend Backend, begin, end int64, addres Topics: topics, } - return newFilter(logger, backend, criteria, createBloomFilters(filtersBz, logger)) + return newFilter(logger, b, criteria, createBloomFilters(filtersBz, logger)) } // newFilter returns a new Filter -func newFilter(logger log.Logger, backend Backend, criteria filters.FilterCriteria, bloomFilters [][]BloomIV) *Filter { +func newFilter(logger log.Logger, b backend.BackendI, criteria filters.FilterCriteria, bloomFilters [][]BloomIV) *Filter { return &Filter{ logger: logger, - backend: backend, + backend: b, criteria: criteria, bloomFilters: bloomFilters, } diff --git a/rpc/namespaces/ethereum/eth/filters/subscription.go b/rpc/namespaces/ethereum/eth/filters/subscription.go index d9b22798..288dac60 100644 --- a/rpc/namespaces/ethereum/eth/filters/subscription.go +++ b/rpc/namespaces/ethereum/eth/filters/subscription.go @@ -3,6 +3,7 @@ package filters import ( "time" + "github.com/stratosnet/stratos-chain/rpc/types" coretypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/ethereum/go-ethereum/common" @@ -20,7 +21,7 @@ type Subscription struct { logsCrit filters.FilterCriteria logs chan []*ethtypes.Log hashes chan []common.Hash - headers chan *ethtypes.Header + headers chan *types.Header installed chan struct{} // closed when the filter is installed eventCh <-chan coretypes.ResultEvent err chan error diff --git a/rpc/namespaces/ethereum/miner/api.go b/rpc/namespaces/ethereum/miner/api.go index 0e89d33d..59084403 100644 --- a/rpc/namespaces/ethereum/miner/api.go +++ b/rpc/namespaces/ethereum/miner/api.go @@ -104,7 +104,7 @@ func (api *API) SetEtherbase(etherbase common.Address) bool { WithChainID(api.clientCtx.ChainID). WithKeybase(api.clientCtx.Keyring). WithTxConfig(api.clientCtx.TxConfig). - WithSequence(uint64(*nonce)). + WithSequence(uint64(nonce)). WithGasAdjustment(1.25) _, gas, err := tx.CalculateGas(api.clientCtx, txFactory, msg) diff --git a/rpc/namespaces/ethereum/net/api.go b/rpc/namespaces/ethereum/net/api.go index 40a908f8..6cc61677 100644 --- a/rpc/namespaces/ethereum/net/api.go +++ b/rpc/namespaces/ethereum/net/api.go @@ -1,55 +1,37 @@ package net import ( - "context" - "fmt" "math/big" - rpcclient "github.com/tendermint/tendermint/rpc/client" - - "github.com/cosmos/cosmos-sdk/client" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stratosnet/stratos-chain/rpc/backend" ) // PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. type PublicAPI struct { - networkVersion uint64 - tmClient rpcclient.Client + backend backend.BackendI } // NewPublicAPI creates an instance of the public Net Web3 API. -func NewPublicAPI(clientCtx client.Context) *PublicAPI { - eip155ChainId, ok := new(big.Int).SetString(clientCtx.ChainID, 10) - if !ok { - panic("failed to parse chainID") - } - +func NewPublicAPI(backend backend.BackendI) *PublicAPI { return &PublicAPI{ - networkVersion: eip155ChainId.Uint64(), - tmClient: clientCtx.Client, + backend: backend, } } // Version returns the current ethereum protocol version. -func (s *PublicAPI) Version() string { - return fmt.Sprintf("%d", s.networkVersion) +func (s *PublicAPI) Version() (string, error) { + ctx := s.backend.GetSdkContext() + params := s.backend.GetEVMKeeper().GetParams(ctx) + return params.ChainConfig.ChainID.String(), nil } // Listening returns if client is actively listening for network connections. func (s *PublicAPI) Listening() bool { - ctx := context.Background() - netInfo, err := s.tmClient.NetInfo(ctx) - if err != nil { - return false - } - return netInfo.Listening + return s.backend.GetNode().IsListening() } // PeerCount returns the number of peers currently connected to the client. -func (s *PublicAPI) PeerCount() int { - ctx := context.Background() - netInfo, err := s.tmClient.NetInfo(ctx) - if err != nil { - return 0 - } - return len(netInfo.Peers) +func (s *PublicAPI) PeerCount() hexutil.Big { + return hexutil.Big(*big.NewInt(int64(len(s.backend.GetSwitch().Peers().List())))) } diff --git a/rpc/namespaces/ethereum/txpool/api.go b/rpc/namespaces/ethereum/txpool/api.go index 48e49dfb..ea1dd0a7 100644 --- a/rpc/namespaces/ethereum/txpool/api.go +++ b/rpc/namespaces/ethereum/txpool/api.go @@ -1,50 +1,90 @@ package txpool import ( + "fmt" + "github.com/tendermint/tendermint/libs/log" + "github.com/cosmos/cosmos-sdk/client" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stratosnet/stratos-chain/rpc/backend" "github.com/stratosnet/stratos-chain/rpc/types" ) // PublicAPI offers and API for the transaction pool. It only operates on data that is non-confidential. type PublicAPI struct { - logger log.Logger + logger log.Logger + clientCtx client.Context + backend backend.BackendI } // NewPublicAPI creates a new tx pool service that gives information about the transaction pool. -func NewPublicAPI(logger log.Logger) *PublicAPI { +func NewPublicAPI(logger log.Logger, clientCtx client.Context, backend backend.BackendI) *PublicAPI { return &PublicAPI{ - logger: logger.With("module", "txpool"), + logger: logger.With("module", "txpool"), + clientCtx: clientCtx, + backend: backend, + } +} + +func (api *PublicAPI) getPendingList() map[common.Address]map[string]*types.Transaction { + pTxs := types.GetPendingTxs(api.clientCtx.TxConfig.TxDecoder(), api.backend.GetMempool()) + pending := make(map[common.Address]map[string]*types.Transaction) + for _, pTx := range pTxs { + addrMap, ok := pending[pTx.From] + if !ok { + addrMap = make(map[string]*types.Transaction) + pending[pTx.From] = addrMap + } + addrMap[fmt.Sprintf("%d", pTx.Nonce)] = pTx } + return pending } // Content returns the transactions contained within the transaction pool -func (api *PublicAPI) Content() (map[string]map[string]map[string]*types.RPCTransaction, error) { +func (api *PublicAPI) Content() map[string]map[common.Address]map[string]*types.Transaction { api.logger.Debug("txpool_content") - content := map[string]map[string]map[string]*types.RPCTransaction{ - "pending": make(map[string]map[string]*types.RPCTransaction), - "queued": make(map[string]map[string]*types.RPCTransaction), + content := map[string]map[common.Address]map[string]*types.Transaction{ + "pending": api.getPendingList(), + "queued": make(map[common.Address]map[string]*types.Transaction), } - return content, nil + return content } // Inspect returns the content of the transaction pool and flattens it into an -func (api *PublicAPI) Inspect() (map[string]map[string]map[string]string, error) { +func (api *PublicAPI) Inspect() map[string]map[string]map[string]string { api.logger.Debug("txpool_inspect") content := map[string]map[string]map[string]string{ "pending": make(map[string]map[string]string), "queued": make(map[string]map[string]string), } - return content, nil + pending := api.getPendingList() + // Define a formatter to flatten a transaction into a string + var format = func(tx *types.Transaction) string { + if to := tx.To; to != nil { + return fmt.Sprintf("%s: %d wei + %d gas × %d wei", tx.To.Hex(), tx.Value.ToInt(), tx.Gas, tx.GasPrice.ToInt()) + } + return fmt.Sprintf("contract creation: %d wei + %d gas × %d wei", tx.Value.ToInt(), tx.Gas, tx.GasPrice.ToInt()) + } + // Flatten the pending transactions + for account, txs := range pending { + dump := make(map[string]string) + for _, tx := range txs { + dump[fmt.Sprintf("%d", tx.Nonce)] = format(tx) + } + content["pending"][account.Hex()] = dump + } + + return content } // Status returns the number of pending and queued transaction in the pool. func (api *PublicAPI) Status() map[string]hexutil.Uint { api.logger.Debug("txpool_status") return map[string]hexutil.Uint{ - "pending": hexutil.Uint(0), + "pending": hexutil.Uint(types.GetPendingTxsLen(api.backend.GetMempool())), "queued": hexutil.Uint(0), } } diff --git a/rpc/server.go b/rpc/server.go new file mode 100644 index 00000000..0c3f9fcd --- /dev/null +++ b/rpc/server.go @@ -0,0 +1,104 @@ +package rpc + +import ( + "context" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + "github.com/stratosnet/stratos-chain/server/config" + "github.com/tendermint/tendermint/libs/log" +) + +type Web3Server struct { + httpURI string + wsURI string + enabled bool + logger log.Logger +} + +func NewWeb3Server(cfg config.Config, logger log.Logger) *Web3Server { + return &Web3Server{ + httpURI: cfg.JSONRPC.Address, + wsURI: cfg.JSONRPC.WsAddress, + enabled: cfg.JSONRPC.Enable, + logger: logger, + } +} + +func (web3 *Web3Server) start(uri string, handler http.Handler) error { + if !web3.enabled { + web3.logger.Info("Web3 api disabled, skipping") + return nil + } + + var ( + err error + ) + channel := make(chan error) + timeout := make(chan error) + + srv := &http.Server{ + Addr: uri, + Handler: handler, + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, + } + srv.SetKeepAlivesEnabled(true) + + //Timeout Go routine + go func() { + time.Sleep(time.Duration(2) * time.Second) + timeout <- nil + }() + + go func(ch chan error) { + web3.logger.Info("starting Web3 RPC server on " + uri) + err := srv.ListenAndServe() + if err != nil { + web3.logger.Error("server error, details: %s", err) + } + srv.Shutdown(context.TODO()) + ch <- err + }(channel) + + select { + case err = <-channel: + case err = <-timeout: + } + + return err +} + +func (web3 *Web3Server) registerAPIs(server *rpc.Server, apis []rpc.API) error { + for _, api := range apis { + if err := server.RegisterName(api.Namespace, api.Service); err != nil { + web3.logger.Error( + "failed to register service in JSON RPC namespace", + "namespace", api.Namespace, + "service", api.Service, + ) + return err + } + } + return nil +} + +func (web3 *Web3Server) StartHTTP(apis []rpc.API) error { + rpcSrv := rpc.NewServer() + handler := node.NewHTTPHandlerStack(rpcSrv, []string{"*"}, []string{"localhost", "host.docker.internal"}, []byte{}) // TODO: Replace cors and vshosts from config + if err := web3.registerAPIs(rpcSrv, apis); err != nil { + return err + } + return web3.start(web3.httpURI, handler) +} + +func (web3 *Web3Server) StartWS(apis []rpc.API) error { + rpcSrv := rpc.NewServer() + handler := rpcSrv.WebsocketHandler([]string{}) // TODO: Add config origins + if err := web3.registerAPIs(rpcSrv, apis); err != nil { + return err + } + return web3.start(web3.wsURI, handler) +} diff --git a/rpc/types/block.go b/rpc/types/block.go index 809505b3..a516f405 100644 --- a/rpc/types/block.go +++ b/rpc/types/block.go @@ -27,6 +27,7 @@ const ( EthPendingBlockNumber = BlockNumber(-2) EthLatestBlockNumber = BlockNumber(-1) EthEarliestBlockNumber = BlockNumber(0) + EthInitialBlockNumber = BlockNumber(1) ) const ( @@ -45,6 +46,7 @@ func NewBlockNumber(n *big.Int) BlockNumber { return BlockNumber(n.Int64()) } +// NOTE: Legacy, should be removed after cli rework // ContextWithHeight wraps a context with the a gRPC block height header. If the provided height is // 0, it will return an empty context and the gRPC query will use the latest block height for querying. // Note that all metadata are processed and removed by tendermint layer, so it wont be accessible at gRPC server level. diff --git a/rpc/types/query_client.go b/rpc/types/query_client.go index 1594a11d..57af7559 100644 --- a/rpc/types/query_client.go +++ b/rpc/types/query_client.go @@ -1,3 +1,4 @@ +// NOTE: Legacy!!! package types import ( @@ -14,9 +15,9 @@ import ( ) // QueryClient defines a gRPC Client used for: -// - Transaction simulation -// - EVM module queries -// - Fee market module queries +// - Transaction simulation +// - EVM module queries +// - Fee market module queries type QueryClient struct { tx.ServiceClient evmtypes.QueryClient diff --git a/rpc/types/types.go b/rpc/types/types.go index 9459cf8e..3ad20188 100644 --- a/rpc/types/types.go +++ b/rpc/types/types.go @@ -29,8 +29,8 @@ type StorageResult struct { Proof []string `json:"proof"` } -// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction -type RPCTransaction struct { +// Transaction represents a transaction that will serialize to the RPC representation of a transaction +type Transaction struct { BlockHash *common.Hash `json:"blockHash"` BlockNumber *hexutil.Big `json:"blockNumber"` From common.Address `json:"from"` @@ -87,3 +87,91 @@ type OneFeeHistory struct { Reward []*big.Int // each element of the array will have the tip provided to miners for the percentile given GasUsedRatio float64 // the ratio of gas used to the gas limit for each block } + +// NOTE: Forked because tendermint block hash calculated in another way +// default ethereum take rlp from the struct +type Header struct { + Hash common.Hash `json:"hash" gencodec:"required"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom ethtypes.Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + Number *big.Int `json:"number" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Extra []byte `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce ethtypes.BlockNonce `json:"nonce"` + Size uint64 `json:"size"` + + // BaseFee was added by EIP-1559 and is ignored in legacy headers. + // TODO: Add support + // NOTE: Do we need this in real? + // BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` +} + +// Block represents a block returned to RPC clients. +type Block struct { + Number hexutil.Uint64 `json:"number"` + Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash"` + Nonce ethtypes.BlockNonce `json:"nonce"` + Sha3Uncles common.Hash `json:"sha3Uncles"` + LogsBloom ethtypes.Bloom `json:"logsBloom"` + TransactionsRoot common.Hash `json:"transactionsRoot"` + StateRoot common.Hash `json:"stateRoot"` + Miner common.Address `json:"miner"` + MixHash common.Hash `json:"mixHash"` + Difficulty hexutil.Uint64 `json:"difficulty"` + TotalDifficulty hexutil.Uint64 `json:"totalDifficulty"` + ExtraData hexutil.Bytes `json:"extraData"` + Size hexutil.Uint64 `json:"size"` + GasLimit *hexutil.Big `json:"gasLimit"` + GasUsed *hexutil.Big `json:"gasUsed"` + Timestamp hexutil.Uint64 `json:"timestamp"` + Uncles []common.Hash `json:"uncles"` + ReceiptsRoot common.Hash `json:"receiptsRoot"` + Transactions []interface{} `json:"transactions"` +} + +// TransactionReceipt represents a mined transaction returned to RPC clients. +type TransactionReceipt struct { + // Consensus fields: These fields are defined by the Yellow Paper + Status hexutil.Uint64 `json:"status"` + CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed"` + LogsBloom ethtypes.Bloom `json:"logsBloom"` + Logs []*ethtypes.Log `json:"logs"` + + // Implementation fields: These fields are added by geth when processing a transaction. + // They are stored in the chain database. + TransactionHash common.Hash `json:"transactionHash"` + ContractAddress *common.Address `json:"contractAddress"` + GasUsed hexutil.Uint64 `json:"gasUsed"` + + // Inclusion information: These fields provide information about the inclusion of the + // transaction corresponding to this receipt. + BlockHash common.Hash `json:"blockHash"` + BlockNumber hexutil.Big `json:"blockNumber"` + TransactionIndex hexutil.Uint64 `json:"transactionIndex"` + + // sender and receiver (contract or EOA) addresses + From common.Address `json:"from"` + To *common.Address `json:"to"` +} + +type TxByNonce []*Transaction + +func (s TxByNonce) Len() int { return len(s) } +func (s TxByNonce) Less(i, j int) bool { return s[i].Nonce < s[j].Nonce } +func (s TxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Transactions implements DerivableList for transactions. +type Transactions []*Transaction + +// Len returns the length of s. +func (s Transactions) Len() int { return len(s) } diff --git a/rpc/types/utils.go b/rpc/types/utils.go index 21dfb729..007686f0 100644 --- a/rpc/types/utils.go +++ b/rpc/types/utils.go @@ -2,26 +2,53 @@ package types import ( "bytes" - "context" + "crypto/sha1" "encoding/hex" + "errors" "fmt" "math/big" + "sort" "strconv" + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/mempool" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtypes "github.com/tendermint/tendermint/types" - "github.com/cosmos/cosmos-sdk/client" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/common/math" ethtypes "github.com/ethereum/go-ethereum/core/types" - + stratos "github.com/stratosnet/stratos-chain/types" evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" + "github.com/tendermint/tendermint/proto/tendermint/crypto" + tmrpccore "github.com/tendermint/tendermint/rpc/core" + tmrpccoretypes "github.com/tendermint/tendermint/rpc/core/types" ) +var bAttributeKeyEthereumBloom = []byte(evmtypes.AttributeKeyEthereumBloom) +var MempoolCapacity = 100 + +// HashToUint64 used to convert string or hash string to uint64 +func HashToUint64(s string) uint64 { + h := sha1.New() + h.Write([]byte(s)) + + hash := h.Sum(nil)[:2] + result := uint64(0) + for i := 0; i < len(hash); i++ { + result = result << 8 + result += uint64(hash[i]) + + } + return result +} + // RawTxToEthTx returns a evm MsgEthereum transaction from raw tx bytes. func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) ([]*evmtypes.MsgEthereumTx, error) { tx, err := clientCtx.TxConfig.TxDecoder()(txBz) @@ -40,37 +67,116 @@ func RawTxToEthTx(clientCtx client.Context, txBz tmtypes.Tx) ([]*evmtypes.MsgEth return ethTxs, nil } +func GetBlockBloom(blockResults *tmrpccoretypes.ResultBlockResults) (ethtypes.Bloom, error) { + for _, event := range blockResults.EndBlockEvents { + if event.Type != evmtypes.EventTypeBlockBloom { + continue + } + + for _, attr := range event.Attributes { + if bytes.Equal(attr.Key, bAttributeKeyEthereumBloom) { + return ethtypes.BytesToBloom(attr.Value), nil + } + } + } + return ethtypes.Bloom{}, errors.New("block bloom event is not found") +} + // EthHeaderFromTendermint is an util function that returns an Ethereum Header // from a tendermint Header. -func EthHeaderFromTendermint(header tmtypes.Header, bloom ethtypes.Bloom, baseFee *big.Int) *ethtypes.Header { - txHash := ethtypes.EmptyRootHash - if len(header.DataHash) == 0 { - txHash = common.BytesToHash(header.DataHash) +func EthHeaderFromTendermint(header tmtypes.Header) (*Header, error) { + results, err := tmrpccore.BlockResults(nil, &header.Height) + if err != nil { + return nil, err + } + gasLimit, err := BlockMaxGasFromConsensusParams(header.Height) + if err != nil { + return nil, err + } + + bloom, err := GetBlockBloom(results) + if err != nil { + return nil, err + } + + gasUsed := int64(0) + for _, txResult := range results.TxsResults { + gasUsed += txResult.GasUsed } - return ðtypes.Header{ - ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()), - UncleHash: ethtypes.EmptyUncleHash, + return &Header{ + Hash: common.BytesToHash(header.Hash()), + ParentHash: common.BytesToHash(header.LastBlockID.Hash), + UncleHash: common.Hash{}, Coinbase: common.BytesToAddress(header.ProposerAddress), Root: common.BytesToHash(header.AppHash), - TxHash: txHash, - ReceiptHash: ethtypes.EmptyRootHash, + TxHash: common.BytesToHash(header.DataHash), + ReceiptHash: common.Hash{}, Bloom: bloom, - Difficulty: big.NewInt(0), + Difficulty: big.NewInt(1), // NOTE: Maybe move to some constant? Number: big.NewInt(header.Height), - GasLimit: 0, - GasUsed: 0, - Time: uint64(header.Time.UTC().Unix()), - Extra: []byte{}, + GasLimit: uint64(gasLimit), + GasUsed: uint64(gasUsed), + Time: uint64(header.Time.Unix()), + Extra: common.Hex2Bytes(""), MixDigest: common.Hash{}, Nonce: ethtypes.BlockNonce{}, - BaseFee: baseFee, + // TODO: Add size somehow for legacy subscription support as for a new Header type after London + // is not exist but still present in newBlockHeaders call on subscription + }, nil +} + +// EthBlockFromTendermint returns a JSON-RPC compatible Ethereum blockfrom a given Tendermint block. +func EthBlockFromTendermint(txDecoder sdk.TxDecoder, block *tmtypes.Block, fullTx bool) (*Block, error) { + header, err := EthHeaderFromTendermint(block.Header) + if err != nil { + return nil, err } + + transactions := make([]interface{}, 0, len(block.Txs)) + for i, tmTx := range block.Txs { + if !fullTx { + transactions = append(transactions, common.Bytes2Hex(tmTx.Hash())) + continue + } + blockHash := common.BytesToHash(block.Hash()) + blockHeight := uint64(block.Height) + txIndex := uint64(i) + tx, err := TmTxToEthTx(txDecoder, tmTx, &blockHash, &blockHeight, &txIndex) + if err != nil { + // NOTE: Add debug? + continue + } + transactions = append(transactions, tx) + } + + return &Block{ + Number: hexutil.Uint64(header.Number.Uint64()), + Hash: header.Hash, + ParentHash: header.ParentHash, + Nonce: ethtypes.BlockNonce{}, // PoW specific + Sha3Uncles: common.Hash{}, // No uncles in Tendermint + LogsBloom: header.Bloom, + TransactionsRoot: header.TxHash, + StateRoot: header.Root, + Miner: header.Coinbase, + MixHash: common.Hash{}, + Difficulty: hexutil.Uint64(header.Difficulty.Uint64()), + TotalDifficulty: hexutil.Uint64(header.Difficulty.Uint64()), + ExtraData: common.Hex2Bytes(""), + Size: hexutil.Uint64(block.Size()), + GasLimit: (*hexutil.Big)(new(big.Int).SetUint64(header.GasLimit)), + GasUsed: (*hexutil.Big)(new(big.Int).SetUint64(header.GasUsed)), + Timestamp: hexutil.Uint64(header.Time), + Transactions: transactions, + Uncles: make([]common.Hash, 0), + ReceiptsRoot: common.Hash{}, + }, nil } // BlockMaxGasFromConsensusParams returns the gas limit for the current block from the chain consensus params. -func BlockMaxGasFromConsensusParams(goCtx context.Context, clientCtx client.Context, blockHeight int64) (int64, error) { - resConsParams, err := clientCtx.Client.ConsensusParams(goCtx, &blockHeight) +func BlockMaxGasFromConsensusParams(blockHeight int64) (int64, error) { + resConsParams, err := tmrpccore.ConsensusParams(nil, &blockHeight) if err != nil { return int64(^uint32(0)), err } @@ -86,51 +192,6 @@ func BlockMaxGasFromConsensusParams(goCtx context.Context, clientCtx client.Cont return gasLimit, nil } -// FormatBlock creates an ethereum block from a tendermint header and ethereum-formatted -// transactions. -func FormatBlock( - header tmtypes.Header, size int, gasLimit int64, - gasUsed *big.Int, transactions []interface{}, bloom ethtypes.Bloom, - validatorAddr common.Address, baseFee *big.Int, -) map[string]interface{} { - var transactionsRoot common.Hash - if len(transactions) == 0 { - transactionsRoot = ethtypes.EmptyRootHash - } else { - transactionsRoot = common.BytesToHash(header.DataHash) - } - - result := map[string]interface{}{ - "number": hexutil.Uint64(header.Height), - "hash": hexutil.Bytes(header.Hash()), - "parentHash": common.BytesToHash(header.LastBlockID.Hash.Bytes()), - "nonce": ethtypes.BlockNonce{}, // PoW specific - "sha3Uncles": ethtypes.EmptyUncleHash, // No uncles in Tendermint - "logsBloom": bloom, - "stateRoot": hexutil.Bytes(header.AppHash), - "miner": validatorAddr, - "mixHash": common.Hash{}, - "difficulty": (*hexutil.Big)(big.NewInt(0)), - "extraData": "0x", - "size": hexutil.Uint64(size), - "gasLimit": hexutil.Uint64(gasLimit), // Static gas limit - "gasUsed": (*hexutil.Big)(gasUsed), - "timestamp": hexutil.Uint64(header.Time.Unix()), - "transactionsRoot": transactionsRoot, - "receiptsRoot": ethtypes.EmptyRootHash, - - "uncles": []common.Hash{}, - "transactions": transactions, - "totalDifficulty": (*hexutil.Big)(big.NewInt(0)), - } - - if baseFee != nil { - result["baseFeePerGas"] = (*hexutil.Big)(baseFee) - } - - return result -} - type DataError interface { Error() string // returns the message ErrorData() interface{} // returns the error data @@ -162,23 +223,230 @@ func ErrRevertedWith(data []byte) DataError { } } -// NewTransactionFromMsg returns a transaction that will serialize to the RPC -// representation, with the given location metadata set (if available). -func NewTransactionFromMsg( - msg *evmtypes.MsgEthereumTx, - blockHash common.Hash, - blockNumber, index uint64, - baseFee *big.Int, -) (*RPCTransaction, error) { - tx := msg.AsTransaction() - return NewRPCTransaction(tx, blockHash, blockNumber, index, baseFee) +// GetPendingTxCountByAddress is used to get pending tx count (nonce) for user address +func GetPendingTxCountByAddress(txDecoder sdk.TxDecoder, mem mempool.Mempool, address common.Address) (total uint64) { + for _, tmTx := range mem.ReapMaxTxs(MempoolCapacity) { + tx, err := txDecoder(tmTx) + if err != nil { + continue + } + msg := tx.GetMsgs()[0] + if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok { + ethTx := ethMsg.AsTransaction() + var signer ethtypes.Signer + if ethTx.Protected() { + signer = ethtypes.LatestSignerForChainID(ethTx.ChainId()) + } else { + signer = ethtypes.HomesteadSigner{} + } + from, _ := ethtypes.Sender(signer, ethTx) + if bytes.Equal(from.Bytes(), address.Bytes()) { + total++ + } + } + } + return +} + +// GetProof performs an ABCI query with the given key and returns a merkle proof. The desired +// tendermint height to perform the query should be set in the client context. The query will be +// performed at one below this height (at the IAVL version) in order to obtain the correct merkle +// proof. Proof queries at height less than or equal to 2 are not supported. +// Issue: https://github.com/cosmos/cosmos-sdk/issues/6567 +func GetProof(height int64, storeKey string, key []byte) ([]byte, *crypto.ProofOps, error) { + // ABCI queries at height less than or equal to 2 are not supported. + // Base app does not support queries for height less than or equal to 1. + // Therefore, a query at height 2 would be equivalent to a query at height 3 + if height <= 2 { + return nil, nil, fmt.Errorf("proof queries at height <= 2 are not supported") + } + + // Use the IAVL height if a valid tendermint height is passed in. + height-- + + abciRes, err := tmrpccore.ABCIQuery(nil, fmt.Sprintf("store/%s/key", storeKey), key, height, true) + if err != nil { + return nil, nil, err + } + + return abciRes.Response.Value, abciRes.Response.ProofOps, nil +} + +func FormatTmHeaderToProto(header *tmtypes.Header) tmproto.Header { + return tmproto.Header{ + Version: header.Version, + ChainID: header.ChainID, + Height: header.Height, + Time: header.Time, + LastBlockId: tmproto.BlockID{ + Hash: header.LastBlockID.Hash, + PartSetHeader: tmproto.PartSetHeader{ + Total: header.LastBlockID.PartSetHeader.Total, + Hash: header.LastBlockID.PartSetHeader.Hash, + }, + }, + LastCommitHash: header.LastCommitHash, + DataHash: header.DataHash, + ValidatorsHash: header.ValidatorsHash, + NextValidatorsHash: header.NextValidatorsHash, + ConsensusHash: header.ConsensusHash, + AppHash: header.AppHash, + LastResultsHash: header.LastResultsHash, + EvidenceHash: header.EvidenceHash, + ProposerAddress: header.ProposerAddress, + } +} + +// GetBlockCumulativeGas returns the cumulative gas used on a block up to a given +// transaction index. The returned gas used includes the gas from both the SDK and +// EVM module transactions. +func GetBlockCumulativeGas(blockResults *tmrpccoretypes.ResultBlockResults, idx int) uint64 { + var gasUsed uint64 + + for i := 0; i < idx && i < len(blockResults.TxsResults); i++ { + tx := blockResults.TxsResults[i] + gasUsed += uint64(tx.GasUsed) + } + return gasUsed +} + +func GetPendingTx(txDecoder sdk.TxDecoder, mem mempool.Mempool, hash common.Hash) (*Transaction, error) { + for _, uTx := range mem.ReapMaxTxs(MempoolCapacity) { + if bytes.Equal(uTx.Hash(), hash.Bytes()) { + return TmTxToEthTx(txDecoder, uTx, nil, nil, nil) + } + } + return nil, nil +} + +func GetPendingTxs(txDecoder sdk.TxDecoder, mem mempool.Mempool) []*Transaction { + txs := make([]*Transaction, 0, MempoolCapacity) + for _, uTx := range mem.ReapMaxTxs(MempoolCapacity) { + if tx, err := TmTxToEthTx(txDecoder, uTx, nil, nil, nil); err == nil { + txs = append(txs, tx) + } + } + sort.Sort(TxByNonce(txs)) + return txs +} + +func GetPendingTxsLen(mem mempool.Mempool) int { + return len(mem.ReapMaxTxs(MempoolCapacity)) +} + +func GetNonEVMSignatures(sig []byte) (v, r, s *big.Int) { + var tmpV byte + if len(sig) == 65 { + tmpV = sig[len(sig)-1:][0] + } else { + // in case of 64 length + tmpV = byte(int(sig[0]) % 2) + } + + v = new(big.Int).SetBytes([]byte{tmpV + 27}) + r = new(big.Int).SetBytes(sig[:32]) + s = new(big.Int).SetBytes(sig[32:64]) + return +} + +// GetTxHash get hash depends on what type, unfortunatelly system support two tx hash algo +// in order to have opportunity for off chain tx hash computation +func GetTxHash(txDecoder sdk.TxDecoder, tmTx tmtypes.Tx) (common.Hash, error) { + tx, err := txDecoder(tmTx) + if err != nil { + return common.Hash{}, err + } + if len(tx.GetMsgs()) == 0 { + return common.Hash{}, errors.New("tx contain empty msgs") + } + msg := tx.GetMsgs()[0] + if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok { + return ethMsg.AsTransaction().Hash(), nil + } else { + return common.BytesToHash(tmTx.Hash()), nil + } +} + +// TmTxToEthTx convert ethereum and rest transaction on ethereum based structure +func TmTxToEthTx( + txDecoder sdk.TxDecoder, + tmTx tmtypes.Tx, + blockHash *common.Hash, + blockNumber, index *uint64, +) (*Transaction, error) { + tx, err := txDecoder(tmTx) + if err != nil { + return nil, err + } + + // the `msgIndex` is inferred from tx events, should be within the bound. + // always taking first into account + msg := tx.GetMsgs()[0] + + if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok { + tx := ethMsg.AsTransaction() + return NewRPCTransaction(tx, blockHash, blockNumber, index) + } else { + addr := msg.GetSigners()[0] + from := common.BytesToAddress(addr.Bytes()) + + nonce := uint64(0) + v := new(big.Int).SetInt64(0) + r := new(big.Int).SetInt64(0) + s := new(big.Int).SetInt64(0) + sigTx, ok := tx.(authsigning.SigVerifiableTx) + if ok { + sigs, _ := sigTx.GetSignaturesV2() + if len(sigs) > 0 { + sig := sigs[0] + sigProto := signing.SignatureDataToProto(sig.Data) + v, r, s = GetNonEVMSignatures(sigProto.GetSingle().GetSignature()) + nonce = sig.Sequence + } + } else { + fmt.Printf("failed to get signature for %s\n", tmTx.Hash()) + } + + gas := uint64(0) + gasPrice := new(big.Int).SetInt64(stratos.DefaultGasPrice) + if feeTx, ok := tx.(sdk.FeeTx); ok { + gas = feeTx.GetGas() + gasPrice = new(big.Int).Div( + feeTx.GetFee().AmountOf("wei").BigInt(), // TODO: mv somehow wei from config + new(big.Int).SetUint64(gas), + ) + } + + var bNumber *big.Int + if blockNumber != nil { + bNumber = new(big.Int).SetUint64(*blockNumber) + } + + return &Transaction{ + BlockHash: blockHash, + BlockNumber: (*hexutil.Big)(bNumber), + Type: hexutil.Uint64(HashToUint64(sdk.MsgTypeURL(msg))), + From: from, + Gas: hexutil.Uint64(gas), + GasPrice: (*hexutil.Big)(gasPrice), + Hash: common.BytesToHash(tmTx.Hash()), + Input: make(hexutil.Bytes, 0), + Nonce: hexutil.Uint64(nonce), + To: new(common.Address), + TransactionIndex: (*hexutil.Uint64)(index), + Value: (*hexutil.Big)(new(big.Int).SetInt64(0)), // NOTE: How to get value in generic way? + V: (*hexutil.Big)(v), + R: (*hexutil.Big)(r), + S: (*hexutil.Big)(s), + }, nil + } } // NewTransactionFromData returns a transaction that will serialize to the RPC // representation, with the given location metadata set (if available). func NewRPCTransaction( - tx *ethtypes.Transaction, blockHash common.Hash, blockNumber, index uint64, baseFee *big.Int, -) (*RPCTransaction, error) { + tx *ethtypes.Transaction, blockHash *common.Hash, blockNumber, index *uint64, +) (*Transaction, error) { // Determine the signer. For replay-protected transactions, use the most permissive // signer, because we assume that signers are backwards-compatible with old // transactions. For non-protected transactions, the homestead signer signer is used @@ -191,7 +459,7 @@ func NewRPCTransaction( } from, _ := ethtypes.Sender(signer, tx) v, r, s := tx.RawSignatureValues() - result := &RPCTransaction{ + result := &Transaction{ Type: hexutil.Uint64(tx.Type()), From: from, Gas: hexutil.Uint64(tx.Gas()), @@ -205,10 +473,10 @@ func NewRPCTransaction( R: (*hexutil.Big)(r), S: (*hexutil.Big)(s), } - if blockHash != (common.Hash{}) { - result.BlockHash = &blockHash - result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) - result.TransactionIndex = (*hexutil.Uint64)(&index) + if blockHash != nil { + result.BlockHash = blockHash + result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(*blockNumber)) + result.TransactionIndex = (*hexutil.Uint64)(index) } switch tx.Type() { case ethtypes.AccessListTxType: @@ -221,14 +489,7 @@ func NewRPCTransaction( result.ChainID = (*hexutil.Big)(tx.ChainId()) result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) - // if the transaction has been mined, compute the effective gas price - if baseFee != nil && blockHash != (common.Hash{}) { - // price = min(tip, gasFeeCap - baseFee) + baseFee - price := math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee), tx.GasFeeCap()) - result.GasPrice = (*hexutil.Big)(price) - } else { - result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) - } + result.GasPrice = (*hexutil.Big)(tx.GasPrice()) } return result, nil } diff --git a/rpc/websockets.go b/rpc/websockets.go deleted file mode 100644 index 1bc2e73b..00000000 --- a/rpc/websockets.go +++ /dev/null @@ -1,659 +0,0 @@ -package rpc - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "math/big" - "net" - "net/http" - "sync" - - "github.com/gorilla/mux" - "github.com/gorilla/websocket" - "github.com/pkg/errors" - - "github.com/tendermint/tendermint/libs/log" - rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" - tmtypes "github.com/tendermint/tendermint/types" - - "github.com/cosmos/cosmos-sdk/client" - - "github.com/ethereum/go-ethereum/common" - ethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/filters" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" - - "github.com/stratosnet/stratos-chain/rpc/ethereum/pubsub" - rpcfilters "github.com/stratosnet/stratos-chain/rpc/namespaces/ethereum/eth/filters" - "github.com/stratosnet/stratos-chain/rpc/types" - "github.com/stratosnet/stratos-chain/server/config" - evmtypes "github.com/stratosnet/stratos-chain/x/evm/types" -) - -type WebsocketsServer interface { - Start() -} - -type SubscriptionResponseJSON struct { - Jsonrpc string `json:"jsonrpc"` - Result interface{} `json:"result"` - ID float64 `json:"id"` -} - -type SubscriptionNotification struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - Params *SubscriptionResult `json:"params"` -} - -type SubscriptionResult struct { - Subscription rpc.ID `json:"subscription"` - Result interface{} `json:"result"` -} - -type ErrorResponseJSON struct { - Jsonrpc string `json:"jsonrpc"` - Error *ErrorMessageJSON `json:"error"` - ID *big.Int `json:"id"` -} - -type ErrorMessageJSON struct { - Code *big.Int `json:"code"` - Message string `json:"message"` -} - -type websocketsServer struct { - rpcAddr string // listen address of rest-server - wsAddr string // listen address of ws server - certFile string - keyFile string - api *pubSubAPI - logger log.Logger -} - -func NewWebsocketsServer(clientCtx client.Context, logger log.Logger, tmWSClient *rpcclient.WSClient, cfg config.Config) WebsocketsServer { - logger = logger.With("api", "websocket-server") - _, port, _ := net.SplitHostPort(cfg.JSONRPC.Address) - - return &websocketsServer{ - rpcAddr: "localhost:" + port, // FIXME: this shouldn't be hardcoded to localhost - wsAddr: cfg.JSONRPC.WsAddress, - certFile: cfg.TLS.CertificatePath, - keyFile: cfg.TLS.KeyPath, - api: newPubSubAPI(clientCtx, logger, tmWSClient), - logger: logger, - } -} - -func (s *websocketsServer) Start() { - ws := mux.NewRouter() - ws.Handle("/", s) - - go func() { - var err error - if s.certFile == "" || s.keyFile == "" { - err = http.ListenAndServe(s.wsAddr, ws) - } else { - err = http.ListenAndServeTLS(s.wsAddr, s.certFile, s.keyFile, ws) - } - - if err != nil { - if err == http.ErrServerClosed { - return - } - - s.logger.Error("failed to start HTTP server for WS", "error", err.Error()) - } - }() -} - -func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - s.logger.Debug("websocket upgrade failed", "error", err.Error()) - return - } - - s.readLoop(&wsConn{ - mux: new(sync.Mutex), - conn: conn, - }) -} - -func (s *websocketsServer) sendErrResponse(wsConn *wsConn, msg string) { - res := &ErrorResponseJSON{ - Jsonrpc: "2.0", - Error: &ErrorMessageJSON{ - Code: big.NewInt(-32600), - Message: msg, - }, - ID: nil, - } - - _ = wsConn.WriteJSON(res) -} - -type wsConn struct { - conn *websocket.Conn - mux *sync.Mutex -} - -func (w *wsConn) WriteJSON(v interface{}) error { - w.mux.Lock() - defer w.mux.Unlock() - - return w.conn.WriteJSON(v) -} - -func (w *wsConn) Close() error { - w.mux.Lock() - defer w.mux.Unlock() - - return w.conn.Close() -} - -func (w *wsConn) ReadMessage() (messageType int, p []byte, err error) { - // not protected by write mutex - - return w.conn.ReadMessage() -} - -func (s *websocketsServer) readLoop(wsConn *wsConn) { - // subscriptions of current connection - subscriptions := make(map[rpc.ID]pubsub.UnsubscribeFunc) - defer func() { - // cancel all subscriptions when connection closed - for _, unsubFn := range subscriptions { - unsubFn() - } - }() - - for { - _, mb, err := wsConn.ReadMessage() - if err != nil { - _ = wsConn.Close() - return - } - - var msg map[string]interface{} - if err = json.Unmarshal(mb, &msg); err != nil { - s.sendErrResponse(wsConn, err.Error()) - continue - } - - // check if method == eth_subscribe or eth_unsubscribe - method, ok := msg["method"].(string) - if !ok { - // otherwise, call the usual rpc server to respond - if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { - s.sendErrResponse(wsConn, err.Error()) - } - - continue - } - - connID, ok := msg["id"].(float64) - if !ok { - s.sendErrResponse( - wsConn, - fmt.Errorf("invalid type for connection ID: %T", msg["id"]).Error(), - ) - continue - } - - switch method { - case "eth_subscribe": - params, ok := msg["params"].([]interface{}) - if !ok { - s.sendErrResponse(wsConn, "invalid parameters") - continue - } - - if len(params) == 0 { - s.sendErrResponse(wsConn, "empty parameters") - continue - } - - subID := rpc.NewID() - unsubFn, err := s.api.subscribe(wsConn, subID, params) - if err != nil { - s.sendErrResponse(wsConn, err.Error()) - continue - } - subscriptions[subID] = unsubFn - - res := &SubscriptionResponseJSON{ - Jsonrpc: "2.0", - ID: connID, - Result: subID, - } - - if err := wsConn.WriteJSON(res); err != nil { - break - } - case "eth_unsubscribe": - params, ok := msg["params"].([]interface{}) - if !ok { - s.sendErrResponse(wsConn, "invalid parameters") - continue - } - - if len(params) == 0 { - s.sendErrResponse(wsConn, "empty parameters") - continue - } - - id, ok := params[0].(string) - if !ok { - s.sendErrResponse(wsConn, "invalid parameters") - continue - } - - subID := rpc.ID(id) - unsubFn, ok := subscriptions[subID] - if ok { - delete(subscriptions, subID) - unsubFn() - } - - res := &SubscriptionResponseJSON{ - Jsonrpc: "2.0", - ID: connID, - Result: ok, - } - - if err := wsConn.WriteJSON(res); err != nil { - break - } - default: - // otherwise, call the usual rpc server to respond - if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { - s.sendErrResponse(wsConn, err.Error()) - } - } - } -} - -// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response -// to the client over websockets -func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) error { - req, err := http.NewRequestWithContext(context.Background(), "POST", "http://"+s.rpcAddr, bytes.NewBuffer(mb)) - if err != nil { - return errors.Wrap(err, "Could not build request") - } - - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return errors.Wrap(err, "Could not perform request") - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return errors.Wrap(err, "could not read body from response") - } - - var wsSend interface{} - err = json.Unmarshal(body, &wsSend) - if err != nil { - return errors.Wrap(err, "failed to unmarshal rest-server response") - } - - return wsConn.WriteJSON(wsSend) -} - -// pubSubAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec -type pubSubAPI struct { - events *rpcfilters.EventSystem - logger log.Logger - clientCtx client.Context -} - -// newPubSubAPI creates an instance of the ethereum PubSub API. -func newPubSubAPI(clientCtx client.Context, logger log.Logger, tmWSClient *rpcclient.WSClient) *pubSubAPI { - logger = logger.With("module", "websocket-client") - return &pubSubAPI{ - events: rpcfilters.NewEventSystem(logger, tmWSClient), - logger: logger, - clientCtx: clientCtx, - } -} - -func (api *pubSubAPI) subscribe(wsConn *wsConn, subID rpc.ID, params []interface{}) (pubsub.UnsubscribeFunc, error) { - method, ok := params[0].(string) - if !ok { - return nil, errors.New("invalid parameters") - } - - switch method { - case "newHeads": - // TODO: handle extra params - return api.subscribeNewHeads(wsConn, subID) - case "logs": - if len(params) > 1 { - return api.subscribeLogs(wsConn, subID, params[1]) - } - return api.subscribeLogs(wsConn, subID, nil) - case "newPendingTransactions": - return api.subscribePendingTransactions(wsConn, subID) - case "syncing": - return api.subscribeSyncing(wsConn, subID) - default: - return nil, errors.Errorf("unsupported method %s", method) - } -} - -func (api *pubSubAPI) subscribeNewHeads(wsConn *wsConn, subID rpc.ID) (pubsub.UnsubscribeFunc, error) { - sub, unsubFn, err := api.events.SubscribeNewHeads() - if err != nil { - return nil, errors.Wrap(err, "error creating block filter") - } - - // TODO: use events - baseFee := big.NewInt(params.InitialBaseFee) - - go func() { - headersCh := sub.Event() - errCh := sub.Err() - for { - select { - case event, ok := <-headersCh: - if !ok { - return - } - - data, ok := event.Data.(tmtypes.EventDataNewBlockHeader) - if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data)) - continue - } - - header := types.EthHeaderFromTendermint(data.Header, ethtypes.Bloom{}, baseFee) - - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: subID, - Result: header, - }, - } - - err = wsConn.WriteJSON(res) - if err != nil { - api.logger.Error("error writing header, will drop peer", "error", err.Error()) - - try(func() { - if err != websocket.ErrCloseSent { - _ = wsConn.Close() - } - }, api.logger, "closing websocket peer sub") - } - case err, ok := <-errCh: - if !ok { - return - } - api.logger.Debug("dropping NewHeads WebSocket subscription", "subscription-id", subID, "error", err.Error()) - } - } - }() - - return unsubFn, nil -} - -func try(fn func(), l log.Logger, desc string) { - defer func() { - if x := recover(); x != nil { - if err, ok := x.(error); ok { - // debug.PrintStack() - l.Debug("panic during "+desc, "error", err.Error()) - return - } - - l.Debug(fmt.Sprintf("panic during %s: %+v", desc, x)) - return - } - }() - - fn() -} - -func (api *pubSubAPI) subscribeLogs(wsConn *wsConn, subID rpc.ID, extra interface{}) (pubsub.UnsubscribeFunc, error) { - crit := filters.FilterCriteria{} - - if extra != nil { - params, ok := extra.(map[string]interface{}) - if !ok { - err := errors.New("invalid criteria") - api.logger.Debug("invalid criteria", "type", fmt.Sprintf("%T", extra)) - return nil, err - } - - if params["address"] != nil { - address, isString := params["address"].(string) - addresses, isSlice := params["address"].([]interface{}) - if !isString && !isSlice { - err := errors.New("invalid addresses; must be address or array of addresses") - api.logger.Debug("invalid addresses", "type", fmt.Sprintf("%T", params["address"])) - return nil, err - } - - if ok { - crit.Addresses = []common.Address{common.HexToAddress(address)} - } - - if isSlice { - crit.Addresses = []common.Address{} - for _, addr := range addresses { - address, ok := addr.(string) - if !ok { - err := errors.New("invalid address") - api.logger.Debug("invalid address", "type", fmt.Sprintf("%T", addr)) - return nil, err - } - - crit.Addresses = append(crit.Addresses, common.HexToAddress(address)) - } - } - } - - if params["topics"] != nil { - topics, ok := params["topics"].([]interface{}) - if !ok { - err := errors.Errorf("invalid topics: %s", topics) - api.logger.Error("invalid topics", "type", fmt.Sprintf("%T", topics)) - return nil, err - } - - crit.Topics = make([][]common.Hash, len(topics)) - - addCritTopic := func(topicIdx int, topic interface{}) error { - tstr, ok := topic.(string) - if !ok { - err := errors.Errorf("invalid topic: %s", topic) - api.logger.Error("invalid topic", "type", fmt.Sprintf("%T", topic)) - return err - } - - crit.Topics[topicIdx] = []common.Hash{common.HexToHash(tstr)} - return nil - } - - for topicIdx, subtopics := range topics { - if subtopics == nil { - continue - } - - // in case we don't have list, but a single topic value - if topic, ok := subtopics.(string); ok { - if err := addCritTopic(topicIdx, topic); err != nil { - return nil, err - } - - continue - } - - // in case we actually have a list of subtopics - subtopicsList, ok := subtopics.([]interface{}) - if !ok { - err := errors.New("invalid subtopics") - api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopics)) - return nil, err - } - - subtopicsCollect := make([]common.Hash, len(subtopicsList)) - for idx, subtopic := range subtopicsList { - tstr, ok := subtopic.(string) - if !ok { - err := errors.Errorf("invalid subtopic: %s", subtopic) - api.logger.Error("invalid subtopic", "type", fmt.Sprintf("%T", subtopic)) - return nil, err - } - - subtopicsCollect[idx] = common.HexToHash(tstr) - } - - crit.Topics[topicIdx] = subtopicsCollect - } - } - } - - sub, unsubFn, err := api.events.SubscribeLogs(crit) - if err != nil { - api.logger.Error("failed to subscribe logs", "error", err.Error()) - return nil, err - } - - go func() { - ch := sub.Event() - errCh := sub.Err() - for { - select { - case event, ok := <-ch: - if !ok { - return - } - - dataTx, ok := event.Data.(tmtypes.EventDataTx) - if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", event.Data)) - continue - } - - txResponse, err := evmtypes.DecodeTxResponse(dataTx.TxResult.Result.Data) - if err != nil { - api.logger.Error("failed to decode tx response", "error", err.Error()) - return - } - - logs := rpcfilters.FilterLogs(evmtypes.LogsToEthereum(txResponse.Logs), crit.FromBlock, crit.ToBlock, crit.Addresses, crit.Topics) - if len(logs) == 0 { - continue - } - - for _, ethLog := range logs { - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: subID, - Result: ethLog, - }, - } - - err = wsConn.WriteJSON(res) - if err != nil { - try(func() { - if err != websocket.ErrCloseSent { - _ = wsConn.Close() - } - }, api.logger, "closing websocket peer sub") - } - } - case err, ok := <-errCh: - if !ok { - return - } - api.logger.Debug("dropping Logs WebSocket subscription", "subscription-id", subID, "error", err.Error()) - } - } - }() - - return unsubFn, nil -} - -func (api *pubSubAPI) subscribePendingTransactions(wsConn *wsConn, subID rpc.ID) (pubsub.UnsubscribeFunc, error) { - sub, unsubFn, err := api.events.SubscribePendingTxs() - if err != nil { - return nil, errors.Wrap(err, "error creating block filter: %s") - } - - go func() { - txsCh := sub.Event() - errCh := sub.Err() - for { - select { - case ev := <-txsCh: - data, ok := ev.Data.(tmtypes.EventDataTx) - if !ok { - api.logger.Debug("event data type mismatch", "type", fmt.Sprintf("%T", ev.Data)) - continue - } - - ethTxs, err := types.RawTxToEthTx(api.clientCtx, data.Tx) - if err != nil { - // not ethereum tx - continue - } - - for _, ethTx := range ethTxs { - // write to ws conn - res := &SubscriptionNotification{ - Jsonrpc: "2.0", - Method: "eth_subscription", - Params: &SubscriptionResult{ - Subscription: subID, - Result: ethTx.Hash, - }, - } - - err = wsConn.WriteJSON(res) - if err != nil { - api.logger.Debug("error writing header, will drop peer", "error", err.Error()) - - try(func() { - if err != websocket.ErrCloseSent { - _ = wsConn.Close() - } - }, api.logger, "closing websocket peer sub") - } - } - case err, ok := <-errCh: - if !ok { - return - } - api.logger.Debug("dropping PendingTransactions WebSocket subscription", subID, "error", err.Error()) - } - } - }() - - return unsubFn, nil -} - -func (api *pubSubAPI) subscribeSyncing(wsConn *wsConn, subID rpc.ID) (pubsub.UnsubscribeFunc, error) { - return nil, errors.New("syncing subscription is not implemented") -} diff --git a/server/json_rpc.go b/server/json_rpc.go index 6c420300..2a67bad8 100644 --- a/server/json_rpc.go +++ b/server/json_rpc.go @@ -1,26 +1,20 @@ package server import ( - "net/http" - "time" - - "github.com/gorilla/mux" - "github.com/rs/cors" + "github.com/tendermint/tendermint/node" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/server" - "github.com/cosmos/cosmos-sdk/server/types" ethlog "github.com/ethereum/go-ethereum/log" - ethrpc "github.com/ethereum/go-ethereum/rpc" + storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/stratosnet/stratos-chain/rpc" "github.com/stratosnet/stratos-chain/server/config" + evmkeeper "github.com/stratosnet/stratos-chain/x/evm/keeper" ) // StartJSONRPC starts the JSON-RPC server -func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEndpoint string, config config.Config) (*http.Server, chan struct{}, error) { - tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) - +func StartJSONRPC(ctx *server.Context, tmNode *node.Node, evmKeeper *evmkeeper.Keeper, ms storetypes.MultiStore, clientCtx client.Context, config config.Config) error { logger := ctx.Logger.With("module", "geth") ethlog.Root().SetHandler(ethlog.FuncHandler(func(r *ethlog.Record) error { switch r.Lvl { @@ -34,65 +28,15 @@ func StartJSONRPC(ctx *server.Context, clientCtx client.Context, tmRPCAddr, tmEn return nil })) - rpcServer := ethrpc.NewServer() - - rpcAPIArr := config.JSONRPC.API - apis := rpc.GetRPCAPIs(ctx, clientCtx, tmWsClient, rpcAPIArr) - - for _, api := range apis { - if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { - ctx.Logger.Error( - "failed to register service in JSON RPC namespace", - "namespace", api.Namespace, - "service", api.Service, - ) - return nil, nil, err - } - } - - r := mux.NewRouter() - r.HandleFunc("/", rpcServer.ServeHTTP).Methods("POST") - - handlerWithCors := cors.Default() - if config.API.EnableUnsafeCORS { - handlerWithCors = cors.AllowAll() + apis := rpc.GetRPCAPIs(ctx, tmNode, evmKeeper, ms, clientCtx, config.JSONRPC.API) + web3Srv := rpc.NewWeb3Server(config, logger) + err := web3Srv.StartHTTP(apis) + if err != nil { + return err } - - httpSrv := &http.Server{ - Addr: config.JSONRPC.Address, - Handler: handlerWithCors.Handler(r), - ReadTimeout: config.JSONRPC.HTTPTimeout, - WriteTimeout: config.JSONRPC.HTTPTimeout, - IdleTimeout: config.JSONRPC.HTTPIdleTimeout, - } - httpSrvDone := make(chan struct{}, 1) - - errCh := make(chan error) - go func() { - ctx.Logger.Info("Starting JSON-RPC server", "address", config.JSONRPC.Address) - if err := httpSrv.ListenAndServe(); err != nil { - if err == http.ErrServerClosed { - close(httpSrvDone) - return - } - - ctx.Logger.Error("failed to start JSON-RPC server", "error", err.Error()) - errCh <- err - } - }() - - select { - case err := <-errCh: - ctx.Logger.Error("failed to boot JSON-RPC server", "error", err.Error()) - return nil, nil, err - case <-time.After(types.ServerStartTime): // assume JSON RPC server started successfully + err = web3Srv.StartWS(apis) + if err != nil { + return err } - - ctx.Logger.Info("Starting JSON WebSocket server", "address", config.JSONRPC.WsAddress) - - // allocate separate WS connection to Tendermint - tmWsClient = ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) - wsSrv := rpc.NewWebsocketsServer(clientCtx, ctx.Logger, tmWsClient, config) - wsSrv.Start() - return httpSrv, httpSrvDone, nil + return nil } diff --git a/server/start.go b/server/start.go index a0d33efc..582a7fbb 100644 --- a/server/start.go +++ b/server/start.go @@ -1,8 +1,6 @@ package server import ( - "context" - "encoding/json" "fmt" "io" "net/http" @@ -38,6 +36,7 @@ import ( "github.com/cosmos/cosmos-sdk/server/types" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + stosapp "github.com/stratosnet/stratos-chain/app" ethdebug "github.com/stratosnet/stratos-chain/rpc/namespaces/ethereum/debug" "github.com/stratosnet/stratos-chain/server/config" @@ -413,28 +412,10 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, appCreator ty } } - var ( - httpSrv *http.Server - httpSrvDone chan struct{} - ) - if config.JSONRPC.Enable { - genDoc, err := genDocProvider() - if err != nil { - return err - } - var genAppState map[string]interface{} - json.Unmarshal([]byte(genDoc.AppState), &genAppState) - genEvm := genAppState["evm"].(map[string]interface{}) - genEvmParams := genEvm["params"].(map[string]interface{}) - genEvmParamsChainCfg := genEvmParams["chain_config"].(map[string]interface{}) - genEvmParamsChainId := genEvmParamsChainCfg["chain_id"].(string) - - clientCtx := clientCtx.WithChainID(genEvmParamsChainId) - - tmEndpoint := "/websocket" - tmRPCAddr := cfg.RPC.ListenAddress - httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, config) + evmApp := app.(stosapp.EVMLKeeperApp) + + err = StartJSONRPC(ctx, tmNode, evmApp.GetEVMKeeper(), app.CommitMultiStore(), clientCtx, config) if err != nil { return err } @@ -462,21 +443,6 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, appCreator ty } } - if httpSrv != nil { - shutdownCtx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) - defer cancelFn() - - if err := httpSrv.Shutdown(shutdownCtx); err != nil { - logger.Error("HTTP server shutdown produced a warning", "error", err.Error()) - } else { - logger.Info("HTTP server shut down, waiting 5 sec") - select { - case <-time.Tick(5 * time.Second): - case <-httpSrvDone: - } - } - } - logger.Info("Bye!") }() diff --git a/x/evm/client/rest/rest.go b/x/evm/client/rest/rest.go index c6927aa5..b4953b55 100644 --- a/x/evm/client/rest/rest.go +++ b/x/evm/client/rest/rest.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "encoding/json" "errors" - "math/big" "net/http" "strings" @@ -19,7 +18,6 @@ import ( "github.com/ethereum/go-ethereum/common" rpctypes "github.com/stratosnet/stratos-chain/rpc/types" - "github.com/stratosnet/stratos-chain/x/evm/types" ) // RegisterTxRoutes - Central function to define routes that get registered by the main application @@ -82,17 +80,6 @@ func getEthTransactionByHash(clientCtx client.Context, hashHex string) ([]byte, return nil, err } - client := types.NewQueryClient(clientCtx) - res, err := client.BaseFee(context.Background(), &types.QueryBaseFeeRequest{}) - if err != nil { - return nil, err - } - - var baseFee *big.Int - if res.BaseFee != nil { - baseFee = res.BaseFee.BigInt() - } - blockHash := common.BytesToHash(block.Block.Header.Hash()) ethTxs, err := rpctypes.RawTxToEthTx(clientCtx, tx.Tx) @@ -101,10 +88,11 @@ func getEthTransactionByHash(clientCtx client.Context, hashHex string) ([]byte, } height := uint64(tx.Height) + txIndex := uint64(tx.Index) for _, ethTx := range ethTxs { if common.HexToHash(ethTx.Hash) == common.BytesToHash(hash) { - rpcTx, err := rpctypes.NewRPCTransaction(ethTx.AsTransaction(), blockHash, height, uint64(tx.Index), baseFee) + rpcTx, err := rpctypes.NewRPCTransaction(ethTx.AsTransaction(), &blockHash, &height, &txIndex) if err != nil { return nil, err } diff --git a/x/evm/keeper/state_transition.go b/x/evm/keeper/state_transition.go index 9103d21a..92c097b8 100644 --- a/x/evm/keeper/state_transition.go +++ b/x/evm/keeper/state_transition.go @@ -3,6 +3,7 @@ package keeper import ( "math" "math/big" + "sync" tmtypes "github.com/tendermint/tendermint/types" @@ -107,6 +108,7 @@ func (k Keeper) VMConfig(ctx sdk.Context, msg core.Message, cfg *types.EVMConfig var debug bool if _, ok := tracer.(types.NoOpTracer); !ok { debug = true + noBaseFee = true } return vm.Config{ @@ -122,6 +124,9 @@ func (k Keeper) VMConfig(ctx sdk.Context, msg core.Message, cfg *types.EVMConfig // 2. The requested height is from an previous height from the same chain epoch // 3. The requested height is from a height greater than the latest one func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { + cache := make(map[int64]common.Hash) + var rw sync.Mutex + return func(height uint64) common.Hash { h, err := stratos.SafeInt64(height) if err != nil { @@ -135,24 +140,20 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { // hash directly from the context. // Note: The headerHash is only set at begin block, it will be nil in case of a query context headerHash := ctx.HeaderHash() - if len(headerHash) != 0 { - return common.BytesToHash(headerHash) - } - - // only recompute the hash if not set (eg: checkTxState) - contextBlockHeader := ctx.BlockHeader() - header, err := tmtypes.HeaderFromProto(&contextBlockHeader) - if err != nil { - k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err) - return common.Hash{} - } - - headerHash = header.Hash() return common.BytesToHash(headerHash) case ctx.BlockHeight() > h: // Case 2: if the chain is not the current height we need to retrieve the hash from the store for the // current chain epoch. This only applies if the current height is greater than the requested height. + + // NOTE: In case of concurrency + rw.Lock() + defer rw.Unlock() + + if hash, ok := cache[h]; ok { + return hash + } + histInfo, found := k.stakingKeeper.GetHistoricalInfo(ctx, h) if !found { k.Logger(ctx).Debug("historical info not found", "height", h) @@ -165,7 +166,9 @@ func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { return common.Hash{} } - return common.BytesToHash(header.Hash()) + hash := common.BytesToHash(header.Hash()) + cache[h] = hash + return hash default: // Case 3: heights greater than the current one returns an empty hash. return common.Hash{} @@ -374,14 +377,29 @@ func (k *Keeper) ApplyMessageWithConfig(ctx sdk.Context, msg core.Message, trace stateDB.PrepareAccessList(msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) } + // NOTE: In order to achive this, nonce should be checked in ante handler and increased, othervise + // it could make pottential nonce overide with double spend or contract rewrite + // take over the nonce management from evm: + // reset sender's nonce to msg.Nonce() before calling evm on msg nonce + // as nonce already increased in db + // TODO: UNCOMMENT THIS FOR PROD!!!!!! + // stateDB.SetNonce(sender.Address(), msg.Nonce()) + if contractCreation { - // take over the nonce management from evm: - // - reset sender's nonce to msg.Nonce() before calling evm. - // - increase sender's nonce by one no matter the result. + // no need to increase nonce here as contract as during contract creation: + // - tx.origin nonce increase automatically + // - if IsEIP158 enabled, contract nonce will be set as 1 + + // NOTE: REMOVE THIS FOR PROD!!!!!!!!!!! stateDB.SetNonce(sender.Address(), msg.Nonce()) ret, _, leftoverGas, vmErr = evm.Create(sender, msg.Data(), leftoverGas, msg.Value()) + + // NOTE: REMOVE THIS FOR PROD!!!!!!!!!!! stateDB.SetNonce(sender.Address(), msg.Nonce()+1) } else { + // should be incresed before call on nonce from msg so we make sure nonce remaining same as on init tx + // TODO: UNCOMMENT THIS FOR PROD!!!!!! + // stateDB.SetNonce(sender.Address(), msg.Nonce()+1) ret, leftoverGas, vmErr = evm.Call(sender, *msg.To(), msg.Data(), leftoverGas, msg.Value()) } diff --git a/x/evm/tracers/js/bigint.go b/x/evm/tracers/js/bigint.go new file mode 100644 index 00000000..9aeb3304 --- /dev/null +++ b/x/evm/tracers/js/bigint.go @@ -0,0 +1,20 @@ +// Copyright 2021 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 + +// 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/x/evm/tracers/js/goja.go b/x/evm/tracers/js/goja.go new file mode 100644 index 00000000..adbcfcaf --- /dev/null +++ b/x/evm/tracers/js/goja.go @@ -0,0 +1,960 @@ +// 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" + "time" + + "github.com/dop251/goja" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" + jsassets "github.com/stratosnet/stratos-chain/x/evm/tracers/js/internal/tracers" +) + +var assetTracers = make(map[string]string) + +// InitTracer retrieves the JavaScript transaction tracers included in go-ethereum. +func InitTracer() { + var err error + assetTracers, err = jsassets.Load() + if err != nil { + panic(err) + } + tracers.RegisterLookup(true, newJsTracer) +} + +// 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(vm *goja.Runtime, val string) (goja.Value, error) +type toBufFn = func(vm *goja.Runtime, val []byte) (goja.Value, error) +type fromBufFn = func(vm *goja.Runtime, buf goja.Value, allowString bool) ([]byte, error) + +func toBuf(vm *goja.Runtime, bufType goja.Value, val []byte) (goja.Value, error) { + // bufType is usually Uint8Array. This is equivalent to `new Uint8Array(val)` in JS. + return vm.New(bufType, vm.ToValue(vm.NewArrayBuffer(val))) +} + +func fromBuf(vm *goja.Runtime, bufType goja.Value, buf goja.Value, allowString bool) ([]byte, error) { + obj := buf.ToObject(vm) + switch obj.ClassName() { + case "String": + if !allowString { + break + } + return common.FromHex(obj.String()), nil + + case "Array": + var b []byte + if err := vm.ExportTo(buf, &b); err != nil { + return nil, err + } + return b, nil + + case "Object": + if !obj.Get("constructor").SameAs(bufType) { + break + } + b := obj.Get("buffer").Export().(goja.ArrayBuffer).Bytes() + return b, nil + } + return nil, fmt.Errorf("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 { + vm *goja.Runtime + env *vm.EVM + 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 []common.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 + err error // Any error that should stop tracing + 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 +} + +// newJsTracer instantiates a new JS tracer instance. code is either +// the name of a built-in JS tracer or 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 *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + if c, ok := assetTracers[code]; ok { + code = c + } + vm := goja.New() + // By default field names are exported to JS as is, i.e. capitalized. + vm.SetFieldNameMapper(goja.UncapFieldNameMapper()) + t := &jsTracer{ + vm: vm, + ctx: make(map[string]goja.Value), + } + if ctx == nil { + ctx = new(tracers.Context) + } + if ctx.BlockHash != (common.Hash{}) { + t.ctx["blockHash"] = vm.ToValue(ctx.BlockHash.Bytes()) + if ctx.TxHash != (common.Hash{}) { + t.ctx["txIndex"] = vm.ToValue(ctx.TxIndex) + t.ctx["txHash"] = vm.ToValue(ctx.TxHash.Bytes()) + } + } + + t.setTypeConverters() + t.setBuiltinFunctions() + ret, err := vm.RunString("(" + code + ")") + if err != nil { + return nil, err + } + // Check tracer's interface for required and optional methods. + obj := ret.ToObject(vm) + 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, vm.ToValue(cfgStr)); err != nil { + return nil, err + } + } + // Setup objects carrying data to JS. These are created once and re-used. + t.log = &steplog{ + vm: vm, + op: &opObj{vm: vm}, + memory: &memoryObj{vm: vm, toBig: t.toBig, toBuf: t.toBuf}, + stack: &stackObj{vm: vm, toBig: t.toBig}, + contract: &contractObj{vm: vm, toBig: t.toBig, toBuf: t.toBuf}, + } + t.frame = &callframe{vm: vm, toBig: t.toBig, toBuf: t.toBuf} + t.frameResult = &callframeResult{vm: vm, toBuf: t.toBuf} + t.frameValue = t.frame.setupObject() + t.frameResultValue = t.frameResult.setupObject() + t.logValue = t.log.setupObject() + return t, nil +} + +// CaptureTxStart implements the Tracer interface and is invoked at the beginning of +// transaction processing. +func (t *jsTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +// CaptureTxStart implements the Tracer interface and is invoked at the end of +// transaction processing. +func (t *jsTracer) CaptureTxEnd(restGas uint64) {} + +// CaptureStart implements the Tracer interface to initialize the tracing operation. +func (t *jsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + db := &dbObj{db: env.StateDB, vm: t.vm, toBig: t.toBig, toBuf: t.toBuf, fromBuf: t.fromBuf} + t.dbValue = db.setupObject() + if create { + t.ctx["type"] = t.vm.ToValue("CREATE") + } else { + t.ctx["type"] = t.vm.ToValue("CALL") + } + t.ctx["from"] = t.vm.ToValue(from.Bytes()) + t.ctx["to"] = t.vm.ToValue(to.Bytes()) + t.ctx["input"] = t.vm.ToValue(input) + t.ctx["gas"] = t.vm.ToValue(gas) + t.ctx["gasPrice"] = t.vm.ToValue(env.TxContext.GasPrice) + valueBig, err := t.toBig(t.vm, value.String()) + if err != nil { + t.err = err + return + } + t.ctx["value"] = valueBig + t.ctx["block"] = t.vm.ToValue(env.Context.BlockNumber.Uint64()) + // Update list of precompiles based on current block + rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil) + t.activePrecompiles = vm.ActivePrecompiles(rules) + t.ctx["intrinsicGas"] = t.vm.ToValue(t.gasLimit - gas) +} + +// CaptureState implements the Tracer interface to trace a single step of VM execution. +func (t *jsTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + if !t.traceStep { + return + } + if t.err != nil { + return + } + + log := t.log + log.op.op = op + log.memory.memory = scope.Memory + log.stack.stack = scope.Stack + log.contract.contract = scope.Contract + log.pc = uint(pc) + log.gas = uint(gas) + log.cost = uint(cost) + log.depth = uint(depth) + log.err = err + if _, err := t.step(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("step", err) + } +} + +// CaptureFault implements the Tracer interface to trace an execution fault +func (t *jsTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { + if t.err != nil { + return + } + // Other log fields have been already set as part of the last CaptureState. + t.log.err = err + if _, err := t.fault(t.obj, t.logValue, t.dbValue); err != nil { + t.onError("fault", err) + } +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *jsTracer) CaptureEnd(output []byte, gasUsed uint64, duration time.Duration, err error) { + t.ctx["output"] = t.vm.ToValue(output) + t.ctx["time"] = t.vm.ToValue(duration.String()) + t.ctx["gasUsed"] = t.vm.ToValue(gasUsed) + if err != nil { + t.ctx["error"] = t.vm.ToValue(err.Error()) + } +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *jsTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if !t.traceFrame { + return + } + if t.err != nil { + return + } + + t.frame.typ = typ.String() + 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) + } +} + +// 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) + } +} + +// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error +func (t *jsTracer) GetResult() (json.RawMessage, error) { + ctx := t.vm.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.err +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *jsTracer) Stop(err error) { + t.vm.Interrupt(err) +} + +// 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) { + t.err = wrapError(context, err) + // `env` is set on CaptureStart which comes before any JS execution. + // So it should be non-nil. + t.env.Cancel() +} + +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.vm + // 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 hexutil.Encode(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 = common.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 = common.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 := common.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 := common.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, common.HexToHash(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 := common.BytesToAddress(a) + for _, p := range t.activePrecompiles { + if p == addr { + return true + } + } + return false + }) + vm.Set("slice", func(slice goja.Value, start, end int) goja.Value { + b, err := t.fromBuf(vm, slice, false) + if err != nil { + vm.Interrupt(err) + return nil + } + if start < 0 || start > end || end > 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.vm.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.vm.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 vm.OpCode +} + +func (o *opObj) ToNumber() int { + return int(o.op) +} + +func (o *opObj) ToString() string { + return o.op.String() +} + +func (o *opObj) IsPush() bool { + return o.op.IsPush() +} + +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 *vm.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) + } + if mo.memory.Len() < int(end) { + return nil, fmt.Errorf("tracer accessed out of bound memory: available %d, offset %d, size %d", mo.memory.Len(), begin, end-begin) + } + return mo.memory.GetCopy(begin, end-begin), 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 *vm.Stack + 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.Data()) <= idx || idx < 0 { + return nil, fmt.Errorf("tracer accessed out of bound stack: size %d, index %d", len(s.stack.Data()), idx) + } + return s.stack.Back(idx).ToBig(), nil +} + +func (s *stackObj) Length() int { + return len(s.stack.Data()) +} + +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 { + db vm.StateDB + 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 := common.BytesToAddress(a) + value := do.db.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 := common.BytesToAddress(a) + return do.db.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 := common.BytesToAddress(a) + code := do.db.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 := common.BytesToAddress(a) + h, err := do.fromBuf(do.vm, hashSlice, false) + if err != nil { + do.vm.Interrupt(err) + return nil + } + hash := common.BytesToHash(h) + state := do.db.GetState(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 := common.BytesToAddress(a) + return do.db.Exist(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 *vm.Contract + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn +} + +func (co *contractObj) GetCaller() goja.Value { + caller := co.contract.Caller().Bytes() + res, err := co.toBuf(co.vm, caller) + if err != nil { + co.vm.Interrupt(err) + return nil + } + return res +} + +func (co *contractObj) GetAddress() goja.Value { + addr := co.contract.Address().Bytes() + res, err := co.toBuf(co.vm, addr) + if err != nil { + co.vm.Interrupt(err) + return nil + } + return res +} + +func (co *contractObj) GetValue() goja.Value { + value := co.contract.Value() + res, err := co.toBig(co.vm, value.String()) + if err != nil { + co.vm.Interrupt(err) + return nil + } + return res +} + +func (co *contractObj) GetInput() goja.Value { + input := common.CopyBytes(co.contract.Input) + res, err := co.toBuf(co.vm, input) + if err != nil { + co.vm.Interrupt(err) + return nil + } + return res +} + +func (c *contractObj) setupObject() *goja.Object { + o := c.vm.NewObject() + o.Set("getCaller", c.vm.ToValue(c.GetCaller)) + o.Set("getAddress", c.vm.ToValue(c.GetAddress)) + o.Set("getValue", c.vm.ToValue(c.GetValue)) + o.Set("getInput", c.vm.ToValue(c.GetInput)) + return o +} + +type callframe struct { + vm *goja.Runtime + toBig toBigFn + toBuf toBufFn + + typ string + from common.Address + to common.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 uint + gas uint + cost uint + depth uint + refund uint + err error +} + +func (l *steplog) GetPC() uint { + return l.pc +} + +func (l *steplog) GetGas() uint { + return l.gas +} + +func (l *steplog) GetCost() uint { + return l.cost +} + +func (l *steplog) GetDepth() uint { + return l.depth +} + +func (l *steplog) GetRefund() uint { + 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 +} diff --git a/x/evm/tracers/js/internal/tracers/4byte_tracer_legacy.js b/x/evm/tracers/js/internal/tracers/4byte_tracer_legacy.js new file mode 100644 index 00000000..e4714b8b --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/4byte_tracer_legacy.js @@ -0,0 +1,86 @@ +// Copyright 2017 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 . + +// 4byteTracer searches for 4byte-identifiers, and collects them for post-processing. +// It collects the methods identifiers along with the size of the supplied data, so +// a reversed signature can be matched against the size of the data. +// +// Example: +// > debug.traceTransaction( "0x214e597e35da083692f5386141e69f47e973b2c56e7a8073b1ea08fd7571e9de", {tracer: "4byteTracer"}) +// { +// 0x27dc297e-128: 1, +// 0x38cc4831-0: 2, +// 0x524f3889-96: 1, +// 0xadf59f99-288: 1, +// 0xc281d19e-0: 1 +// } +{ + // ids aggregates the 4byte ids found. + ids : {}, + + // callType returns 'false' for non-calls, or the peek-index for the first param + // after 'value', i.e. meminstart. + callType: function(opstr){ + switch(opstr){ + case "CALL": case "CALLCODE": + // gas, addr, val, memin, meminsz, memout, memoutsz + return 3; // stack ptr to memin + + case "DELEGATECALL": case "STATICCALL": + // gas, addr, memin, meminsz, memout, memoutsz + return 2; // stack ptr to memin + } + return false; + }, + + // store save the given identifier and datasize. + store: function(id, size){ + var key = "" + toHex(id) + "-" + size; + this.ids[key] = this.ids[key] + 1 || 1; + }, + + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + // Skip any opcodes that are not internal calls + var ct = this.callType(log.op.toString()); + if (!ct) { + return; + } + // Skip any pre-compile invocations, those are just fancy opcodes + if (isPrecompiled(toAddress(log.stack.peek(1).toString(16)))) { + return; + } + // Gather internal call details + var inSz = log.stack.peek(ct + 1).valueOf(); + if (inSz >= 4) { + var inOff = log.stack.peek(ct).valueOf(); + this.store(log.memory.slice(inOff, inOff + 4), inSz-4); + } + }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) { }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx) { + // Save the outer calldata also + if (ctx.input.length >= 4) { + this.store(slice(ctx.input, 0, 4), ctx.input.length-4) + } + return this.ids; + }, +} diff --git a/x/evm/tracers/js/internal/tracers/bigram_tracer.js b/x/evm/tracers/js/internal/tracers/bigram_tracer.js new file mode 100644 index 00000000..421c360a --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/bigram_tracer.js @@ -0,0 +1,47 @@ +// Copyright 2018 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 . + +{ + // hist is the counters of opcode bigrams + hist: {}, + // lastOp is last operation + lastOp: '', + // execution depth of last op + lastDepth: 0, + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + var op = log.op.toString(); + var depth = log.getDepth(); + if (depth == this.lastDepth){ + var key = this.lastOp+'-'+op; + if (this.hist[key]){ + this.hist[key]++; + } + else { + this.hist[key] = 1; + } + } + this.lastOp = op; + this.lastDepth = depth; + }, + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) {}, + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx) { + return this.hist; + }, +} diff --git a/x/evm/tracers/js/internal/tracers/call_tracer_legacy.js b/x/evm/tracers/js/internal/tracers/call_tracer_legacy.js new file mode 100644 index 00000000..3ca73777 --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/call_tracer_legacy.js @@ -0,0 +1,252 @@ +// Copyright 2017 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 . + +// callTracer is a full blown transaction tracer that extracts and reports all +// the internal calls made by a transaction, along with any useful information. +{ + // callstack is the current recursive call stack of the EVM execution. + callstack: [{}], + + // descended tracks whether we've just descended from an outer transaction into + // an inner call. + descended: false, + + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + // Capture any errors immediately + var error = log.getError(); + if (error !== undefined) { + this.fault(log, db); + return; + } + // We only care about system opcodes, faster if we pre-check once + var syscall = (log.op.toNumber() & 0xf0) == 0xf0; + if (syscall) { + var op = log.op.toString(); + } + // If a new contract is being created, add to the call stack + if (syscall && (op == 'CREATE' || op == "CREATE2")) { + var inOff = log.stack.peek(1).valueOf(); + var inEnd = inOff + log.stack.peek(2).valueOf(); + + // Assemble the internal call report and store for completion + var call = { + type: op, + from: toHex(log.contract.getAddress()), + input: toHex(log.memory.slice(inOff, inEnd)), + gasIn: log.getGas(), + gasCost: log.getCost(), + value: '0x' + log.stack.peek(0).toString(16) + }; + this.callstack.push(call); + this.descended = true + return; + } + // If a contract is being self destructed, gather that as a subcall too + if (syscall && op == 'SELFDESTRUCT') { + var left = this.callstack.length; + if (this.callstack[left-1].calls === undefined) { + this.callstack[left-1].calls = []; + } + this.callstack[left-1].calls.push({ + type: op, + from: toHex(log.contract.getAddress()), + to: toHex(toAddress(log.stack.peek(0).toString(16))), + gasIn: log.getGas(), + gasCost: log.getCost(), + value: '0x' + db.getBalance(log.contract.getAddress()).toString(16) + }); + return + } + // If a new method invocation is being done, add to the call stack + if (syscall && (op == 'CALL' || op == 'CALLCODE' || op == 'DELEGATECALL' || op == 'STATICCALL')) { + // Skip any pre-compile invocations, those are just fancy opcodes + var to = toAddress(log.stack.peek(1).toString(16)); + if (isPrecompiled(to)) { + return + } + var off = (op == 'DELEGATECALL' || op == 'STATICCALL' ? 0 : 1); + + var inOff = log.stack.peek(2 + off).valueOf(); + var inEnd = inOff + log.stack.peek(3 + off).valueOf(); + + // Assemble the internal call report and store for completion + var call = { + type: op, + from: toHex(log.contract.getAddress()), + to: toHex(to), + input: toHex(log.memory.slice(inOff, inEnd)), + gasIn: log.getGas(), + gasCost: log.getCost(), + outOff: log.stack.peek(4 + off).valueOf(), + outLen: log.stack.peek(5 + off).valueOf() + }; + if (op != 'DELEGATECALL' && op != 'STATICCALL') { + call.value = '0x' + log.stack.peek(2).toString(16); + } + this.callstack.push(call); + this.descended = true + return; + } + // If we've just descended into an inner call, retrieve it's true allowance. We + // need to extract if from within the call as there may be funky gas dynamics + // with regard to requested and actually given gas (2300 stipend, 63/64 rule). + if (this.descended) { + if (log.getDepth() >= this.callstack.length) { + this.callstack[this.callstack.length - 1].gas = log.getGas(); + } else { + // TODO(karalabe): The call was made to a plain account. We currently don't + // have access to the true gas amount inside the call and so any amount will + // mostly be wrong since it depends on a lot of input args. Skip gas for now. + } + this.descended = false; + } + // If an existing call is returning, pop off the call stack + if (syscall && op == 'REVERT') { + this.callstack[this.callstack.length - 1].error = "execution reverted"; + return; + } + if (log.getDepth() == this.callstack.length - 1) { + // Pop off the last call and get the execution results + var call = this.callstack.pop(); + + if (call.type == 'CREATE' || call.type == "CREATE2") { + // If the call was a CREATE, retrieve the contract address and output code + call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost - log.getGas()).toString(16); + delete call.gasIn; delete call.gasCost; + + var ret = log.stack.peek(0); + if (!ret.equals(0)) { + call.to = toHex(toAddress(ret.toString(16))); + call.output = toHex(db.getCode(toAddress(ret.toString(16)))); + } else if (call.error === undefined) { + call.error = "internal failure"; // TODO(karalabe): surface these faults somehow + } + } else { + // If the call was a contract call, retrieve the gas usage and output + if (call.gas !== undefined) { + call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost + call.gas - log.getGas()).toString(16); + } + var ret = log.stack.peek(0); + if (!ret.equals(0)) { + call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen)); + } else if (call.error === undefined) { + call.error = "internal failure"; // TODO(karalabe): surface these faults somehow + } + delete call.gasIn; delete call.gasCost; + delete call.outOff; delete call.outLen; + } + if (call.gas !== undefined) { + call.gas = '0x' + bigInt(call.gas).toString(16); + } + // Inject the call into the previous one + var left = this.callstack.length; + if (this.callstack[left-1].calls === undefined) { + this.callstack[left-1].calls = []; + } + this.callstack[left-1].calls.push(call); + } + }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) { + // If the topmost call already reverted, don't handle the additional fault again + if (this.callstack[this.callstack.length - 1].error !== undefined) { + return; + } + // Pop off the just failed call + var call = this.callstack.pop(); + call.error = log.getError(); + + // Consume all available gas and clean any leftovers + if (call.gas !== undefined) { + call.gas = '0x' + bigInt(call.gas).toString(16); + call.gasUsed = call.gas + } + delete call.gasIn; delete call.gasCost; + delete call.outOff; delete call.outLen; + + // Flatten the failed call into its parent + var left = this.callstack.length; + if (left > 0) { + if (this.callstack[left-1].calls === undefined) { + this.callstack[left-1].calls = []; + } + this.callstack[left-1].calls.push(call); + return; + } + // Last call failed too, leave it in the stack + this.callstack.push(call); + }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx, db) { + var result = { + type: ctx.type, + from: toHex(ctx.from), + to: toHex(ctx.to), + value: '0x' + ctx.value.toString(16), + gas: '0x' + bigInt(ctx.gas).toString(16), + gasUsed: '0x' + bigInt(ctx.gasUsed).toString(16), + input: toHex(ctx.input), + output: toHex(ctx.output), + time: ctx.time, + }; + if (this.callstack[0].calls !== undefined) { + result.calls = this.callstack[0].calls; + } + if (this.callstack[0].error !== undefined) { + result.error = this.callstack[0].error; + } else if (ctx.error !== undefined) { + result.error = ctx.error; + } + if (result.error !== undefined && (result.error !== "execution reverted" || result.output ==="0x")) { + delete result.output; + } + return this.finalize(result); + }, + + // finalize recreates a call object using the final desired field oder for json + // serialization. This is a nicety feature to pass meaningfully ordered results + // to users who don't interpret it, just display it. + finalize: function(call) { + var sorted = { + type: call.type, + from: call.from, + to: call.to, + value: call.value, + gas: call.gas, + gasUsed: call.gasUsed, + input: call.input, + output: call.output, + error: call.error, + time: call.time, + calls: call.calls, + } + for (var key in sorted) { + if (sorted[key] === undefined) { + delete sorted[key]; + } + } + if (sorted.calls !== undefined) { + for (var i=0; i. + +// evmdisTracer returns sufficient information from a trace to perform evmdis-style +// disassembly. +{ + stack: [{ops: []}], + + npushes: {0: 0, 1: 1, 2: 1, 3: 1, 4: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1, 10: 1, 11: 1, 16: 1, 17: 1, 18: 1, 19: 1, 20: 1, 21: 1, 22: 1, 23: 1, 24: 1, 25: 1, 26: 1, 32: 1, 48: 1, 49: 1, 50: 1, 51: 1, 52: 1, 53: 1, 54: 1, 55: 0, 56: 1, 57: 0, 58: 1, 59: 1, 60: 0, 64: 1, 65: 1, 66: 1, 67: 1, 68: 1, 69: 1, 80: 0, 81: 1, 82: 0, 83: 0, 84: 1, 85: 0, 86: 0, 87: 0, 88: 1, 89: 1, 90: 1, 91: 0, 96: 1, 97: 1, 98: 1, 99: 1, 100: 1, 101: 1, 102: 1, 103: 1, 104: 1, 105: 1, 106: 1, 107: 1, 108: 1, 109: 1, 110: 1, 111: 1, 112: 1, 113: 1, 114: 1, 115: 1, 116: 1, 117: 1, 118: 1, 119: 1, 120: 1, 121: 1, 122: 1, 123: 1, 124: 1, 125: 1, 126: 1, 127: 1, 128: 2, 129: 3, 130: 4, 131: 5, 132: 6, 133: 7, 134: 8, 135: 9, 136: 10, 137: 11, 138: 12, 139: 13, 140: 14, 141: 15, 142: 16, 143: 17, 144: 2, 145: 3, 146: 4, 147: 5, 148: 6, 149: 7, 150: 8, 151: 9, 152: 10, 153: 11, 154: 12, 155: 13, 156: 14, 157: 15, 158: 16, 159: 17, 160: 0, 161: 0, 162: 0, 163: 0, 164: 0, 240: 1, 241: 1, 242: 1, 243: 0, 244: 0, 255: 0}, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function() { return this.stack[0].ops; }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) { }, + + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + var frame = this.stack[this.stack.length - 1]; + + var error = log.getError(); + if (error) { + frame["error"] = error; + } else if (log.getDepth() == this.stack.length) { + opinfo = { + op: log.op.toNumber(), + depth : log.getDepth(), + result: [], + }; + if (frame.ops.length > 0) { + var prevop = frame.ops[frame.ops.length - 1]; + for(var i = 0; i < this.npushes[prevop.op]; i++) + prevop.result.push(log.stack.peek(i).toString(16)); + } + switch(log.op.toString()) { + case "CALL": case "CALLCODE": + var instart = log.stack.peek(3).valueOf(); + var insize = log.stack.peek(4).valueOf(); + opinfo["gas"] = log.stack.peek(0).valueOf(); + opinfo["to"] = log.stack.peek(1).toString(16); + opinfo["value"] = log.stack.peek(2).toString(); + opinfo["input"] = log.memory.slice(instart, instart + insize); + opinfo["error"] = null; + opinfo["return"] = null; + opinfo["ops"] = []; + this.stack.push(opinfo); + break; + case "DELEGATECALL": case "STATICCALL": + var instart = log.stack.peek(2).valueOf(); + var insize = log.stack.peek(3).valueOf(); + opinfo["op"] = log.op.toString(); + opinfo["gas"] = log.stack.peek(0).valueOf(); + opinfo["to"] = log.stack.peek(1).toString(16); + opinfo["input"] = log.memory.slice(instart, instart + insize); + opinfo["error"] = null; + opinfo["return"] = null; + opinfo["ops"] = []; + this.stack.push(opinfo); + break; + case "RETURN": case "REVERT": + var out = log.stack.peek(0).valueOf(); + var outsize = log.stack.peek(1).valueOf(); + frame.return = log.memory.slice(out, out + outsize); + break; + case "STOP": case "SELFDESTRUCT": + frame.return = log.memory.slice(0, 0); + break; + case "JUMPDEST": + opinfo["pc"] = log.getPC(); + } + if(log.op.isPush()) { + opinfo["len"] = log.op.toNumber() - 0x5e; + } + frame.ops.push(opinfo); + } else { + this.stack = this.stack.slice(0, log.getDepth()); + } + } +} diff --git a/x/evm/tracers/js/internal/tracers/noop_tracer_legacy.js b/x/evm/tracers/js/internal/tracers/noop_tracer_legacy.js new file mode 100644 index 00000000..fe7ddc85 --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/noop_tracer_legacy.js @@ -0,0 +1,29 @@ +// Copyright 2017 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 . + +// noopTracer is just the barebone boilerplate code required from a JavaScript +// object to be usable as a transaction tracer. +{ + // step is invoked for every opcode that the VM executes. + step: function(log, db) { }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) { }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx, db) { return {}; } +} diff --git a/x/evm/tracers/js/internal/tracers/opcount_tracer.js b/x/evm/tracers/js/internal/tracers/opcount_tracer.js new file mode 100644 index 00000000..f7984c74 --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/opcount_tracer.js @@ -0,0 +1,32 @@ +// Copyright 2017 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 . + +// opcountTracer is a sample tracer that just counts the number of instructions +// executed by the EVM before the transaction terminated. +{ + // count tracks the number of EVM instructions executed. + count: 0, + + // step is invoked for every opcode that the VM executes. + step: function(log, db) { this.count++ }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) { }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx, db) { return this.count } +} diff --git a/x/evm/tracers/js/internal/tracers/prestate_tracer_legacy.js b/x/evm/tracers/js/internal/tracers/prestate_tracer_legacy.js new file mode 100644 index 00000000..77f25209 --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/prestate_tracer_legacy.js @@ -0,0 +1,115 @@ +// Copyright 2017 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 . + +// prestateTracer outputs sufficient information to create a local execution of +// the transaction from a custom assembled genesis block. +{ + // prestate is the genesis that we're building. + prestate: null, + + // lookupAccount injects the specified account into the prestate object. + lookupAccount: function(addr, db){ + var acc = toHex(addr); + if (this.prestate[acc] === undefined) { + this.prestate[acc] = { + balance: '0x' + db.getBalance(addr).toString(16), + nonce: db.getNonce(addr), + code: toHex(db.getCode(addr)), + storage: {} + }; + } + }, + + // lookupStorage injects the specified storage entry of the given account into + // the prestate object. + lookupStorage: function(addr, key, db){ + var acc = toHex(addr); + var idx = toHex(key); + + if (this.prestate[acc].storage[idx] === undefined) { + this.prestate[acc].storage[idx] = toHex(db.getState(addr, key)); + } + }, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx, db) { + if (this.prestate === null) { + this.prestate = {}; + // If tx is transfer-only, the recipient account + // hasn't been populated. + this.lookupAccount(ctx.to, db); + } + + // At this point, we need to deduct the 'value' from the + // outer transaction, and move it back to the origin + this.lookupAccount(ctx.from, db); + + var fromBal = bigInt(this.prestate[toHex(ctx.from)].balance.slice(2), 16); + var toBal = bigInt(this.prestate[toHex(ctx.to)].balance.slice(2), 16); + + this.prestate[toHex(ctx.to)].balance = '0x'+toBal.subtract(ctx.value).toString(16); + this.prestate[toHex(ctx.from)].balance = '0x'+fromBal.add(ctx.value).add((ctx.gasUsed + ctx.intrinsicGas) * ctx.gasPrice).toString(16); + + // Decrement the caller's nonce, and remove empty create targets + this.prestate[toHex(ctx.from)].nonce--; + if (ctx.type == 'CREATE') { + // We can blibdly delete the contract prestate, as any existing state would + // have caused the transaction to be rejected as invalid in the first place. + delete this.prestate[toHex(ctx.to)]; + } + // Return the assembled allocations (prestate) + return this.prestate; + }, + + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + // Add the current account if we just started tracing + if (this.prestate === null){ + this.prestate = {}; + // Balance will potentially be wrong here, since this will include the value + // sent along with the message. We fix that in 'result()'. + this.lookupAccount(log.contract.getAddress(), db); + } + // Whenever new state is accessed, add it to the prestate + switch (log.op.toString()) { + case "EXTCODECOPY": case "EXTCODESIZE": case "EXTCODEHASH": case "BALANCE": + this.lookupAccount(toAddress(log.stack.peek(0).toString(16)), db); + break; + case "CREATE": + var from = log.contract.getAddress(); + this.lookupAccount(toContract(from, db.getNonce(from)), db); + break; + case "CREATE2": + var from = log.contract.getAddress(); + // stack: salt, size, offset, endowment + var offset = log.stack.peek(1).valueOf() + var size = log.stack.peek(2).valueOf() + var end = offset + size + this.lookupAccount(toContract2(from, log.stack.peek(3).toString(16), log.memory.slice(offset, end)), db); + break; + case "CALL": case "CALLCODE": case "DELEGATECALL": case "STATICCALL": + this.lookupAccount(toAddress(log.stack.peek(1).toString(16)), db); + break; + case 'SSTORE':case 'SLOAD': + this.lookupStorage(log.contract.getAddress(), toWord(log.stack.peek(0).toString(16)), db); + break; + } + }, + + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) {} +} diff --git a/x/evm/tracers/js/internal/tracers/tracers.go b/x/evm/tracers/js/internal/tracers/tracers.go new file mode 100644 index 00000000..6547f1b0 --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/tracers.go @@ -0,0 +1,59 @@ +// Copyright 2017 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 tracers contains the actual JavaScript tracer assets. +package tracers + +import ( + "embed" + "io/fs" + "strings" + "unicode" +) + +//go:embed *.js +var files embed.FS + +// Load reads the built-in JS tracer files embedded in the binary and +// returns a mapping of tracer name to source. +func Load() (map[string]string, error) { + var assetTracers = make(map[string]string) + err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + b, err := fs.ReadFile(files, path) + if err != nil { + return err + } + name := camel(strings.TrimSuffix(path, ".js")) + assetTracers[name] = string(b) + return nil + }) + return assetTracers, err +} + +// camel converts a snake cased input string into a camel cased output. +func camel(str string) string { + pieces := strings.Split(str, "_") + for i := 1; i < len(pieces); i++ { + pieces[i] = string(unicode.ToUpper(rune(pieces[i][0]))) + pieces[i][1:] + } + return strings.Join(pieces, "") +} diff --git a/x/evm/tracers/js/internal/tracers/trigram_tracer.js b/x/evm/tracers/js/internal/tracers/trigram_tracer.js new file mode 100644 index 00000000..8756490d --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/trigram_tracer.js @@ -0,0 +1,49 @@ +// Copyright 2018 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 . + +{ + // hist is the map of trigram counters + hist: {}, + // lastOp is last operation + lastOps: ['',''], + lastDepth: 0, + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + var depth = log.getDepth(); + if (depth != this.lastDepth){ + this.lastOps = ['','']; + this.lastDepth = depth; + return; + } + var op = log.op.toString(); + var key = this.lastOps[0]+'-'+this.lastOps[1]+'-'+op; + if (this.hist[key]){ + this.hist[key]++; + } + else { + this.hist[key] = 1; + } + this.lastOps[0] = this.lastOps[1]; + this.lastOps[1] = op; + }, + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) {}, + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx) { + return this.hist; + }, +} diff --git a/x/evm/tracers/js/internal/tracers/unigram_tracer.js b/x/evm/tracers/js/internal/tracers/unigram_tracer.js new file mode 100644 index 00000000..51107d8f --- /dev/null +++ b/x/evm/tracers/js/internal/tracers/unigram_tracer.js @@ -0,0 +1,41 @@ +// Copyright 2018 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 . + +{ + // hist is the map of opcodes to counters + hist: {}, + // nops counts number of ops + nops: 0, + // step is invoked for every opcode that the VM executes. + step: function(log, db) { + var op = log.op.toString(); + if (this.hist[op]){ + this.hist[op]++; + } + else { + this.hist[op] = 1; + } + this.nops++; + }, + // fault is invoked when the actual execution of an opcode fails. + fault: function(log, db) {}, + + // result is invoked when all the opcodes have been iterated over and returns + // the final result of the tracing. + result: function(ctx) { + return this.hist; + }, +} diff --git a/x/evm/tracers/native/4byte.go b/x/evm/tracers/native/4byte.go new file mode 100644 index 00000000..34e608bf --- /dev/null +++ b/x/evm/tracers/native/4byte.go @@ -0,0 +1,152 @@ +// Copyright 2021 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 native + +import ( + "encoding/json" + "math/big" + "strconv" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +func init() { + register("4byteTracer", newFourByteTracer) +} + +// fourByteTracer searches for 4byte-identifiers, and collects them for post-processing. +// It collects the methods identifiers along with the size of the supplied data, so +// a reversed signature can be matched against the size of the data. +// +// Example: +// > debug.traceTransaction( "0x214e597e35da083692f5386141e69f47e973b2c56e7a8073b1ea08fd7571e9de", {tracer: "4byteTracer"}) +// { +// 0x27dc297e-128: 1, +// 0x38cc4831-0: 2, +// 0x524f3889-96: 1, +// 0xadf59f99-288: 1, +// 0xc281d19e-0: 1 +// } +type fourByteTracer struct { + env *vm.EVM + ids map[string]int // ids aggregates the 4byte ids found + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption + activePrecompiles []common.Address // Updated on CaptureStart based on given rules +} + +// newFourByteTracer returns a native go tracer which collects +// 4 byte-identifiers of a tx, and implements vm.EVMLogger. +func newFourByteTracer(ctx *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { + t := &fourByteTracer{ + ids: make(map[string]int), + } + return t, nil +} + +// isPrecompiled returns whether the addr is a precompile. Logic borrowed from newJsTracer in eth/tracers/js/tracer.go +func (t *fourByteTracer) isPrecompiled(addr common.Address) bool { + for _, p := range t.activePrecompiles { + if p == addr { + return true + } + } + return false +} + +// store saves the given identifier and datasize. +func (t *fourByteTracer) store(id []byte, size int) { + key := bytesToHex(id) + "-" + strconv.Itoa(size) + t.ids[key] += 1 +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *fourByteTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + + // Update list of precompiles based on current block + rules := env.ChainConfig().Rules(env.Context.BlockNumber, env.Context.Random != nil) + t.activePrecompiles = vm.ActivePrecompiles(rules) + + // Save the outer calldata also + if len(input) >= 4 { + t.store(input[0:4], len(input)-4) + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *fourByteTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *fourByteTracer) CaptureEnter(op vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + t.env.Cancel() + return + } + if len(input) < 4 { + return + } + // primarily we want to avoid CREATE/CREATE2/SELFDESTRUCT + if op != vm.DELEGATECALL && op != vm.STATICCALL && + op != vm.CALL && op != vm.CALLCODE { + return + } + // Skip any pre-compile invocations, those are just fancy opcodes + if t.isPrecompiled(to) { + return + } + t.store(input[0:4], len(input)-4) +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *fourByteTracer) CaptureExit(output []byte, gasUsed uint64, err error) { +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *fourByteTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *fourByteTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { +} + +func (*fourByteTracer) CaptureTxStart(gasLimit uint64) {} + +func (*fourByteTracer) CaptureTxEnd(restGas uint64) {} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *fourByteTracer) GetResult() (json.RawMessage, error) { + res, err := json.Marshal(t.ids) + if err != nil { + return nil, err + } + return res, t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *fourByteTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} diff --git a/x/evm/tracers/native/call.go b/x/evm/tracers/native/call.go new file mode 100644 index 00000000..7af0e658 --- /dev/null +++ b/x/evm/tracers/native/call.go @@ -0,0 +1,202 @@ +// Copyright 2021 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 native + +import ( + "encoding/json" + "errors" + "math/big" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +func init() { + register("callTracer", newCallTracer) +} + +type callFrame struct { + Type string `json:"type"` + From string `json:"from"` + To string `json:"to,omitempty"` + Value string `json:"value,omitempty"` + Gas string `json:"gas"` + GasUsed string `json:"gasUsed"` + Input string `json:"input"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` + Calls []callFrame `json:"calls,omitempty"` +} + +type callTracer struct { + env *vm.EVM + callstack []callFrame + config callTracerConfig + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +type callTracerConfig struct { + OnlyTopCall bool `json:"onlyTopCall"` // If true, call tracer won't collect any subcalls +} + +// newCallTracer returns a native go tracer which tracks +// call frames of a tx, and implements vm.EVMLogger. +func newCallTracer(ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + var config callTracerConfig + if cfg != nil { + if err := json.Unmarshal(cfg, &config); err != nil { + return nil, err + } + } + // First callframe contains tx context info + // and is populated on start and end. + return &callTracer{callstack: make([]callFrame, 1), config: config}, nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *callTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + t.callstack[0] = callFrame{ + Type: "CALL", + From: addrToHex(from), + To: addrToHex(to), + Input: bytesToHex(input), + Gas: uintToHex(gas), + Value: bigToHex(value), + } + if create { + t.callstack[0].Type = "CREATE" + } +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *callTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { + t.callstack[0].GasUsed = uintToHex(gasUsed) + if err != nil { + t.callstack[0].Error = err.Error() + if err.Error() == "execution reverted" && len(output) > 0 { + t.callstack[0].Output = bytesToHex(output) + } + } else { + t.callstack[0].Output = bytesToHex(output) + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *callTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *callTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *callTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + if t.config.OnlyTopCall { + return + } + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + t.env.Cancel() + return + } + + call := callFrame{ + Type: typ.String(), + From: addrToHex(from), + To: addrToHex(to), + Input: bytesToHex(input), + Gas: uintToHex(gas), + Value: bigToHex(value), + } + t.callstack = append(t.callstack, call) +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *callTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + if t.config.OnlyTopCall { + return + } + size := len(t.callstack) + if size <= 1 { + return + } + // pop call + call := t.callstack[size-1] + t.callstack = t.callstack[:size-1] + size -= 1 + + call.GasUsed = uintToHex(gasUsed) + if err == nil { + call.Output = bytesToHex(output) + } else { + call.Error = err.Error() + if call.Type == "CREATE" || call.Type == "CREATE2" { + call.To = "" + } + } + t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call) +} + +func (*callTracer) CaptureTxStart(gasLimit uint64) {} + +func (*callTracer) CaptureTxEnd(restGas uint64) {} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *callTracer) GetResult() (json.RawMessage, error) { + if len(t.callstack) != 1 { + return nil, errors.New("incorrect number of top-level calls") + } + res, err := json.Marshal(t.callstack[0]) + if err != nil { + return nil, err + } + return json.RawMessage(res), t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *callTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} + +func bytesToHex(s []byte) string { + return "0x" + common.Bytes2Hex(s) +} + +func bigToHex(n *big.Int) string { + if n == nil { + return "" + } + return "0x" + n.Text(16) +} + +func uintToHex(n uint64) string { + return "0x" + strconv.FormatUint(n, 16) +} + +func addrToHex(a common.Address) string { + return strings.ToLower(a.Hex()) +} diff --git a/x/evm/tracers/native/noop.go b/x/evm/tracers/native/noop.go new file mode 100644 index 00000000..c252b240 --- /dev/null +++ b/x/evm/tracers/native/noop.go @@ -0,0 +1,78 @@ +// Copyright 2021 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 native + +import ( + "encoding/json" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +func init() { + register("noopTracer", newNoopTracer) +} + +// noopTracer is a go implementation of the Tracer interface which +// performs no action. It's mostly useful for testing purposes. +type noopTracer struct{} + +// newNoopTracer returns a new noop tracer. +func newNoopTracer(ctx *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { + return &noopTracer{}, nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *noopTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *noopTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *noopTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *noopTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *noopTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *noopTracer) CaptureExit(output []byte, gasUsed uint64, err error) { +} + +func (*noopTracer) CaptureTxStart(gasLimit uint64) {} + +func (*noopTracer) CaptureTxEnd(restGas uint64) {} + +// GetResult returns an empty json object. +func (t *noopTracer) GetResult() (json.RawMessage, error) { + return json.RawMessage(`{}`), nil +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *noopTracer) Stop(err error) { +} diff --git a/x/evm/tracers/native/prestate.go b/x/evm/tracers/native/prestate.go new file mode 100644 index 00000000..b513f383 --- /dev/null +++ b/x/evm/tracers/native/prestate.go @@ -0,0 +1,178 @@ +// 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 native + +import ( + "encoding/json" + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +func init() { + register("prestateTracer", newPrestateTracer) +} + +type prestate = map[common.Address]*account +type account struct { + Balance string `json:"balance"` + Nonce uint64 `json:"nonce"` + Code string `json:"code"` + Storage map[common.Hash]common.Hash `json:"storage"` +} + +type prestateTracer struct { + env *vm.EVM + prestate prestate + create bool + to common.Address + gasLimit uint64 // Amount of gas bought for the whole tx + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +func newPrestateTracer(ctx *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { + // First callframe contains tx context info + // and is populated on start and end. + return &prestateTracer{prestate: prestate{}}, nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *prestateTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + t.create = create + t.to = to + + t.lookupAccount(from) + t.lookupAccount(to) + + // The recipient balance includes the value transferred. + toBal := hexutil.MustDecodeBig(t.prestate[to].Balance) + toBal = new(big.Int).Sub(toBal, value) + t.prestate[to].Balance = hexutil.EncodeBig(toBal) + + // The sender balance is after reducing: value and gasLimit. + // We need to re-add them to get the pre-tx balance. + fromBal := hexutil.MustDecodeBig(t.prestate[from].Balance) + gasPrice := env.TxContext.GasPrice + consumedGas := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(t.gasLimit)) + fromBal.Add(fromBal, new(big.Int).Add(value, consumedGas)) + t.prestate[from].Balance = hexutil.EncodeBig(fromBal) + t.prestate[from].Nonce-- +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *prestateTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { + if t.create { + // Exclude created contract. + delete(t.prestate, t.to) + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *prestateTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, err error) { + stack := scope.Stack + stackData := stack.Data() + stackLen := len(stackData) + switch { + case stackLen >= 1 && (op == vm.SLOAD || op == vm.SSTORE): + slot := common.Hash(stackData[stackLen-1].Bytes32()) + t.lookupStorage(scope.Contract.Address(), slot) + case stackLen >= 1 && (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT): + addr := common.Address(stackData[stackLen-1].Bytes20()) + t.lookupAccount(addr) + case stackLen >= 5 && (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE): + addr := common.Address(stackData[stackLen-2].Bytes20()) + t.lookupAccount(addr) + case op == vm.CREATE: + addr := scope.Contract.Address() + nonce := t.env.StateDB.GetNonce(addr) + t.lookupAccount(crypto.CreateAddress(addr, nonce)) + case stackLen >= 4 && op == vm.CREATE2: + offset := stackData[stackLen-2] + size := stackData[stackLen-3] + init := scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) + inithash := crypto.Keccak256(init) + salt := stackData[stackLen-4] + t.lookupAccount(crypto.CreateAddress2(scope.Contract.Address(), salt.Bytes32(), inithash)) + } +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *prestateTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, _ *vm.ScopeContext, depth int, err error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *prestateTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *prestateTracer) CaptureExit(output []byte, gasUsed uint64, err error) { +} + +func (t *prestateTracer) CaptureTxStart(gasLimit uint64) { + t.gasLimit = gasLimit +} + +func (t *prestateTracer) CaptureTxEnd(restGas uint64) {} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *prestateTracer) GetResult() (json.RawMessage, error) { + res, err := json.Marshal(t.prestate) + if err != nil { + return nil, err + } + return json.RawMessage(res), t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *prestateTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} + +// lookupAccount fetches details of an account and adds it to the prestate +// if it doesn't exist there. +func (t *prestateTracer) lookupAccount(addr common.Address) { + if _, ok := t.prestate[addr]; ok { + return + } + t.prestate[addr] = &account{ + Balance: bigToHex(t.env.StateDB.GetBalance(addr)), + Nonce: t.env.StateDB.GetNonce(addr), + Code: bytesToHex(t.env.StateDB.GetCode(addr)), + Storage: make(map[common.Hash]common.Hash), + } +} + +// lookupStorage fetches the requested storage slot and adds +// it to the prestate of the given contract. It assumes `lookupAccount` +// has been performed on the contract before. +func (t *prestateTracer) lookupStorage(addr common.Address, key common.Hash) { + if _, ok := t.prestate[addr].Storage[key]; ok { + return + } + t.prestate[addr].Storage[key] = t.env.StateDB.GetState(addr, key) +} diff --git a/x/evm/tracers/native/revertreason.go b/x/evm/tracers/native/revertreason.go new file mode 100644 index 00000000..d09b8610 --- /dev/null +++ b/x/evm/tracers/native/revertreason.go @@ -0,0 +1,108 @@ +// 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 native + +import ( + "bytes" + "encoding/json" + "math/big" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" +) + +func init() { + register("revertReasonTracer", newRevertReasonTracer) +} + +var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] + +// revertReasonTracer is a go implementation of the Tracer interface which +// track the error message or revert reason return by the contract. +type revertReasonTracer struct { + env *vm.EVM + revertReason string // The revert reason return from the tx, if tx success, empty string return + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +// newRevertReasonTracer returns a new revert reason tracer. +func newRevertReasonTracer(_ *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) { + return &revertReasonTracer{}, nil +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *revertReasonTracer) CaptureStart(env *vm.EVM, _ common.Address, _ common.Address, _ bool, _ []byte, _ uint64, _ *big.Int) { + t.env = env +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *revertReasonTracer) CaptureEnd(output []byte, _ uint64, _ time.Duration, err error) { + if err != nil { + if err == vm.ErrExecutionReverted && len(output) > 4 && bytes.Equal(output[:4], revertSelector) { + errMsg, _ := abi.UnpackRevert(output) + t.revertReason = err.Error() + ": " + errMsg + } else { + t.revertReason = err.Error() + } + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *revertReasonTracer) CaptureState(_ uint64, _ vm.OpCode, _, _ uint64, _ *vm.ScopeContext, _ []byte, _ int, _ error) { +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *revertReasonTracer) CaptureFault(_ uint64, _ vm.OpCode, _, _ uint64, _ *vm.ScopeContext, _ int, _ error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *revertReasonTracer) CaptureEnter(_ vm.OpCode, _ common.Address, _ common.Address, _ []byte, _ uint64, _ *big.Int) { + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + t.env.Cancel() + return + } +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *revertReasonTracer) CaptureExit(_ []byte, _ uint64, _ error) {} + +func (t *revertReasonTracer) CaptureTxStart(_ uint64) {} + +func (t *revertReasonTracer) CaptureTxEnd(_ uint64) {} + +// GetResult returns an error message json object. +func (t *revertReasonTracer) GetResult() (json.RawMessage, error) { + res, err := json.Marshal(t.revertReason) + if err != nil { + return nil, err + } + return res, t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *revertReasonTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} diff --git a/x/evm/tracers/native/tracer.go b/x/evm/tracers/native/tracer.go new file mode 100644 index 00000000..fda3e659 --- /dev/null +++ b/x/evm/tracers/native/tracer.go @@ -0,0 +1,83 @@ +// Copyright 2021 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 native is a collection of tracers written in go. + +In order to add a native tracer and have it compiled into the binary, a new +file needs to be added to this folder, containing an implementation of the +`eth.tracers.Tracer` interface. + +Aside from implementing the tracer, it also needs to register itself, using the +`register` method -- and this needs to be done in the package initialization. + +Example: + +```golang +func init() { + register("noopTracerNative", newNoopTracer) +} +``` +*/ +package native + +import ( + "encoding/json" + "errors" + + "github.com/ethereum/go-ethereum/eth/tracers" +) + +// InitTracer retrieves the Go transaction tracers included in go-ethereum. +func InitTracer() { + tracers.RegisterLookup(false, lookup) +} + +// ctorFn is the constructor signature of a native tracer. +type ctorFn = func(*tracers.Context, json.RawMessage) (tracers.Tracer, error) + +/* +ctors is a map of package-local tracer constructors. + +We cannot be certain about the order of init-functions within a package, +The go spec (https://golang.org/ref/spec#Package_initialization) says + +> To ensure reproducible initialization behavior, build systems +> are encouraged to present multiple files belonging to the same +> package in lexical file name order to a compiler. + +Hence, we cannot make the map in init, but must make it upon first use. +*/ +var ctors map[string]ctorFn + +// register is used by native tracers to register their presence. +func register(name string, ctor ctorFn) { + if ctors == nil { + ctors = make(map[string]ctorFn) + } + ctors[name] = ctor +} + +// lookup returns a tracer, if one can be matched to the given name. +func lookup(name string, ctx *tracers.Context, cfg json.RawMessage) (tracers.Tracer, error) { + if ctors == nil { + ctors = make(map[string]ctorFn) + } + if ctor, ok := ctors[name]; ok { + return ctor(ctx, cfg) + } + return nil, errors.New("no tracer found") +}