diff --git a/app/ante.go b/app/ante.go index 42482b076..265b50dc6 100644 --- a/app/ante.go +++ b/app/ante.go @@ -57,14 +57,14 @@ func NewAnteHandler(options HandlerOptions) (sdk.AnteHandler, error) { // must be first to ensure that injected txs bypass the remaining ante handlers, as they do not have gas. ebifrost.NewInjectedTxDecorator(), - // Check for MsgMimir early and skip fee deduction - thorchain.NewMimirBypassDecorator(options.THORChainKeeper), - ante.NewSetUpContextDecorator(), // outermost AnteDecorator. SetUpContext must be called first // replace gas meter immediately after setting up ctx thorchain.NewGasDecorator(options.THORChainKeeper), + // Check for MsgMimir and bypass remaining ante after context/gas are set + thorchain.NewMimirBypassDecorator(options.THORChainKeeper), + wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), // after setup context to enforce limits early wasmkeeper.NewCountTXDecorator(options.TXCounterStoreService), wasmkeeper.NewGasRegisterDecorator(options.WasmKeeper.GetGasRegister()), diff --git a/app/app.go b/app/app.go index 9c6be9af3..69d23a151 100644 --- a/app/app.go +++ b/app/app.go @@ -1,12 +1,12 @@ package app import ( + "os" "encoding/json" "errors" "fmt" "io" "net/http" - "os" "path/filepath" "sort" "sync" @@ -163,6 +163,7 @@ type THORChainApp struct { DenomKeeper denomkeeper.Keeper msgServiceRouter *MsgServiceRouter // router for redirecting Msg service messages WasmKeeper wasmkeeper.Keeper + wasmDir string // forking services forkingServices []forking.ForkingKVStoreService @@ -178,6 +179,9 @@ type THORChainApp struct { configurator module.Configurator queryServiceRouter *QueryServiceRouter once sync.Once + forkGRPC string + forkHeight int64 + } @@ -389,9 +393,15 @@ func NewChainApp( CacheSize: cast.ToInt(appOpts.Get("fork.cache-size")), GasCostPerFetch: cast.ToUint64(appOpts.Get("fork.gas-cost-per-fetch")), } + forking.Enabled = true + logger.Info("Forking config", "height", forkingConfig.ForkHeight, "cache_enabled", forkingConfig.CacheEnabled, "cache_size", forkingConfig.CacheSize) + app.forkGRPC = forkingGRPC + app.forkHeight = forkingConfig.ForkHeight + if forkingConfig.TrustingPeriod == 0 { + forkingConfig.TrustingPeriod = 24 * time.Hour } if forkingConfig.MaxClockDrift == 0 { @@ -491,22 +501,20 @@ func NewChainApp( app.appCodec, thorchainStoreService, app.BankKeeper, app.AccountKeeper, app.UpgradeKeeper, ) - wasmDir := filepath.Join(homePath, "data") // "wasm" subdirectory created here + wasmDir := filepath.Join(homePath, "data") + app.wasmDir = wasmDir wasmConfig, err := wasm.ReadWasmConfig(appOpts) if err != nil { panic(fmt.Sprintf("error while reading wasm config: %s", err)) } + fmt.Fprintf(os.Stderr, "[wasm-open] init wasmDir=%s homePath=%s forking=%v\n", wasmDir, homePath, forkingEnabled) wasmOpts = append(wasmOpts, - wasmkeeper.WithQueryPlugins( - &wasmkeeper.QueryPlugins{ - Grpc: wasmkeeper.AcceptListGrpcQuerier( - wasmAcceptedQueries, - app.BaseApp.GRPCQueryRouter(), - app.appCodec), - }, - ), wasmkeeper.WithGasRegister(WasmGasRegister), + wasmkeeper.WithQueryPlugins(&wasmkeeper.QueryPlugins{ + Stargate: wasmkeeper.AcceptListStargateQuerier(wasmAcceptedQueries, app.GRPCQueryRouter(), app.appCodec), + Grpc: wasmkeeper.AcceptListGrpcQuerier(wasmAcceptedQueries, app.GRPCQueryRouter(), app.appCodec), + }), ) // The last arguments can contain custom message handlers, and custom query handlers, @@ -534,6 +542,7 @@ func NewChainApp( authtypes.NewModuleAddress(thorchain.ModuleName).String(), wasmOpts..., ) + fmt.Fprintf(os.Stderr, "[wasm-open] keeper constructed with wasmDir=%s\n", wasmDir) app.DenomKeeper = denomkeeper.NewKeeper( app.appCodec, @@ -549,6 +558,8 @@ func NewChainApp( mgrs := thorchain.NewManagers(app.ThorchainKeeper, app.appCodec, thorchainStoreService, app.BankKeeper, app.AccountKeeper, app.UpgradeKeeper, app.WasmKeeper) app.msgServiceRouter.AddCustomRoute("cosmos.bank.v1beta1.Msg", thorchain.NewBankSendHandler(thorchain.NewSendHandler(mgrs))) + app.msgServiceRouter.AddCustomRoute("cosmwasm.wasm.v1.Msg", NewWasmMsgWrapper(app, &app.WasmKeeper, wasmkeeper.NewMsgServerImpl(&app.WasmKeeper))) + app.msgServiceRouter.AddCustomRoute("types.Msg", NewThorchainMsgWrapper(app, &app.WasmKeeper, thorchain.NewMsgServerImpl(mgrs))) thorchainModule := thorchain.NewAppModule(mgrs, telemetryEnabled, testApp) @@ -670,9 +681,9 @@ func NewChainApp( // Uncomment if you want to set a custom migration order here. // app.ModuleManager.SetOrderMigrations(custom order) - app.queryServiceRouter = NewQueryServiceRouter(app.BaseApp.GRPCQueryRouter()) app.queryServiceRouter.AddCustomRoute("cosmos.bank.v1beta1.Query", NewBankQueryWrapper(app.BankKeeper)) + app.queryServiceRouter.AddCustomRoute("cosmwasm.wasm.v1.Query", NewWasmQueryWrapper(app, &app.WasmKeeper, wasmkeeper.NewGrpcQuerier(app.appCodec, wasmStoreService, &app.WasmKeeper, wasmConfig.SmartQueryGasLimit))) app.configurator = module.NewConfigurator(app.appCodec, app.msgServiceRouter, app.queryServiceRouter) err = app.ModuleManager.RegisterServices(app.configurator) if err != nil { @@ -781,7 +792,7 @@ func NewChainApp( // Initialize pinned codes in wasmvm as they are not persisted there if err := app.WasmKeeper.InitializePinnedCodes(ctx); err != nil { - panic(fmt.Sprintf("failed initialize pinned codes %s", err)) + app.BaseApp.Logger().Error("failed initialize pinned codes", "err", err) } } @@ -794,10 +805,6 @@ func (app *THORChainApp) FinalizeBlock(req *abci.RequestFinalizeBlock) (*abci.Re app.once.Do(func() { ctx := app.NewUncachedContext(false, tmproto.Header{}) if _, err := app.ConsensusParamsKeeper.Params(ctx, &consensusparamtypes.QueryParamsRequest{}); err != nil { - // prevents panic: consensus key is nil: collections: not found: key 'no_key' of type github.com/cosmos/gogoproto/tendermint.types.ConsensusParams - // sdk 47: - // Migrate Tendermint consensus parameters from x/params module to a dedicated x/consensus module. - // see https://github.com/cosmos/cosmos-sdk/blob/v0.47.0/simapp/upgrades.go#L66 baseAppLegacySS := app.GetSubspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) err = baseapp.MigrateParams(sdk.UnwrapSDKContext(ctx), baseAppLegacySS, app.ConsensusParamsKeeper.ParamsStore) if err != nil { @@ -806,6 +813,14 @@ func (app *THORChainApp) FinalizeBlock(req *abci.RequestFinalizeBlock) (*abci.Re } }) + defer func() { + for _, service := range app.forkingServices { + if e := service.EndBlock(); e != nil { + app.Logger().Error("failed to end block on forking service", "error", e) + } + } + }() + return app.BaseApp.FinalizeBlock(req) } @@ -827,6 +842,7 @@ func (app *THORChainApp) PreBlocker(ctx sdk.Context, req *abci.RequestFinalizeBl return app.ModuleManager.PreBlock(ctx) } + func (a *THORChainApp) Configurator() module.Configurator { return a.configurator } diff --git a/app/msg_thorchain_wrapper.go b/app/msg_thorchain_wrapper.go new file mode 100644 index 000000000..3bad8dd95 --- /dev/null +++ b/app/msg_thorchain_wrapper.go @@ -0,0 +1,209 @@ +package app + +import ( + "context" + "fmt" + "net" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + cwtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + + thtypes "gitlab.com/thorchain/thornode/v3/x/thorchain/types" +) + +type ThorchainMsgWrapper struct { + thtypes.UnimplementedMsgServer + app *THORChainApp + keeper *wasmkeeper.Keeper + original thtypes.MsgServer +} + +func NewThorchainMsgWrapper(app *THORChainApp, k *wasmkeeper.Keeper, original thtypes.MsgServer) *ThorchainMsgWrapper { + return &ThorchainMsgWrapper{ + app: app, + keeper: k, + original: original, + } +} + +func (w *ThorchainMsgWrapper) withMaterialized(goCtx context.Context, codeID uint64) (context.Context, error) { + if codeID != 0 { + if err := w.app.materializeAndPinWasm(sdk.UnwrapSDKContext(goCtx), codeID); err != nil { + return goCtx, err + } + } + return goCtx, nil +} + +func (w *ThorchainMsgWrapper) ensureMaterializedByAddress(ctx sdk.Context, bech32Addr string) { + addr, err := sdk.AccAddressFromBech32(bech32Addr) + if err == nil { + if ci := w.keeper.GetContractInfo(ctx, addr); ci != nil { + _ = w.app.materializeAndPinWasm(ctx, ci.CodeID) + return + } + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(nil)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return + } + defer conn.Close() + wq := cwtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(ctx.Context(), md) + resp, rerr := wq.ContractInfo(qctx, &cwtypes.QueryContractInfoRequest{Address: bech32Addr}) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + resp, rerr = wq.ContractInfo(ctx.Context(), &cwtypes.QueryContractInfoRequest{Address: bech32Addr}) + } + if rerr != nil || resp == nil || resp.ContractInfo.CodeID == 0 { + return + } + _ = w.app.materializeAndPinWasm(ctx, resp.ContractInfo.CodeID) +} + + +func (w *ThorchainMsgWrapper) StoreCode(ctx context.Context, req *cwtypes.MsgStoreCode) (*cwtypes.MsgStoreCodeResponse, error) { + return w.original.StoreCode(ctx, req) +} + +func (w *ThorchainMsgWrapper) InstantiateContract(goCtx context.Context, req *cwtypes.MsgInstantiateContract) (*cwtypes.MsgInstantiateContractResponse, error) { + fmt.Printf("[thorchain-msg] InstantiateContract codeID=%d admin=%s\n", req.CodeID, req.Admin) + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.InstantiateContract(goCtx, req) +} + +func (w *ThorchainMsgWrapper) InstantiateContract2(goCtx context.Context, req *cwtypes.MsgInstantiateContract2) (*cwtypes.MsgInstantiateContract2Response, error) { + fmt.Printf("[thorchain-msg] InstantiateContract2 codeID=%d admin=%s\n", req.CodeID, req.Admin) + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.InstantiateContract2(goCtx, req) +} + +func (w *ThorchainMsgWrapper) ExecuteContract(goCtx context.Context, req *cwtypes.MsgExecuteContract) (*cwtypes.MsgExecuteContractResponse, error) { + fmt.Printf("[thorchain-msg] ExecuteContract contract=%s sender=%s\n", req.Contract, req.Sender) + ctx := sdk.UnwrapSDKContext(goCtx) + w.ensureMaterializedByAddress(ctx, req.Contract) + fmt.Printf("[thorchain-msg] ExecuteContract dispatched contract=%s\n", req.Contract) + return w.original.ExecuteContract(goCtx, req) +} + +func (w *ThorchainMsgWrapper) MigrateContract(goCtx context.Context, req *cwtypes.MsgMigrateContract) (*cwtypes.MsgMigrateContractResponse, error) { + fmt.Printf("[thorchain-msg] MigrateContract codeID=%d contract=%s\n", req.CodeID, req.Contract) + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.MigrateContract(goCtx, req) +} + +func (w *ThorchainMsgWrapper) SudoContract(ctx context.Context, req *cwtypes.MsgSudoContract) (*cwtypes.MsgSudoContractResponse, error) { + return w.original.SudoContract(ctx, req) +} + +func (w *ThorchainMsgWrapper) UpdateAdmin(ctx context.Context, req *cwtypes.MsgUpdateAdmin) (*cwtypes.MsgUpdateAdminResponse, error) { + return w.original.UpdateAdmin(ctx, req) +} + +func (w *ThorchainMsgWrapper) ClearAdmin(ctx context.Context, req *cwtypes.MsgClearAdmin) (*cwtypes.MsgClearAdminResponse, error) { + return w.original.ClearAdmin(ctx, req) +} + + +func (w *ThorchainMsgWrapper) Ban(ctx context.Context, msg *thtypes.MsgBan) (*thtypes.MsgEmpty, error) { + return w.original.Ban(ctx, msg) +} +func (w *ThorchainMsgWrapper) Deposit(ctx context.Context, msg *thtypes.MsgDeposit) (*thtypes.MsgEmpty, error) { + return w.original.Deposit(ctx, msg) +} +func (w *ThorchainMsgWrapper) ErrataTx(ctx context.Context, msg *thtypes.MsgErrataTx) (*thtypes.MsgEmpty, error) { + return w.original.ErrataTx(ctx, msg) +} +func (w *ThorchainMsgWrapper) ErrataTxQuorum(ctx context.Context, msg *thtypes.MsgErrataTxQuorum) (*thtypes.MsgEmpty, error) { + return w.original.ErrataTxQuorum(ctx, msg) +} +func (w *ThorchainMsgWrapper) Mimir(ctx context.Context, msg *thtypes.MsgMimir) (*thtypes.MsgEmpty, error) { + return w.original.Mimir(ctx, msg) +} +func (w *ThorchainMsgWrapper) ModifyLimitSwap(ctx context.Context, msg *thtypes.MsgModifyLimitSwap) (*thtypes.MsgEmpty, error) { + return w.original.ModifyLimitSwap(ctx, msg) +} +func (w *ThorchainMsgWrapper) NetworkFee(ctx context.Context, msg *thtypes.MsgNetworkFee) (*thtypes.MsgEmpty, error) { + return w.original.NetworkFee(ctx, msg) +} +func (w *ThorchainMsgWrapper) NetworkFeeQuorum(ctx context.Context, msg *thtypes.MsgNetworkFeeQuorum) (*thtypes.MsgEmpty, error) { + return w.original.NetworkFeeQuorum(ctx, msg) +} +func (w *ThorchainMsgWrapper) NodePauseChain(ctx context.Context, msg *thtypes.MsgNodePauseChain) (*thtypes.MsgEmpty, error) { + return w.original.NodePauseChain(ctx, msg) +} +func (w *ThorchainMsgWrapper) ObservedTxIn(ctx context.Context, msg *thtypes.MsgObservedTxIn) (*thtypes.MsgEmpty, error) { + return w.original.ObservedTxIn(ctx, msg) +} +func (w *ThorchainMsgWrapper) ObservedTxOut(ctx context.Context, msg *thtypes.MsgObservedTxOut) (*thtypes.MsgEmpty, error) { + return w.original.ObservedTxOut(ctx, msg) +} +func (w *ThorchainMsgWrapper) ObservedTxQuorum(ctx context.Context, msg *thtypes.MsgObservedTxQuorum) (*thtypes.MsgEmpty, error) { + return w.original.ObservedTxQuorum(ctx, msg) +} +func (w *ThorchainMsgWrapper) ThorSend(ctx context.Context, msg *thtypes.MsgSend) (*thtypes.MsgEmpty, error) { + return w.original.ThorSend(ctx, msg) +} +func (w *ThorchainMsgWrapper) SetIPAddress(ctx context.Context, msg *thtypes.MsgSetIPAddress) (*thtypes.MsgEmpty, error) { + return w.original.SetIPAddress(ctx, msg) +} +func (w *ThorchainMsgWrapper) SetNodeKeys(ctx context.Context, msg *thtypes.MsgSetNodeKeys) (*thtypes.MsgEmpty, error) { + return w.original.SetNodeKeys(ctx, msg) +} +func (w *ThorchainMsgWrapper) Solvency(ctx context.Context, msg *thtypes.MsgSolvency) (*thtypes.MsgEmpty, error) { + return w.original.Solvency(ctx, msg) +} +func (w *ThorchainMsgWrapper) SolvencyQuorum(ctx context.Context, msg *thtypes.MsgSolvencyQuorum) (*thtypes.MsgEmpty, error) { + return w.original.SolvencyQuorum(ctx, msg) +} +func (w *ThorchainMsgWrapper) TssKeysignFail(ctx context.Context, msg *thtypes.MsgTssKeysignFail) (*thtypes.MsgEmpty, error) { + return w.original.TssKeysignFail(ctx, msg) +} +func (w *ThorchainMsgWrapper) TssPool(ctx context.Context, msg *thtypes.MsgTssPool) (*thtypes.MsgEmpty, error) { + return w.original.TssPool(ctx, msg) +} +func (w *ThorchainMsgWrapper) SetVersion(ctx context.Context, msg *thtypes.MsgSetVersion) (*thtypes.MsgEmpty, error) { + return w.original.SetVersion(ctx, msg) +} +func (w *ThorchainMsgWrapper) ProposeUpgrade(ctx context.Context, msg *thtypes.MsgProposeUpgrade) (*thtypes.MsgEmpty, error) { + return w.original.ProposeUpgrade(ctx, msg) +} +func (w *ThorchainMsgWrapper) ApproveUpgrade(ctx context.Context, msg *thtypes.MsgApproveUpgrade) (*thtypes.MsgEmpty, error) { + return w.original.ApproveUpgrade(ctx, msg) +} +func (w *ThorchainMsgWrapper) RejectUpgrade(ctx context.Context, msg *thtypes.MsgRejectUpgrade) (*thtypes.MsgEmpty, error) { + return w.original.RejectUpgrade(ctx, msg) +} diff --git a/app/msg_wasm_wrapper.go b/app/msg_wasm_wrapper.go new file mode 100644 index 000000000..87ce9834a --- /dev/null +++ b/app/msg_wasm_wrapper.go @@ -0,0 +1,208 @@ +package app + +import ( + "context" + "crypto/tls" + "encoding/hex" + "fmt" + "net" + "os" + "path/filepath" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) +type WasmMsgWrapper struct { + wasmtypes.UnimplementedMsgServer + app *THORChainApp + original wasmtypes.MsgServer + keeper *wasmkeeper.Keeper +} + +func NewWasmMsgWrapper(app *THORChainApp, k *wasmkeeper.Keeper, original wasmtypes.MsgServer) *WasmMsgWrapper { + return &WasmMsgWrapper{ + app: app, + original: original, + keeper: k, + } +} + +func (w *WasmMsgWrapper) withMaterialized(goCtx context.Context, codeID uint64) (context.Context, error) { + if codeID != 0 { + if err := w.app.materializeAndPinWasm(sdk.UnwrapSDKContext(goCtx), codeID); err != nil { + return goCtx, err + } + } + return goCtx, nil +} + +func (w *WasmMsgWrapper) ensureMaterializedByAddress(ctx sdk.Context, bech32Addr string) { + addr := sdk.MustAccAddressFromBech32(bech32Addr) + if ci := w.keeper.GetContractInfo(ctx, addr); ci != nil { + _ = w.app.materializeAndPinWasm(ctx, ci.CodeID) + return + } + if ctx.IsCheckTx() { + return + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, err := grpc.Dial(normalized, dialOpt) + if err != nil { + return + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(ctx.Context(), md) + resp, rerr := wq.ContractInfo(qctx, &wasmtypes.QueryContractInfoRequest{Address: bech32Addr}) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + resp, rerr = wq.ContractInfo(ctx.Context(), &wasmtypes.QueryContractInfoRequest{Address: bech32Addr}) + } + if rerr != nil || resp == nil || resp.ContractInfo.CodeID == 0 { + return + } + _ = w.app.materializeAndPinWasm(ctx, resp.ContractInfo.CodeID) +} + +func (w *WasmMsgWrapper) ExecuteContract(goCtx context.Context, req *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) { + fmt.Printf("[wasm-exec] ExecuteContract addr=%s\n", req.Contract) + ctx := sdk.UnwrapSDKContext(goCtx) + w.ensureMaterializedByAddress(ctx, req.Contract) + + addr := sdk.MustAccAddressFromBech32(req.Contract) + if ci := w.keeper.GetContractInfo(ctx, addr); ci != nil { + codeID := ci.CodeID + if cinfo := w.keeper.GetCodeInfo(ctx, codeID); cinfo != nil && len(cinfo.CodeHash) > 0 { + hashHex := strings.ToLower(hex.EncodeToString(cinfo.CodeHash)) + base := w.app.wasmDir + fmt.Fprintf(os.Stderr, "[wasm-open] canonical=%s\n", filepath.Join(base, "wasm", "state", "wasm", hashHex+".wasm")) + parent := filepath.Dir(base) + candidates := []string{ + filepath.Join(base, "wasm", "wasm", hashHex+".wasm"), + filepath.Join(base, "wasm", hashHex+".wasm"), + filepath.Join(parent, "wasm", "wasm", hashHex+".wasm"), + filepath.Join(parent, "wasm", hashHex+".wasm"), + filepath.Join(base, "wasm", "wasm", hashHex), + filepath.Join(base, "wasm", hashHex), + filepath.Join(parent, "wasm", "wasm", hashHex), + filepath.Join(parent, "wasm", hashHex), + } + fmt.Printf("[wasm-exec] wasmDir=%s codeID=%d codeHash=%s\n", base, codeID, hashHex) + for _, p := range candidates { + if st, err := os.Stat(p); err == nil && !st.IsDir() { + fmt.Printf("[wasm-exec] exists: %s size=%d\n", p, st.Size()) + } else { + fmt.Printf("[wasm-exec] missing: %s err=%v\n", p, err) + } + } + } else { + fmt.Printf("[wasm-exec] no CodeInfo or CodeHash for codeID=%d\n", codeID) + } + } else { + fmt.Printf("[wasm-exec] no ContractInfo for %s\n", req.Contract) + } + + fmt.Printf("[wasm-exec] ExecuteContract dispatched addr=%s\n", req.Contract) + return w.original.ExecuteContract(goCtx, req) +} + +func (w *WasmMsgWrapper) InstantiateContract(goCtx context.Context, req *wasmtypes.MsgInstantiateContract) (*wasmtypes.MsgInstantiateContractResponse, error) { + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.InstantiateContract(goCtx, req) +} + +func (w *WasmMsgWrapper) InstantiateContract2(goCtx context.Context, req *wasmtypes.MsgInstantiateContract2) (*wasmtypes.MsgInstantiateContract2Response, error) { + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.InstantiateContract2(goCtx, req) +} + +func (w *WasmMsgWrapper) MigrateContract(goCtx context.Context, req *wasmtypes.MsgMigrateContract) (*wasmtypes.MsgMigrateContractResponse, error) { + if _, err := w.withMaterialized(goCtx, req.CodeID); err != nil { + } + return w.original.MigrateContract(goCtx, req) +} + +func (w *WasmMsgWrapper) UpdateAdmin(ctx context.Context, req *wasmtypes.MsgUpdateAdmin) (*wasmtypes.MsgUpdateAdminResponse, error) { + return w.original.UpdateAdmin(ctx, req) +} +func (w *WasmMsgWrapper) ClearAdmin(ctx context.Context, req *wasmtypes.MsgClearAdmin) (*wasmtypes.MsgClearAdminResponse, error) { + return w.original.ClearAdmin(ctx, req) +} +func (w *WasmMsgWrapper) UpdateContractLabel(ctx context.Context, req *wasmtypes.MsgUpdateContractLabel) (*wasmtypes.MsgUpdateContractLabelResponse, error) { + return w.original.UpdateContractLabel(ctx, req) +} +func (w *WasmMsgWrapper) StoreCode(ctx context.Context, req *wasmtypes.MsgStoreCode) (*wasmtypes.MsgStoreCodeResponse, error) { + return w.original.StoreCode(ctx, req) +} +func (w *WasmMsgWrapper) RemoveCodeUploadParamsAddresses(ctx context.Context, req *wasmtypes.MsgRemoveCodeUploadParamsAddresses) (*wasmtypes.MsgRemoveCodeUploadParamsAddressesResponse, error) { + return w.original.RemoveCodeUploadParamsAddresses(ctx, req) +} +func (w *WasmMsgWrapper) AddCodeUploadParamsAddresses(ctx context.Context, req *wasmtypes.MsgAddCodeUploadParamsAddresses) (*wasmtypes.MsgAddCodeUploadParamsAddressesResponse, error) { + return w.original.AddCodeUploadParamsAddresses(ctx, req) +} +func (w *WasmMsgWrapper) UpdateInstantiateConfig(ctx context.Context, req *wasmtypes.MsgUpdateInstantiateConfig) (*wasmtypes.MsgUpdateInstantiateConfigResponse, error) { + return w.original.UpdateInstantiateConfig(ctx, req) +} +func (w *WasmMsgWrapper) UpdateParams(ctx context.Context, req *wasmtypes.MsgUpdateParams) (*wasmtypes.MsgUpdateParamsResponse, error) { + return w.original.UpdateParams(ctx, req) +} +func (w *WasmMsgWrapper) SudoContract(ctx context.Context, req *wasmtypes.MsgSudoContract) (*wasmtypes.MsgSudoContractResponse, error) { + return w.original.SudoContract(ctx, req) +} +func (w *WasmMsgWrapper) PinCodes(ctx context.Context, req *wasmtypes.MsgPinCodes) (*wasmtypes.MsgPinCodesResponse, error) { + return w.original.PinCodes(ctx, req) +} +func (w *WasmMsgWrapper) UnpinCodes(ctx context.Context, req *wasmtypes.MsgUnpinCodes) (*wasmtypes.MsgUnpinCodesResponse, error) { + return w.original.UnpinCodes(ctx, req) +} +func (w *WasmMsgWrapper) StoreAndInstantiateContract(ctx context.Context, req *wasmtypes.MsgStoreAndInstantiateContract) (*wasmtypes.MsgStoreAndInstantiateContractResponse, error) { + return w.original.StoreAndInstantiateContract(ctx, req) +} +func (w *WasmMsgWrapper) StoreAndMigrateContract(ctx context.Context, req *wasmtypes.MsgStoreAndMigrateContract) (*wasmtypes.MsgStoreAndMigrateContractResponse, error) { + return w.original.StoreAndMigrateContract(ctx, req) +} + +func contractAddrFromString(s string) []byte { + addr, _ := sdk.AccAddressFromBech32(s) + return address.MustLengthPrefix(addr) +} diff --git a/app/query_wasm_wrapper.go b/app/query_wasm_wrapper.go new file mode 100644 index 000000000..4caa92617 --- /dev/null +++ b/app/query_wasm_wrapper.go @@ -0,0 +1,536 @@ +package app + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +type WasmQueryWrapper struct { + wasmtypes.UnimplementedQueryServer + app *THORChainApp + original wasmtypes.QueryServer + keeper *wasmkeeper.Keeper +} + +func NewWasmQueryWrapper(app *THORChainApp, k *wasmkeeper.Keeper, original wasmtypes.QueryServer) *WasmQueryWrapper { + return &WasmQueryWrapper{ + app: app, + original: original, + keeper: k, + } +} + +func shouldRetryWithoutHeight(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "invalid height") { + return true + } + if strings.Contains(msg, "version mismatch") { + return true + } + if strings.Contains(msg, "pruned") { + return true + } + return false +} + +func isUserAPICall(goCtx context.Context) bool { + return true +} + +func (w *WasmQueryWrapper) ensureMaterializedByAddress(ctx sdk.Context, bech32Addr string, allowRemote bool) { + addr := sdk.MustAccAddressFromBech32(bech32Addr) + if ci := w.keeper.GetContractInfo(ctx, addr); ci != nil { + _ = w.app.materializeAndPinWasm(ctx, ci.CodeID) + return + } + if !allowRemote { + return + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, err := grpc.Dial(normalized, dialOpt) + if err != nil { + return + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(ctx.Context(), md) + resp, rerr := wq.ContractInfo(qctx, &wasmtypes.QueryContractInfoRequest{Address: bech32Addr}) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + resp, rerr = wq.ContractInfo(ctx.Context(), &wasmtypes.QueryContractInfoRequest{Address: bech32Addr}) + } + if rerr != nil || resp == nil || resp.ContractInfo.CodeID == 0 { + return + } + _ = w.app.materializeAndPinWasm(ctx, resp.ContractInfo.CodeID) +} + +func (w *WasmQueryWrapper) SmartContractState(goCtx context.Context, req *wasmtypes.QuerySmartContractStateRequest) (*wasmtypes.QuerySmartContractStateResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + w.ensureMaterializedByAddress(ctx, req.Address, isUserAPICall(goCtx)) + resp, err := w.original.SmartContractState(goCtx, req) + if err == nil && resp != nil { + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.SmartContractState(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.SmartContractState(goCtx, req) + } + if rerr == nil && rem != nil { + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) RawContractState(goCtx context.Context, req *wasmtypes.QueryRawContractStateRequest) (*wasmtypes.QueryRawContractStateResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + w.ensureMaterializedByAddress(ctx, req.Address, isUserAPICall(goCtx)) + resp, err := w.original.RawContractState(goCtx, req) + if err == nil && resp != nil { + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.RawContractState(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.RawContractState(goCtx, req) + } + if rerr == nil && rem != nil { + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) Code(goCtx context.Context, req *wasmtypes.QueryCodeRequest) (*wasmtypes.QueryCodeResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + resp, err := w.original.Code(goCtx, req) + if err == nil && resp != nil && len(resp.Data) > 0 { + _ = w.app.materializeAndPinWasm(ctx, req.CodeId) + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.Code(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.Code(goCtx, req) + } + if rerr == nil && rem != nil && len(rem.Data) > 0 { + _ = w.app.materializeAndPinWasm(ctx, req.CodeId) + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) CodeInfo(goCtx context.Context, req *wasmtypes.QueryCodeRequest) (*wasmtypes.QueryCodeResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + resp, err := w.original.Code(goCtx, req) + if err == nil && resp != nil { + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.Code(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.Code(goCtx, req) + } + if rerr == nil && rem != nil { + _ = w.app.materializeAndPinWasm(ctx, req.CodeId) + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) Codes(goCtx context.Context, req *wasmtypes.QueryCodesRequest) (*wasmtypes.QueryCodesResponse, error) { + resp, err := w.original.Codes(goCtx, req) + if err == nil && resp != nil && len(resp.CodeInfos) > 0 { + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.Codes(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.Codes(goCtx, req) + } + if rerr == nil && rem != nil && len(rem.CodeInfos) > 0 { + for _, ci := range rem.CodeInfos { + _ = w.app.materializeAndPinWasm(sdk.UnwrapSDKContext(goCtx), ci.CodeID) + } + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) PinnedCodes(goCtx context.Context, req *wasmtypes.QueryPinnedCodesRequest) (*wasmtypes.QueryPinnedCodesResponse, error) { + resp, err := w.original.PinnedCodes(goCtx, req) + if err == nil && resp != nil && len(resp.CodeIDs) > 0 { + return resp, nil + } + if !isUserAPICall(goCtx) { + return resp, err + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr != nil { + return resp, err + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.PinnedCodes(qctx, req) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.PinnedCodes(goCtx, req) + } + if rerr == nil && rem != nil && len(rem.CodeIDs) > 0 { + for _, id := range rem.CodeIDs { + _ = w.app.materializeAndPinWasm(sdk.UnwrapSDKContext(goCtx), id) + } + return rem, nil + } + return resp, err +} + +func (w *WasmQueryWrapper) ContractInfo(goCtx context.Context, req *wasmtypes.QueryContractInfoRequest) (*wasmtypes.QueryContractInfoResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + addr := sdk.MustAccAddressFromBech32(req.Address) + if ci := w.keeper.GetContractInfo(ctx, addr); ci != nil { + return &wasmtypes.QueryContractInfoResponse{Address: req.Address, ContractInfo: *ci}, nil + } + if !isUserAPICall(goCtx) { + return nil, nil + } + target := w.app.forkGRPC + if strings.TrimSpace(target) == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := strings.TrimSpace(target) + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, err := grpc.Dial(normalized, dialOpt) + if err != nil { + return w.original.ContractInfo(goCtx, req) + } + defer conn.Close() + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if w.app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", w.app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(goCtx, md) + rem, rerr := wq.ContractInfo(qctx, &wasmtypes.QueryContractInfoRequest{Address: req.Address}) + if rerr != nil && shouldRetryWithoutHeight(rerr) { + rem, rerr = wq.ContractInfo(goCtx, &wasmtypes.QueryContractInfoRequest{Address: req.Address}) + } + if rerr != nil || rem == nil { + return w.original.ContractInfo(goCtx, req) + } + _ = w.app.materializeAndPinWasm(ctx, rem.ContractInfo.CodeID) + return rem, nil +} +func (w *WasmQueryWrapper) ContractHistory(ctx context.Context, req *wasmtypes.QueryContractHistoryRequest) (*wasmtypes.QueryContractHistoryResponse, error) { + return w.original.ContractHistory(ctx, req) +} +func (w *WasmQueryWrapper) ContractsByCode(ctx context.Context, req *wasmtypes.QueryContractsByCodeRequest) (*wasmtypes.QueryContractsByCodeResponse, error) { + return w.original.ContractsByCode(ctx, req) +} +func (w *WasmQueryWrapper) AllContractState(ctx context.Context, req *wasmtypes.QueryAllContractStateRequest) (*wasmtypes.QueryAllContractStateResponse, error) { +return w.original.AllContractState(ctx, req) +} diff --git a/app/wasm_materialize.go b/app/wasm_materialize.go new file mode 100644 index 000000000..08a09d291 --- /dev/null +++ b/app/wasm_materialize.go @@ -0,0 +1,198 @@ +package app + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +func (app *THORChainApp) materializeAndPinWasm(ctx sdk.Context, codeID uint64) error { + fmt.Printf("[materialize] start codeID=%d forkHeight=%d wasmDir=%s\n", codeID, app.forkHeight, app.wasmDir) + + if codeID == 0 { + return nil + } + + var codeHash []byte + var remoteCreator string + var remoteInst wasmtypes.AccessConfig + if ci := app.WasmKeeper.GetCodeInfo(ctx, codeID); ci != nil && len(ci.CodeHash) > 0 { + codeHash = ci.CodeHash + } + + bz, err := app.WasmKeeper.GetByteCode(ctx, codeID) + if err != nil || len(bz) == 0 || len(codeHash) == 0 { + target := strings.TrimSpace(app.forkGRPC) + if target == "" { + target = "grpc.thor.pfc.zone:443" + } + useTLS := false + normalized := target + if strings.HasPrefix(normalized, "grpcs://") || strings.HasPrefix(normalized, "https://") { + useTLS = true + normalized = strings.TrimPrefix(strings.TrimPrefix(normalized, "grpcs://"), "https://") + } else { + if _, p, e := net.SplitHostPort(normalized); e == nil && p == "443" { + useTLS = true + } + } + var dialOpt grpc.DialOption + if useTLS { + hostForTLS := normalized + if h, _, e := net.SplitHostPort(normalized); e == nil { + hostForTLS = h + } + tlsCfg := &tls.Config{ + ServerName: hostForTLS, + MinVersion: tls.VersionTLS12, + } + dialOpt = grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)) + } else { + dialOpt = grpc.WithTransportCredentials(insecure.NewCredentials()) + } + conn, derr := grpc.Dial(normalized, dialOpt) + if derr == nil { + wq := wasmtypes.NewQueryClient(conn) + md := metadata.New(nil) + if app.forkHeight > 0 { + md.Set("x-cosmos-block-height", fmt.Sprintf("%d", app.forkHeight)) + } + qctx := metadata.NewOutgoingContext(context.Background(), md) + resp, qerr := wq.Code(qctx, &wasmtypes.QueryCodeRequest{CodeId: codeID}) + needRetry := false + if qerr != nil { + msg := strings.ToLower(qerr.Error()) + if strings.Contains(msg, "invalid height") || strings.Contains(msg, "version mismatch") || strings.Contains(msg, "pruned") { + needRetry = true + } + } + if resp == nil || len(resp.Data) == 0 { + needRetry = true + } + if needRetry { + resp2, qerr2 := wq.Code(context.Background(), &wasmtypes.QueryCodeRequest{CodeId: codeID}) + if qerr2 == nil && resp2 != nil { + resp = resp2 + qerr = nil + } + } + if qerr == nil && resp != nil { + if len(resp.Data) > 0 && len(bz) == 0 { + bz = resp.Data + } + if len(resp.DataHash) > 0 && len(codeHash) == 0 { + codeHash = resp.DataHash + } + if resp.Creator != "" && remoteCreator == "" { + remoteCreator = resp.Creator + } + remoteInst = resp.InstantiatePermission + } + _ = conn.Close() + } + } + + if len(bz) == 0 { + fmt.Printf("[materialize] no bytecode available for codeID=%d\n", codeID) + return nil + } + + raw := bz + if len(bz) >= 2 && bz[0] == 0x1f && bz[1] == 0x8b { + gr, gerr := gzip.NewReader(bytes.NewReader(bz)) + if gerr == nil { + defer gr.Close() + if ub, rerr := io.ReadAll(gr); rerr == nil && len(ub) > 0 { + raw = ub + } + } + } + + sum := sha256.Sum256(raw) + codeHash = sum[:] + shaFilename := hex.EncodeToString(codeHash) + ".wasm" + hashFilename := shaFilename + + parentDir := filepath.Dir(app.wasmDir) + baseNameNoExt := strings.TrimSuffix(hashFilename, ".wasm") + targets := []string{ + filepath.Join(app.wasmDir, "wasm", "state", "wasm", hashFilename), + filepath.Join(app.wasmDir, "wasm", "state", "wasm", baseNameNoExt), + filepath.Join(app.wasmDir, "wasm", "state", "wasm", shaFilename), + + filepath.Join(parentDir, "wasm", "state", "wasm", hashFilename), + filepath.Join(parentDir, "wasm", "state", "wasm", baseNameNoExt), + filepath.Join(parentDir, "wasm", "state", "wasm", shaFilename), + + filepath.Join(app.wasmDir, "wasm", "wasm", hashFilename), + filepath.Join(app.wasmDir, "wasm", "wasm", baseNameNoExt), + filepath.Join(app.wasmDir, "wasm", "wasm", shaFilename), + filepath.Join(app.wasmDir, "wasm", hashFilename), + filepath.Join(app.wasmDir, "wasm", baseNameNoExt), + filepath.Join(app.wasmDir, "wasm", shaFilename), + + filepath.Join(parentDir, "wasm", "wasm", hashFilename), + filepath.Join(parentDir, "wasm", "wasm", baseNameNoExt), + filepath.Join(parentDir, "wasm", "wasm", shaFilename), + filepath.Join(parentDir, "wasm", hashFilename), + filepath.Join(parentDir, "wasm", baseNameNoExt), + filepath.Join(parentDir, "wasm", shaFilename), + } + + store := ctx.KVStore(app.GetKey(wasmtypes.StoreKey)) + codeKey := make([]byte, 1+8) + codeKey[0] = 0x01 + binary.BigEndian.PutUint64(codeKey[1:], codeID) + { + ci := wasmtypes.CodeInfo{ + CodeHash: codeHash, + Creator: remoteCreator, + InstantiateConfig: remoteInst, + } + if b, merr := app.appCodec.Marshal(&ci); merr == nil { + store.Set(codeKey, b) + } + } + + for _, p := range targets { + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + return err + } + if _, err := os.Stat(p); err != nil { + fmt.Printf("[materialize] writing wasm file: %s\n", p) + if writeErr := os.WriteFile(p, raw, 0o644); writeErr != nil { + return writeErr + } + } + } + + pk := wasmkeeper.NewGovPermissionKeeper(app.WasmKeeper) + _ = pk.UnpinCode(ctx, codeID) + fmt.Printf("[materialize] pin codeID=%d\n", codeID) + if err := pk.PinCode(ctx, codeID); err != nil { + return err + } + return nil +} + +func (app *THORChainApp) materializeAndPinWasmGO(ctx context.Context, codeID uint64) error { + return app.materializeAndPinWasm(sdk.UnwrapSDKContext(ctx), codeID) +} diff --git a/cmd/thornode/commands.go b/cmd/thornode/commands.go index 99b5749fc..64dc682f0 100644 --- a/cmd/thornode/commands.go +++ b/cmd/thornode/commands.go @@ -139,6 +139,28 @@ func initRootCmd( server.AddCommands(rootCmd, app.DefaultNodeHome, newApp, appExport, addModuleInitFlags) wasmcli.ExtendUnsafeResetAllCmd(rootCmd) + forking.AddModuleInitFlags(rootCmd) + + // Bind flags for any command so appOpts see them during app construction in CLI paths + if rootCmd.PersistentPreRunE == nil { + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { + serverCtx := server.GetServerContextFromCmd(cmd) + if serverCtx != nil { + if err := serverCtx.Viper.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("fail to bind flags: %w", err) + } + if err := serverCtx.Viper.BindPFlags(cmd.PersistentFlags()); err != nil { + return fmt.Errorf("fail to bind persistent flags: %w", err) + } + if err := serverCtx.Viper.BindPFlags(cmd.InheritedFlags()); err != nil { + return fmt.Errorf("fail to bind inherited flags: %w", err) + } + return server.SetCmdServerContext(cmd, serverCtx) + } + return nil + } + } + // add keybase, auxiliary RPC, query, genesis, and tx child commands rootCmd.AddCommand( server.StatusCommand(), @@ -233,6 +255,8 @@ func queryCommand() *cobra.Command { RunE: client.ValidateCmd, } + forking.AddModuleInitFlags(cmd) + cmd.AddCommand( rpc.QueryEventForTxCmd(), server.QueryBlockCmd(), @@ -255,6 +279,8 @@ func txCommand() *cobra.Command { RunE: client.ValidateCmd, } + forking.AddModuleInitFlags(cmd) + cmd.AddCommand( authcmd.GetSignCommand(), authcmd.GetSignBatchCommand(), diff --git a/x/thorchain/ante.go b/x/thorchain/ante.go index e6707aeda..328f8de95 100644 --- a/x/thorchain/ante.go +++ b/x/thorchain/ante.go @@ -16,6 +16,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" "gitlab.com/thorchain/thornode/v3/x/thorchain/types" ) @@ -54,6 +55,9 @@ func (mbd MimirBypassDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate if _, err := validateMimirAuth(ctx, mbd.keeper, *m); err != nil { return ctx, err } + if forking.Enabled { + ctx.Logger().Info("[forking][mimir] ante bypass", "key", m.Key, "value", m.Value) + } // Set gas meter to infinite to bypass gas checks ctx = ctx.WithGasMeter(storetypes.NewInfiniteGasMeter()) // Skip ALL remaining ante handlers - go straight to message handling diff --git a/x/thorchain/client/cli/tx.go b/x/thorchain/client/cli/tx.go index 33c4d3bad..b1448ad22 100644 --- a/x/thorchain/client/cli/tx.go +++ b/x/thorchain/client/cli/tx.go @@ -19,6 +19,7 @@ import ( openapi "gitlab.com/thorchain/thornode/v3/openapi/gen" "gitlab.com/thorchain/thornode/v3/x/thorchain/types" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) func GetTxCmd() *cobra.Command { @@ -29,6 +30,8 @@ func GetTxCmd() *cobra.Command { RunE: client.ValidateCmd, } + forking.AddModuleInitFlags(cmd) + cmd.AddCommand(GetCmdSetNodeKeys()) cmd.AddCommand(GetCmdSetVersion()) cmd.AddCommand(GetCmdProposeUpgrade()) diff --git a/x/thorchain/forking/client.go b/x/thorchain/forking/client.go index 2315fc9c6..2fcb5c23f 100644 --- a/x/thorchain/forking/client.go +++ b/x/thorchain/forking/client.go @@ -1,23 +1,31 @@ package forking import ( + "bytes" "context" "crypto/tls" "fmt" "net" "strings" + "encoding/binary" + "encoding/hex" storepb "cosmossdk.io/api/cosmos/store/v1beta1" sdkmath "cosmossdk.io/math" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protowire" "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/x/thorchain/types" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/query" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "google.golang.org/grpc/status" "google.golang.org/grpc/codes" ) @@ -25,6 +33,8 @@ import ( type remoteClient struct { grpcConn *grpc.ClientConn queryClient types.QueryClient + wasmClient wasmtypes.QueryClient + bankClient banktypes.QueryClient config RemoteConfig codec codec.Codec } @@ -78,10 +88,14 @@ func NewRemoteClient(config RemoteConfig, cdc codec.Codec) (RemoteClient, error) } client := types.NewQueryClient(conn) + wq := wasmtypes.NewQueryClient(conn) + bq := banktypes.NewQueryClient(conn) cli := &remoteClient{ grpcConn: conn, queryClient: client, + wasmClient: wq, + bankClient: bq, config: config, codec: cdc, } @@ -101,6 +115,37 @@ func isNotFoundErr(err error) bool { } return false } +func shouldRetryWithoutHeight(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "invalid height") { + return true + } + if strings.Contains(msg, "version mismatch") { + return true + } + if strings.Contains(msg, "pruned") { + return true + } + return false +} + + +func (c *remoteClient) ctxWithHeight(ctx context.Context, height int64) context.Context { + safe := height + if height > 0 { + if latest, err := c.GetLatestHeight(ctx); err == nil && latest > 0 && height > latest { + safe = latest + } + } + if safe > 0 { + md := metadata.Pairs("x-cosmos-block-height", fmt.Sprintf("%d", safe)) + return metadata.NewOutgoingContext(ctx, md) + } + return ctx +} func (c *remoteClient) GetWithProof(ctx context.Context, storeKey string, key []byte, height int64) ([]byte, error) { return c.fetchViaGRPC(ctx, storeKey, key, height) @@ -111,8 +156,189 @@ func (c *remoteClient) fetchViaGRPC(ctx context.Context, storeKey string, key [] lkey := strings.ToLower(keyStr) lstore := strings.ToLower(storeKey) + if lstore == "bank" { + return nil, nil + } + + if lstore == strings.ToLower(wasmtypes.StoreKey) { + if len(key) == 0 { + return nil, nil + } + switch key[0] { + case 0x02: // ContractInfo: 0x02 | addr (no length prefix) + b := key[1:] + addr, ok := c.parseWasmContractAddrStrict(b) + if !ok { + fmt.Printf("[forking][wasm][0x02] failed to parse addr from key=%s\n", hex.EncodeToString(key)) + return nil, nil + } + fmt.Printf("[forking][wasm][0x02] parsed addr=%s from key=%s\n", addr, hex.EncodeToString(key)) + resp, err := c.wasmClient.ContractInfo(c.ctxWithHeight(ctx, height), &wasmtypes.QueryContractInfoRequest{Address: addr}) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.ContractInfo(ctx, &wasmtypes.QueryContractInfoRequest{Address: addr}) + } + } + if err != nil { + low := strings.ToLower(err.Error()) + if isNotFoundErr(err) || strings.Contains(low, "no such contract") { + fmt.Printf("[forking][wasm][0x02] remote miss for addr=%s err=%v\n", addr, err) + return nil, nil + } + fmt.Printf("[forking][wasm][0x02] remote error for addr=%s err=%v\n", addr, err) + return nil, fmt.Errorf("wasm ContractInfo: %w", err) + } + if resp == nil { + fmt.Printf("[forking][wasm][0x02] empty response for addr=%s\n", addr) + return nil, nil + } + fmt.Printf("[forking][wasm][0x02] remote success for addr=%s code_id=%d\n", addr, resp.ContractInfo.CodeID) + return c.codec.Marshal(&resp.ContractInfo) + case 0x01: // CodeInfo: 0x01 | codeID(8 bytes, big-endian) + if codeID, ok := c.parseWasmCodeID(key[1:]); ok { + resp, err := c.wasmClient.Code(c.ctxWithHeight(ctx, height), &wasmtypes.QueryCodeRequest{CodeId: codeID}) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.Code(ctx, &wasmtypes.QueryCodeRequest{CodeId: codeID}) + } + } + if err != nil { + if isNotFoundErr(err) || strings.Contains(strings.ToLower(err.Error()), "no such code") { + return nil, nil + } + return nil, fmt.Errorf("wasm CodeInfo: %w", err) + } + if resp == nil { + return nil, nil + } + ci := wasmtypes.CodeInfo{ + CodeHash: resp.DataHash, + Creator: resp.Creator, + InstantiateConfig: resp.InstantiatePermission, + } + return c.codec.Marshal(&ci) + } + return nil, nil + case 0x03: + if addr, suffix, ok := c.parseWasmContractStoreKeyNoLen(key[1:]); ok { + if len(suffix) == 0 { + return nil, nil + } + resp, err := c.wasmClient.RawContractState(c.ctxWithHeight(ctx, height), &wasmtypes.QueryRawContractStateRequest{ + Address: addr, + QueryData: suffix, + }) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.RawContractState(ctx, &wasmtypes.QueryRawContractStateRequest{ + Address: addr, + QueryData: suffix, + }) + } + } + if err != nil { + low := strings.ToLower(err.Error()) + if isNotFoundErr(err) || strings.Contains(low, "no such contract") { + items, aerr := c.fetchAllContractState(ctx, addr, height, key[0]) + if aerr == nil && len(items) > 0 { + for _, kv := range items { + if bytes.Equal(kv.Key, key) { + return kv.Value, nil + } + } + } + return nil, nil + } + return nil, fmt.Errorf("wasm RawContractState: %w", err) + } + if resp == nil || len(resp.Data) == 0 { + items, aerr := c.fetchAllContractState(ctx, addr, height, key[0]) + if aerr == nil && len(items) > 0 { + for _, kv := range items { + if bytes.Equal(kv.Key, key) { + return kv.Value, nil + } + } + } + return nil, nil + } + return resp.Data, nil + } + if codeID, ok := c.parseWasmCodeID(key[1:]); ok { + resp, err := c.wasmClient.Code(c.ctxWithHeight(ctx, height), &wasmtypes.QueryCodeRequest{CodeId: codeID}) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.Code(ctx, &wasmtypes.QueryCodeRequest{CodeId: codeID}) + } + } + if err != nil { + if isNotFoundErr(err) || strings.Contains(strings.ToLower(err.Error()), "no such code") { + return nil, nil + } + return nil, fmt.Errorf("wasm CodeBytes: %w", err) + } + if resp == nil || len(resp.Data) == 0 { + return nil, nil + } + return resp.Data, nil + } + return nil, nil + case 0x05: // ContractStore: 0x05 | addr | key... + if addr, suffix, ok := c.parseWasmContractStoreKeyNoLen(key[1:]); ok { + if len(suffix) == 0 { + return nil, nil + } + resp, err := c.wasmClient.RawContractState(c.ctxWithHeight(ctx, height), &wasmtypes.QueryRawContractStateRequest{ + Address: addr, + QueryData: suffix, + }) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.RawContractState(ctx, &wasmtypes.QueryRawContractStateRequest{ + Address: addr, + QueryData: suffix, + }) + } + } + if err != nil { + low := strings.ToLower(err.Error()) + if isNotFoundErr(err) || strings.Contains(low, "no such contract") { + items, aerr := c.fetchAllContractState(ctx, addr, height, key[0]) + if aerr == nil && len(items) > 0 { + for _, kv := range items { + if bytes.Equal(kv.Key, key) { + return kv.Value, nil + } + } + } + return nil, nil + } + return nil, fmt.Errorf("wasm RawContractState: %w", err) + } + if resp == nil || len(resp.Data) == 0 { + items, aerr := c.fetchAllContractState(ctx, addr, height, key[0]) + if aerr == nil && len(items) > 0 { + for _, kv := range items { + if bytes.Equal(kv.Key, key) { + return kv.Value, nil + } + } + } + return nil, nil + } + return resp.Data, nil + } + return nil, nil + default: + return nil, nil + } + } + + switch { - case strings.Contains(lkey, "mimir//"): + + case strings.Contains(lkey, "mimir//") || (strings.Contains(lstore, "thorchain") && strings.Contains(lkey, "/mimir/")): + fmt.Printf("[forking][mimir] matched key=%s height=%d\n", keyStr, height) return c.fetchMimirData(ctx, keyStr, height) case strings.Contains(lkey, "ragnarok"): return c.fetchRagnarokData(ctx, height) @@ -454,16 +680,22 @@ func (c *remoteClient) fetchBalanceData(ctx context.Context, key string, height if address == "" { return nil, nil } - - req := &types.QueryBalancesRequest{ + + req := &banktypes.QueryAllBalancesRequest{ Address: address, } - - resp, err := c.queryClient.Balances(ctx, req) + resp, err := c.bankClient.AllBalances(c.ctxWithHeight(ctx, height), req) if err != nil { - return nil, fmt.Errorf("gRPC balances query failed: %w", err) + if shouldRetryWithoutHeight(err) { + resp, err = c.bankClient.AllBalances(ctx, req) + } + } + if err != nil { + return nil, fmt.Errorf("bank gRPC AllBalances failed: %w", err) + } + if resp == nil { + return nil, nil } - return c.codec.Marshal(resp) } @@ -626,25 +858,140 @@ func (c *remoteClient) GetLatestHeight(ctx context.Context) (int64, error) { return 0, fmt.Errorf("no block data available") } + func (c *remoteClient) GetRange(ctx context.Context, storeKey string, start, end []byte, height int64) ([]KeyValue, error) { if storeKey == "thorchain" { if len(start) > 0 { - if strings.HasPrefix(string(start), "pool/") { + s := string(start) + if strings.HasPrefix(s, "pool/") { + fmt.Printf("[forking][RANGE][thorchain] pools via gRPC height=%d\n", height) return c.getRangeViaPoolsGRPC(ctx, height) } - if strings.HasPrefix(string(start), "node_account/") { + if strings.HasPrefix(s, "node_account/") { + fmt.Printf("[forking][RANGE][thorchain] nodes via gRPC height=%d\n", height) return c.getRangeViaNodesGRPC(ctx, height) } + if strings.HasPrefix(s, "lp/") { + fmt.Printf("[forking][RANGE][thorchain] LPs via gRPC height=%d\n", height) + return c.getRangeViaLPsGRPC(ctx, height) + } + if strings.HasPrefix(s, "mimir/") { + fmt.Printf("[forking][RANGE][thorchain] mimir via gRPC height=%d\n", height) + return c.getRangeViaMimirGRPC(ctx, height) + } } if len(end) > 0 { - if strings.HasPrefix(string(end), "pool/") { + e := string(end) + if strings.HasPrefix(e, "pool/") { + fmt.Printf("[forking][RANGE][thorchain] pools via gRPC (end) height=%d\n", height) return c.getRangeViaPoolsGRPC(ctx, height) } - if strings.HasPrefix(string(end), "node_account/") { + if strings.HasPrefix(e, "node_account/") { + fmt.Printf("[forking][RANGE][thorchain] nodes via gRPC (end) height=%d\n", height) return c.getRangeViaNodesGRPC(ctx, height) } + if strings.HasPrefix(e, "lp/") { + fmt.Printf("[forking][RANGE][thorchain] LPs via gRPC (end) height=%d\n", height) + return c.getRangeViaLPsGRPC(ctx, height) + } + if strings.HasPrefix(e, "mimir/") { + fmt.Printf("[forking][RANGE][thorchain] mimir via gRPC (end) height=%d\n", height) + return c.getRangeViaMimirGRPC(ctx, height) + } } } + if strings.EqualFold(storeKey, wasmtypes.StoreKey) { + if len(start) >= 1 && start[0] == 0x01 && len(end) >= 1 && end[0] == 0x02 { + var out []KeyValue + var pageKey []byte + for { + resp, err := c.wasmClient.Codes(c.ctxWithHeight(ctx, height), &wasmtypes.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Key: pageKey, + Limit: 1000, + }, + }) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.Codes(ctx, &wasmtypes.QueryCodesRequest{ + Pagination: &query.PageRequest{ + Key: pageKey, + Limit: 1000, + }, + }) + } + } + if err != nil { + return nil, fmt.Errorf("wasm Codes: %w", err) + } + if resp == nil { + break + } + for _, ci := range resp.CodeInfos { + key := make([]byte, 1+8) + key[0] = 0x01 + binary.BigEndian.PutUint64(key[1:], ci.CodeID) + val, merr := c.codec.Marshal(&wasmtypes.CodeInfo{ + CodeHash: ci.DataHash, + Creator: ci.Creator, + InstantiateConfig: ci.InstantiatePermission, + }) + if merr == nil { + out = append(out, KeyValue{Key: key, Value: val}) + } + } + if resp.Pagination == nil || len(resp.Pagination.NextKey) == 0 { + break + } + pageKey = resp.Pagination.NextKey + } + return out, nil + } + + // Contract store prefix 0x03/0x05 | addr | key... + if len(start) >= 2 && (start[0] == 0x05 || start[0] == 0x03) { + if addr, _, ok := c.parseWasmContractStoreKeyNoLen(start[1:]); ok { + items, err := c.fetchAllContractState(ctx, addr, height, start[0]) + if err != nil || len(items) == 0 { + return []KeyValue{}, err + } + if len(start) > 0 || len(end) > 0 { + var filtered []KeyValue + for _, kv := range items { + if (len(start) == 0 || bytes.Compare(kv.Key, start) >= 0) && (len(end) == 0 || bytes.Compare(kv.Key, end) < 0) { + filtered = append(filtered, kv) + } + } + return filtered, nil + } + return items, nil + } + } + + if len(start) >= 1 && start[0] == 0x05 && len(end) >= 1 && end[0] == 0x06 { + var out []KeyValue + resp, err := c.wasmClient.PinnedCodes(c.ctxWithHeight(ctx, height), &wasmtypes.QueryPinnedCodesRequest{}) + if err != nil { + if shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.PinnedCodes(ctx, &wasmtypes.QueryPinnedCodesRequest{}) + } + } + if err != nil { + return nil, fmt.Errorf("wasm PinnedCodes: %w", err) + } + if resp != nil { + for _, id := range resp.CodeIDs { + key := make([]byte, 1+8) + key[0] = 0x05 + binary.BigEndian.PutUint64(key[1:], id) + out = append(out, KeyValue{Key: key, Value: []byte{1}}) + } + } + return out, nil + } + return []KeyValue{}, nil + } + switch storeKey { case "pools": @@ -655,6 +1002,40 @@ func (c *remoteClient) GetRange(ctx context.Context, storeKey string, start, end return []KeyValue{}, nil } } +func (c *remoteClient) fetchAllContractState(ctx context.Context, addr string, height int64, prefixByte byte) ([]KeyValue, error) { + pg := &query.PageRequest{Limit: 200} + var out []KeyValue + for { + req := &wasmtypes.QueryAllContractStateRequest{ + Address: addr, + Pagination: pg, + } + resp, err := c.wasmClient.AllContractState(c.ctxWithHeight(ctx, height), req) + if err != nil && shouldRetryWithoutHeight(err) { + resp, err = c.wasmClient.AllContractState(ctx, req) + } + if err != nil { + return nil, err + } + if resp == nil || len(resp.Models) == 0 { + break + } + prefix := c.makeWasmContractStorePrefix(addr) + for _, m := range resp.Models { + k := make([]byte, 0, 1+len(prefix)+len(m.Key)) + k = append(k, prefixByte) + k = append(k, prefix...) + k = append(k, m.Key...) + out = append(out, KeyValue{Key: k, Value: m.Value}) + } + if resp.Pagination == nil || resp.Pagination.NextKey == nil || len(resp.Pagination.NextKey) == 0 { + break + } + pg.Key = resp.Pagination.NextKey + } + return out, nil +} + func (c *remoteClient) getRangeViaPoolsGRPC(ctx context.Context, height int64) ([]KeyValue, error) { req := &types.QueryPoolsRequest{ @@ -662,11 +1043,13 @@ func (c *remoteClient) getRangeViaPoolsGRPC(ctx context.Context, height int64) ( } resp, err := c.queryClient.Pools(ctx, req) if err != nil { + fmt.Printf("[forking][RANGE][thorchain] gRPC pools error height=%d err=%v\n", height, err) return nil, fmt.Errorf("gRPC pools range query failed: %w", err) } var kvPairs []KeyValue for _, p := range resp.Pools { + fmt.Printf("[forking][RANGE][thorchain] pool %s status=%s\n", p.Asset, p.Status) asset, err := common.NewAsset(p.Asset) if err != nil { continue @@ -778,6 +1161,76 @@ func (c *remoteClient) getRangeViaNodesGRPC(ctx context.Context, height int64) ( return kvPairs, nil } +func (c *remoteClient) getRangeViaLPsGRPC(ctx context.Context, height int64) ([]KeyValue, error) { + reqPools := &types.QueryPoolsRequest{ + Height: fmt.Sprintf("%d", height), + } + poolsResp, err := c.queryClient.Pools(ctx, reqPools) + if err != nil { + return nil, fmt.Errorf("gRPC LPs: pools list failed: %w", err) + } + var out []KeyValue + for _, p := range poolsResp.Pools { + if p.Asset == "" { + continue + } + lpsReq := &types.QueryLiquidityProvidersRequest{ + Asset: p.Asset, + Height: fmt.Sprintf("%d", height), + } + lpsResp, err := c.queryClient.LiquidityProviders(ctx, lpsReq) + if err != nil { + continue + } + asset, aerr := common.NewAsset(p.Asset) + if aerr != nil { + continue + } + for _, it := range lpsResp.LiquidityProviders { + var runeAddr, assetAddr common.Address + if it.RuneAddress != "" { + runeAddr, _ = common.NewAddress(it.RuneAddress) + } + if it.AssetAddress != "" { + assetAddr, _ = common.NewAddress(it.AssetAddress) + } + rec := types.LiquidityProvider{ + Asset: asset, + RuneAddress: runeAddr, + AssetAddress: assetAddr, + LastAddHeight: it.LastAddHeight, + LastWithdrawHeight: it.LastWithdrawHeight, + Units: sdkmath.NewUintFromString(it.Units), + PendingRune: sdkmath.NewUintFromString(it.PendingRune), + PendingAsset: sdkmath.NewUintFromString(it.PendingAsset), + RuneDepositValue: sdkmath.NewUintFromString(it.RuneDepositValue), + AssetDepositValue: sdkmath.NewUintFromString(it.AssetDepositValue), + } + key := fmt.Sprintf("lp//%s/%s", strings.ToUpper(asset.String()), strings.ToUpper(rec.GetAddress().String())) + val, _ := c.codec.Marshal(&rec) + out = append(out, KeyValue{Key: []byte(key), Value: val}) + } + } + return out, nil +} + +func (c *remoteClient) getRangeViaMimirGRPC(ctx context.Context, height int64) ([]KeyValue, error) { + req := &types.QueryMimirValuesRequest{ + Height: fmt.Sprintf("%d", height), + } + resp, err := c.queryClient.MimirValues(ctx, req) + if err != nil || resp == nil { + return []KeyValue{}, err + } + var out []KeyValue + for _, m := range resp.Mimirs { + key := fmt.Sprintf("mimir//%s", strings.ToUpper(m.Key)) + val, _ := c.codec.Marshal(&types.ProtoInt64{Value: m.Value}) + out = append(out, KeyValue{Key: []byte(key), Value: val}) + } + return out, nil +} + func decodeStoreKVPairs(b []byte) ([]*storepb.StoreKVPair, error) { pairs := make([]*storepb.StoreKVPair, 0, 64) @@ -848,6 +1301,155 @@ func decodeStoreKVPairs(b []byte) ([]*storepb.StoreKVPair, error) { return pairs, nil } +func (c *remoteClient) parseWasmContractAddrCandidates(b []byte) []string { + var out []string + if len(b) == 0 { + return out + } + if ln, n := protowire.ConsumeVarint(b); n > 0 && int(ln) == 20 && len(b) >= n+int(ln) { + addrBz := b[n : n+int(ln)] + out = append(out, cosmos.AccAddress(addrBz).String()) + } + for i := 0; i+1+20 <= len(b); i++ { + if b[i] == 0x14 { + addrBz := b[i+1 : i+1+20] + out = append(out, cosmos.AccAddress(addrBz).String()) + } + } + for i := 0; i+20 <= len(b); i++ { + addrBz := b[i : i+20] + out = append(out, cosmos.AccAddress(addrBz).String()) + } + if len(b) == 20 { + out = append(out, cosmos.AccAddress(b).String()) + } + if len(b) > 20 { + h := b[:20] + out = append(out, cosmos.AccAddress(h).String()) + t := b[len(b)-20:] + out = append(out, cosmos.AccAddress(t).String()) + if len(b) >= 21 { + midStart := (len(b) - 20) / 2 + out = append(out, cosmos.AccAddress(b[midStart:midStart+20]).String()) + } + } + seen := make(map[string]struct{}, len(out)) + dedup := make([]string, 0, len(out)) + for _, a := range out { + if a == "" { + continue + } + if _, ok := seen[a]; ok { + continue + } + seen[a] = struct{}{} + dedup = append(dedup, a) + } + return dedup +} +func (c *remoteClient) parseWasmContractAddrStrict(b []byte) (string, bool) { + if len(b) == 0 { + return "", false + } + if ln, n := protowire.ConsumeVarint(b); n > 0 && int(ln) == 20 && len(b) >= n+int(ln) { + return cosmos.AccAddress(b[n : n+int(ln)]).String(), true + } + if len(b) == 20 { + return cosmos.AccAddress(b).String(), true + } + if len(b) == 21 && b[0] == 0x14 { + return cosmos.AccAddress(b[1:21]).String(), true + } + if len(b) == 32 { + return cosmos.AccAddress(b).String(), true + } + return "", false +} + +func (c *remoteClient) parseWasmContractAddr(b []byte) (string, bool) { + if len(b) >= 1 { + ln, n := protowire.ConsumeVarint(b) + if n > 0 && int(ln) == 20 && len(b) >= n+int(ln) { + addrBz := b[n : n+int(ln)] + return cosmos.AccAddress(addrBz).String(), true + } + } + if len(b) == 20 { + return cosmos.AccAddress(b).String(), true + } + if len(b) > 20 { + return cosmos.AccAddress(b[len(b)-20:]).String(), true + } + return "", false +} + +func (c *remoteClient) parseWasmCodeID(b []byte) (uint64, bool) { + if len(b) < 8 { + return 0, false + } + return binary.BigEndian.Uint64(b[:8]), true +} + +func (c *remoteClient) parseWasmContractStoreKey(b []byte) (string, []byte, bool) { + if len(b) >= 1 { + if ln, n := protowire.ConsumeVarint(b); n > 0 && int(ln) == 20 && len(b) >= n+int(ln) { + addrBz := b[n : n+int(ln)] + suffix := b[n+int(ln):] + return cosmos.AccAddress(addrBz).String(), suffix, true + } + } + for i := 0; i+1+20 <= len(b); i++ { + if b[i] == 0x14 { + addrBz := b[i+1 : i+1+20] + suffix := b[i+1+20:] + return cosmos.AccAddress(addrBz).String(), suffix, true + } + } + if len(b) >= 20 { + addrBz := b[:20] + suffix := b[20:] + return cosmos.AccAddress(addrBz).String(), suffix, true + } + if len(b) > 20 { + addrBz := b[len(b)-20:] + suffix := b[:len(b)-20] + return cosmos.AccAddress(addrBz).String(), suffix, true + } + return "", nil, false +} + +func (c *remoteClient) makeWasmContractStorePrefix(addr string) []byte { + acc, err := cosmos.AccAddressFromBech32(addr) + if err != nil { + return nil + } + return []byte(acc) +} +func (c *remoteClient) parseWasmContractStoreKeyNoLen(b []byte) (string, []byte, bool) { + if ln, n := protowire.ConsumeVarint(b); n > 0 && int(ln) == 20 && len(b) >= n+int(ln) { + addrBz := b[n : n+int(ln)] + return cosmos.AccAddress(addrBz).String(), b[n+int(ln):], true + } + if len(b) >= 21 && b[0] == 0x14 { + addrBz := b[1:21] + return cosmos.AccAddress(addrBz).String(), b[21:], true + } + if len(b) > 32 { + addrBz := b[:32] + if addr, ok := c.parseWasmContractAddrStrict(addrBz); ok { + return addr, b[32:], true + } + } + if len(b) > 20 { + addrBz := b[:20] + if addr, ok := c.parseWasmContractAddrStrict(addrBz); ok { + return addr, b[20:], true + } + } + return "", nil, false +} + + func (c *remoteClient) Close() error { if c.grpcConn != nil { diff --git a/x/thorchain/forking/config.go b/x/thorchain/forking/config.go new file mode 100644 index 000000000..48496e4c6 --- /dev/null +++ b/x/thorchain/forking/config.go @@ -0,0 +1,3 @@ +package forking + +var Enabled bool diff --git a/x/thorchain/forking/flags.go b/x/thorchain/forking/flags.go index d48ad59db..d07842190 100644 --- a/x/thorchain/forking/flags.go +++ b/x/thorchain/forking/flags.go @@ -21,15 +21,15 @@ const ( ) func AddModuleInitFlags(startCmd *cobra.Command) { - startCmd.Flags().String(FlagForkGRPC, "", "Remote gRPC endpoint for forking (e.g., thornode.ninerealms.com:9090)") - startCmd.Flags().String(FlagForkChainID, "", "Chain ID of the remote chain to fork from (e.g., thorchain-1)") - startCmd.Flags().Int64(FlagForkHeight, 0, "Block height to fork from (0 = latest block)") - startCmd.Flags().Int64(FlagForkTrustHeight, 0, "Trusted block height for light client verification (0 = auto-detect)") - startCmd.Flags().String(FlagForkTrustHash, "", "Trusted block hash for light client verification (empty = auto-detect)") - startCmd.Flags().Duration(FlagForkTrustingPeriod, 24*time.Hour, "Trusting period for light client verification") - startCmd.Flags().Duration(FlagForkMaxClockDrift, 10*time.Second, "Maximum allowed clock drift for header verification") - startCmd.Flags().Duration(FlagForkTimeout, 30*time.Second, "Timeout for remote gRPC calls") - startCmd.Flags().Bool(FlagForkCacheEnabled, true, "Enable caching of remote state") - startCmd.Flags().Int(FlagForkCacheSize, 10000, "Maximum number of entries in the cache") - startCmd.Flags().Uint64(FlagForkGasCostPerFetch, 1000, "Gas cost charged per remote fetch operation") + startCmd.PersistentFlags().String(FlagForkGRPC, "", "Remote gRPC endpoint for forking (e.g., thornode.ninerealms.com:9090)") + startCmd.PersistentFlags().String(FlagForkChainID, "", "Chain ID of the remote chain to fork from (e.g., thorchain-1)") + startCmd.PersistentFlags().Int64(FlagForkHeight, 0, "Block height to fork from (0 = latest block)") + startCmd.PersistentFlags().Int64(FlagForkTrustHeight, 0, "Trusted block height for light client verification (0 = auto-detect)") + startCmd.PersistentFlags().String(FlagForkTrustHash, "", "Trusted block hash for light client verification (empty = auto-detect)") + startCmd.PersistentFlags().Duration(FlagForkTrustingPeriod, 24*time.Hour, "Trusting period for light client verification") + startCmd.PersistentFlags().Duration(FlagForkMaxClockDrift, 10*time.Second, "Maximum allowed clock drift for header verification") + startCmd.PersistentFlags().Duration(FlagForkTimeout, 30*time.Second, "Timeout for remote gRPC calls") + startCmd.PersistentFlags().Bool(FlagForkCacheEnabled, true, "Enable caching of remote state") + startCmd.PersistentFlags().Int(FlagForkCacheSize, 10000, "Maximum number of entries in the cache") + startCmd.PersistentFlags().Uint64(FlagForkGasCostPerFetch, 1000, "Gas cost charged per remote fetch operation") } diff --git a/x/thorchain/forking/store.go b/x/thorchain/forking/store.go index a76bbbc8d..244bbeae7 100644 --- a/x/thorchain/forking/store.go +++ b/x/thorchain/forking/store.go @@ -45,10 +45,31 @@ func NewForkingKVStore( } func (f *forkingKVStore) shouldAllowRemoteFetch() bool { + if f.storeKey == "bank" { + return false + } + if f.service.IsGenesisMode() { return false } + if f.storeKey == "wasm" { + return true + } + if f.storeKey == "thorchain" || f.storeKey == "auth" { + return true + } + + if f.sdkCtx != nil { + if f.sdkCtx.IsCheckTx() || f.sdkCtx.IsReCheckTx() { + fmt.Printf("[forking] checking tx\n") + if f.storeKey == "acc" || f.storeKey == "auth" { + return true + } + return false + } + } + if f.remoteClient == nil { fmt.Printf("[forking] remote client is nil\n") return false @@ -59,11 +80,6 @@ func (f *forkingKVStore) shouldAllowRemoteFetch() bool { fmt.Printf("[forking] user API call detected\n") return true } - - if f.sdkCtx.IsCheckTx() || f.sdkCtx.IsReCheckTx() { - fmt.Printf("[forking] checking tx\n") - return false - } } if f.service.IsBlockProcessing() { @@ -74,6 +90,9 @@ func (f *forkingKVStore) shouldAllowRemoteFetch() bool { } func (f *forkingKVStore) Get(key []byte) ([]byte, error) { + if f.storeKey == "wasm" && len(key) > 0 && key[0] == 0x02 { + fmt.Printf("[forking][GET][wasm] ContractInfo key len=%d key=%s\n", len(key), hex.EncodeToString(key)) + } if v, err := f.parent.Get(key); err == nil && v != nil { return v, nil } @@ -81,13 +100,17 @@ func (f *forkingKVStore) Get(key []byte) ([]byte, error) { if f.config.CacheEnabled { if cached := f.cache.Get(key); cached != nil { if len(cached) == 0 { - fmt.Printf("[forking][GET] negative-cache-hit store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) + if f.storeKey == "wasm" { + } else { + fmt.Printf("[forking][GET] negative-cache-hit store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) + f.service.updateStats(false, true, 0, false) + return nil, nil + } + } else { + fmt.Printf("[forking][GET] cache-hit store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) f.service.updateStats(false, true, 0, false) - return nil, nil + return cached, nil } - fmt.Printf("[forking][GET] cache-hit store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) - f.service.updateStats(false, true, 0, false) - return cached, nil } } @@ -98,8 +121,10 @@ func (f *forkingKVStore) Get(key []byte) ([]byte, error) { height := f.service.GetPinnedHeight() fmt.Printf("[forking][GET] pinned-height=%d store=%s key=%s\n", height, f.storeKey, hex.EncodeToString(key)) if height == 0 { - fmt.Printf("[forking][GET] remote-disabled(height=0) store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) - return nil, nil + if f.storeKey != "wasm" { + fmt.Printf("[forking][GET] remote-disabled(height=0) store=%s key=%s\n", f.storeKey, hex.EncodeToString(key)) + return nil, nil + } } fmt.Printf("[forking][GET] remote-fetch store=%s key=%s height=%d\n", f.storeKey, hex.EncodeToString(key), height) @@ -116,13 +141,18 @@ func (f *forkingKVStore) Get(key []byte) ([]byte, error) { f.gasMeter.ConsumeGas(f.config.GasCostPerFetch, "forking_remote_fetch_failed") } f.service.updateStats(true, false, f.config.GasCostPerFetch, true) - return nil, err + if f.config.CacheEnabled && f.storeKey != "wasm" { + f.cache.Set(key, []byte{}) + } + return nil, nil } if v == nil { fmt.Printf("[forking][GET] remote-miss store=%s key=%s height=%d duration=%v\n", f.storeKey, hex.EncodeToString(key), height, duration) if f.config.CacheEnabled { - f.cache.Set(key, []byte{}) + if f.storeKey != "wasm" { + f.cache.Set(key, []byte{}) + } } } else { fmt.Printf("[forking][GET] remote-success store=%s key=%s height=%d duration=%v size=%d bytes\n", f.storeKey, hex.EncodeToString(key), height, duration, len(v)) @@ -211,7 +241,7 @@ func (f *forkingKVStore) fetchRemoteRange(start, end []byte, reverse bool) (stor } height := f.service.GetPinnedHeight() - if height == 0 || f.remoteClient == nil { + if (height == 0 && f.storeKey != "wasm") || f.remoteClient == nil { return &EmptyIterator{}, nil } @@ -226,6 +256,9 @@ func (f *forkingKVStore) fetchRemoteRange(start, end []byte, reverse bool) (stor if err != nil { fmt.Printf("[forking][RANGE] remote-error store=%s start=%s end=%s height=%d duration=%v err=%v\n", f.storeKey, hex.EncodeToString(start), hex.EncodeToString(end), height, duration, err) + if f.storeKey == "wasm" { + return &EmptyIterator{}, nil + } return &EmptyIterator{}, err } diff --git a/x/thorchain/handler_deposit.go b/x/thorchain/handler_deposit.go index 478fc190e..34e1b912a 100644 --- a/x/thorchain/handler_deposit.go +++ b/x/thorchain/handler_deposit.go @@ -10,6 +10,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" ) @@ -63,7 +64,7 @@ func (h DepositHandler) validateV3_0_0(ctx cosmos.Context, msg MsgDeposit) error } func (h DepositHandler) handle(ctx cosmos.Context, msg MsgDeposit) (*cosmos.Result, error) { - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgDeposit while THORChain is halted") } diff --git a/x/thorchain/handler_mimir.go b/x/thorchain/handler_mimir.go index c4c57a524..f1683e1c6 100644 --- a/x/thorchain/handler_mimir.go +++ b/x/thorchain/handler_mimir.go @@ -11,6 +11,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" ) @@ -123,6 +124,8 @@ func (h MimirHandler) handleV3_0_0(ctx cosmos.Context, msg MsgMimir) error { ctx.Logger().Error("fail to save node mimir", "error", err) return err } + ctx.Logger().Info("set_node_mimir ok", "key", msg.Key, "value", msg.Value) + nodeMimirEvent := NewEventSetNodeMimir(strings.ToUpper(msg.Key), strconv.FormatInt(msg.Value, 10), msg.Signer.String()) if err = h.mgr.EventMgr().EmitEvent(ctx, nodeMimirEvent); err != nil { ctx.Logger().Error("fail to emit set_node_mimir event", "error", err) @@ -139,6 +142,17 @@ func (h MimirHandler) handleV3_0_0(ctx cosmos.Context, msg MsgMimir) error { return nil } + // In forking mode, apply the value directly to mirror mainnet behavior locally. + if forking.Enabled { + h.mgr.Keeper().SetMimir(ctx, msg.Key, msg.Value) + ctx.Logger().Info("set_mimir ok", "key", msg.Key, "effective", msg.Value) + mimirEvent := NewEventSetMimir(strings.ToUpper(msg.Key), strconv.FormatInt(msg.Value, 10)) + if err = h.mgr.EventMgr().EmitEvent(ctx, mimirEvent); err != nil { + ctx.Logger().Error("fail to emit set_mimir event", "error", err) + } + return nil + } + nodeMimirs, err := h.mgr.Keeper().GetNodeMimirs(ctx, msg.Key) if err != nil { ctx.Logger().Error("fail to get node mimirs", "error", err) @@ -177,6 +191,8 @@ func (h MimirHandler) handleV3_0_0(ctx cosmos.Context, msg MsgMimir) error { } // Reaching this point indicates a new mimir value is to be set. h.mgr.Keeper().SetMimir(ctx, msg.Key, effectiveValue) + ctx.Logger().Info("set_mimir ok", "key", msg.Key, "effective", effectiveValue) + mimirEvent := NewEventSetMimir(strings.ToUpper(msg.Key), strconv.FormatInt(effectiveValue, 10)) if err = h.mgr.EventMgr().EmitEvent(ctx, mimirEvent); err != nil { ctx.Logger().Error("fail to emit set_mimir event", "error", err) @@ -186,6 +202,9 @@ func (h MimirHandler) handleV3_0_0(ctx cosmos.Context, msg MsgMimir) error { } func validateMimirAuth(ctx cosmos.Context, k keeper.Keeper, msg MsgMimir) (cosmos.Context, error) { + if forking.Enabled { + return ctx, nil + } return activeNodeAccountsSignerPriority(ctx, k, msg.GetSigners()) } diff --git a/x/thorchain/handler_send.go b/x/thorchain/handler_send.go index 677c51140..cf2f1bcab 100644 --- a/x/thorchain/handler_send.go +++ b/x/thorchain/handler_send.go @@ -14,6 +14,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" ) @@ -191,7 +192,7 @@ func MsgSendHandleV3_0_0(ctx cosmos.Context, mgr Manager, m sdk.Msg) (*cosmos.Re k := mgr.Keeper() - if k.IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && k.IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgSend while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_clear_admin.go b/x/thorchain/handler_wasm_clear_admin.go index 63592f319..8c2b96d1b 100644 --- a/x/thorchain/handler_wasm_clear_admin.go +++ b/x/thorchain/handler_wasm_clear_admin.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmClearAdminHandler processes incoming MsgClearAdmin messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmClearAdminHandler) validate(ctx cosmos.Context, msg wasmtypes.MsgCle func (h WasmClearAdminHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgClearAdmin) (*wasmtypes.MsgClearAdminResponse, error) { ctx.Logger().Info("receive MsgClearAdmin", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgClearAdmin while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_exec.go b/x/thorchain/handler_wasm_exec.go index 4319a1c01..ca107e35b 100644 --- a/x/thorchain/handler_wasm_exec.go +++ b/x/thorchain/handler_wasm_exec.go @@ -7,6 +7,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmExecHandler handles Exec memo calls from L1 integrations @@ -43,7 +44,7 @@ func (h WasmExecHandler) validate(ctx cosmos.Context, msg MsgWasmExec) error { func (h WasmExecHandler) handle(ctx cosmos.Context, msg MsgWasmExec) (*cosmos.Result, error) { ctx.Logger().Info("receive MsgWasmExec", "from", msg.Signer) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgWasmExec while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_execute_contract.go b/x/thorchain/handler_wasm_execute_contract.go index a43c9bd02..c375e62da 100644 --- a/x/thorchain/handler_wasm_execute_contract.go +++ b/x/thorchain/handler_wasm_execute_contract.go @@ -15,6 +15,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" ) @@ -54,7 +55,7 @@ func (h WasmExecuteContractHandler) validate(ctx cosmos.Context, msg wasmtypes.M func (h WasmExecuteContractHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) { ctx.Logger().Info("receive MsgExecuteContract", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgExecuteContract while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_instantiate_contract.go b/x/thorchain/handler_wasm_instantiate_contract.go index 1b3ebe58b..cd52ead0f 100644 --- a/x/thorchain/handler_wasm_instantiate_contract.go +++ b/x/thorchain/handler_wasm_instantiate_contract.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmInstantiateContractHandler processes incoming MsgInstantiateContract messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmInstantiateContractHandler) validate(ctx cosmos.Context, msg wasmtyp func (h WasmInstantiateContractHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgInstantiateContract) (*wasmtypes.MsgInstantiateContractResponse, error) { ctx.Logger().Info("receive MsgInstantiateContract", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgInstantiateContract while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_instantiate_contract_2.go b/x/thorchain/handler_wasm_instantiate_contract_2.go index b3be05854..9026d3dcb 100644 --- a/x/thorchain/handler_wasm_instantiate_contract_2.go +++ b/x/thorchain/handler_wasm_instantiate_contract_2.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmInstantiateContract2Handler processes incoming MsgInstantiateContract2 messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmInstantiateContract2Handler) validate(ctx cosmos.Context, msg wasmty func (h WasmInstantiateContract2Handler) handle(ctx cosmos.Context, msg wasmtypes.MsgInstantiateContract2) (*wasmtypes.MsgInstantiateContract2Response, error) { ctx.Logger().Info("receive MsgInstantiateContract2", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgInstantiateContract2 while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_migrate_contract.go b/x/thorchain/handler_wasm_migrate_contract.go index 9ea1ee0d3..9d9adb520 100644 --- a/x/thorchain/handler_wasm_migrate_contract.go +++ b/x/thorchain/handler_wasm_migrate_contract.go @@ -9,6 +9,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmMigrateContractHandler processes incoming MsgMigrateContract messages from x/wasm @@ -47,7 +48,7 @@ func (h WasmMigrateContractHandler) validate(ctx cosmos.Context, msg wasmtypes.M func (h WasmMigrateContractHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgMigrateContract) (*wasmtypes.MsgMigrateContractResponse, error) { ctx.Logger().Info("receive MsgMigrateContract", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgMigrateContract while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_store_code.go b/x/thorchain/handler_wasm_store_code.go index 468f2a0fc..3e0c84b31 100644 --- a/x/thorchain/handler_wasm_store_code.go +++ b/x/thorchain/handler_wasm_store_code.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmStoreCodeHandler processes incoming MsgStoreCode messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmStoreCodeHandler) validate(ctx cosmos.Context, msg wasmtypes.MsgStor func (h WasmStoreCodeHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgStoreCode) (*wasmtypes.MsgStoreCodeResponse, error) { ctx.Logger().Info("receive MsgStoreCode", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgStoreCode while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_sudo_contract.go b/x/thorchain/handler_wasm_sudo_contract.go index db2b0ae13..f62df5a20 100644 --- a/x/thorchain/handler_wasm_sudo_contract.go +++ b/x/thorchain/handler_wasm_sudo_contract.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmSudoContractHandler processes incoming MsgSudoContract messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmSudoContractHandler) validate(ctx cosmos.Context, msg wasmtypes.MsgS func (h WasmSudoContractHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgSudoContract) (*wasmtypes.MsgSudoContractResponse, error) { ctx.Logger().Info("receive MsgSudoContract", "from", msg.Authority) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgSudoContract while THORChain is halted") } diff --git a/x/thorchain/handler_wasm_update_admin.go b/x/thorchain/handler_wasm_update_admin.go index 87222a132..e73fb97c3 100644 --- a/x/thorchain/handler_wasm_update_admin.go +++ b/x/thorchain/handler_wasm_update_admin.go @@ -8,6 +8,7 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) // WasmUpdateAdminHandler processes incoming MsgUpdateAdmin messages from x/wasm @@ -46,7 +47,7 @@ func (h WasmUpdateAdminHandler) validate(ctx cosmos.Context, msg wasmtypes.MsgUp func (h WasmUpdateAdminHandler) handle(ctx cosmos.Context, msg wasmtypes.MsgUpdateAdmin) (*wasmtypes.MsgUpdateAdminResponse, error) { ctx.Logger().Info("receive MsgUpdateAdmin", "from", msg.Sender) - if h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { + if !forking.Enabled && h.mgr.Keeper().IsChainHalted(ctx, common.THORChain) { return nil, fmt.Errorf("unable to use MsgUpdateAdmin while THORChain is halted") } diff --git a/x/thorchain/keeper/v1/keeper_halt.go b/x/thorchain/keeper/v1/keeper_halt.go index 52da7285e..420d25f2d 100644 --- a/x/thorchain/keeper/v1/keeper_halt.go +++ b/x/thorchain/keeper/v1/keeper_halt.go @@ -6,9 +6,13 @@ import ( "gitlab.com/thorchain/thornode/v3/common" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" ) func (k KVStore) IsTradingHalt(ctx cosmos.Context, msg cosmos.Msg) bool { + if forking.Enabled { + return false + } // consider halted if ragnarok in progress for either asset or chain gas asset // gather source and target assets checkAssets := []common.Asset{} @@ -55,6 +59,9 @@ func (k KVStore) IsTradingHalt(ctx cosmos.Context, msg cosmos.Msg) bool { } func (k KVStore) IsGlobalTradingHalted(ctx cosmos.Context) bool { + if forking.Enabled { + return false + } haltTrading, err := k.GetMimir(ctx, "HaltTrading") if err == nil && ((haltTrading > 0 && haltTrading <= ctx.BlockHeight()) || k.RagnarokInProgress(ctx)) { return true @@ -63,6 +70,9 @@ func (k KVStore) IsGlobalTradingHalted(ctx cosmos.Context) bool { } func (k KVStore) IsChainTradingHalted(ctx cosmos.Context, chain common.Chain) bool { + if forking.Enabled { + return false + } mimirKey := fmt.Sprintf("Halt%sTrading", chain) haltChainTrading, err := k.GetMimir(ctx, mimirKey) if err == nil && (haltChainTrading > 0 && haltChainTrading <= ctx.BlockHeight()) { @@ -74,6 +84,9 @@ func (k KVStore) IsChainTradingHalted(ctx cosmos.Context, chain common.Chain) bo } func (k KVStore) IsChainHalted(ctx cosmos.Context, chain common.Chain) bool { + if forking.Enabled { + return false + } haltChain, err := k.GetMimir(ctx, "HaltChainGlobal") if err == nil && (haltChain > 0 && haltChain <= ctx.BlockHeight()) { ctx.Logger().Debug("global is halt") @@ -105,6 +118,9 @@ func (k KVStore) IsChainHalted(ctx cosmos.Context, chain common.Chain) bool { // TODO: This is key is named `Pause` yet behaves like a `Halt` // (halt from a height rather than pause until a height). func (k KVStore) IsLPPaused(ctx cosmos.Context, chain common.Chain) bool { + if forking.Enabled { + return false + } // check if global LP is paused pauseLPGlobal, err := k.GetMimir(ctx, "PauseLP") if err == nil && pauseLPGlobal > 0 && pauseLPGlobal <= ctx.BlockHeight() { @@ -120,6 +136,9 @@ func (k KVStore) IsLPPaused(ctx cosmos.Context, chain common.Chain) bool { } func (k KVStore) IsPoolDepositPaused(ctx cosmos.Context, asset common.Asset) bool { + if forking.Enabled { + return false + } // check if deposits into pool are paused v, err := k.GetMimirWithRef(ctx, constants.MimirTemplatePauseLPDeposit, asset.MimirString()) if err == nil && v > 0 { @@ -129,6 +148,9 @@ func (k KVStore) IsPoolDepositPaused(ctx cosmos.Context, asset common.Asset) boo } func (k KVStore) IsTCYTradingHalted(ctx cosmos.Context) bool { + if forking.Enabled { + return false + } haltTCYTrading, err := k.GetMimir(ctx, "HaltTCYTrading") if err == nil && (haltTCYTrading > 0 && haltTCYTrading < ctx.BlockHeight()) { ctx.Logger().Debug("TCY trading is halt") diff --git a/x/thorchain/keeper/v1/keeper_mimir.go b/x/thorchain/keeper/v1/keeper_mimir.go index 904180453..ef3101226 100644 --- a/x/thorchain/keeper/v1/keeper_mimir.go +++ b/x/thorchain/keeper/v1/keeper_mimir.go @@ -10,6 +10,7 @@ import ( // GetMimir get a mimir value from key value store func (k KVStore) GetMimir(ctx cosmos.Context, key string) (int64, error) { + key = strings.ToUpper(key) record := int64(-1) _, err := k.getInt64(ctx, k.GetKey(prefixMimir, key), &record) return record, err @@ -25,6 +26,7 @@ func (k KVStore) GetMimirWithRef(ctx cosmos.Context, template string, ref ...any // SetMimir save a mimir value to key value store func (k KVStore) SetMimir(ctx cosmos.Context, key string, value int64) { + key = strings.ToUpper(key) k.setInt64(ctx, k.GetKey(prefixMimir, key), value) } diff --git a/x/thorchain/manager_wasm_current.go b/x/thorchain/manager_wasm_current.go index 132257349..d1eaf42a5 100644 --- a/x/thorchain/manager_wasm_current.go +++ b/x/thorchain/manager_wasm_current.go @@ -2,12 +2,17 @@ package thorchain import ( "encoding/base32" + "encoding/hex" + "fmt" + "os" + "path/filepath" "strings" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "gitlab.com/thorchain/thornode/v3/common/cosmos" "gitlab.com/thorchain/thornode/v3/constants" + "gitlab.com/thorchain/thornode/v3/x/thorchain/forking" "gitlab.com/thorchain/thornode/v3/x/thorchain/keeper" errorsmod "cosmossdk.io/errors" @@ -171,6 +176,23 @@ func (m WasmMgrVCUR) ExecuteContract( if err := m.checkContractHalt(ctx, contractAddress); err != nil { return nil, err } + hashHex := strings.ToLower(hex.EncodeToString(codeInfo.CodeHash)) + candidates := []string{ + filepath.Join("/root/.thornode/data", "wasm", "wasm", hashHex+".wasm"), + filepath.Join("/root/.thornode/data", "wasm", "wasm", hashHex), + filepath.Join("/root/.thornode/wasm", "wasm", hashHex+".wasm"), + filepath.Join("/root/.thornode/wasm", "wasm", hashHex), + } + fmt.Printf("[wasm-exec-mgr] codeID=%d codeHash=%s\n", contractInfo.CodeID, hashHex) + fmt.Fprintf(os.Stderr, "[wasm-open] canonical=%s\n", filepath.Join("/root/.thornode/data", "wasm", "state", "wasm", hashHex+".wasm")) + for _, p := range candidates { + if st, err := os.Stat(p); err == nil && !st.IsDir() { + fmt.Printf("[wasm-exec-mgr] exists: %s size=%d\n", p, st.Size()) + } else { + fmt.Printf("[wasm-exec-mgr] missing: %s err=%v\n", p, err) + } + } + if err := m.checkChecksumHalt(ctx, codeInfo.CodeHash); err != nil { return nil, err @@ -294,6 +316,9 @@ func (m WasmMgrVCUR) ClearAdmin( } func (m WasmMgrVCUR) checkGlobalHalt(ctx cosmos.Context) error { + if forking.Enabled { + return nil + } v, err := m.keeper.GetMimir(ctx, constants.MimirKeyWasmHaltGlobal) if err != nil { return err @@ -337,6 +362,9 @@ func (m WasmMgrVCUR) permissionedKeeper() *wasmkeeper.PermissionedKeeper { } func (m WasmMgrVCUR) checkCanStore(ctx cosmos.Context, actor cosmos.AccAddress) error { + if forking.Enabled { + return nil + } err := m.checkActor(ctx, actor) if err != nil { return err @@ -354,6 +382,9 @@ func (m WasmMgrVCUR) checkCanStore(ctx cosmos.Context, actor cosmos.AccAddress) } func (m WasmMgrVCUR) checkCanInstantiate(ctx cosmos.Context, actor cosmos.AccAddress) error { + if forking.Enabled { + return nil + } err := m.checkActor(ctx, actor) if err != nil { return err diff --git a/x/thorchain/module.go b/x/thorchain/module.go index 67a569a7b..efcfe0fba 100644 --- a/x/thorchain/module.go +++ b/x/thorchain/module.go @@ -298,26 +298,21 @@ func CustomGRPCGatewayRouter(apiSvr *api.Server) { // This function will extract the height query param and set it in the metadata for the sdk to consume. runtime.WithMetadata(func(ctx context.Context, req *http.Request) metadata.MD { md := make(metadata.MD, 2) - md.Set("user-api-call", "true") - for key := range req.Header { - // if the GRPCBlockHeightHeader is set, use that and ignore the height query parameter if key == sdkgrpc.GRPCBlockHeightHeader { return md } } - // The following checked endpoint prefixes have the height query parameter extracted. if strings.HasPrefix(req.URL.Path, "/thorchain/") || strings.HasPrefix(req.URL.Path, "/cosmos/") || strings.HasPrefix(req.URL.Path, "/bank/balances/") || strings.HasPrefix(req.URL.Path, "/auth/accounts/") { - heightStr, ok := req.URL.Query()["height"] - if ok && len(heightStr) > 0 { - _, err := strconv.ParseInt(heightStr[0], 10, 64) - // if a valid int, set the GRPCBlockHeightHeader, the query server will error later on invalid height params - if err == nil { + if heightStr, ok := req.URL.Query()["height"]; ok && len(heightStr) > 0 { + if _, err := strconv.ParseInt(heightStr[0], 10, 64); err == nil { md.Set(sdkgrpc.GRPCBlockHeightHeader, heightStr...) + } else { + fmt.Printf("[gateway] invalid height param: path=%s height=%q err=%v\n", req.URL.Path, heightStr[0], err) } } }