diff --git a/Makefile b/Makefile index 2159d403fd..c5bb295410 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,9 @@ BINANCE_TGORACLE_DIR=$(GOPATH)/src/$(PKG_BINANCE_TGORACLE) # specific commit. GO_LOOM_GIT_REV = HEAD # Specifies the loomnetwork/transfer-gateway branch/revision to use. -TG_GIT_REV = HEAD +TG_GIT_REV = debugeth # loomnetwork/go-ethereum loomchain branch -ETHEREUM_GIT_REV = 6128fa1a8c767035d3da6ef0c27ebb7778ce3713 +ETHEREUM_GIT_REV = ethapi # use go-plugin we get 'timeout waiting for connection info' error HASHICORP_GIT_REV = f4c3476bd38585f9ec669d10ed1686abd52b9961 LEVIGO_GIT_REV = c42d9e0ca023e2198120196f842701bb4c55d7b9 diff --git a/app.go b/app.go index bce2ca9c2e..b8d0c5d55d 100644 --- a/app.go +++ b/app.go @@ -8,8 +8,11 @@ import ( "fmt" "time" + "github.com/ethereum/go-ethereum/core/vm" "github.com/loomnetwork/go-loom/config" "github.com/loomnetwork/go-loom/util" + "github.com/pkg/errors" + "github.com/loomnetwork/loomchain/eth/utils" "github.com/loomnetwork/loomchain/features" "github.com/loomnetwork/loomchain/registry" @@ -20,14 +23,15 @@ import ( cctypes "github.com/loomnetwork/go-loom/builtin/types/chainconfig" "github.com/loomnetwork/go-loom/plugin" "github.com/loomnetwork/go-loom/types" - "github.com/loomnetwork/loomchain/log" - "github.com/loomnetwork/loomchain/store" - blockindex "github.com/loomnetwork/loomchain/store/block_index" - evmaux "github.com/loomnetwork/loomchain/store/evm_aux" stdprometheus "github.com/prometheus/client_golang/prometheus" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/common" ttypes "github.com/tendermint/tendermint/types" + + "github.com/loomnetwork/loomchain/log" + "github.com/loomnetwork/loomchain/store" + blockindex "github.com/loomnetwork/loomchain/store/block_index" + evmaux "github.com/loomnetwork/loomchain/store/evm_aux" ) type ReadOnlyState interface { @@ -302,6 +306,12 @@ type TxHandler interface { ProcessTx(state State, txBytes []byte, isCheckTx bool) (TxHandlerResult, error) } +type TxHandlerFactory interface { + TxHandler(metrics bool) (TxHandler, error) + TxHandlerWithTracerAndDefaultVmManager(tracer vm.Tracer, metrics bool) (TxHandler, error) + Copy(newStore store.VersionedKVStore) TxHandlerFactory +} + type TxHandlerFunc func(state State, txBytes []byte, isCheckTx bool) (TxHandlerResult, error) type TxHandlerResult struct { @@ -353,6 +363,7 @@ type Application struct { Store store.VersionedKVStore Init func(State) error TxHandler + TxHandlerFactory QueryHandler EventHandler ReceiptHandlerProvider @@ -368,6 +379,7 @@ type Application struct { config *cctypes.Config childTxRefs []evmaux.ChildTxRef // links Tendermint txs to EVM txs ReceiptsVersion int32 + EVMTracer vm.Tracer committedTxs []CommittedTx } @@ -583,17 +595,19 @@ func (a *Application) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { } // TODO: receiptHandler.CommitBlock() should be moved to Application.Commit() - storeTx := store.WrapAtomic(a.Store).BeginTx() - receiptHandler := a.ReceiptHandlerProvider.Store() - if err := receiptHandler.CommitBlock(a.height()); err != nil { - storeTx.Rollback() - // TODO: maybe panic instead? - log.Error(fmt.Sprintf("aborted committing block receipts, %v", err.Error())) - } else { - storeTx.Commit() + if a.ReceiptHandlerProvider != nil { + storeTx := store.WrapAtomic(a.Store).BeginTx() + receiptHandler := a.ReceiptHandlerProvider.Store() + if err := receiptHandler.CommitBlock(a.height()); err != nil { + storeTx.Rollback() + // TODO: maybe panic instead? + log.Error(fmt.Sprintf("aborted committing block receipts, %v", err.Error())) + } else { + storeTx.Commit() + } } - storeTx = store.WrapAtomic(a.Store).BeginTx() + storeTx := store.WrapAtomic(a.Store).BeginTx() state := NewStoreState( context.Background(), storeTx, @@ -701,7 +715,9 @@ func (a *Application) DeliverTx(txBytes []byte) abci.ResponseDeliverTx { } else { r = a.deliverTx(storeTx, txBytes) } - + if a.EVMTracer != nil { + log.Debug("evm trace", "trace", a.EVMTracer) + } txFailed = r.Code != abci.CodeTypeOK // TODO: this isn't 100% reliable when txFailed == true isEvmTx = r.Info == utils.CallEVM || r.Info == utils.DeployEvm @@ -780,15 +796,18 @@ func (a *Application) deliverTx2(storeTx store.KVStoreTx, txBytes []byte) abci.R a.GetValidatorSet, ).WithOnChainConfig(a.config) - receiptHandler := a.ReceiptHandlerProvider.Store() - defer receiptHandler.DiscardCurrentReceipt() - defer a.EventHandler.Rollback() + if a.ReceiptHandlerProvider != nil { + defer a.ReceiptHandlerProvider.Store().DiscardCurrentReceipt() + } + if a.EventHandler != nil { + defer a.EventHandler.Rollback() + } r, txErr := a.TxHandler.ProcessTx(state, txBytes, false) // Store the receipt even if the tx itself failed var receiptTxHash []byte - if a.ReceiptHandlerProvider.Reader().GetCurrentReceipt() != nil { + if a.ReceiptHandlerProvider != nil && a.ReceiptHandlerProvider.Reader().GetCurrentReceipt() != nil { receiptTxHash = a.ReceiptHandlerProvider.Reader().GetCurrentReceipt().TxHash txHash := ttypes.Tx(txBytes).Hash() // If a receipt was generated for an EVM tx add a link between the TM tx hash and the EVM tx hash @@ -799,7 +818,7 @@ func (a *Application) deliverTx2(storeTx store.KVStoreTx, txBytes []byte) abci.R ChildTxHash: receiptTxHash, }) } - receiptHandler.CommitCurrentReceipt() + a.ReceiptHandlerProvider.Store().CommitCurrentReceipt() } if txErr != nil { @@ -810,7 +829,9 @@ func (a *Application) deliverTx2(storeTx store.KVStoreTx, txBytes []byte) abci.R return abci.ResponseDeliverTx{Code: 1, Data: r.Data, Log: txErr.Error()} } - a.EventHandler.Commit(uint64(a.curBlockHeader.GetHeight())) + if a.EventHandler != nil { + a.EventHandler.Commit(uint64(a.curBlockHeader.GetHeight())) + } storeTx.Commit() a.committedTxs = append(a.committedTxs, CommittedTx{ @@ -836,9 +857,11 @@ func (a *Application) Commit() abci.ResponseCommit { height := a.curBlockHeader.GetHeight() - if err := a.EvmAuxStore.SaveChildTxRefs(a.childTxRefs); err != nil { - // TODO: consider panic instead - log.Error("Failed to save Tendermint -> EVM tx hash refs", "height", height, "err", err) + if a.EvmAuxStore != nil { + if err := a.EvmAuxStore.SaveChildTxRefs(a.childTxRefs); err != nil { + // TODO: consider panic instead + log.Error("Failed to save Tendermint -> EVM tx hash refs", "height", height, "err", err) + } } a.childTxRefs = nil @@ -852,28 +875,31 @@ func (a *Application) Commit() abci.ResponseCommit { // the latest committed state as soon as they receive an event. a.lastBlockHeader = a.curBlockHeader - go func(height int64, blockHeader abci.Header, committedTxs []CommittedTx) { - if err := a.EventHandler.EmitBlockTx(uint64(height), blockHeader.Time); err != nil { - log.Error("Emit Block Event error", "err", err) - } - if err := a.EventHandler.LegacyEthSubscriptionSet().EmitBlockEvent(blockHeader); err != nil { - log.Error("Emit Block Event error", "err", err) - } - if err := a.EventHandler.EthSubscriptionSet().EmitBlockEvent(blockHeader); err != nil { - log.Error("Emit Block Event error", "err", err) - } - for _, tx := range committedTxs { - if err := a.EventHandler.LegacyEthSubscriptionSet().EmitTxEvent(tx.result.Data, tx.result.Info); err != nil { - log.Error("Emit Tx Event error", "err", err) + if a.EventHandler != nil { + go func(height int64, blockHeader abci.Header, committedTxs []CommittedTx) { + if err := a.EventHandler.EmitBlockTx(uint64(height), blockHeader.Time); err != nil { + log.Error("Emit Block Event error", "err", err) + } + if err := a.EventHandler.LegacyEthSubscriptionSet().EmitBlockEvent(blockHeader); err != nil { + log.Error("Emit Block Event error", "err", err) } + if err := a.EventHandler.EthSubscriptionSet().EmitBlockEvent(blockHeader); err != nil { + log.Error("Emit Block Event error", "err", err) + } + for _, tx := range committedTxs { + if err := a.EventHandler.LegacyEthSubscriptionSet().EmitTxEvent(tx.result.Data, tx.result.Info); err != nil { + log.Error("Emit Tx Event error", "err", err) + } - if len(tx.txHash) > 0 { - if err := a.EventHandler.EthSubscriptionSet().EmitTxEvent(tx.txHash); err != nil { - log.Error("failed to emit tx event to subscribers", "err", err) + if len(tx.txHash) > 0 { + if err := a.EventHandler.EthSubscriptionSet().EmitTxEvent(tx.txHash); err != nil { + log.Error("failed to emit tx event to subscribers", "err", err) + } } } - } - }(height, a.curBlockHeader, a.committedTxs) + }(height, a.curBlockHeader, a.committedTxs) + } + a.committedTxs = nil if err := a.Store.Prune(); err != nil { @@ -913,3 +939,55 @@ func (a *Application) ReadOnlyState() State { a.GetValidatorSet, ) } + +func (a *Application) ReplayApplication(blockNumber uint64, blockstore store.BlockStore) (*Application, int64, error) { + startVersion := int64(blockNumber) - 1 + if startVersion < 0 { + return nil, 0, errors.Errorf("invalid block number %d", blockNumber) + } + var snapshot store.Snapshot + for err := error(nil); (snapshot == nil || err != nil) && startVersion > 0; startVersion-- { + snapshot, err = a.Store.GetSnapshotAt(startVersion) + } + if startVersion == 0 { + return nil, 0, errors.Errorf("no saved version for height %d", blockNumber) + } + + splitStore := store.NewSplitStore(snapshot, startVersion-1) + factory := a.TxHandlerFactory.Copy(splitStore) + txHandle, err := factory.TxHandlerWithTracerAndDefaultVmManager(nil, false) + if err != nil { + return nil, 0, err + } + newApp := &Application{ + Store: splitStore, + Init: func(state State) error { + panic("init should not be called") + }, + TxHandler: txHandle, + TxHandlerFactory: factory, + BlockIndexStore: nil, + EventHandler: nil, + ReceiptHandlerProvider: nil, + CreateValidatorManager: a.CreateValidatorManager, + CreateChainConfigManager: a.CreateChainConfigManager, + CreateContractUpkeepHandler: a.CreateContractUpkeepHandler, + EventStore: nil, + GetValidatorSet: a.GetValidatorSet, + EvmAuxStore: nil, + ReceiptsVersion: a.ReceiptsVersion, + config: a.config, + } + return newApp, startVersion, nil +} + +// This modifies the tx handler to use a tracer. +// Danger, this looses the receipt handle and account balance manager information +func (a *Application) SetTracer(tracer vm.Tracer, metrics bool) error { + newTxHandle, err := a.TxHandlerFactory.TxHandlerWithTracerAndDefaultVmManager(tracer, metrics) + if err != nil { + return errors.Wrap(err, "making transaction handle") + } + a.TxHandler = newTxHandle + return nil +} diff --git a/builtin/plugins/sample_go_contract/sample_go_contract_test.go b/builtin/plugins/sample_go_contract/sample_go_contract_test.go index b125518992..4e3ea74821 100644 --- a/builtin/plugins/sample_go_contract/sample_go_contract_test.go +++ b/builtin/plugins/sample_go_contract/sample_go_contract_test.go @@ -59,7 +59,7 @@ func deployContractToEVM(ctx *plugin.FakeContextWithEVM, filename string, caller byteCode := common.FromHex(string(hexByteCode)) byteCode, err = hex.DecodeString(string(hexByteCode)) - vm := evm.NewLoomVm(ctx.State, nil, nil, nil, false) + vm := evm.NewLoomVm(ctx.State, nil, nil) _, contractAddr, err = vm.Create(caller, byteCode, loom.NewBigUIntFromInt(0)) if err != nil { return contractAddr, err diff --git a/cmd/loom/db/evm.go b/cmd/loom/db/evm.go index fdda84a2d9..f6c4a95013 100644 --- a/cmd/loom/db/evm.go +++ b/cmd/loom/db/evm.go @@ -108,7 +108,7 @@ func newDumpEVMStateCommand() *cobra.Command { } } - vm, err := evm.NewLoomEvm(state, accountBalanceManager, nil, false) + vm, err := evm.NewLoomEvm(state, accountBalanceManager, nil, nil) if err != nil { return err } @@ -221,7 +221,7 @@ func newDumpEVMStateMultiWriterAppStoreCommand() *cobra.Command { } } - vm, err := evm.NewLoomEvm(state, accountBalanceManager, nil, false) + vm, err := evm.NewLoomEvm(state, accountBalanceManager, nil, nil) if err != nil { return err } diff --git a/cmd/loom/gateway_reactors.go b/cmd/loom/gateway_reactors.go index a8b82f90af..19ab7b18b6 100644 --- a/cmd/loom/gateway_reactors.go +++ b/cmd/loom/gateway_reactors.go @@ -15,10 +15,9 @@ import ( ) const ( - GatewayName = "gateway" - LoomGatewayName = "loomcoin-gateway" - BinanceGatewayName = "binance-gateway" - TronGatewayName = "tron-gateway" + GatewayName = "gateway" + LoomGatewayName = "loomcoin-gateway" + TronGatewayName = "tron-gateway" ) func startGatewayReactors( diff --git a/cmd/loom/loom.go b/cmd/loom/loom.go index 55caa2bd2f..f5eb9be3fb 100644 --- a/cmd/loom/loom.go +++ b/cmd/loom/loom.go @@ -16,9 +16,8 @@ import ( "syscall" "time" - "github.com/prometheus/client_golang/prometheus/push" - "github.com/tendermint/tendermint/libs/db" - + ethvm "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth" kitprometheus "github.com/go-kit/kit/metrics/prometheus" "github.com/gogo/protobuf/proto" "github.com/loomnetwork/go-loom" @@ -29,17 +28,27 @@ import ( "github.com/loomnetwork/go-loom/crypto" "github.com/loomnetwork/go-loom/plugin/contractpb" "github.com/loomnetwork/go-loom/util" + "github.com/prometheus/client_golang/prometheus/push" + "github.com/tendermint/tendermint/libs/db" + "github.com/loomnetwork/loomchain" "github.com/loomnetwork/loomchain/abci/backend" - "github.com/loomnetwork/loomchain/auth" "github.com/loomnetwork/loomchain/builtin/plugins/dposv2" "github.com/loomnetwork/loomchain/builtin/plugins/dposv3" plasmaConfig "github.com/loomnetwork/loomchain/builtin/plugins/plasma_cash/config" plasmaOracle "github.com/loomnetwork/loomchain/builtin/plugins/plasma_cash/oracle" "github.com/loomnetwork/loomchain/features" "github.com/loomnetwork/loomchain/receipts/leveldb" + "github.com/loomnetwork/loomchain/rpc/debug" + "github.com/loomnetwork/loomchain/tx_handler/factory" + "github.com/prometheus/client_golang/prometheus" + "github.com/pkg/errors" + stdprometheus "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cobra" + "golang.org/x/crypto/ed25519" + "github.com/loomnetwork/loomchain/chainconfig" chaincfgcmd "github.com/loomnetwork/loomchain/cmd/loom/chainconfig" "github.com/loomnetwork/loomchain/cmd/loom/common" @@ -57,7 +66,6 @@ import ( "github.com/loomnetwork/loomchain/fnConsensus" karma_handler "github.com/loomnetwork/loomchain/karma" "github.com/loomnetwork/loomchain/log" - "github.com/loomnetwork/loomchain/migrations" "github.com/loomnetwork/loomchain/plugin" "github.com/loomnetwork/loomchain/receipts" "github.com/loomnetwork/loomchain/receipts/handler" @@ -67,13 +75,7 @@ import ( "github.com/loomnetwork/loomchain/store" blockindex "github.com/loomnetwork/loomchain/store/block_index" evmaux "github.com/loomnetwork/loomchain/store/evm_aux" - "github.com/loomnetwork/loomchain/throttle" - "github.com/loomnetwork/loomchain/tx_handler" "github.com/loomnetwork/loomchain/vm" - "github.com/pkg/errors" - stdprometheus "github.com/prometheus/client_golang/prometheus" - "github.com/spf13/cobra" - "golang.org/x/crypto/ed25519" ) var ( @@ -491,14 +493,6 @@ func contractInfoCommand() *cobra.Command { return cmd } -//nolint:deadcode -func recovery() { - if r := recover(); r != nil { - log.Error("caught RPC proxy exception, exiting", r) - os.Exit(1) - } -} - func startFeatureAutoEnabler( chainID string, cfg *config.ChainConfigConfig, nodeSigner glAuth.Signer, node backend.Backend, logger *loom.Logger, @@ -795,6 +789,26 @@ func loadApp( receiptHandlerProvider := receipts.NewReceiptHandlerProvider(eventHandler, cfg.EVMPersistentTxReceiptsMax, evmAuxStore) + tracer := ethvm.Tracer(nil) + if cfg.EVMTracer.Enabled { + traceCfg := eth.TraceConfig{ + Tracer: &cfg.EVMTracer.Tracer, + } + if len(cfg.EVMTracer.Tracer) == 0 { + traceCfg.LogConfig = ðvm.LogConfig{ + DisableMemory: cfg.EVMTracer.DisableMemory, + DisableStack: cfg.EVMTracer.DisableStack, + DisableStorage: cfg.EVMTracer.DisableStorage, + Debug: true, + Limit: cfg.EVMTracer.Limit, + } + } + tracer, err = debug.CreateTracer(traceCfg) + if err != nil { + return nil, errors.Wrapf(err, "creating tracer from %v", cfg.EVMTracer) + } + } + var newABMFactory plugin.NewAccountBalanceManagerFactoryFunc if evm.EVMEnabled && cfg.EVMAccountsEnabled { newABMFactory = plugin.NewAccountBalanceManagerFactory @@ -834,36 +848,12 @@ func loadApp( return nil, err } } - return evm.NewLoomVm(state, eventHandler, receiptHandlerProvider.Writer(), createABM, cfg.EVMDebugEnabled), nil + lvm := evm.NewLoomVm(state, receiptHandlerProvider.Writer(), createABM) + return lvm.WithTracer(tracer), nil }) } evm.LogEthDbBatch = cfg.LogEthDbBatch - deployTxHandler := &vm.DeployTxHandler{ - Manager: vmManager, - CreateRegistry: createRegistry, - AllowNamedEVMContracts: cfg.AllowNamedEvmContracts, - } - - callTxHandler := &vm.CallTxHandler{ - Manager: vmManager, - } - - ethTxHandler := &tx_handler.EthTxHandler{ - Manager: vmManager, - CreateRegistry: createRegistry, - } - - migrationTxHandler := &tx_handler.MigrationTxHandler{ - Manager: vmManager, - CreateRegistry: createRegistry, - Migrations: map[int32]tx_handler.MigrationFunc{ - 1: migrations.DPOSv3Migration, - 2: migrations.GatewayMigration, - 3: migrations.GatewayMigration, - }, - } - gen, err := config.ReadGenesis(cfg.GenesisPath()) if err != nil { return nil, err @@ -897,107 +887,8 @@ func loadApp( return nil } - router := loomchain.NewTxRouter() - - isEvmTx := func(txID uint32, state loomchain.State, txBytes []byte, isCheckTx bool) bool { - var msg vm.MessageTx - err := proto.Unmarshal(txBytes, &msg) - if err != nil { - return false - } - - switch txID { - case 1: - var tx vm.DeployTx - err = proto.Unmarshal(msg.Data, &tx) - if err != nil { - // In case of error, let's give safest response, - // let's TxHandler down the line, handle it. - return false - } - return tx.VmType == vm.VMType_EVM - case 2: - var tx vm.CallTx - err = proto.Unmarshal(msg.Data, &tx) - if err != nil { - // In case of error, let's give safest response, - // let's TxHandler down the line, handle it. - return false - } - return tx.VmType == vm.VMType_EVM - case 3: - return false - default: - return false - } - } - - router.HandleDeliverTx(1, loomchain.GeneratePassthroughRouteHandler(deployTxHandler)) - router.HandleDeliverTx(2, loomchain.GeneratePassthroughRouteHandler(callTxHandler)) - router.HandleDeliverTx(3, loomchain.GeneratePassthroughRouteHandler(migrationTxHandler)) - router.HandleDeliverTx(4, loomchain.GeneratePassthroughRouteHandler(ethTxHandler)) - - // TODO: Write this in more elegant way - router.HandleCheckTx(1, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, deployTxHandler)) - router.HandleCheckTx(2, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, callTxHandler)) - router.HandleCheckTx(3, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, migrationTxHandler)) - router.HandleCheckTx(4, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, ethTxHandler)) - - txMiddleWare := []loomchain.TxMiddleware{ - loomchain.LogTxMiddleware, - loomchain.RecoveryTxMiddleware, - } - - postCommitMiddlewares := []loomchain.PostCommitMiddleware{ - loomchain.LogPostCommitMiddleware, - } - - txMiddleWare = append(txMiddleWare, auth.NewChainConfigMiddleware( - cfg.Auth, - getContractStaticCtx("addressmapper", vmManager), - )) - createKarmaContractCtx := getContractCtx("karma", vmManager) - if cfg.Karma.Enabled { - txMiddleWare = append(txMiddleWare, throttle.GetKarmaMiddleWare( - cfg.Karma.Enabled, - cfg.Karma.MaxCallCount, - cfg.Karma.SessionDuration, - createKarmaContractCtx, - )) - } - - if cfg.TxLimiter.Enabled { - txMiddleWare = append(txMiddleWare, throttle.NewTxLimiterMiddleware(cfg.TxLimiter)) - } - - if cfg.ContractTxLimiter.Enabled { - contextFactory := getContractCtx("user-deployer-whitelist", vmManager) - txMiddleWare = append( - txMiddleWare, throttle.NewContractTxLimiterMiddleware(cfg.ContractTxLimiter, contextFactory), - ) - } - - if cfg.DeployerWhitelist.ContractEnabled { - contextFactory := getContractCtx("deployerwhitelist", vmManager) - dwMiddleware, err := throttle.NewDeployerWhitelistMiddleware(contextFactory) - if err != nil { - return nil, err - } - txMiddleWare = append(txMiddleWare, dwMiddleware) - - } - - if cfg.UserDeployerWhitelist.ContractEnabled { - contextFactory := getContractCtx("user-deployer-whitelist", vmManager) - evmDeployRecorderMiddleware, err := throttle.NewEVMDeployRecorderPostCommitMiddleware(contextFactory) - if err != nil { - return nil, err - } - postCommitMiddlewares = append(postCommitMiddlewares, evmDeployRecorderMiddleware) - } - createContractUpkeepHandler := func(state loomchain.State) (loomchain.KarmaHandler, error) { // TODO: This setting should be part of the config stored within the Karma contract itself, // that will allow us to switch the upkeep on & off via a tx. @@ -1045,19 +936,6 @@ func loadApp( return loom.NewValidatorSet(b.GenesisValidators()...), nil } - nonceTxHandler := auth.NewNonceHandler() - txMiddleWare = append(txMiddleWare, nonceTxHandler.TxMiddleware(appStore)) - - if cfg.GoContractDeployerWhitelist.Enabled { - goDeployers, err := cfg.GoContractDeployerWhitelist.DeployerAddresses(chainID) - if err != nil { - return nil, errors.Wrapf(err, "getting list of users allowed go deploys") - } - txMiddleWare = append(txMiddleWare, throttle.GetGoDeployTxMiddleWare(goDeployers)) - } - - txMiddleWare = append(txMiddleWare, loomchain.NewInstrumentingTxMiddleware()) - createValidatorsManager := func(state loomchain.State) (loomchain.ValidatorsManager, error) { pvm, err := vmManager.InitVM(vm.VMType_PLUGIN, state) if err != nil { @@ -1112,18 +990,17 @@ func loadApp( } } - // We need to make sure nonce post commit middleware is last - // as it doesn't pass control to other middlewares after it. - postCommitMiddlewares = append(postCommitMiddlewares, nonceTxHandler.PostCommitMiddleware()) + txHandlerFactory := factory.NewTxHandlerFactory(*cfg, vmManager, chainID, appStore, createRegistry) + chainTxHandler, err := txHandlerFactory.TxHandler(true) + if err != nil { + return nil, err + } return &loomchain.Application{ - Store: appStore, - Init: init, - TxHandler: loomchain.MiddlewareTxHandler( - txMiddleWare, - router, - postCommitMiddlewares, - ), + Store: appStore, + Init: init, + TxHandler: chainTxHandler, + TxHandlerFactory: txHandlerFactory, BlockIndexStore: blockIndexStore, EventHandler: eventHandler, ReceiptHandlerProvider: receiptHandlerProvider, @@ -1134,6 +1011,7 @@ func loadApp( GetValidatorSet: getValidatorSet, EvmAuxStore: evmAuxStore, ReceiptsVersion: cfg.ReceiptsVersion, + EVMTracer: tracer, }, nil } @@ -1183,8 +1061,6 @@ func deployContract( type contextFactory func(state loomchain.State) (contractpb.Context, error) -type staticContextFactory func(state loomchain.State) (contractpb.StaticContext, error) - func getContractCtx(pluginName string, vmManager *vm.Manager) contextFactory { return func(state loomchain.State) (contractpb.Context, error) { pvm, err := vmManager.InitVM(vm.VMType_PLUGIN, state) @@ -1195,16 +1071,6 @@ func getContractCtx(pluginName string, vmManager *vm.Manager) contextFactory { } } -func getContractStaticCtx(pluginName string, vmManager *vm.Manager) staticContextFactory { - return func(state loomchain.State) (contractpb.StaticContext, error) { - pvm, err := vmManager.InitVM(vm.VMType_PLUGIN, state) - if err != nil { - return nil, err - } - return plugin.NewInternalContractContext(pluginName, pvm.(*plugin.PluginVM), true) - } -} - func initBackend(cfg *config.Config, abciServerAddr string, fnRegistry fnConsensus.FnRegistry) backend.Backend { ovCfg := &backend.OverrideConfig{ LogLevel: cfg.BlockchainLogLevel, diff --git a/config/config.go b/config/config.go index 3f39230d22..72c9506225 100755 --- a/config/config.go +++ b/config/config.go @@ -11,9 +11,13 @@ import ( "path/filepath" "strings" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/loomnetwork/loomchain/auth" plasmacfg "github.com/loomnetwork/loomchain/builtin/plugins/plasma_cash/config" genesiscfg "github.com/loomnetwork/loomchain/config/genesis" + "github.com/loomnetwork/loomchain/db" "github.com/loomnetwork/loomchain/events" "github.com/loomnetwork/loomchain/evm" hsmpv "github.com/loomnetwork/loomchain/privval/hsm" @@ -23,10 +27,6 @@ import ( "github.com/loomnetwork/loomchain/store" blockindex "github.com/loomnetwork/loomchain/store/block_index" "github.com/loomnetwork/loomchain/throttle" - "github.com/pkg/errors" - "github.com/spf13/viper" - - "github.com/loomnetwork/loomchain/db" "github.com/loomnetwork/loomchain/fnConsensus" ) @@ -145,7 +145,7 @@ type Config struct { AllowNamedEvmContracts bool // Dragons - EVMDebugEnabled bool + EVMTracer *EVMTracerConfig // Set to true to disable minimum required build number check on node startup SkipMinBuildCheck bool @@ -184,6 +184,21 @@ func DefaultFnConsensusConfig() *FnConsensusConfig { } } +type EVMTracerConfig struct { + Enabled bool // enable tracer + Tracer string // enable JavaScript-based transaction tracing + DisableMemory bool // disable memory capture + DisableStack bool // disable stack capture + DisableStorage bool // disable storage capture + Limit int // maximum length of output, but zero means unlimited +} + +func DefaultEvmTraceConfig() *EVMTracerConfig { + return &EVMTracerConfig{ + Enabled: false, + } +} + type DPOSConfig struct { BootstrapNodes []string TotalStakedCacheDuration int64 @@ -431,7 +446,6 @@ func DefaultConfig() *Config { EVMPersistentTxReceiptsMax: receipts.DefaultMaxReceipts, SessionDuration: 600, EVMAccountsEnabled: false, - EVMDebugEnabled: false, SampleGoContractEnabled: false, Oracle: "", @@ -466,6 +480,7 @@ func DefaultConfig() *Config { cfg.EventStore = events.DefaultEventStoreConfig() cfg.EvmStore = evm.DefaultEvmStoreConfig() cfg.Web3 = eth.DefaultWeb3Config() + cfg.EVMTracer = DefaultEvmTraceConfig() cfg.Geth = DefaultGethConfig() cfg.DPOS = DefaultDPOSConfig() @@ -849,7 +864,15 @@ DPOS: # # Here be dragons, don't change the defaults unless you know what you're doing # -EVMDebugEnabled: {{ .EVMDebugEnabled }} +{{- if .EVMTracer }} +EVMTracer: + Enabled: {{ .EVMTracer.Enabled }} + Tracer: "{{ .EVMTracer.Tracer }}" + DisableMemory: {{ .EVMTracer.DisableMemory }} + DisableStack: {{ .EVMTracer.DisableStack }} + DisableStorage: {{ .EVMTracer.DisableStorage }} + Limit: {{ .EVMTracer.Limit }} +{{- end}} AllowNamedEvmContracts: {{ .AllowNamedEvmContracts }} # Set to true to disable minimum required build number check on node startup SkipMinBuildCheck: {{ .SkipMinBuildCheck }} diff --git a/config/config_test.go b/config/config_test.go index 6aa34a77e0..a37e2bec5b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,3 +1,5 @@ +// +build evm + package config import ( diff --git a/config/loom.example.yaml b/config/loom.example.yaml index 2cec2c8de3..6f4a5b0b76 100644 --- a/config/loom.example.yaml +++ b/config/loom.example.yaml @@ -83,6 +83,14 @@ DPOSv2OracleConfig: # ReadURI: "tcp://0.0.0.1" # PrivateKeyPath: "tcp://0.0.0.2" +EVMTracer: + Enabled: true + Tracer: "" + DisableMemory: true + DisableStack: false + DisableStorage: true + Limit: 1000 + EventDispatcher: Dispatcher: log diff --git a/e2e/tests/receipts/run_truffle_tests.sh b/e2e/tests/receipts/run_truffle_tests.sh index c176227a81..2591bdd9c1 100755 --- a/e2e/tests/receipts/run_truffle_tests.sh +++ b/e2e/tests/receipts/run_truffle_tests.sh @@ -17,7 +17,7 @@ if [[ "$OSTYPE" == "darwin"* ]] && [[ "$NODE_NAME" == "osx"* ]]; then # Jenkins OSX machine is slugish so give it more time to spin up the test cluster. sleep 5 else - sleep 1 + sleep 3 fi # Run Truffle tests using Truffle HDWallet provider & /eth endpoint diff --git a/e2e/tests/truffle/contracts/EventTestContract.sol b/e2e/tests/truffle/contracts/EventTestContract.sol index 89e3f8ea81..0aa294a863 100644 --- a/e2e/tests/truffle/contracts/EventTestContract.sol +++ b/e2e/tests/truffle/contracts/EventTestContract.sol @@ -1,3 +1,4 @@ +pragma solidity ^0.5.8; contract EventTestContract { uint256 public value; diff --git a/e2e/tests/truffle/test/DebugFunctions.js b/e2e/tests/truffle/test/DebugFunctions.js new file mode 100644 index 0000000000..32fcd0217d --- /dev/null +++ b/e2e/tests/truffle/test/DebugFunctions.js @@ -0,0 +1,69 @@ +const Web3 = require('web3'); +const fs = require('fs'); +const path = require('path'); +const EthereumTx = require('ethereumjs-tx').Transaction; +const { getLoomEvmTxHash, waitForXBlocks } = require('./helpers'); +const { + SpeculativeNonceTxMiddleware, SignedTxMiddleware, Client, + LocalAddress, CryptoUtils, LoomProvider +} = require('loom-js'); +const TxHashTestContract = artifacts.require('TxHashTestContract'); + +contract('debug_traceTransaction', async (accounts) => { + let contract, fromAddr, nodeAddr, txHashTestContract, web3, web3js; + beforeEach(async () => { + nodeAddr = fs.readFileSync(path.join(process.env.CLUSTER_DIR, '0', 'node_rpc_addr'), 'utf-8').trim() + const chainID = 'default'; + const writeUrl = `ws://${nodeAddr}/websocket`; + const readUrl = `ws://${nodeAddr}/queryws`; + + const privateKey = CryptoUtils.generatePrivateKey(); + const publicKey = CryptoUtils.publicKeyFromPrivateKey(privateKey); + + fromAddr = LocalAddress.fromPublicKey(publicKey); + const from = fromAddr.toString(); + + var client = new Client(chainID, writeUrl, readUrl); + client.on('error', msg => { + console.error('Error on connect to client', msg); + console.warn('Please verify if loom cluster is running') + }); + const setupMiddlewareFn = function(client, privateKey) { + const publicKey = CryptoUtils.publicKeyFromPrivateKey(privateKey); + return [new SpeculativeNonceTxMiddleware(publicKey, client), new SignedTxMiddleware(privateKey)] + }; + var loomProvider = new LoomProvider(client, privateKey, setupMiddlewareFn); + + web3 = new Web3(loomProvider); + web3js = new Web3(new Web3.providers.HttpProvider(`http://${nodeAddr}/eth`)); + + txHashTestContract = await TxHashTestContract.deployed(); + contract = new web3.eth.Contract(TxHashTestContract._json.abi, txHashTestContract.address, {from}); + }); + + it('Test debug_traceTransaction', async () => { + const setValue = 1111; + const txResult = await contract.methods.set(setValue).send(); + await waitForXBlocks(nodeAddr, 2) + await web3js.currentProvider.send({ + method: "debug_traceTransaction", + params: [txResult.transactionHash,{"disableStorage":false,"disableMemory":false,"disableStack":false}], + jsonrpc: "2.0", + id: new Date().getTime() + }, function (error, result) { + assert.equal(null, error, "debug_traceTransaction returned error"); + assert.equal(true, result.result.structLogs.length > 0, "trace did not return any data"); + }); + await web3js.currentProvider.send({ + method: "debug_storageRangeAt", + params: [txResult.blockHash, txResult.transactionIndex, txHashTestContract.address, "", 1000], + jsonrpc: "2.0", + id: new Date().getTime() + }, function (error, result) { + assert.equal(1, Object.keys(result.result.storage).length, "count of items returned by debug_storageRangeAt"); + onlyEntry = result.result.storage[Object.keys(result.result.storage)[0]].value; + assert.equal(setValue, Number(onlyEntry), "memory value same as value set"); + }); + await waitForXBlocks(nodeAddr, 2) + }) +}); \ No newline at end of file diff --git a/eth/polls/polls_test.go b/eth/polls/polls_test.go index a2168b922e..b0611c5df6 100644 --- a/eth/polls/polls_test.go +++ b/eth/polls/polls_test.go @@ -29,9 +29,8 @@ var ( ) const ( - deployId = uint32(1) - callId = uint32(2) - migrationTx = uint32(3) + deployId = uint32(1) + callId = uint32(2) ) func TestLogPoll(t *testing.T) { @@ -248,7 +247,7 @@ func testTimeout(t *testing.T, version handler.ReceiptHandlerVersion) { state40 := common.MockStateAt(state, uint64(40)) _ = sub.AddTxPoll(uint64(40)) - result, err = sub.LegacyPoll(state40, id, receiptHandler) + _, err = sub.LegacyPoll(state40, id, receiptHandler) require.Error(t, err, "poll did not timed out") require.NoError(t, receiptHandler.Close()) } @@ -360,6 +359,7 @@ func TestAddRemove(t *testing.T) { Topics: nil, } myFilter, err := eth.DecLogFilter(jsonFilter) + require.NoError(t, err) id, err := s.AddLogPoll(myFilter, 1) require.NoError(t, err) _, ok := s.polls[id] @@ -407,6 +407,6 @@ func mockSignedTx(t *testing.T, id uint32, to loom.Address, from loom.Address, d signedTx, err := proto.Marshal(&auth.SignedTx{ Inner: nonceTx, }) - + require.NoError(t, err) return signedTx } diff --git a/eth/query/tx.go b/eth/query/tx.go index 2937f18eec..a3623c3e2c 100644 --- a/eth/query/tx.go +++ b/eth/query/tx.go @@ -38,6 +38,9 @@ func GetTxByBlockAndIndex( iHeight := int64(height) blockResult, err := blockStore.GetBlockByHeight(&iHeight) + if err != nil { + return eth.GetEmptyTxObject(), errors.Errorf("error getting block for height %v", height) + } if blockResult == nil || blockResult.Block == nil { return eth.GetEmptyTxObject(), errors.Errorf("no block results found at height %v", height) } diff --git a/evm/evm.go b/evm/evm.go index 21a2126f4a..8bb6251eee 100644 --- a/evm/evm.go +++ b/evm/evm.go @@ -15,13 +15,12 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/go-kit/kit/metrics" kitprometheus "github.com/go-kit/kit/metrics/prometheus" + "github.com/loomnetwork/go-loom" "github.com/pkg/errors" stdprometheus "github.com/prometheus/client_golang/prometheus" - "github.com/loomnetwork/go-loom" "github.com/loomnetwork/loomchain" "github.com/loomnetwork/loomchain/features" - "github.com/loomnetwork/loomchain/log" ) // EVMEnabled indicates whether or not Loom EVM integration is available @@ -137,8 +136,7 @@ func (m *evmAccountBalanceManager) Transfer(from, to common.Address, amount *big m.abm.Transfer(fromAddr, toAddr, loom.NewBigUInt(amount)) } -// TODO: this shouldn't be exported, rename to wrappedEVM -type Evm struct { +type wrappedEVM struct { sdb vm.StateDB context vm.Context chainConfig params.ChainConfig @@ -147,8 +145,10 @@ type Evm struct { gasLimit uint64 } -func NewEvm(sdb vm.StateDB, lstate loomchain.State, abm *evmAccountBalanceManager, debug bool) *Evm { - p := new(Evm) +func NewEvm( + sdb vm.StateDB, lstate loomchain.State, abm *evmAccountBalanceManager, tracer vm.Tracer, +) (*wrappedEVM, error) { + p := new(wrappedEVM) p.sdb = sdb p.gasLimit = lstate.Config().GetEvm().GetGasLimit() if p.gasLimit == 0 { @@ -157,7 +157,11 @@ func NewEvm(sdb vm.StateDB, lstate loomchain.State, abm *evmAccountBalanceManage p.chainConfig = defaultChainConfig(lstate.FeatureEnabled(features.EvmConstantinopleFeature, false)) - p.vmConfig = defaultVmConfig(debug) + var err error + p.vmConfig, err = createVmConfig(tracer) + if err != nil { + return nil, errors.Wrap(err, "creating vm.Config") + } p.validateTxValue = lstate.FeatureEnabled(features.CheckTxValueFeature, false) p.context = vm.Context{ CanTransfer: core.CanTransfer, @@ -180,10 +184,10 @@ func NewEvm(sdb vm.StateDB, lstate loomchain.State, abm *evmAccountBalanceManage abm.Transfer(from, to, amount) } } - return p + return p, nil } -func (e Evm) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]byte, loom.Address, error) { +func (e wrappedEVM) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]byte, loom.Address, error) { var err error var usedGas uint64 defer func(begin time.Time) { @@ -194,7 +198,7 @@ func (e Evm) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]by }(time.Now()) origin := common.BytesToAddress(caller.Local) - vmenv := e.NewEnv(origin) + vmenv := e.newEnv(origin) var val *big.Int if value == nil { @@ -215,7 +219,7 @@ func (e Evm) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]by return runCode, loomAddress, err } -func (e Evm) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) ([]byte, error) { +func (e wrappedEVM) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) ([]byte, error) { var err error var usedGas uint64 defer func(begin time.Time) { @@ -227,7 +231,7 @@ func (e Evm) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) }(time.Now()) origin := common.BytesToAddress(caller.Local) contract := common.BytesToAddress(addr.Local) - vmenv := e.NewEnv(origin) + vmenv := e.newEnv(origin) var val *big.Int if value == nil { @@ -247,25 +251,24 @@ func (e Evm) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) return ret, err } -func (e Evm) StaticCall(caller, addr loom.Address, input []byte) ([]byte, error) { +func (e wrappedEVM) StaticCall(caller, addr loom.Address, input []byte) ([]byte, error) { origin := common.BytesToAddress(caller.Local) contract := common.BytesToAddress(addr.Local) - vmenv := e.NewEnv(origin) + vmenv := e.newEnv(origin) ret, _, err := vmenv.StaticCall(vm.AccountRef(origin), contract, input, e.gasLimit) return ret, err } -func (e Evm) GetCode(addr loom.Address) []byte { +func (e wrappedEVM) GetCode(addr loom.Address) []byte { return e.sdb.GetCode(common.BytesToAddress(addr.Local)) } -func (e Evm) GetStorageAt(addr loom.Address, key []byte) ([]byte, error) { +func (e wrappedEVM) GetStorageAt(addr loom.Address, key []byte) ([]byte, error) { result := e.sdb.GetState(common.BytesToAddress(addr.Local), common.BytesToHash(key)) return result.Bytes(), nil } -// TODO: this doesn't need to be exported, rename to newEVM -func (e Evm) NewEnv(origin common.Address) *vm.EVM { +func (e wrappedEVM) newEnv(origin common.Address) *vm.EVM { e.context.Origin = origin return vm.NewEVM(e.context, e.sdb, &e.chainConfig, e.vmConfig) } @@ -299,31 +302,12 @@ func defaultChainConfig(enableConstantinople bool) params.ChainConfig { } } -func defaultVmConfig(evmDebuggingEnabled bool) vm.Config { - logCfg := vm.LogConfig{ - DisableMemory: true, // disable memory capture - DisableStack: true, // disable stack capture - DisableStorage: true, // disable storage capture - Limit: 0, // maximum length of output, but zero means unlimited - } - debug := false - - if evmDebuggingEnabled { - log.Error("WARNING!!!! EVM Debug mode enabled, do NOT run this on a production server!!!") - logCfg = vm.LogConfig{ - DisableMemory: true, // disable memory capture - DisableStack: true, // disable stack capture - DisableStorage: true, // disable storage capture - Limit: 0, // maximum length of output, but zero means unlimited - } - debug = true - } - logger := vm.NewStructLogger(&logCfg) - return vm.Config{ +func createVmConfig(tracer vm.Tracer) (vm.Config, error) { + vmConfig := vm.Config{ // Debug enabled debugging Interpreter options - Debug: debug, + Debug: tracer != nil, // Tracer is the op code logger - Tracer: logger, + Tracer: tracer, // NoRecursion disabled Interpreter call, callcode, // delegate call and create. NoRecursion: false, @@ -334,4 +318,14 @@ func defaultVmConfig(evmDebuggingEnabled bool) vm.Config { // table. //JumpTable: [256]operation, } + if tracer == nil { + logCfg := vm.LogConfig{ + DisableMemory: true, // disable memory capture + DisableStack: true, // disable stack capture + DisableStorage: true, // disable storage capture + Limit: 0, // maximum length of output, but zero means unlimited + } + vmConfig.Tracer = vm.NewStructLogger(&logCfg) + } + return vmConfig, nil } diff --git a/evm/loomethdb.go b/evm/loomethdb.go index 9d5024f333..41981df589 100644 --- a/evm/loomethdb.go +++ b/evm/loomethdb.go @@ -30,7 +30,7 @@ type LoomEthdb struct { func NewLoomEthdb(_state loomchain.State, logContext *ethdbLogContext) *LoomEthdb { p := new(LoomEthdb) - p.state = store.PrefixKVStore(vmPrefix, _state) + p.state = store.PrefixKVStore(VmPrefix, _state) p.logContext = logContext return p } diff --git a/evm/loomevm.go b/evm/loomevm.go index 3d3ae3ff3f..5bfb0d1ec7 100644 --- a/evm/loomevm.go +++ b/evm/loomevm.go @@ -27,8 +27,8 @@ import ( ) var ( - vmPrefix = []byte("vm") - rootKey = []byte("vmroot") + VmPrefix = []byte("vm") + RootKey = []byte("vmroot") ) type StateDB interface { @@ -44,21 +44,23 @@ type ethdbLogContext struct { callerAddr loom.Address } -// TODO: this doesn't need to be exported, rename to loomEvmWithState -type LoomEvm struct { - *Evm +type loomEvmWithState struct { + *wrappedEVM db ethdb.Database sdb StateDB } -// TODO: this doesn't need to be exported, rename to newLoomEvmWithState +// NewLoomEvm is exported to db.newDumpEVMStateMultiWriterAppStoreCommand. +// TODO: this doesn't need to be exported, rename to newLoomEvmWithState. func NewLoomEvm( - loomState loomchain.State, accountBalanceManager AccountBalanceManager, - logContext *ethdbLogContext, debug bool, -) (*LoomEvm, error) { - p := new(LoomEvm) + loomState loomchain.State, + accountBalanceManager AccountBalanceManager, + logContext *ethdbLogContext, + tracer ethvm.Tracer, +) (*loomEvmWithState, error) { + p := new(loomEvmWithState) p.db = NewLoomEthdb(loomState, logContext) - oldRoot, err := p.db.Get(rootKey) + oldRoot, err := p.db.Get(RootKey) if err != nil { return nil, err } @@ -74,11 +76,14 @@ func NewLoomEvm( return nil, err } - p.Evm = NewEvm(p.sdb, loomState, abm, debug) + p.wrappedEVM, err = NewEvm(p.sdb, loomState, abm, tracer) + if err != nil { + return nil, errors.Wrap(err, "failed to create EVM") + } return p, nil } -func (levm LoomEvm) Commit() (common.Hash, error) { +func (levm loomEvmWithState) Commit() (common.Hash, error) { root, err := levm.sdb.Commit(true) if err != nil { return root, err @@ -86,13 +91,13 @@ func (levm LoomEvm) Commit() (common.Hash, error) { if err := levm.sdb.Database().TrieDB().Commit(root, false); err != nil { return root, err } - if err := levm.db.Put(rootKey, root[:]); err != nil { + if err := levm.db.Put(RootKey, root[:]); err != nil { return root, err } return root, err } -func (levm LoomEvm) RawDump() []byte { +func (levm loomEvmWithState) RawDump() []byte { d := levm.sdb.RawDump() output, err := json.MarshalIndent(d, "", " ") if err != nil { @@ -102,8 +107,6 @@ func (levm LoomEvm) RawDump() []byte { } var LoomVmFactory = func(state loomchain.State) (vm.VM, error) { - //TODO , debug bool, We should be able to pass in config - debug := false eventHandler := loomchain.NewDefaultEventHandler(events.NewLogEventDispatcher()) receiptHandlerProvider := receipts.NewReceiptHandlerProvider( eventHandler, @@ -111,47 +114,48 @@ var LoomVmFactory = func(state loomchain.State) (vm.VM, error) { nil, ) receiptHandler := receiptHandlerProvider.Writer() - return NewLoomVm(state, eventHandler, receiptHandler, nil, debug), nil + return NewLoomVm(state, receiptHandler, nil), nil } -// LoomVm implements the loomchain/vm.VM interface using the EVM. -// TODO: rename to LoomEVM -type LoomVm struct { +// LoomEVM implements the loomchain/vm.VM interface using the EVM. +type LoomEvm struct { state loomchain.State receiptHandler loomchain.WriteReceiptHandler createABM AccountBalanceManagerFactoryFunc - debug bool + tracer ethvm.Tracer } func NewLoomVm( loomState loomchain.State, - eventHandler loomchain.EventHandler, receiptHandler loomchain.WriteReceiptHandler, createABM AccountBalanceManagerFactoryFunc, - debug bool, -) vm.VM { - return &LoomVm{ +) *LoomEvm { + return &LoomEvm{ state: loomState, receiptHandler: receiptHandler, createABM: createABM, - debug: debug, } } -func (lvm LoomVm) accountBalanceManager(readOnly bool) AccountBalanceManager { +func (lvm LoomEvm) WithTracer(tracer ethvm.Tracer) LoomEvm { + lvm.tracer = tracer + return lvm +} + +func (lvm LoomEvm) accountBalanceManager(readOnly bool) AccountBalanceManager { if lvm.createABM == nil { return nil } return lvm.createABM(readOnly) } -func (lvm LoomVm) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]byte, loom.Address, error) { +func (lvm LoomEvm) Create(caller loom.Address, code []byte, value *loom.BigUInt) ([]byte, loom.Address, error) { logContext := ðdbLogContext{ blockHeight: lvm.state.Block().Height, contractAddr: loom.Address{}, callerAddr: caller, } - levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(false), logContext, lvm.debug) + levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(false), logContext, lvm.tracer) if err != nil { return nil, loom.Address{}, err } @@ -208,13 +212,13 @@ func (lvm LoomVm) Create(caller loom.Address, code []byte, value *loom.BigUInt) return response, addr, err } -func (lvm LoomVm) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) ([]byte, error) { +func (lvm LoomEvm) Call(caller, addr loom.Address, input []byte, value *loom.BigUInt) ([]byte, error) { logContext := ðdbLogContext{ blockHeight: lvm.state.Block().Height, contractAddr: addr, callerAddr: caller, } - levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(false), logContext, lvm.debug) + levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(false), logContext, lvm.tracer) if err != nil { return nil, err } @@ -261,24 +265,24 @@ func (lvm LoomVm) Call(caller, addr loom.Address, input []byte, value *loom.BigU return txHash, err } -func (lvm LoomVm) StaticCall(caller, addr loom.Address, input []byte) ([]byte, error) { - levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(true), nil, lvm.debug) +func (lvm LoomEvm) StaticCall(caller, addr loom.Address, input []byte) ([]byte, error) { + levm, err := NewLoomEvm(lvm.state, lvm.accountBalanceManager(true), nil, lvm.tracer) if err != nil { return nil, err } return levm.StaticCall(caller, addr, input) } -func (lvm LoomVm) GetCode(addr loom.Address) ([]byte, error) { - levm, err := NewLoomEvm(lvm.state, nil, nil, lvm.debug) +func (lvm LoomEvm) GetCode(addr loom.Address) ([]byte, error) { + levm, err := NewLoomEvm(lvm.state, nil, nil, lvm.tracer) if err != nil { return nil, err } return levm.GetCode(addr), nil } -func (lvm LoomVm) GetStorageAt(addr loom.Address, key []byte) ([]byte, error) { - levm, err := NewLoomEvm(lvm.state, nil, nil, lvm.debug) +func (lvm LoomEvm) GetStorageAt(addr loom.Address, key []byte) ([]byte, error) { + levm, err := NewLoomEvm(lvm.state, nil, nil, lvm.tracer) if err != nil { return nil, err } diff --git a/evm/noevm.go b/evm/noevm.go index 80427c9327..2b20130af1 100644 --- a/evm/noevm.go +++ b/evm/noevm.go @@ -15,11 +15,9 @@ var ( const EVMEnabled = false func NewLoomVm( - loomState loomchain.State, - eventHandler loomchain.EventHandler, - receiptHandler loomchain.WriteReceiptHandler, - createABM AccountBalanceManagerFactoryFunc, - debug bool, + _ loomchain.State, + _ loomchain.WriteReceiptHandler, + _ AccountBalanceManagerFactoryFunc, ) lvm.VM { return nil } diff --git a/integration_tests/ethcoin_evm_test.go b/integration_tests/ethcoin_evm_test.go index fbbd24b9f2..7a657a4cc3 100644 --- a/integration_tests/ethcoin_evm_test.go +++ b/integration_tests/ethcoin_evm_test.go @@ -208,7 +208,7 @@ func (c *ethCoinIntegrationTestHelper) callEVM( if err != nil { return err } - vm := evm.NewLoomVm(ctx.State, nil, nil, ctx.AccountBalanceManager, false) + vm := evm.NewLoomVm(ctx.State, nil, ctx.AccountBalanceManager) _, err = vm.Call(ctx.Message().Sender, c.Address, input, loom.NewBigUIntFromInt(0)) if err != nil { return err @@ -223,7 +223,7 @@ func (c *ethCoinIntegrationTestHelper) staticCallEVM( if err != nil { return err } - vm := evm.NewLoomVm(ctx.State, nil, nil, ctx.AccountBalanceManager, false) + vm := evm.NewLoomVm(ctx.State, nil, ctx.AccountBalanceManager) output, err := vm.StaticCall(ctx.Message().Sender, c.Address, input) if err != nil { return err @@ -239,7 +239,7 @@ func deployContractToEVM(ctx *plugin.FakeContextWithEVM, filename string, caller } byteCode := common.FromHex(string(hexByteCode)) - vm := evm.NewLoomVm(ctx.State, nil, nil, nil, false) + vm := evm.NewLoomVm(ctx.State, nil, nil) _, contractAddr, err = vm.Create(caller, byteCode, loom.NewBigUIntFromInt(0)) if err != nil { return contractAddr, err diff --git a/plugin/external.go b/plugin/external.go index 69e8c150b7..7d401fc57d 100644 --- a/plugin/external.go +++ b/plugin/external.go @@ -30,7 +30,7 @@ func (f *FileNameInfo) String() string { return f.Base + f.Ext + f.Version } -var fileInfoRE = regexp.MustCompile("(.+?)(\\.[a-zA-Z]+?)?\\.([0-9\\.]+)") +var fileInfoRE = regexp.MustCompile(`(.+?)(\.[a-zA-Z]+?)?\.([0-9\.]+)`) func parseFileName(name string) (*FileNameInfo, error) { groups := fileInfoRE.FindSubmatch([]byte(name)) diff --git a/plugin/fake_context.go b/plugin/fake_context.go index 84ddb384fe..d80e664991 100644 --- a/plugin/fake_context.go +++ b/plugin/fake_context.go @@ -103,7 +103,7 @@ func (c *FakeContextWithEVM) CallEVM(addr loom.Address, input []byte, value *loo if c.useAccountBalanceManager { createABM = c.AccountBalanceManager } - vm := levm.NewLoomVm(c.State, nil, nil, createABM, false) + vm := levm.NewLoomVm(c.State, nil, createABM) return vm.Call(c.ContractAddress(), addr, input, value) } @@ -112,7 +112,7 @@ func (c *FakeContextWithEVM) StaticCallEVM(addr loom.Address, input []byte) ([]b if c.useAccountBalanceManager { createABM = c.AccountBalanceManager } - vm := levm.NewLoomVm(c.State, nil, nil, createABM, false) + vm := levm.NewLoomVm(c.State, nil, createABM) return vm.StaticCall(c.ContractAddress(), addr, input) } diff --git a/plugin/loader.go b/plugin/loader.go index 4e2468925d..cbe960796c 100644 --- a/plugin/loader.go +++ b/plugin/loader.go @@ -121,7 +121,7 @@ func (m *StaticLoader) LoadContract(name string, blockHeight int64) (plugin.Cont } func (m *StaticLoader) findOverride(name string, blockHeight int64) plugin.Contract { - if overrides, _ := m.overrides[name]; overrides != nil { + if overrides := m.overrides[name]; overrides != nil { for i := len(overrides) - 1; i >= 0; i-- { if blockHeight >= overrides[i].BlockHeight { return overrides[i].Contract diff --git a/plugin/vm.go b/plugin/vm.go index 4a12c17d47..f0bf7fff26 100644 --- a/plugin/vm.go +++ b/plugin/vm.go @@ -15,12 +15,13 @@ import ( "github.com/loomnetwork/go-loom" lp "github.com/loomnetwork/go-loom/plugin" "github.com/loomnetwork/go-loom/util" + "github.com/pkg/errors" + "github.com/loomnetwork/loomchain" "github.com/loomnetwork/loomchain/auth" - levm "github.com/loomnetwork/loomchain/evm" + "github.com/loomnetwork/loomchain/evm" "github.com/loomnetwork/loomchain/registry" "github.com/loomnetwork/loomchain/vm" - "github.com/pkg/errors" ) type ( @@ -188,7 +189,7 @@ func (vm *PluginVM) StaticCall(caller, addr loom.Address, input []byte) ([]byte, } func (vm *PluginVM) CallEVM(caller, addr loom.Address, input []byte, value *loom.BigUInt) ([]byte, error) { - var createABM levm.AccountBalanceManagerFactoryFunc + var createABM evm.AccountBalanceManagerFactoryFunc var err error if vm.newABMFactory != nil { createABM, err = vm.newABMFactory(vm) @@ -196,12 +197,12 @@ func (vm *PluginVM) CallEVM(caller, addr loom.Address, input []byte, value *loom return nil, err } } - evm := levm.NewLoomVm(vm.State, vm.EventHandler, vm.receiptWriter, createABM, false) - return evm.Call(caller, addr, input, value) + levm := evm.NewLoomVm(vm.State, vm.receiptWriter, createABM) + return levm.Call(caller, addr, input, value) } func (vm *PluginVM) StaticCallEVM(caller, addr loom.Address, input []byte) ([]byte, error) { - var createABM levm.AccountBalanceManagerFactoryFunc + var createABM evm.AccountBalanceManagerFactoryFunc var err error if vm.newABMFactory != nil { createABM, err = vm.newABMFactory(vm) @@ -209,8 +210,8 @@ func (vm *PluginVM) StaticCallEVM(caller, addr loom.Address, input []byte) ([]by return nil, err } } - evm := levm.NewLoomVm(vm.State, vm.EventHandler, vm.receiptWriter, createABM, false) - return evm.StaticCall(caller, addr, input) + levm := evm.NewLoomVm(vm.State, vm.receiptWriter, createABM) + return levm.StaticCall(caller, addr, input) } func (vm *PluginVM) GetCode(addr loom.Address) ([]byte, error) { diff --git a/plugin/vm_test.go b/plugin/vm_test.go index f089fab0e3..0d50d21ca4 100644 --- a/plugin/vm_test.go +++ b/plugin/vm_test.go @@ -142,7 +142,7 @@ func TestPluginVMContractContextCaller(t *testing.T) { require.NoError(t, err) vm := NewPluginVM(loader, state, createRegistry(state), &fakeEventHandler{}, nil, nil, nil, nil) - evm := levm.NewLoomVm(state, nil, nil, nil, false) + evm := levm.NewLoomVm(state, nil, nil) // Deploy contracts owner := loom.RootAddress("chain") diff --git a/receipts/leveldb/leveldb_test.go b/receipts/leveldb/leveldb_test.go index e2fc61b10e..925691de3a 100644 --- a/receipts/leveldb/leveldb_test.go +++ b/receipts/leveldb/leveldb_test.go @@ -2,15 +2,15 @@ package leveldb import ( "bytes" - "fmt" "os" "testing" "github.com/gogo/protobuf/proto" "github.com/loomnetwork/go-loom/plugin/types" + "github.com/stretchr/testify/require" + "github.com/loomnetwork/loomchain/receipts/common" evmaux "github.com/loomnetwork/loomchain/store/evm_aux" - "github.com/stretchr/testify/require" ) const ( @@ -173,19 +173,6 @@ func TestConfirmTransactionReceipts(t *testing.T) { require.Error(t, err) } -//nolint:deadcode -func dumpDbEntries(evmAuxStore *evmaux.EvmAuxStore) error { - fmt.Println("\nDumping leveldb") - db := evmAuxStore.DB() - iter := db.NewIterator(nil, nil) - defer iter.Release() - for iter.Next() { - fmt.Printf("key %s\t\tvalue %s", iter.Key(), iter.Value()) - } - fmt.Println() - return iter.Error() -} - func countDbEntries(evmAuxStore *evmaux.EvmAuxStore) (uint64, error) { count := uint64(0) db := evmAuxStore.DB() diff --git a/registry/v1/registry.go b/registry/v1/registry.go index 55313cd9c6..862aecea94 100644 --- a/registry/v1/registry.go +++ b/registry/v1/registry.go @@ -17,7 +17,7 @@ const ( ) var ( - validNameRE = regexp.MustCompile("^[a-zA-Z0-9\\.\\-]+$") + validNameRE = regexp.MustCompile(`^[a-zA-Z0-9\.\-]+$`) ) func recordKey(name string) []byte { diff --git a/registry/v2/registry.go b/registry/v2/registry.go index 152cb0ff5c..886e20ffdc 100644 --- a/registry/v2/registry.go +++ b/registry/v2/registry.go @@ -18,7 +18,7 @@ const ( ) var ( - validNameRE = regexp.MustCompile("^[a-zA-Z0-9\\.\\-]+$") + validNameRE = regexp.MustCompile(`^[a-zA-Z0-9\.\-]+$`) // Store Keys contractAddrKeyPrefix = []byte("reg_caddr") diff --git a/rpc/debug/jsonrpc_conversion.go b/rpc/debug/jsonrpc_conversion.go new file mode 100644 index 0000000000..5d3d565164 --- /dev/null +++ b/rpc/debug/jsonrpc_conversion.go @@ -0,0 +1,50 @@ +// +build evm + +package debug + +import ( + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth" +) + +type JsonTraceConfig struct { + LogConfig *JsonLogConfig `json:"logconfig,omitempty"` + Tracer string `json:"tracer,omitempty"` + Timeout string `json:"address,omitempty"` +} + +type JsonLogConfig struct { + DisableStorage bool `json:"disableStorage,omitempty"` + DisableMemory bool `json:"disableMemory,omitempty"` + DisableStack bool `json:"disableStack,omitempty"` +} + +type JsonStorageRangeResult struct { + eth.StorageRangeResult + Complete bool `json:"complete"` +} + +func DecTraceConfig(jcfg *JsonTraceConfig) eth.TraceConfig { + var logConfig *vm.LogConfig + if jcfg == nil { + return eth.TraceConfig{ + LogConfig: logConfig, + Tracer: nil, + Timeout: nil, + Reexec: nil, + } + } + if jcfg.LogConfig != nil { + logConfig = &vm.LogConfig{ + DisableMemory: jcfg.LogConfig.DisableMemory, + DisableStack: jcfg.LogConfig.DisableStack, + DisableStorage: jcfg.LogConfig.DisableStorage, + } + } + return eth.TraceConfig{ + LogConfig: logConfig, + Tracer: &jcfg.Tracer, + Timeout: &jcfg.Timeout, + Reexec: nil, + } +} diff --git a/rpc/debug/noevmtrace.go b/rpc/debug/noevmtrace.go new file mode 100644 index 0000000000..5781d01c97 --- /dev/null +++ b/rpc/debug/noevmtrace.go @@ -0,0 +1,19 @@ +// +build !evm + +package debug + +import ( + "github.com/ethereum/go-ethereum/eth" + + "github.com/loomnetwork/loomchain" + "github.com/loomnetwork/loomchain/store" +) + +func TraceTransaction( + _ loomchain.Application, + _ store.BlockStore, + _, _, _ int64, + _ eth.TraceConfig, +) (trace interface{}, err error) { + return nil, nil +} diff --git a/rpc/debug/trace.go b/rpc/debug/trace.go new file mode 100644 index 0000000000..df57c2db4a --- /dev/null +++ b/rpc/debug/trace.go @@ -0,0 +1,199 @@ +// +build evm + +package debug + +import ( + "bytes" + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/ethapi" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/loomnetwork/go-loom/util" + "github.com/pkg/errors" + abci "github.com/tendermint/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + ttypes "github.com/tendermint/tendermint/types" + + "github.com/loomnetwork/loomchain" + "github.com/loomnetwork/loomchain/evm" + "github.com/loomnetwork/loomchain/store" +) + +func TraceTransaction( + app loomchain.Application, + blockstore store.BlockStore, + startBlockNumber, targetBlockNumber, txIndex int64, + config eth.TraceConfig, +) (trace interface{}, err error) { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("loomchain panicked %v", r) + } + }() + + block, err := runTxsTo(&app, blockstore, startBlockNumber, targetBlockNumber, txIndex, false) + if err != nil { + return nil, err + } + + tracer, err := CreateTracer(config) + if err != nil { + return nil, err + } + if err := app.SetTracer(tracer, false); err != nil { + return nil, err + } + + txResult, err := blockstore.GetTxResult(block.Block.Data.Txs[txIndex].Hash()) + if err != nil { + return nil, err + } + + if err := app.SetTracer(tracer, false); err != nil { + return nil, err + } + + result := app.DeliverTx(block.Block.Data.Txs[txIndex]) + match, err := resultsMatch(txResult.TxResult, result) + if !match { + return nil, err + } + + switch tracer := tracer.(type) { + case *vm.StructLogger: + return ðapi.ExecutionResult{ + Gas: 0, + Failed: result.Code != abci.CodeTypeOK, + ReturnValue: fmt.Sprintf("%x", result), + StructLogs: ethapi.FormatLogs(tracer.StructLogs()), + }, nil + case *tracers.Tracer: + return tracer.GetResult() + default: + return nil, errors.New(fmt.Sprintf("bad tracer type %T", tracer)) + } +} + +func StorageRangeAt( + app loomchain.Application, + blockstore store.BlockStore, + address, begin []byte, + startBlockNumber, targetBlockNumber, txIndex int64, + maxResults int, +) (results JsonStorageRangeResult, err error) { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("loomchain panicked %v", r) + } + }() + + block, err := runTxsTo(&app, blockstore, startBlockNumber, targetBlockNumber, txIndex, true) + if err != nil { + return JsonStorageRangeResult{}, err + } + + storeState := loomchain.NewStoreState( + context.Background(), + app.Store, + ttypes.TM2PB.Header(&block.Block.Header), + block.BlockMeta.BlockID.Hash, + app.GetValidatorSet, + ) + ethDb := evm.NewLoomEthdb(storeState, nil) + root := app.Store.Get(util.PrefixKey(evm.VmPrefix, evm.RootKey)) + + stateDb, err := state.New(common.BytesToHash(root), state.NewDatabase(ethDb)) + if err != nil { + return JsonStorageRangeResult{}, err + } + st := stateDb.StorageTrie(common.BytesToAddress(address)) + result, err := eth.StorageRangeAt(st, begin, maxResults) + if err != nil { + return JsonStorageRangeResult{}, err + } + + return JsonStorageRangeResult{ + StorageRangeResult: result, + Complete: result.NextKey == nil, + }, nil +} + +func runTxsTo(app *loomchain.Application, blockstore store.BlockStore, startHeight, height, index int64, includeEnd bool) (*ctypes.ResultBlock, error) { + for h := startHeight; h <= height; h++ { + resultBlock, err := blockstore.GetBlockByHeight(&h) + if err != nil { + return nil, errors.Wrapf(err, "getting block information at height %v", h) + } + + _ = app.BeginBlock(requestBeginBlock(*resultBlock)) + + if h != height { + for i := 0; i < len(resultBlock.Block.Data.Txs); i++ { + _ = app.DeliverTx(resultBlock.Block.Data.Txs[i]) + } + } else { + for i := 0; i < len(resultBlock.Block.Data.Txs); i++ { + if i != int(index) { + _ = app.DeliverTx(resultBlock.Block.Data.Txs[i]) + } else { + if includeEnd { + _ = app.DeliverTx(resultBlock.Block.Data.Txs[i]) + } + return resultBlock, nil + } + } + } + + _ = app.EndBlock(requestEndBlock(h)) + + // todo we should really be matching the app hash returned here with the one that was + // stored in the next block, otherwise we can't really know if the replay was successful or not. + _ = app.Commit() + } + return nil, errors.Errorf("cannot find transaction at height %d index %d", height, index) +} + +func resultsMatch(expected, actual abci.ResponseDeliverTx) (bool, error) { + if expected.Code != actual.Code { + return false, errors.Errorf("transaction result codes do not match, expected %v got $v", expected.Code, actual.Code) + } + if 0 == bytes.Compare(expected.Data, actual.Data) { + return false, errors.Errorf("transaction result data does not match, expected %v got $v", expected.Data, actual.Data) + } + if expected.Log != actual.Log { + return false, errors.Errorf("transaction logs do not match, expected %v got $v", expected.Log, actual.Log) + } + if expected.Info != actual.Info { + return false, errors.Errorf("transaction info does not match, expected %v got $v", expected.Info, actual.Info) + } + return true, nil +} + +func requestBeginBlock(resultBlock ctypes.ResultBlock) abci.RequestBeginBlock { + return abci.RequestBeginBlock{ + Header: ttypes.TM2PB.Header(&resultBlock.BlockMeta.Header), + Hash: resultBlock.BlockMeta.BlockID.Hash, + } +} + +func requestEndBlock(height int64) abci.RequestEndBlock { + return abci.RequestEndBlock{ + Height: height, + } +} + +func CreateTracer(traceCfg eth.TraceConfig) (vm.Tracer, error) { + if traceCfg.Tracer == nil || len(*traceCfg.Tracer) == 0 { + return vm.NewStructLogger(traceCfg.LogConfig), nil + } + tracer, err := tracers.New(*traceCfg.Tracer) + if err != nil { + return nil, err + } + return tracer, nil +} diff --git a/rpc/instrumenting.go b/rpc/instrumenting.go index 00e6742399..00b449e76f 100755 --- a/rpc/instrumenting.go +++ b/rpc/instrumenting.go @@ -7,10 +7,12 @@ import ( "github.com/go-kit/kit/metrics" "github.com/gorilla/websocket" "github.com/loomnetwork/go-loom/plugin/types" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/loomnetwork/loomchain/config" + "github.com/loomnetwork/loomchain/rpc/debug" "github.com/loomnetwork/loomchain/rpc/eth" "github.com/loomnetwork/loomchain/vm" - rpctypes "github.com/tendermint/tendermint/rpc/lib/types" ) // InstrumentingMiddleware implements QuerySerice interface @@ -583,3 +585,29 @@ func (m InstrumentingMiddleware) EthGetTransactionCount( resp, err = m.next.EthGetTransactionCount(local, block) return } + +func (m InstrumentingMiddleware) DebugTraceTransaction( + hash eth.Data, config *debug.JsonTraceConfig, +) (resp interface{}, err error) { + defer func(begin time.Time) { + lvs := []string{"method", "DebugTraceTransaction", "error", fmt.Sprint(err != nil)} + m.requestCount.With(lvs...).Add(1) + m.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) + }(time.Now()) + + resp, err = m.next.DebugTraceTransaction(hash, config) + return +} + +func (m InstrumentingMiddleware) DebugStorageRangeAt( + blockHashOrNumber string, txIndex int, address, begin string, maxResults int, +) (resp debug.JsonStorageRangeResult, err error) { + defer func(begin time.Time) { + lvs := []string{"method", "DebugTraceTransaction", "error", fmt.Sprint(err != nil)} + m.requestCount.With(lvs...).Add(1) + m.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) + }(time.Now()) + + resp, err = m.next.DebugStorageRangeAt(blockHashOrNumber, txIndex, address, begin, maxResults) + return +} diff --git a/rpc/mock_query_server.go b/rpc/mock_query_server.go index 002d137767..b888aa86d5 100644 --- a/rpc/mock_query_server.go +++ b/rpc/mock_query_server.go @@ -4,11 +4,11 @@ import ( "sync" "github.com/gorilla/websocket" - rpctypes "github.com/tendermint/tendermint/rpc/lib/types" - "github.com/loomnetwork/go-loom/plugin/types" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" "github.com/loomnetwork/loomchain/config" + "github.com/loomnetwork/loomchain/rpc/debug" "github.com/loomnetwork/loomchain/rpc/eth" "github.com/loomnetwork/loomchain/vm" ) @@ -373,3 +373,19 @@ func (m *MockQueryService) EvmUnSubscribe(id string) (bool, error) { m.MethodsCalled = append([]string{"EvmUnSubscribe"}, m.MethodsCalled...) return true, nil } + +func (m *MockQueryService) DebugTraceTransaction(hash eth.Data, config *debug.JsonTraceConfig) (interface{}, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.MethodsCalled = append([]string{"DebugTraceTransaction"}, m.MethodsCalled...) + return nil, nil +} + +func (m MockQueryService) DebugStorageRangeAt( + blockHashOrNumber string, txIndex int, address, begin string, maxResults int, +) (resp debug.JsonStorageRangeResult, err error) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.MethodsCalled = append([]string{"DebugTraceTransaction"}, m.MethodsCalled...) + return debug.JsonStorageRangeResult{}, nil +} diff --git a/rpc/query_server.go b/rpc/query_server.go index 4bc726c0c7..1ce8d8966d 100755 --- a/rpc/query_server.go +++ b/rpc/query_server.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/tendermint/tendermint/crypto/tmhash" "github.com/gogo/protobuf/proto" gtypes "github.com/loomnetwork/go-loom/types" @@ -41,6 +42,7 @@ import ( "github.com/loomnetwork/loomchain/receipts/common" "github.com/loomnetwork/loomchain/registry" registryFac "github.com/loomnetwork/loomchain/registry/factory" + "github.com/loomnetwork/loomchain/rpc/debug" "github.com/loomnetwork/loomchain/rpc/eth" "github.com/loomnetwork/loomchain/store" blockindex "github.com/loomnetwork/loomchain/store/block_index" @@ -62,6 +64,7 @@ const ( // StateProvider interface is used by QueryServer to access the read-only application state type StateProvider interface { ReadOnlyState() loomchain.State + ReplayApplication(uint64, store.BlockStore) (*loomchain.Application, int64, error) } // QueryServer provides the ability to query the current state of the DAppChain via RPC. @@ -278,7 +281,7 @@ func (s *QueryServer) queryEvm(state loomchain.State, caller, contract loom.Addr return nil, err } } - vm := levm.NewLoomVm(state, nil, nil, createABM, false) + vm := levm.NewLoomVm(state, nil, createABM) return vm.StaticCall(callerAddr, contract, query) } @@ -323,7 +326,7 @@ func (s *QueryServer) GetEvmCode(contract string) ([]byte, error) { snapshot := s.StateProvider.ReadOnlyState() defer snapshot.Release() - vm := levm.NewLoomVm(snapshot, nil, nil, nil, false) + vm := levm.NewLoomVm(snapshot, nil, nil) return vm.GetCode(contractAddr) } @@ -337,7 +340,7 @@ func (s *QueryServer) EthGetCode(address eth.Data, block eth.BlockHeight) (eth.D snapshot := s.StateProvider.ReadOnlyState() defer snapshot.Release() - evm := levm.NewLoomVm(snapshot, nil, nil, nil, false) + evm := levm.NewLoomVm(snapshot, nil, nil) code, err := evm.GetCode(addr) if err != nil { return "", errors.Wrapf(err, "getting evm code for %v", address) @@ -1143,7 +1146,7 @@ func (s *QueryServer) EthGetStorageAt(local eth.Data, position string, block eth return "", errors.Wrapf(err, "unable to get storage at height %v", block) } - evm := levm.NewLoomVm(snapshot, nil, nil, nil, false) + evm := levm.NewLoomVm(snapshot, nil, nil) storage, err := evm.GetStorageAt(address, ethcommon.HexToHash(position).Bytes()) if err != nil { return "", errors.Wrapf(err, "failed to get EVM storage at %v", address.Local.String()) @@ -1170,6 +1173,80 @@ func (s *QueryServer) EthAccounts() ([]eth.Data, error) { return []eth.Data{}, nil } +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#debug_tracetransaction +func (s *QueryServer) DebugTraceTransaction(hash eth.Data, config *debug.JsonTraceConfig) (interface{}, error) { + receipt, err := s.EthGetTransactionReceipt(hash) + if err != nil || receipt == nil { + return nil, errors.Wrap(err, "cant find transaction matching hash") + } + blockNumber, err := eth.DecQuantityToUint(receipt.BlockNumber) + if err != nil { + return nil, errors.Wrapf(err, "cant parse block number %v", receipt.BlockNumber) + } + txIndex, err := eth.DecQuantityToUint(receipt.TransactionIndex) + if err != nil { + return nil, errors.Wrapf(err, "cant parse transaction index %v", receipt.TransactionIndex) + } + cfg := debug.DecTraceConfig(config) + replayApp, startBlockNumber, err := s.ReplayApplication(blockNumber, s.BlockStore) + if err != nil { + return nil, err + } + return debug.TraceTransaction( + *replayApp, + s.BlockStore, + startBlockNumber, + int64(blockNumber), + int64(txIndex), + cfg, + ) +} + +// https://github.com/ethereum/retesteth/wiki/RPC-Methods#debug_storagerangeat +func (s QueryServer) DebugStorageRangeAt( + blockHashOrNumber string, txIndex int, address, begin string, maxResults int, +) (resp debug.JsonStorageRangeResult, err error) { + if len(address) >= 2 && address[:2] != "0x" { + address = "0x" + address + } + local, err := loom.LocalAddressFromHexString(address) + if err != nil { + return debug.JsonStorageRangeResult{}, err + } + + var blockNumber uint64 + if len(blockHashOrNumber) >= tmhash.Size { + if blockHashOrNumber[:2] == "0x" { + blockHashOrNumber = blockHashOrNumber[2:] + } + hash, err := hex.DecodeString(blockHashOrNumber) + if err != nil { + return debug.JsonStorageRangeResult{}, err + } + blockNumber, err = s.getBlockHeightFromHash(hash) + } else { + blockNumber, err = strconv.ParseUint(blockHashOrNumber, 0, 64) + } + if err != nil { + return debug.JsonStorageRangeResult{}, err + } + + replayApp, startBlockNumber, err := s.ReplayApplication(blockNumber, s.BlockStore) + if err != nil { + return debug.JsonStorageRangeResult{}, err + } + return debug.StorageRangeAt( + *replayApp, + s.BlockStore, + local, + nil, + startBlockNumber, + int64(blockNumber), + int64(txIndex), + maxResults, + ) +} + func (s *QueryServer) getBlockHeightFromHash(hash []byte) (uint64, error) { if nil != s.BlockIndexStore { return s.BlockIndexStore.GetBlockHeightByHash(hash) diff --git a/rpc/query_server_test.go b/rpc/query_server_test.go index 2939f703cf..d2862884e2 100644 --- a/rpc/query_server_test.go +++ b/rpc/query_server_test.go @@ -101,6 +101,10 @@ func (s *stateProvider) ReadOnlyState() loomchain.State { ) } +func (s *stateProvider) ReplayApplication(uint64, store.BlockStore) (*loomchain.Application, int64, error) { + return nil, 0, fmt.Errorf("Not implemented") +} + var testlog llog.TMLogger func TestQueryServer(t *testing.T) { diff --git a/rpc/query_service.go b/rpc/query_service.go index fce1e653ab..714890280a 100755 --- a/rpc/query_service.go +++ b/rpc/query_service.go @@ -17,6 +17,7 @@ import ( "github.com/loomnetwork/loomchain/config" "github.com/loomnetwork/loomchain/eth/subs" "github.com/loomnetwork/loomchain/log" + "github.com/loomnetwork/loomchain/rpc/debug" "github.com/loomnetwork/loomchain/rpc/eth" "github.com/loomnetwork/loomchain/vm" ) @@ -65,6 +66,10 @@ type QueryService interface { GetContractRecord(contractAddr string) (*types.ContractRecordResponse, error) DPOSTotalStaked() (*DPOSTotalStakedResponse, error) + // debug transactions. + DebugTraceTransaction(hash eth.Data, config *debug.JsonTraceConfig) (interface{}, error) + DebugStorageRangeAt(blockHashOrNumber string, txIndex int, address, begin string, maxResults int) (debug.JsonStorageRangeResult, error) + // deprecated function EvmTxReceipt(txHash []byte) ([]byte, error) GetEvmCode(contract string) ([]byte, error) @@ -189,6 +194,8 @@ func createDefaultEthRoutes(svc QueryService, chainID string) map[string]eth.RPC routes["net_version"] = eth.NewRPCFunc(svc.EthNetVersion, "") routes["eth_getTransactionCount"] = eth.NewRPCFunc(svc.EthGetTransactionCount, "local,block") routes["eth_sendRawTransaction"] = NewSendRawTransactionRPCFunc(chainID, rpccore.BroadcastTxSync) + routes["debug_traceTransaction"] = eth.NewRPCFunc(svc.DebugTraceTransaction, "hash,config") + routes["debug_storageRangeAt"] = eth.NewRPCFunc(svc.DebugStorageRangeAt, "blockHashOrNumber,txIndex,address,begin,maxResults") return routes } diff --git a/store/block_store.go b/store/block_store.go index 2cb2d3d39d..9f0443be89 100644 --- a/store/block_store.go +++ b/store/block_store.go @@ -87,6 +87,7 @@ func (s *TendermintBlockStore) GetBlockByHeight(height *int64) (*ctypes.ResultBl LastBlockID: blockResult.Block.Header.LastBlockID, Time: blockResult.Block.Header.Time, ProposerAddress: blockResult.Block.Header.ProposerAddress, + ChainID: blockResult.Block.Header.ChainID, } blockMeta := types.BlockMeta{ BlockID: blockResult.BlockMeta.BlockID, diff --git a/store/block_store_cache_test.go b/store/block_store_cache_test.go index 6edf12d98c..4faad52979 100644 --- a/store/block_store_cache_test.go +++ b/store/block_store_cache_test.go @@ -220,7 +220,13 @@ func TestBlockFetchAtHeight2Q(t *testing.T) { //request for a block for nil height, maximum height data is returned and cached blockstoreData, err = cachedblockStore.GetBlockByHeight(nil) require.NoError(t, err, "Gives maximum height block") - require.Equal(t, int64(50), blockstoreData.Block.Height, "Expecting blockstore height 50 as maximum height block is fetched,this is default functionality of corresponding tendermint blockstore API also") + require.Equal( + t, + int64(50), + blockstoreData.Block.Height, + "Expecting blockstore height 50 as maximum height block is fetched,this is default"+ + "functionality of corresponding tendermint blockstore API also", + ) //block at maximum height present in cache _, ok = cachedblockStore.TwoQueueCache.Get(height) @@ -249,7 +255,12 @@ func TestGetBlockRangeByHeight2Q(t *testing.T) { //Blocks returned by MockBlockStore are verified for data accuracy for i := minheight; i <= maxheight; i++ { - require.Equal(t, int64(i), int64(blockrangeData.BlockMetas[i-1].Header.Height), "Expecting height field in blockMetas equal height supplied to API for accuracy check,Expecting Data from API") + require.Equal( + t, + int64(i), + int64(blockrangeData.BlockMetas[i-1].Header.Height), + "Expecting height field in blockMetas equal height supplied to API for accuracy check,Expecting Data from API", + ) } //Block infos in above provided height range gets cached resulting in cache hit for each height in height range @@ -264,7 +275,13 @@ func TestGetBlockRangeByHeight2Q(t *testing.T) { //Blocks returned by Cached are verified for data accuracy for i := minheight; i <= maxheight; i++ { - require.Equal(t, int64(i), int64(blockrangeData.BlockMetas[i-1].Header.Height), "Expecting Data from Cache,Expecting height field in blockMetas equal height supplied to API for Cache accuracy check") + require.Equal( + t, + int64(i), + int64(blockrangeData.BlockMetas[i-1].Header.Height), + "Expecting Data from Cache,Expecting height field in blockMetas equal height"+ + " supplied to API for Cache accuracy check", + ) } //request for blockinfo for height range where max height is greater than maximum height of blockchain, error is returned by Cache, caching occurs till max height of blockchain diff --git a/store/iavlstore.go b/store/iavlstore.go index 253d6f5a00..87cf976e60 100755 --- a/store/iavlstore.go +++ b/store/iavlstore.go @@ -216,6 +216,13 @@ func (s *IAVLStore) GetSnapshot() Snapshot { } } +func (s *IAVLStore) GetSnapshotAt(version int64) (Snapshot, error) { + // This isn't an actual snapshot obviously, and never will be, but lets pretend... + return &iavlStoreSnapshot{ + IAVLStore: s, + }, nil +} + // NewIAVLStore creates a new IAVLStore. // maxVersions can be used to specify how many versions should be retained, if set to zero then // old versions will never been deleted. diff --git a/store/logstore.go b/store/logstore.go index 2e14a6b981..4de507b068 100644 --- a/store/logstore.go +++ b/store/logstore.go @@ -129,3 +129,7 @@ func (s *LogStore) Prune() error { func (s *LogStore) GetSnapshot() Snapshot { return s.store.GetSnapshot() } + +func (s *LogStore) GetSnapshotAt(version int64) (Snapshot, error) { + return s.store.GetSnapshotAt(version) +} diff --git a/store/memstore.go b/store/memstore.go index badf02b59f..dfe89e143c 100644 --- a/store/memstore.go +++ b/store/memstore.go @@ -79,3 +79,7 @@ func (m *MemStore) Prune() error { func (m *MemStore) GetSnapshot() Snapshot { panic("not implemented") } + +func (m *MemStore) GetSnapshotAt(version int64) (Snapshot, error) { + panic("not implemented") +} diff --git a/store/multi_writer_app_store.go b/store/multi_writer_app_store.go index 598da8a296..db45b7b06e 100644 --- a/store/multi_writer_app_store.go +++ b/store/multi_writer_app_store.go @@ -119,7 +119,9 @@ func NewMultiWriterAppStore( store.onlySaveEvmStateToEvmStore = bytes.Equal(store.appStore.Get(evmDBFeatureKey), []byte{1}) } - store.setLastSavedTreeToVersion(appStore.Version()) + if err := store.setLastSavedTreeToVersion(appStore.Version()); err != nil { + return nil, err + } return store, nil } @@ -262,12 +264,30 @@ func (s *MultiWriterAppStore) Prune() error { } func (s *MultiWriterAppStore) GetSnapshot() Snapshot { + snapshot, err := s.GetSnapshotAt(0) + if err != nil { + panic(err) + } + return snapshot +} + +func (s *MultiWriterAppStore) GetSnapshotAt(version int64) (Snapshot, error) { defer func(begin time.Time) { getSnapshotDuration.Observe(time.Since(begin).Seconds()) }(time.Now()) - appStoreTree := (*iavl.ImmutableTree)(atomic.LoadPointer(&s.lastSavedTree)) + + var err error + var appStoreTree *iavl.ImmutableTree + if version == 0 { + appStoreTree = (*iavl.ImmutableTree)(atomic.LoadPointer(&s.lastSavedTree)) + } else { + appStoreTree, err = s.appStore.tree.GetImmutable(version) + if err != nil { + return nil, errors.Wrapf(err, "failed to load immutable tree for version %v", version) + } + } evmDbSnapshot := s.evmStore.GetSnapshot(appStoreTree.Version()) - return newMultiWriterStoreSnapshot(evmDbSnapshot, appStoreTree) + return newMultiWriterStoreSnapshot(evmDbSnapshot, appStoreTree), nil } type multiWriterStoreSnapshot struct { diff --git a/store/pruning_iavlstore.go b/store/pruning_iavlstore.go index 4e73cbad31..3887d94b2d 100644 --- a/store/pruning_iavlstore.go +++ b/store/pruning_iavlstore.go @@ -2,7 +2,6 @@ package store import ( "fmt" - "runtime" "sync" "time" @@ -10,10 +9,11 @@ import ( kitprometheus "github.com/go-kit/kit/metrics/prometheus" "github.com/loomnetwork/go-loom" "github.com/loomnetwork/go-loom/plugin" - "github.com/loomnetwork/loomchain/log" "github.com/pkg/errors" stdprometheus "github.com/prometheus/client_golang/prometheus" dbm "github.com/tendermint/tendermint/libs/db" + + "github.com/loomnetwork/loomchain/log" ) var ( @@ -178,10 +178,11 @@ func (s *PruningIAVLStore) Prune() error { } func (s *PruningIAVLStore) GetSnapshot() Snapshot { - // This isn't an actual snapshot obviously, and never will be, but lets pretend... - return &pruningIAVLStoreSnapshot{ - PruningIAVLStore: s, - } + return s.GetSnapshot() +} + +func (s *PruningIAVLStore) GetSnapshotAt(version int64) (Snapshot, error) { + return s.store.GetSnapshotAt(version) } func (s *PruningIAVLStore) prune() error { @@ -233,24 +234,6 @@ func (s *PruningIAVLStore) deleteVersion(ver int64) error { return err } -// runWithRecovery should run in a goroutine, it will ensure the given function keeps on running in -// a goroutine as long as it doesn't panic due to a runtime error. -//[MGC] I believe this function shouldn't be used as we should just fail fast if this breaks -func (s *PruningIAVLStore) runWithRecovery(run func()) { - defer func() { - if r := recover(); r != nil { - s.logger.Error("Recovered from panic in PruningIAVLStore goroutine", "r", r) - // Unless it's a runtime error restart the goroutine - if _, ok := r.(runtime.Error); !ok { - time.Sleep(30 * time.Second) - s.logger.Info("Restarting PruningIAVLStore goroutine...\n") - go s.runWithRecovery(run) - } - } - }() - run() -} - // loopWithInterval will execute the step function in an endless loop, sleeping for the specified // interval at the end of each loop iteration. func (s *PruningIAVLStore) loopWithInterval(step func() error, interval time.Duration) { @@ -261,11 +244,3 @@ func (s *PruningIAVLStore) loopWithInterval(step func() error, interval time.Dur time.Sleep(interval) } } - -type pruningIAVLStoreSnapshot struct { - *PruningIAVLStore -} - -func (s *pruningIAVLStoreSnapshot) Release() { - // noop -} diff --git a/store/splitstore.go b/store/splitstore.go new file mode 100644 index 0000000000..b4c4d9cd7b --- /dev/null +++ b/store/splitstore.go @@ -0,0 +1,101 @@ +package store + +import ( + "bytes" + "sort" + + "github.com/loomnetwork/go-loom/plugin" +) + +type splitStore struct { + KVReader + VersionedKVStore + deleted map[string]bool + version int64 +} + +func NewSplitStore(history KVReader, version int64) VersionedKVStore { + return &splitStore{ + KVReader: history, + VersionedKVStore: NewMemStore(), + deleted: make(map[string]bool), + version: version, + } +} + +func (ss *splitStore) Get(key []byte) []byte { + if ss.VersionedKVStore.Has(key) { + return ss.VersionedKVStore.Get(key) + } + if ss.deleted[string(key)] { + return nil + } + return ss.KVReader.Get(key) +} + +func (ss *splitStore) Range(prefix []byte) plugin.RangeData { + readerRange := ss.KVReader.Range(prefix) + updateRange := ss.VersionedKVStore.Range(prefix) + for _, re := range readerRange { + if !ss.VersionedKVStore.Has(re.Key) && !ss.deleted[string(re.Key)] { + updateRange = append(updateRange, re) + } + } + + // VersionedKVStore comes from a MemStore, hence updateRange is not deterministic + sort.Slice(updateRange, func(i, j int) bool { + return bytes.Compare(updateRange[i].Key, updateRange[j].Key) < 0 + }) + return updateRange +} + +func (ss *splitStore) Has(key []byte) bool { + if ss.VersionedKVStore.Has(key) { + return true + } + if ss.deleted[string(key)] { + return false + } + return ss.KVReader.Has(key) +} + +func (ss *splitStore) Set(key, value []byte) { + ss.VersionedKVStore.Set(key, value) + ss.deleted[string(key)] = false +} + +func (ss *splitStore) Delete(key []byte) { + ss.VersionedKVStore.Delete(key) + ss.deleted[string(key)] = true +} + +func (ss *splitStore) Hash() []byte { + return []byte{} +} + +func (ss *splitStore) Version() int64 { + return ss.version +} + +func (ss *splitStore) SaveVersion() ([]byte, int64, error) { + ss.version++ + return ss.Hash(), ss.version, nil +} + +func (ss *splitStore) Prune() error { + return nil +} + +func (ss *splitStore) GetSnapshot() Snapshot { + return &splitStoreSnapShot{*ss} +} + +func (ss *splitStore) GetSnapshotAt(version int64) (Snapshot, error) { + panic("should not be called") +} + +type splitStoreSnapShot struct { + splitStore +} + +func (ss *splitStoreSnapShot) Release() {} diff --git a/store/store.go b/store/store.go index f2aaae4320..85e0c7fe72 100644 --- a/store/store.go +++ b/store/store.go @@ -54,6 +54,7 @@ type VersionedKVStore interface { // Delete old version of the store Prune() error GetSnapshot() Snapshot + GetSnapshotAt(version int64) (Snapshot, error) } type cacheItem struct { diff --git a/store/store_test.go b/store/store_test.go index a2c3841ef5..879ed969d8 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -255,7 +255,7 @@ func (ts *StoreTestSuite) VerifyRange(s KVReader, prefixes [][]byte, entries []* } actual := s.Range(prefixes[0]) require.Len(actual, len(expected), ts.StoreName) - if ts.StoreName != "MemStore" { + if ts.StoreName != "MemStore" && ts.StoreName != "SplitStore" { for i := range expected { require.EqualValues(expected[i], actual[i], ts.StoreName) } @@ -279,7 +279,7 @@ func (ts *StoreTestSuite) VerifyRange(s KVReader, prefixes [][]byte, entries []* require.Len(actual, len(expected), ts.StoreName) // TODO: MemStore keys should be iterated in ascending order - if ts.StoreName != "MemStore" { + if ts.StoreName != "MemStore" && ts.StoreName != "SplitStore" { for i := range expected { require.EqualValues(expected[i], actual[i], ts.StoreName) } @@ -297,7 +297,7 @@ func (ts *StoreTestSuite) VerifyRange(s KVReader, prefixes [][]byte, entries []* } actual = s.Range(prefixes[2]) require.Len(actual, len(expected), ts.StoreName) - if ts.StoreName != "MemStore" { + if ts.StoreName != "MemStore" && ts.StoreName != "SplitStore" { for i := range expected { require.EqualValues(expected[i], actual[i], ts.StoreName) } @@ -438,6 +438,26 @@ func (ts *MemStoreTestSuite) SetupSuite() { ts.supportsSnapshots = false } +func TestSplitStoreTestSuite(t *testing.T) { + suite.Run(t, &SplitStoreTestSuite{}) +} + +type SplitStoreTestSuite struct { + KVReader + VersionedKVStore + StoreTestSuite +} + +// runs before each test in this suite +func (ts *SplitStoreTestSuite) SetupTest() { + ts.store = NewSplitStore(NewMemStore(), 1) +} + +func (ts *SplitStoreTestSuite) SetupSuite() { + ts.StoreName = "SplitStore" + ts.supportsSnapshots = false +} + // // PruningIAVLStore // diff --git a/store/versioned_cachingstore.go b/store/versioned_cachingstore.go index 8f9575efff..452964260f 100644 --- a/store/versioned_cachingstore.go +++ b/store/versioned_cachingstore.go @@ -12,7 +12,6 @@ import ( "github.com/go-kit/kit/metrics" kitprometheus "github.com/go-kit/kit/metrics/prometheus" loom "github.com/loomnetwork/go-loom" - "github.com/pkg/errors" stdprometheus "github.com/prometheus/client_golang/prometheus" ) @@ -371,11 +370,29 @@ func (c *versionedCachingStore) SaveVersion() ([]byte, int64, error) { return hash, version, err } -func (c *versionedCachingStore) GetSnapshot() Snapshot { +func (s *versionedCachingStore) GetSnapshot() Snapshot { + snapshot, err := s.GetSnapshotAt(0) + if err != nil { + panic(err) + } + return snapshot +} + +func (c *versionedCachingStore) GetSnapshotAt(version int64) (Snapshot, error) { + if version == 0 { + return newVersionedCachingStoreSnapshot( + c.VersionedKVStore.GetSnapshot(), + c.cache, c.version-1, c.logger, + ), nil + } + snapshot, err := c.VersionedKVStore.GetSnapshotAt(version) + if err != nil { + return nil, err + } return newVersionedCachingStoreSnapshot( - c.VersionedKVStore.GetSnapshot(), - c.cache, c.version-1, c.logger, - ) + snapshot, + c.cache, version, c.logger, + ), nil } // CachingStoreSnapshot is a read-only CachingStore with specified version @@ -396,14 +413,6 @@ func newVersionedCachingStoreSnapshot(snapshot Snapshot, cache *versionedBigCach } } -func (c *versionedCachingStoreSnapshot) Delete(key []byte) { - panic("[versionedCachingStoreSnapshot] Delete() not implemented") -} - -func (c *versionedCachingStoreSnapshot) Set(key, val []byte) { - panic("[versionedCachingStoreSnapshot] Set() not implemented") -} - func (c *versionedCachingStoreSnapshot) Has(key []byte) bool { var err error @@ -489,14 +498,6 @@ func (c *versionedCachingStoreSnapshot) Get(key []byte) []byte { return data } -func (c *versionedCachingStoreSnapshot) SaveVersion() ([]byte, int64, error) { - return nil, 0, errors.New("[VersionedCachingStoreSnapshot] SaveVersion() not implemented") -} - -func (c *versionedCachingStoreSnapshot) Prune() error { - return errors.New("[VersionedCachingStoreSnapshot] Prune() not implemented") -} - func (c *versionedCachingStoreSnapshot) Release() { c.Snapshot.Release() } diff --git a/store/versioned_cachingstore_test.go b/store/versioned_cachingstore_test.go index 5345b77e5d..e00095e1a5 100644 --- a/store/versioned_cachingstore_test.go +++ b/store/versioned_cachingstore_test.go @@ -59,6 +59,14 @@ func (m *MockStore) Prune() error { } func (m *MockStore) GetSnapshot() Snapshot { + snapshot, err := m.GetSnapshotAt(0) + if err != nil { + panic(err) + } + return snapshot +} + +func (m *MockStore) GetSnapshotAt(version int64) (Snapshot, error) { snapshotStore := make(map[string][]byte) for k, v := range m.storage { snapshotStore[k] = v @@ -68,7 +76,7 @@ func (m *MockStore) GetSnapshot() Snapshot { } return &mockStoreSnapshot{ MockStore: mstore, - } + }, nil } type mockStoreSnapshot struct { diff --git a/tx_handler/factory/handler_factory.go b/tx_handler/factory/handler_factory.go new file mode 100644 index 0000000000..7241bcfe23 --- /dev/null +++ b/tx_handler/factory/handler_factory.go @@ -0,0 +1,278 @@ +// +build evm + +package factory + +import ( + ethvm "github.com/ethereum/go-ethereum/core/vm" + "github.com/gogo/protobuf/proto" + "github.com/loomnetwork/go-loom/plugin/contractpb" + "github.com/pkg/errors" + + "github.com/loomnetwork/loomchain" + "github.com/loomnetwork/loomchain/auth" + "github.com/loomnetwork/loomchain/config" + "github.com/loomnetwork/loomchain/evm" + "github.com/loomnetwork/loomchain/migrations" + "github.com/loomnetwork/loomchain/plugin" + "github.com/loomnetwork/loomchain/registry/factory" + "github.com/loomnetwork/loomchain/store" + "github.com/loomnetwork/loomchain/throttle" + "github.com/loomnetwork/loomchain/tx_handler" + "github.com/loomnetwork/loomchain/vm" +) + +func NewTxHandlerFactory( + cfg config.Config, + vmManager *vm.Manager, + chainID string, + store store.VersionedKVStore, + createRegistry factory.RegistryFactoryFunc, +) loomchain.TxHandlerFactory { + return txHandleFactory{ + cfg: cfg, + vmManager: vmManager, + chainID: chainID, + store: store, + createRegistry: createRegistry, + } +} + +type txHandleFactory struct { + cfg config.Config + vmManager *vm.Manager + chainID string + store store.VersionedKVStore + createRegistry factory.RegistryFactoryFunc +} + +func (f txHandleFactory) Copy(newStore store.VersionedKVStore) loomchain.TxHandlerFactory { + return txHandleFactory{ + cfg: f.cfg, + vmManager: f.vmManager, + chainID: f.chainID, + store: newStore, + createRegistry: f.createRegistry, + } +} + +// Creates a handle with an entirely new vmManager with dummy account balance manager factory and receipt handler. +func (f txHandleFactory) TxHandlerWithTracerAndDefaultVmManager(tracer ethvm.Tracer, metrics bool) (loomchain.TxHandler, error) { + f.vmManager = createVmManager(tracer) + return f.TxHandler(metrics) +} + +func (f txHandleFactory) TxHandler(metrics bool) (loomchain.TxHandler, error) { + nonceTxHandler := auth.NewNonceHandler() + + txMiddleware, err := txMiddleWare(f.cfg, *f.vmManager, nonceTxHandler, f.chainID, f.store, metrics) + if err != nil { + return nil, err + } + postCommitMiddlewares, err := postCommitMiddleware(f.cfg, *f.vmManager, nonceTxHandler) + if err != nil { + return nil, err + } + + return loomchain.MiddlewareTxHandler( + txMiddleware, + router(f.cfg, *f.vmManager, f.createRegistry), + postCommitMiddlewares, + ), nil +} + +func createVmManager(tracer ethvm.Tracer) *vm.Manager { + managerWithTracer := vm.NewManager() + managerWithTracer.Register(vm.VMType_EVM, func(_state loomchain.State) (vm.VM, error) { + var createABM evm.AccountBalanceManagerFactoryFunc + lvm := evm.NewLoomVm(_state, nil, createABM) + return lvm.WithTracer(tracer), nil + }) + return managerWithTracer +} + +func txMiddleWare( + cfg config.Config, + vmManager vm.Manager, + nonceTxHandler *auth.NonceHandler, + chainID string, + appStore store.VersionedKVStore, + metrics bool, +) ([]loomchain.TxMiddleware, error) { + txMiddleWare := []loomchain.TxMiddleware{ + loomchain.LogTxMiddleware, + loomchain.RecoveryTxMiddleware, + } + + txMiddleWare = append(txMiddleWare, auth.NewChainConfigMiddleware( + cfg.Auth, + getContractStaticCtx("addressmapper", vmManager), + )) + + if cfg.Karma.Enabled { + txMiddleWare = append(txMiddleWare, throttle.GetKarmaMiddleWare( + cfg.Karma.Enabled, + cfg.Karma.MaxCallCount, + cfg.Karma.SessionDuration, + getContractCtx("karma", vmManager), + )) + } + + if cfg.TxLimiter.Enabled { + txMiddleWare = append(txMiddleWare, throttle.NewTxLimiterMiddleware(cfg.TxLimiter)) + } + + if cfg.ContractTxLimiter.Enabled { + udwCtxFactory := getContractCtx("user-deployer-whitelist", vmManager) + txMiddleWare = append( + txMiddleWare, throttle.NewContractTxLimiterMiddleware(cfg.ContractTxLimiter, udwCtxFactory), + ) + } + + if cfg.DeployerWhitelist.ContractEnabled { + dwMiddleware, err := throttle.NewDeployerWhitelistMiddleware(getContractCtx("deployerwhitelist", vmManager)) + if err != nil { + return nil, err + } + txMiddleWare = append(txMiddleWare, dwMiddleware) + + } + + txMiddleWare = append(txMiddleWare, nonceTxHandler.TxMiddleware(appStore)) + + if cfg.GoContractDeployerWhitelist.Enabled { + goDeployers, err := cfg.GoContractDeployerWhitelist.DeployerAddresses(chainID) + if err != nil { + return nil, errors.Wrapf(err, "getting list of users allowed go deploys") + } + txMiddleWare = append(txMiddleWare, throttle.GetGoDeployTxMiddleWare(goDeployers)) + } + + if metrics { + txMiddleWare = append(txMiddleWare, loomchain.NewInstrumentingTxMiddleware()) + } + + return txMiddleWare, nil +} + +func router( + cfg config.Config, + vmManager vm.Manager, + createRegistry factory.RegistryFactoryFunc, +) loomchain.TxHandler { + router := loomchain.NewTxRouter() + isEvmTx := func(txID uint32, _ loomchain.State, txBytes []byte, isCheckTx bool) bool { + var msg vm.MessageTx + err := proto.Unmarshal(txBytes, &msg) + if err != nil { + return false + } + + switch txID { + case 1: + var tx vm.DeployTx + err = proto.Unmarshal(msg.Data, &tx) + if err != nil { + // In case of error, let's give safest response, + // let's TxHandler down the line, handle it. + return false + } + return tx.VmType == vm.VMType_EVM + case 2: + var tx vm.CallTx + err = proto.Unmarshal(msg.Data, &tx) + if err != nil { + // In case of error, let's give safest response, + // let's TxHandler down the line, handle it. + return false + } + return tx.VmType == vm.VMType_EVM + case 3: + return false + default: + return false + } + } + + deployTxHandler := &vm.DeployTxHandler{ + Manager: &vmManager, + CreateRegistry: createRegistry, + AllowNamedEVMContracts: cfg.AllowNamedEvmContracts, + } + + callTxHandler := &vm.CallTxHandler{ + Manager: &vmManager, + } + + ethTxHandler := &tx_handler.EthTxHandler{ + Manager: &vmManager, + CreateRegistry: createRegistry, + } + + migrationTxHandler := &tx_handler.MigrationTxHandler{ + Manager: &vmManager, + CreateRegistry: createRegistry, + Migrations: map[int32]tx_handler.MigrationFunc{ + 1: migrations.DPOSv3Migration, + 2: migrations.GatewayMigration, + 3: migrations.GatewayMigration, + }, + } + + router.HandleDeliverTx(1, loomchain.GeneratePassthroughRouteHandler(deployTxHandler)) + router.HandleDeliverTx(2, loomchain.GeneratePassthroughRouteHandler(callTxHandler)) + router.HandleDeliverTx(3, loomchain.GeneratePassthroughRouteHandler(migrationTxHandler)) + router.HandleDeliverTx(4, loomchain.GeneratePassthroughRouteHandler(ethTxHandler)) + + // TODO: Write this in more elegant way + router.HandleCheckTx(1, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, deployTxHandler)) + router.HandleCheckTx(2, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, callTxHandler)) + router.HandleCheckTx(3, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, migrationTxHandler)) + router.HandleCheckTx(4, loomchain.GenerateConditionalRouteHandler(isEvmTx, loomchain.NoopTxHandler, ethTxHandler)) + + return router +} + +func postCommitMiddleware( + cfg config.Config, + vmManager vm.Manager, + nonceTxHandler *auth.NonceHandler, +) ([]loomchain.PostCommitMiddleware, error) { + postCommitMiddlewares := []loomchain.PostCommitMiddleware{ + loomchain.LogPostCommitMiddleware, + } + + if cfg.UserDeployerWhitelist.ContractEnabled { + udwContractFactory := getContractCtx("user-deployer-whitelist", vmManager) + evmDeployRecorderMiddleware, err := throttle.NewEVMDeployRecorderPostCommitMiddleware(udwContractFactory) + if err != nil { + return nil, err + } + postCommitMiddlewares = append(postCommitMiddlewares, evmDeployRecorderMiddleware) + } + + // We need to make sure nonce post commit middleware is last as + // it doesn't pass control to other middlewares after it. + postCommitMiddlewares = append(postCommitMiddlewares, nonceTxHandler.PostCommitMiddleware()) + + return postCommitMiddlewares, nil +} + +func getContractCtx(pluginName string, vmManager vm.Manager) func(_ loomchain.State) (contractpb.Context, error) { + return func(_state loomchain.State) (contractpb.Context, error) { + pvm, err := vmManager.InitVM(vm.VMType_PLUGIN, _state) + if err != nil { + return nil, err + } + return plugin.NewInternalContractContext(pluginName, pvm.(*plugin.PluginVM), false) + } +} + +func getContractStaticCtx(pluginName string, vmManager vm.Manager) func(_ loomchain.State) (contractpb.StaticContext, error) { + return func(_state loomchain.State) (contractpb.StaticContext, error) { + pvm, err := vmManager.InitVM(vm.VMType_PLUGIN, _state) + if err != nil { + return nil, err + } + return plugin.NewInternalContractContext(pluginName, pvm.(*plugin.PluginVM), true) + } +}