Repository: celestiaorg/celestia-app

Issue:
ICA broken after v4/v5 upgrades
### Summary of Bug

After the v5 upgrade, we can now create transfer channels successfully, however ICA channels can not be created.

Hermes gets a panic `ica/host params are not set in store` (from `ibc-go/v8/modules/apps/27-interchain-accounts/host/keeper.Keeper.GetParams()`) on handling a `MsgChanOpenTry`.

Full stack:

```
Output: {"timestamp":"2025-08-01T10:07:21.187011Z","level":"ERROR","fields":{"message":"failed to simulate tx. propagating error to caller: gRPC call `send_tx_simulate` failed with status: status: Unknown, message: \"recovered: ica/host params are not set in store\\nstack:\\ngoroutine 94185673 [running]:\\nruntime/debug.Stack()\\n\\t/usr/local/go/src/runtime/debug/stack.go:26 +0x5e\\ngithub.com/cosmos/cosmos-sdk/baseapp.NewBaseApp.newDefaultRecoveryMiddleware.func5({0x3d7df20, 0xc0d8447170})\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/recovery.go:74 +0x25\\ngithub.com/cosmos/cosmos-sdk/baseapp.NewBaseApp.newDefaultRecoveryMiddleware.newRecoveryMiddleware.func7({0x3d7df20?, 0xc0d8447170?})\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/recovery.go:42 +0x2d\\ngithub.com/cosmos/cosmos-sdk/baseapp.processRecovery({0x3d7df20, 0xc0d8447170}, 0xc0001d8060?)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/recovery.go:31 +0x2f\\ngithub.com/cosmos/cosmos-sdk/baseapp.processRecovery({0x3d7df20, 0xc0d8447170}, 0x0?)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/recovery.go:36 +0x53\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).runTx.func1()\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/baseapp.go:849 +0x12c\\npanic({0x3d7df20?, 0xc0d8447170?})\\n\\t/usr/local/go/src/runtime/panic.go:792 +0x132\\ngithub.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/host/keeper.Keeper.GetParams({{0x4e25390, 0xc00228b4f0}, {0x4eb33d0, 0xc00228b250}, {0x4e254a8, 0xc002203ec0}, {0x4e36170, 0xc00064b030}, {0x4e5e070, 0xc00064b0a0}, ...}, ...)\\n\\t/go/pkg/mod/github.com/cosmos/ibc-go/v8@v8.7.0/modules/apps/27-interchain-accounts/host/keeper/keeper.go:268 +0x171\\ngithub.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/host.IBCModule.OnChanOpenTry({{{0x4e25390, 0xc00228b4f0}, {0x4eb33d0, 0xc00228b250}, {0x4e254a8, 0xc002203ec0}, {0x4e36170, 0xc00064b030}, {0x4e5e070, 0xc00064b0a0}, ...}}, ...)\\n\\t/go/pkg/mod/github.com/cosmos/ibc-go/v8@v8.7.0/modules/apps/27-interchain-accounts/host/ibc_module.go:63 +0xd8\\ngithub.com/cosmos/ibc-go/v8/modules/core/keeper.Keeper.ChannelOpenTry({{0x0, 0x0}, {0x4e82c50, 0xc00228b250}, {{0x4e25390, 0xc00228b4b0}, {0x4e82c50, 0xc00228b250}, {0x4e27e78, 0xc00228b710}, ...}, ...}, ...)\\n\\t/go/pkg/mod/github.com/cosmos/ibc-go/v8@v8.7.0/modules/core/keeper/msg_server.go:269 +0x8f1\\ngithub.com/cosmos/ibc-go/v8/modules/core/04-channel/types._Msg_ChannelOpenTry_Handler.func1({0x4e5e680?, 0xc0421da870?}, {0x44039c0?, 0xc130ba1d40?})\\n\\t/go/pkg/mod/github.com/cosmos/ibc-go/v8@v8.7.0/modules/core/04-channel/types/tx.pb.go:2177 +0xcb\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*MsgServiceRouter).registerMsgServiceHandler.func2.1({0x4e5e0e0, 0xc003394388}, {0xc1f23cbd90?, 0x416385?}, 0x458abe0?, 0xc00ac086d8)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/msg_service_router.go:175 +0x89\\ngithub.com/cosmos/ibc-go/v8/modules/core/04-channel/types._Msg_ChannelOpenTry_Handler({0x457d180, 0xc00201b680}, {0x4e5e0e0, 0xc003394388}, 0x4838f08, 0xc094c368e0)\\n\\t/go/pkg/mod/github.com/cosmos/ibc-go/v8@v8.7.0/modules/core/04-channel/types/tx.pb.go:2179 +0x143\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*MsgServiceRouter).registerMsgServiceHandler.func2({{0x4e5e150, 0xede8a20}, {0x4e83350, 0xc0d7fc3e40}, {{0x0, 0x0}, {0xc0026860e0, 0x7}, 0x715cf7, {0xba738ca, ...}, ...}, ...}, ...)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/msg_service_router.go:198 +0x37f\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).runMsgs(_, {{0x4e5e150, 0xede8a20}, {0x4e83350, 0xc0d7fc3e40}, {{0x0, 0x0}, {0xc0026860e0, 0x7}, 0x715cf7, ...}, ...}, ...)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/baseapp.go:1029 +0x1e6\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).runTx(0xc0034958c8, 0x2, {0xc130ba2c80, 0xc2d, 0xc80})\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/baseapp.go:967 +0x12cc\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).Simulate(...)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/test_helpers.go:28\\ngithub.com/cosmos/cosmos-sdk/x/auth/tx.txServer.Simulate({{{0x0, 0x0, 0x0}, {0x4eacaa0, 0xc002bd0d50}, 0x0, {0x0, 0x0}, {0x4eb33d0, 0xc0020cbc20}, ...}, ...}, ...)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/x/auth/tx/service.go:98 +0x12c\\ngithub.com/cosmos/cosmos-sdk/types/tx._Service_Simulate_Handler.func1({0x4e5e680?, 0xc03fe43770?}, {0x4405d80?, 0xc0bd9511c0?})\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/types/tx/service.pb.go:1443 +0xcb\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).RegisterGRPCServer.func1({0x4e5e680, 0xc05cd12450}, {0x4405d80, 0xc0bd9511c0}, 0xc10fe59c10?, 0xc002eb8030)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/grpcserver.go:67 +0x458\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).RegisterGRPCServer.func2.ChainUnaryServer.2.1({0x4e5e680?, 0xc05cd12450?}, {0x4405d80?, 0xc0bd9511c0?})\\n\\t/go/pkg/mod/github.com/grpc-ecosystem/go-grpc-middleware@v1.4.0/chain.go:48 +0x45\\ngithub.com/grpc-ecosystem/go-grpc-middleware/recovery.UnaryServerInterceptor.func1({0x4e5e680?, 0xc05cd12450?}, {0x4405d80?, 0xc0bd9511c0?}, 0x500000002d05800?, 0x7f7a8515d7f8?)\\n\\t/go/pkg/mod/github.com/grpc-ecosystem/go-grpc-middleware@v1.4.0/recovery/interceptors.go:33 +0x9e\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).RegisterGRPCServer.func2.ChainUnaryServer.2({0x4e5e680, 0xc05cd12450}, {0x4405d80, 0xc0bd9511c0}, 0xc0bd9511e0, 0x28?)\\n\\t/go/pkg/mod/github.com/grpc-ecosystem/go-grpc-middleware@v1.4.0/chain.go:53 +0x123\\ngithub.com/cosmos/cosmos-sdk/types/tx._Service_Simulate_Handler({0x43bdc40, 0xc00227a5a0}, {0x4e5e680, 0xc05cd12450}, 0xc1b8b19700, 0xc05cd126f0)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/types/tx/service.pb.go:1445 +0x143\\ngithub.com/cosmos/cosmos-sdk/baseapp.(*BaseApp).RegisterGRPCServer.func2({0x43bdc40, 0xc00227a5a0}, {0x4e5e680, 0xc05cd12450}, 0xc1b8b19700, 0xc130a925a0?)\\n\\t/go/pkg/mod/github.com/celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/grpcserver.go:81 +0x112\\ngoogle.golang.org/grpc.(*Server).processUnaryRPC(0xc002218200, {0x4e5e680, 0xc018960240}, 0xc130a925a0, 0xc003bd50e0, 0xc00293a700, 0x0)\\n\\t/go/pkg/mod/google.golang.org/grpc@v1.73.0/server.go:1405 +0x1036\\ngoogle.golang.org/grpc.(*Server).handleStream(0xc002218200, {0x4e60400, 0xc001f19520}, 0xc130a925a0)\\n\\t/go/pkg/mod/google.golang.org/grpc@v1.73.0/server.go:1815 +0xb88\\ngoogle.golang.org/grpc.(*Server).serveStreams.func2.1()\\n\\t/go/pkg/mod/google.golang.org/grpc@v1.73.0/server.go:1035 +0x7f\\ncreated by google.golang.org/grpc.(*Server).serveStreams.func2 in goroutine 94189843\\n\\t/go/pkg/mod/google.golang.org/grpc@v1.73.0/server.go:1046 +0x11d\\n: panic [celestiaorg/cosmos-sdk@v1.29.4-sdk-v0.50.14/baseapp/recovery.go:72] with gas used: '155638'\", details: [], metadata: MetadataMap { headers: {\"content-type\": \"application/grpc\", \"x-cosmos-block-height\": \"7429367\"} }"},"span":{"name":"estimate_gas"},"spans":[{"chain":"mocha-4","tracking_id":"ChannelOpenTry","name":"send_messages_and_wait_commit"},{"account.sequence":"34285","chain":"mocha-4","name":"send_tx_with_account_sequence_retry"},{"name":"estimate_gas"}],"threadId":"ThreadId(69)"}
```

These params were migrated in ibc-go v7 -> v8: https://github.com/cosmos/ibc-go/blob/v8.0.0/modules/apps/27-interchain-accounts/host/keeper/migrations.go

Fortunately, an upgrade handler calling MigrateParams() should suffice!

### Version

v5.0.1-mocha

### Steps to Reproduce

Open an ICA channel; let relayer (Hermes v1.13.1 in this case) ship MsgChanOpenTry.

Hints from issue comments:
@evan-forbes: cc @damiannolan hmm interesting

the tables appear to be migrated but apparently we're still missing something here

https://github.com/celestiaorg/celestia-app/blob/55581ae3afe4b21c838203cfaa1c59b0109eaa79/app/upgrades.go#L56-L57

thanks for identifying and your patience here @joe-bowman, we're adding e2e tests beyond our existing interchaintest tests to ensure that these regressions don't fall through the cracks again@joe-bowman: @evan-forbes - the migration I posted isn't about migrating the ParamKeyTable(), but rather the params themselves away from the ParamsKeeper and to the ICAHostKeeper module itself. This doesn't appear to have been executed on the upgrade to ibc-go v8 (needed to be explicitly called in an upgrade handler).@rootulp: After https://github.com/celestiaorg/celestia-app/pull/5426, I'm able to reproduce a similar error.

```
$ ./scripts/single-node-upgrades.sh

// while chain is on v5 this is expected because v5 didn't register the ICA module
$ celestia-appd query interchain-accounts host params
Error: rpc error: code = Unknown desc = unknown query path: unknown request

// wait until chain upgrades from v5 -> v6
$ celestia-appd query interchain-accounts host params
Error: rpc error: code = Unknown desc = ica/host params are not set in store: panic
```
@rootulp: @joe-bowman thanks for opening the super detailed issue. I have a draft of the fix ready but it is consensus breaking because it involves two things:
1. Registering the ICA module
2. Migrating the ICA module params from the x/params module to a self managed store

We recently released a v5 emergency release and Mainnet upgraded to it on Friday which caused a lot of friction for node operators and uncovered a number of other bugs (namely with the multiplexer introduced in celestia-app v4) so we're hestitant to immediately do another emergency upgrade.

We're currently evaluating how urgent it is to resolve the ICA breakage in v5. From my perspective there are two options:
1. Do another emergency upgrade with just this fix (order of ~1-2 weeks)
2. Do a regular upgrade with the remaining features planned for v6 (order of ~1-2 months)

Are you able to comment on how impactful this regression is for QuickSilver? Based on the [Quicksilver architecture](https://docs.quicksilver.zone/protocol/protocol-architecture) I assume qTIA uses ICA. Do you have an estimate for how much qTIA is impacted by the ICA regression? Or how many users are impacted?@Subfortytwo: Hi there, I am also following this bug because I am impacted on Pryzm (cTia). @madrezaz: hey @rootulp 

[Pryzm](https://app.pryzm.zone) chain also uses ICA, and cTIA, our liquid staking token for TIA is affected by this issue ([around $150K worth](https://defillama.com/protocol/tvl/pryzm)). We also have principal and yield derivatives and holders of the yield derivatives are directly affected by the issue since the staking reward has not been paid since the v4 upgrade.

appreciate if you strongly consider an emergency upgrade since it is urgent for us@rootulp: Hi @joe-bowman @madrezaz , v6 will activate on Mocha at block height https://www.mintscan.io/celestia-testnet/block/8236886 which will fix ICA. After the upgrade activates, are you able to verify that your usage of ICA works again?

Solution patch:
diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml
new file mode 100644
index 00000000..cfdd3648
--- /dev/null
+++ b/.github/workflows/dependabot-tidy.yml
@@ -0,0 +1,50 @@
+name: dependabot-tidy
+
+on:
+  pull_request:
+    branches: [main]
+    paths:
+      - '**/go.mod'
+      - '**/go.sum'
+
+permissions:
+  contents: write
+  pull-requests: write
+
+jobs:
+  tidy:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
+
+      - name: Set up Go
+        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 #v5.5.0
+        with:
+          go-version-file: "go.mod"
+
+      - name: Run go mod tidy in all modules
+        run: |
+          find . -name 'go.mod' -execdir sh -c '
+            echo "Running go mod tidy in $(pwd)"
+            go mod tidy
+          ' \;
+
+      - name: Commit and push tidy changes (only Dependabot PRs)
+        if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]'
+        run: |
+          git config user.name "github-actions"
+          git config user.email "github-actions@github.com"
+
+
+          git config --global credential.helper store
+          echo "https://oauth2:${{ secrets.DEPENDABOT_TIDY_TOKEN }}@github.com" > ~/.git-credentials
+
+          git add .
+
+          if git diff --cached --quiet; then
+            echo "No changes to commit after go mod tidy."
+            exit 0
+          fi
+
+          git commit -m "chore: run go mod tidy on all modules"
+          git push --force origin "HEAD:${{ github.head_ref }}"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9f3ad729..b24f638c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -131,6 +131,7 @@ jobs:
           - TestBlockSync
           - TestE2EFullStackPFB
           - TestCelestiaAppUpgrade
+          - TestIBC
 
     steps:
       - name: Checkout code
diff --git a/Makefile b/Makefile
index 7a3758a3..fecc6ff8 100644
--- a/Makefile
+++ b/Makefile
@@ -283,7 +283,7 @@ test-docker-e2e-upgrade:
 	@echo "--> Building celestia-appd docker image (tag $(CELESTIA_TAG))"
 	@docker build -t "ghcr.io/celestiaorg/celestia-app:$(CELESTIA_TAG)" . -f docker/multiplexer.Dockerfile
 	@echo "--> Running upgrade test"
-	cd test/docker-e2e && go test -v -run ^TestCelestiaTestSuite/TestCelestiaAppUpgrade$$ -count=1 ./...
+	cd test/docker-e2e && go test -v -run ^TestCelestiaTestSuite/TestCelestiaAppUpgrade$$ -count=1 ./... -timeout 15m
 .PHONY: test-docker-e2e-upgrade
 
 ## test-multiplexer: Run unit tests for the multiplexer package.
diff --git a/app/app.go b/app/app.go
index 86af1dc0..b057e2c1 100644
--- a/app/app.go
+++ b/app/app.go
@@ -449,7 +449,6 @@ func New(
 		panic(err)
 	}
 
-	// RegisterUpgradeHandlers is used for registering any on-chain upgrades.
 	app.RegisterUpgradeHandlers() // must be called after module manager & configurator are initialized
 
 	// Initialize the KV stores for the base modules (e.g. params). The base modules will be included in every app version.
diff --git a/app/default_overrides.go b/app/default_overrides.go
index c500d660..cc7625e2 100644
--- a/app/default_overrides.go
+++ b/app/default_overrides.go
@@ -148,7 +148,7 @@ type icaModule struct {
 // DefaultGenesis returns custom ica module genesis state.
 func (icaModule) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
 	gs := icagenesistypes.DefaultGenesis()
-	gs.HostGenesisState.Params.AllowMessages = icaAllowMessages()
+	gs.HostGenesisState.Params.AllowMessages = IcaAllowMessages()
 	gs.HostGenesisState.Params.HostEnabled = true
 	gs.ControllerGenesisState.Params.ControllerEnabled = false
 	return cdc.MustMarshalJSON(gs)
diff --git a/app/ica_host.go b/app/ica_host.go
index 8e81685a..1d1663fe 100644
--- a/app/ica_host.go
+++ b/app/ica_host.go
@@ -1,6 +1,7 @@
 package app
 
-func icaAllowMessages() []string {
+// IcaAllowMessages returns the list of messages that are allowed to be sent via ICA.
+func IcaAllowMessages() []string {
 	return []string{
 		"/ibc.applications.transfer.v1.MsgTransfer",
 		"/cosmos.bank.v1beta1.MsgSend",
diff --git a/app/upgrades.go b/app/upgrades.go
index f7c2dd95..70c31793 100644
--- a/app/upgrades.go
+++ b/app/upgrades.go
@@ -27,9 +27,9 @@ import (
 	ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported"
 )
 
+// RegisterUpgradeHandlers is used for registering any on-chain upgrades.
 func (app App) RegisterUpgradeHandlers() {
 	for _, subspace := range app.ParamsKeeper.GetSubspaces() {
-
 		var keyTable paramstypes.KeyTable
 		var set bool
 
@@ -76,7 +76,14 @@ func (app App) RegisterUpgradeHandlers() {
 
 			start := time.Now()
 			sdkCtx.Logger().Info("running upgrade handler", "upgrade-name", upgradeName, "start", start)
-			// TODO: Add any upgrade logic here
+
+			err := app.setICAHostParams(sdkCtx)
+			if err != nil {
+				sdkCtx.Logger().Error("failed to set ica/host submodule params", "error", err)
+				return nil, err
+			}
+			// TODO: add any other migrations here.
+
 			sdkCtx.Logger().Info("finished to upgrade", "upgrade-name", upgradeName, "duration-sec", time.Since(start).Seconds())
 
 			return fromVM, nil
@@ -92,3 +99,20 @@ func (app App) RegisterUpgradeHandlers() {
 		// TODO: Apply any store upgrades here.
 	}
 }
+
+// setICAHostParams sets the ICA host params to the values defined in CIP-14.
+// This is needed because the ICA host params were previously stored in x/params
+// and in ibc-go v8 they were migrated to use a self-managed store.
+//
+// NOTE: the param migrator included in ibc-go v8 does not work as expected
+// because it sets the params to the default values which do not match the
+// values defined in CIP-14.
+func (a App) setICAHostParams(ctx context.Context) error {
+	sdkCtx := sdk.UnwrapSDKContext(ctx)
+	params := icahosttypes.Params{
+		HostEnabled:   true,
+		AllowMessages: IcaAllowMessages(),
+	}
+	a.ICAHostKeeper.SetParams(sdkCtx, params)
+	return nil
+}
diff --git a/cmd/celestia-appd/cmd/migrate_config.go b/cmd/celestia-appd/cmd/migrate_config.go
new file mode 100644
index 00000000..be13e883
--- /dev/null
+++ b/cmd/celestia-appd/cmd/migrate_config.go
@@ -0,0 +1,214 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/celestiaorg/celestia-app/v6/app"
+	"github.com/celestiaorg/celestia-app/v6/pkg/appconsts"
+	"github.com/cometbft/cometbft/config"
+	"github.com/cosmos/cosmos-sdk/client/flags"
+	serverconfig "github.com/cosmos/cosmos-sdk/server/config"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+// ConfigUpdater defines a function type for applying version-specific updates
+type ConfigUpdater func(*config.Config, *serverconfig.Config) (*config.Config, *serverconfig.Config)
+
+// updateRegistry maps version strings to their corresponding update functions
+var updateRegistry = map[string]ConfigUpdater{
+	"v6": applyV6Config,
+}
+
+// updateConfigCmd returns the update-config command that updates
+// configuration files based on the target version.
+func updateConfigCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "update-config",
+		Short:   "Update configuration values to be that of a specific app version",
+		Long:    "Update configuration files (config.toml and app.toml) to be compatible with a specific app version.",
+		Example: "celestia-appd update-config --home ~/.celestia-app\ncelestia-appd update-config --version v6 --home ~/.celestia-app",
+		Args:    cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			homeDir, err := cmd.Flags().GetString(flags.FlagHome)
+			if err != nil {
+				return err
+			}
+
+			backup, err := cmd.Flags().GetBool("backup")
+			if err != nil {
+				return err
+			}
+
+			targetVersion, err := cmd.Flags().GetString("app-version")
+			if err != nil {
+				return err
+			}
+
+			return updateConfig(homeDir, targetVersion, backup)
+		},
+	}
+
+	cmd.Flags().String(flags.FlagHome, app.NodeHome, "The application home directory")
+	cmd.Flags().Bool("backup", true, "Create backups of config files before updating them")
+	cmd.Flags().String("app-version", fmt.Sprintf("v%d", appconsts.Version), "Target version for config changes")
+	return cmd
+}
+
+// updateConfig performs the actual update of configuration files.
+func updateConfig(homeDir, targetVersion string, backup bool) error {
+	fmt.Printf("updating configuration files to version %s...\n", targetVersion)
+
+	updater, exists := updateRegistry[targetVersion]
+	if !exists {
+		return fmt.Errorf("unsupported target version: %s. Supported versions: %v", targetVersion, getSupportedVersions())
+	}
+
+	configDir := filepath.Join(homeDir, "config")
+	cometConfigPath := filepath.Join(configDir, "config.toml")
+	appConfigPath := filepath.Join(configDir, "app.toml")
+
+	if backup {
+		if err := backupConfigFiles(cometConfigPath, appConfigPath); err != nil {
+			return fmt.Errorf("failed to backup config files: %w", err)
+		}
+	}
+
+	cometConfig, err := loadCometBFTConfig(cometConfigPath, homeDir)
+	if err != nil {
+		return fmt.Errorf("failed to load CometBFT config: %w", err)
+	}
+
+	serverConfig, err := loadServerConfig(appConfigPath)
+	if err != nil {
+		return fmt.Errorf("failed to load server config: %w", err)
+	}
+
+	fmt.Printf("Loaded configs successfully. Applying %s config updates...\n", targetVersion)
+
+	cometConfig, serverConfig = updater(cometConfig, serverConfig)
+
+	config.WriteConfigFile(cometConfigPath, cometConfig)
+	serverconfig.WriteConfigFile(appConfigPath, serverConfig)
+
+	fmt.Printf("Successfully updated configuration to version %s values\n", targetVersion)
+	return nil
+}
+
+// loadCometBFTConfig loads the CometBFT configuration from config.toml
+func loadCometBFTConfig(configPath, homeDir string) (*config.Config, error) {
+	cfg := app.DefaultConsensusConfig()
+	cfg.SetRoot(homeDir)
+
+	v := viper.New()
+	v.SetConfigFile(configPath)
+	v.SetConfigType("toml")
+
+	if err := v.ReadInConfig(); err != nil {
+		return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
+	}
+
+	if err := v.Unmarshal(cfg); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal config: %w", err)
+	}
+
+	return cfg, nil
+}
+
+// loadServerConfig loads the Cosmos SDK server configuration from app.toml
+func loadServerConfig(configPath string) (*serverconfig.Config, error) {
+	cfg := app.DefaultAppConfig()
+
+	v := viper.New()
+	v.SetConfigFile(configPath)
+	v.SetConfigType("toml")
+
+	if err := v.ReadInConfig(); err != nil {
+		return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
+	}
+
+	if err := v.Unmarshal(cfg); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal config: %w", err)
+	}
+
+	return cfg, nil
+}
+
+// backupConfigFiles creates timestamped backups of the configuration files
+func backupConfigFiles(cometConfigPath, appConfigPath string) error {
+	timestamp := time.Now().Format("20060102-150405")
+
+	if err := backupFile(cometConfigPath, timestamp); err != nil {
+		return fmt.Errorf("failed to backup config.toml: %w", err)
+	}
+
+	if err := backupFile(appConfigPath, timestamp); err != nil {
+		return fmt.Errorf("failed to backup app.toml: %w", err)
+	}
+
+	fmt.Printf("Created backups with timestamp: %s\n", timestamp)
+	return nil
+}
+
+// backupFile creates a timestamped backup of a single file
+func backupFile(filePath, timestamp string) error {
+	if _, err := os.Stat(filePath); os.IsNotExist(err) {
+		fmt.Printf("Config file %s does not exist, skipping backup\n", filePath)
+		return nil
+	}
+
+	dir := filepath.Dir(filePath)
+	filename := filepath.Base(filePath)
+	ext := filepath.Ext(filename)
+	name := filename[:len(filename)-len(ext)]
+
+	backupPath := filepath.Join(dir, fmt.Sprintf("%s.%s.backup.%s", name, timestamp, ext))
+
+	data, err := os.ReadFile(filePath)
+	if err != nil {
+		return fmt.Errorf("failed to read file %s: %w", filePath, err)
+	}
+
+	if err := os.WriteFile(backupPath, data, 0o644); err != nil {
+		return fmt.Errorf("failed to write backup file %s: %w", backupPath, err)
+	}
+
+	fmt.Printf("Backed up %s to %s\n", filePath, backupPath)
+	return nil
+}
+
+// getSupportedVersions returns a list of supported updatable versions
+func getSupportedVersions() []string {
+	versions := make([]string, 0, len(updateRegistry))
+	for version := range updateRegistry {
+		versions = append(versions, version)
+	}
+	return versions
+}
+
+// applyV6Configs applies configuration changes needed for v6
+func applyV6Config(cmtCfg *config.Config, appCfg *serverconfig.Config) (*config.Config, *serverconfig.Config) {
+	fmt.Println("Applying v6 updates to configs...")
+
+	defaultCfg := app.DefaultConsensusConfig()
+	cmtCfg.RPC.MaxBodyBytes = defaultCfg.RPC.MaxBodyBytes
+
+	cmtCfg.Mempool.TTLNumBlocks = defaultCfg.Mempool.TTLNumBlocks
+	cmtCfg.Mempool.TTLDuration = defaultCfg.Mempool.TTLDuration
+	cmtCfg.Mempool.MaxTxBytes = defaultCfg.Mempool.MaxTxBytes
+	cmtCfg.Mempool.MaxTxsBytes = defaultCfg.Mempool.MaxTxsBytes
+	cmtCfg.Mempool.Type = defaultCfg.Mempool.Type
+	cmtCfg.Mempool.MaxGossipDelay = defaultCfg.Mempool.MaxGossipDelay
+
+	cmtCfg.P2P.SendRate = defaultCfg.P2P.SendRate
+	cmtCfg.P2P.RecvRate = defaultCfg.P2P.RecvRate
+
+	defaultAppCfg := app.DefaultAppConfig()
+	appCfg.MinGasPrices = defaultAppCfg.MinGasPrices
+	appCfg.GRPC.MaxRecvMsgSize = defaultAppCfg.GRPC.MaxRecvMsgSize
+
+	return cmtCfg, appCfg
+}
diff --git a/cmd/celestia-appd/cmd/root.go b/cmd/celestia-appd/cmd/root.go
index c1b14565..f8d70460 100644
--- a/cmd/celestia-appd/cmd/root.go
+++ b/cmd/celestia-appd/cmd/root.go
@@ -125,6 +125,7 @@ func initRootCommand(rootCommand *cobra.Command, capp *app.App) {
 		txCommand(capp.BasicManager),
 		keys.Commands(),
 		snapshot.Cmd(NewAppServer),
+		updateConfigCmd(),
 	)
 
 	modifyRootCommand(rootCommand)
diff --git a/go.mod b/go.mod
index b1f3d736..9543992c 100644
--- a/go.mod
+++ b/go.mod
@@ -16,9 +16,9 @@ require (
 	cosmossdk.io/x/feegrant v0.1.1
 	cosmossdk.io/x/tx v0.13.8
 	cosmossdk.io/x/upgrade v0.1.4
-	github.com/aws/aws-sdk-go-v2 v1.36.5
+	github.com/aws/aws-sdk-go-v2 v1.37.2
 	github.com/aws/aws-sdk-go-v2/config v1.29.17
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.70
+	github.com/aws/aws-sdk-go-v2/credentials v1.18.3
 	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.83
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0
 	github.com/bcp-innovations/hyperlane-cosmos v1.0.1
@@ -79,19 +79,19 @@ require (
 	github.com/Workiva/go-datastructures v1.0.53 // indirect
 	github.com/aws/aws-sdk-go v1.55.5 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
-	github.com/aws/smithy-go v1.22.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect
+	github.com/aws/smithy-go v1.22.5 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
 	github.com/bgentry/speakeasy v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index b98123a0..02f019f5 100644
--- a/go.sum
+++ b/go.sum
@@ -285,44 +285,44 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX
 github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
 github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
-github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
-github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
+github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
+github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
 github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
 github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.3 h1:ptfyXmv+ooxzFwyuBth0yqABcjVIkjDL0iTYZBSbum8=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.3/go.mod h1:Q43Nci++Wohb0qUh4m54sNln0dbxJw8PvQWkrwOkGOI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 h1:nRniHAvjFJGUCl04F3WaAj7qp/rcz5Gi1OVoj5ErBkc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2/go.mod h1:eJDFKAMHHUvv4a0Zfa7bQb//wFNUXGrbFpYRCHe2kD0=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.83 h1:08otkOELsIi0toRRGMytlJhOctcN8xfKfKFR2NXz3kE=
 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.83/go.mod h1:dGsGb2wI8JDWeMAhjVPP+z+dqvYjL6k6o+EujcRNk5c=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 h1:oxmDEO14NBZJbK/M8y3brhMFEIGN4j8a6Aq8eY0sqlo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2/go.mod h1:4hH+8QCrk1uRWDPsVfsNDUup3taAjO8Dnx63au7smAU=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 h1:j7/jTOjWeJDolPwZ/J4yZ7dUsxsWZEsxNwH5O7F8eEA=
+github.com/aws/aws-sdk-go-v2/service/sso v1.27.0/go.mod h1:M0xdEPQtgpNT7kdAX4/vOAPkFj60hSQRb7TvW9B0iug=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 h1:ywQF2N4VjqX+Psw+jLjMmUL2g1RDHlvri3NxHA08MGI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
+github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
+github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
+github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
+github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/bcp-innovations/hyperlane-cosmos v1.0.1 h1:gT8OqyJ866Q6AHOlIXKxSdLjd0p8crKG9XXERIWoh4c=
 github.com/bcp-innovations/hyperlane-cosmos v1.0.1/go.mod h1:3yfa0io5Ii6GmhHWsWl2LEAOEHsqWuMgw2R02+LPogw=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
diff --git a/multiplexer/abci/abci.go b/multiplexer/abci/abci.go
index 9cfc1e3d..fc425d9c 100644
--- a/multiplexer/abci/abci.go
+++ b/multiplexer/abci/abci.go
@@ -50,8 +50,17 @@ func (m *Multiplexer) Commit(context.Context, *abci.RequestCommit) (*abci.Respon
 		return nil, fmt.Errorf("failed to commit: %w", err)
 	}
 
-	// after a successful commit, we start using the app version specified in FinalizeBlock.
+	// after a successful commit, start using the app version specified in FinalizeBlock. If
+	// there is an upgrade, perform that now.
+	oldAppVersion := m.appVersion
 	m.appVersion = m.nextAppVersion
+	if oldAppVersion != m.nextAppVersion {
+		// this effectively performs the upgrade immediately instead of waiting until the next call to getApp.
+		_, err = m.getApp()
+		if err != nil {
+			return nil, fmt.Errorf("multiplexer failed upgrade: %w", err)
+		}
+	}
 
 	return resp, nil
 }
diff --git a/multiplexer/appd/run.go b/multiplexer/appd/run.go
index ce9ae752..d9385012 100644
--- a/multiplexer/appd/run.go
+++ b/multiplexer/appd/run.go
@@ -4,6 +4,7 @@ import (
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
+	"context"
 	"fmt"
 	"io"
 	"log"
@@ -11,6 +12,7 @@ import (
 	"os/exec"
 	"path/filepath"
 	"syscall"
+	"time"
 )
 
 // Appd represents a celestia-appd binary.
@@ -102,6 +104,7 @@ func (a *Appd) IsStopped() bool {
 
 // Stop interrupts and then kills the running appd process if it exists and
 // waits for it to fully exit. If the process is not running, it returns nil.
+// The method will wait up to 6 seconds for graceful shutdown before force killing.
 func (a *Appd) Stop() error {
 	if a.cmd == nil {
 		return nil
@@ -116,15 +119,42 @@ func (a *Appd) Stop() error {
 		if err := a.cmd.Process.Kill(); err != nil {
 			return fmt.Errorf("failed to kill process with PID %d: %w", a.cmd.Process.Pid, err)
 		}
+
+		if err := a.cmd.Wait(); err != nil {
+			log.Printf("Process finished with error: %v\n", err)
+		}
+		return nil
 	}
 
-	// Wait for the process to actually exit
-	if err := a.cmd.Wait(); err != nil {
-		log.Printf("Process finished with error: %v\n", err)
-	} else {
-		log.Printf("Process finished with no error\n")
+	ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
+	defer cancel()
+
+	done := make(chan error, 1)
+	go func() {
+		done <- a.cmd.Wait()
+	}()
+
+	select {
+	case err := <-done:
+		if err != nil {
+			log.Printf("Process finished with error: %v\n", err)
+		} else {
+			log.Printf("Process finished with no error\n")
+		}
+		return nil
+	case <-ctx.Done():
+		log.Printf("Process did not exit within 6 seconds, force killing")
+		if err := a.cmd.Process.Kill(); err != nil {
+			return fmt.Errorf("failed to kill process with PID %d after timeout: %w", a.cmd.Process.Pid, err)
+		}
+
+		if err := <-done; err != nil {
+			log.Printf("Process finished with error after force kill: %v\n", err)
+		} else {
+			log.Printf("Process finished after force kill\n")
+		}
+		return nil
 	}
-	return nil
 }
 
 // CreateExecCommand creates an exec.Cmd for the appd binary.
diff --git a/scripts/single-node-all-upgrades.sh b/scripts/single-node-all-upgrades.sh
index 754a4029..07ad353b 100755
--- a/scripts/single-node-all-upgrades.sh
+++ b/scripts/single-node-all-upgrades.sh
@@ -1,16 +1,14 @@
 #!/bin/sh
 
 # This script starts a local single node testnet on app version 1 and then
-# upgrades to app version 2, 3, and 4.
+# upgrades to app version 2, 3, 4, 5, and 6.
 #
 # Prerequisites:
 # - Modify the `Makefile` and set V2_UPGRADE_HEIGHT = 2
 # - Run `make install`
 
-# Stop script execution if an error is encountered
-set -o errexit
-# Stop script execution if an undefined variable is used
-set -o nounset
+set -o errexit # Stop script execution if an error is encountered
+set -o nounset # Stop script execution if an undefined variable is used
 
 if ! [ -x "$(command -v celestia-appd)" ]
 then
@@ -22,7 +20,7 @@ fi
 CHAIN_ID="test"
 KEY_NAME="validator"
 KEYRING_BACKEND="test"
-FEES="500utia"
+FEES="200000utia"
 BROADCAST_MODE="sync"
 
 # Use argument as home directory if provided, else default to ~/.celestia-app
@@ -107,21 +105,12 @@ startCelestiaApp() {
     --force-no-bbr
 }
 
-upgradeToV3AndV4() {
-    sleep 30
-    echo "Waiting for app version 2 before proceeding..."
-    while true; do
-        current_version=$(celestia-appd status | jq -r '.node_info.protocol_version.app')
-        if [ "$current_version" = "2" ]; then
-            echo "App version 2 detected, proceeding with v3 upgrade..."
-            break
-        fi
-        echo "Current version: $current_version, waiting for version 2..."
-        sleep 1
-    done
+# Function to perform upgrade to a specific version
+performUpgrade() {
+    local target_version=$1
 
-    echo "Submitting signal for v3..."
-    celestia-appd tx signal signal 3 \
+    echo "Submitting signal for v${target_version}..."
+    celestia-appd tx signal signal ${target_version} \
         --keyring-backend=${KEYRING_BACKEND} \
         --home ${APP_HOME} \
         --from ${KEY_NAME} \
@@ -131,8 +120,8 @@ upgradeToV3AndV4() {
         --yes
 
     sleep 1
-    echo "Querying the tally for v3..."
-    celestia-appd query signal tally 3
+    echo "Querying the tally for v${target_version}..."
+    celestia-appd query signal tally ${target_version}
 
     echo "Submitting msg try upgrade..."
     celestia-appd tx signal try-upgrade \
@@ -147,52 +136,36 @@ upgradeToV3AndV4() {
     echo "Waiting for upgrade to complete..."
     while true; do
         current_version=$(celestia-appd status | jq -r '.node_info.protocol_version.app')
-        if [ "$current_version" = "3" ]; then
-            echo "Upgrade to version 3 complete!"
+        if [ "$current_version" = "${target_version}" ]; then
+            echo "Upgrade to version ${target_version} complete!"
             break
         fi
-        echo "Current version: $current_version, waiting for version 3..."
+        echo "Current version: $current_version, waiting for version ${target_version}..."
         sleep 1
     done
+}
 
-
-    echo "Submitting signal for v4..."
-    celestia-appd tx signal signal 4 \
-        --keyring-backend=${KEYRING_BACKEND} \
-        --home ${APP_HOME} \
-        --from ${KEY_NAME} \
-        --fees ${FEES} \
-        --chain-id ${CHAIN_ID} \
-        --broadcast-mode ${BROADCAST_MODE} \
-        --yes
-
-    sleep 1
-    echo "Querying the tally for v4..."
-    celestia-appd query signal tally 4
-
-    echo "Submitting msg try upgrade..."
-    celestia-appd tx signal try-upgrade \
-        --keyring-backend=${KEYRING_BACKEND} \
-        --home ${APP_HOME} \
-        --from ${KEY_NAME} \
-        --fees ${FEES} \
-        --chain-id ${CHAIN_ID} \
-        --broadcast-mode ${BROADCAST_MODE} \
-        --yes
-
-    echo "Waiting for upgrade to complete..."
+startUpgrades() {
+    sleep 30
+    echo "Waiting for app version 2 before proceeding..."
     while true; do
         current_version=$(celestia-appd status | jq -r '.node_info.protocol_version.app')
-        if [ "$current_version" = "4" ]; then
-            echo "Upgrade to version 4 complete!"
+        if [ "$current_version" = "2" ]; then
+            echo "App version 2 detected, proceeding with v3 upgrade..."
             break
         fi
-        echo "Current version: $current_version, waiting for version 4..."
+        echo "Current version: $current_version, waiting for version 2..."
         sleep 1
     done
+
+    # Perform upgrades to versions 3, 4, 5, and 6
+    performUpgrade 3
+    performUpgrade 4
+    performUpgrade 5
+    performUpgrade 6
 }
 
 deleteCelestiaAppHome
 createGenesis
-upgradeToV3AndV4 & # Upgrade to app version 3 and 4 in the background.
+startUpgrades & # Upgrade to app version 3, 4, 5, and 6 in the background.
 startCelestiaApp # Start celestia-app in the foreground.
diff --git a/scripts/single-node.sh b/scripts/single-node.sh
index 872f67fa..bc5206a8 100755
--- a/scripts/single-node.sh
+++ b/scripts/single-node.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-# This script starts a single node testnet on app version 4.
+# This script starts a single node testnet on the latest app version.
 
 # Stop script execution if an error is encountered
 set -o errexit



Test patch:
diff --git a/app/default_overrides_test.go b/app/default_overrides_test.go
index d4b7ab8c..b52be368 100644
--- a/app/default_overrides_test.go
+++ b/app/default_overrides_test.go
@@ -105,7 +105,7 @@ func Test_icaDefaultGenesis(t *testing.T) {
 	got := icagenesistypes.GenesisState{}
 	enc.Codec.MustUnmarshalJSON(raw, &got)
 
-	assert.Equal(t, got.HostGenesisState.Params.AllowMessages, icaAllowMessages())
+	assert.Equal(t, got.HostGenesisState.Params.AllowMessages, IcaAllowMessages())
 	assert.True(t, got.HostGenesisState.Params.HostEnabled)
 	assert.False(t, got.ControllerGenesisState.Params.ControllerEnabled)
 }
diff --git a/app/ica_host_test.go b/app/ica_host_test.go
index 5f8c349f..df787331 100644
--- a/app/ica_host_test.go
+++ b/app/ica_host_test.go
@@ -6,8 +6,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func Test_icaAllowMessages(t *testing.T) {
-	got := icaAllowMessages()
+func TestIcaAllowMessages(t *testing.T) {
+	got := IcaAllowMessages()
 	want := []string{
 		"/ibc.applications.transfer.v1.MsgTransfer",
 		"/cosmos.bank.v1beta1.MsgSend",
diff --git a/app/upgrades_test.go b/app/upgrades_test.go
new file mode 100644
index 00000000..706c94bf
--- /dev/null
+++ b/app/upgrades_test.go
@@ -0,0 +1,67 @@
+package app_test
+
+import (
+	"testing"
+	"time"
+
+	"cosmossdk.io/log"
+	upgradetypes "cosmossdk.io/x/upgrade/types"
+	"github.com/celestiaorg/celestia-app/v6/app"
+	"github.com/celestiaorg/celestia-app/v6/test/util"
+	"github.com/celestiaorg/celestia-app/v6/test/util/testfactory"
+	tmdb "github.com/cosmos/cosmos-db"
+	"github.com/cosmos/cosmos-sdk/baseapp"
+	icahosttypes "github.com/cosmos/ibc-go/v8/modules/apps/27-interchain-accounts/host/types"
+	"github.com/stretchr/testify/require"
+)
+
+func TestUpgrades(t *testing.T) {
+	t.Run("app.New() should register a v6 upgrade handler", func(t *testing.T) {
+		logger := log.NewNopLogger()
+		db := tmdb.NewMemDB()
+		traceStore := &NoopWriter{}
+		timeoutCommit := time.Second
+		appOptions := NoopAppOptions{}
+
+		testApp := app.New(logger, db, traceStore, timeoutCommit, appOptions, baseapp.SetChainID(testfactory.ChainID))
+
+		require.False(t, testApp.UpgradeKeeper.HasHandler("v5"))
+		require.True(t, testApp.UpgradeKeeper.HasHandler("v6"))
+	})
+}
+
+func TestApplyUpgrade(t *testing.T) {
+	t.Run("apply upgrade should set ICA host params to an explicit allowlist of messages", func(t *testing.T) {
+		consensusParams := app.DefaultConsensusParams()
+		consensusParams.Version.App = 5
+		testApp, _, _ := util.NewTestAppWithGenesisSet(consensusParams)
+		require.True(t, testApp.UpgradeKeeper.HasHandler("v6"))
+		plan := upgradetypes.Plan{
+			Name:   "v6",
+			Time:   time.Now(),
+			Height: 1,
+			Info:   "info",
+		}
+
+		// Note: v5 didn't have the ICA module registered so no params were set
+		// but this test explicitly sets the params to values to verify they get
+		// overridden during ApplyUpgrade.
+		allMessages := []string{"*"}
+		ctx := testApp.NewContext(false)
+		testApp.ICAHostKeeper.SetParams(ctx, icahosttypes.Params{
+			HostEnabled:   false,
+			AllowMessages: allMessages,
+		})
+		got := testApp.ICAHostKeeper.GetParams(ctx)
+		require.False(t, got.HostEnabled)
+		require.Equal(t, allMessages, got.AllowMessages)
+
+		err := testApp.UpgradeKeeper.ApplyUpgrade(ctx, plan)
+		require.NoError(t, err)
+
+		ctx = testApp.NewContext(false)
+		got = testApp.ICAHostKeeper.GetParams(ctx)
+		require.True(t, got.HostEnabled)
+		require.Equal(t, got.AllowMessages, app.IcaAllowMessages())
+	})
+}
diff --git a/cmd/celestia-appd/cmd/migrate_config_test.go b/cmd/celestia-appd/cmd/migrate_config_test.go
new file mode 100644
index 00000000..2950eff0
--- /dev/null
+++ b/cmd/celestia-appd/cmd/migrate_config_test.go
@@ -0,0 +1,142 @@
+package cmd
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/celestiaorg/celestia-app/v6/app"
+	"github.com/cometbft/cometbft/config"
+	serverconfig "github.com/cosmos/cosmos-sdk/server/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestUpdateConfig(t *testing.T) {
+	tests := []struct {
+		name          string
+		version       string
+		expectError   bool
+		errorContains string
+	}{
+		{
+			name:        "valid v6 update",
+			version:     "v6",
+			expectError: false,
+		},
+		{
+			name:          "unsupported version",
+			version:       "v99",
+			expectError:   true,
+			errorContains: "unsupported target version",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Create temporary directory for test
+			tempDir := t.TempDir()
+			configDir := filepath.Join(tempDir, "config")
+			require.NoError(t, os.MkdirAll(configDir, 0o755))
+
+			// Create test config files
+			setupTestConfigFiles(t, configDir)
+
+			// Run update
+			err := updateConfig(tempDir, tt.version, false)
+
+			if tt.expectError {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.errorContains)
+			} else {
+				require.NoError(t, err)
+
+				// Verify configs were updated
+				verifyUpdatedConfigs(t, configDir, tt.version)
+			}
+		})
+	}
+}
+
+func TestLoadAndWriteConfigs(t *testing.T) {
+	tempDir := t.TempDir()
+	configDir := filepath.Join(tempDir, "config")
+	require.NoError(t, os.MkdirAll(configDir, 0o755))
+
+	cometConfigPath := filepath.Join(configDir, "config.toml")
+	appConfigPath := filepath.Join(configDir, "app.toml")
+
+	// Create test configs
+	setupTestConfigFiles(t, configDir)
+
+	t.Run("load and write CometBFT config", func(t *testing.T) {
+		cfg, err := loadCometBFTConfig(cometConfigPath, tempDir)
+		require.NoError(t, err)
+		assert.NotNil(t, cfg)
+
+		originalTimeout := cfg.RPC.TimeoutBroadcastTxCommit
+		cfg.RPC.TimeoutBroadcastTxCommit = 60000000000
+
+		config.WriteConfigFile(cometConfigPath, cfg)
+
+		// Load again and verify change persisted
+		reloadedCfg, err := loadCometBFTConfig(cometConfigPath, tempDir)
+		require.NoError(t, err)
+		assert.NotEqual(t, originalTimeout, reloadedCfg.RPC.TimeoutBroadcastTxCommit)
+		assert.Equal(t, cfg.RPC.TimeoutBroadcastTxCommit, reloadedCfg.RPC.TimeoutBroadcastTxCommit)
+	})
+
+	t.Run("load and write server config", func(t *testing.T) {
+		cfg, err := loadServerConfig(appConfigPath)
+		require.NoError(t, err)
+		assert.NotNil(t, cfg)
+
+		newConfigVal := !cfg.API.Enable
+		cfg.API.Enable = newConfigVal
+
+		serverconfig.WriteConfigFile(appConfigPath, cfg)
+
+		// Load again and verify change persisted
+		reloadedCfg, err := loadServerConfig(appConfigPath)
+		require.NoError(t, err)
+		assert.Equal(t, newConfigVal, reloadedCfg.API.Enable)
+	})
+}
+
+// setupTestConfigFiles creates test config files with default configurations
+func setupTestConfigFiles(t *testing.T, configDir string) {
+	t.Helper()
+
+	cometConfigPath := filepath.Join(configDir, "config.toml")
+	appConfigPath := filepath.Join(configDir, "app.toml")
+
+	// Create CometBFT config
+	cometConfig := app.DefaultConsensusConfig()
+	config.WriteConfigFile(cometConfigPath, cometConfig)
+
+	// Create server config
+	serverConfig := app.DefaultAppConfig()
+	serverconfig.WriteConfigFile(appConfigPath, serverConfig)
+}
+
+// verifyUpdatedConfigs verifies that configs were properly updated for the given version
+func verifyUpdatedConfigs(t *testing.T, configDir, version string) {
+	t.Helper()
+
+	cometConfigPath := filepath.Join(configDir, "config.toml")
+	appConfigPath := filepath.Join(configDir, "app.toml")
+
+	if version == "v6" {
+		cometConfig, err := loadCometBFTConfig(cometConfigPath, filepath.Dir(configDir))
+		require.NoError(t, err)
+
+		expectedCometConfig := app.DefaultConsensusConfig()
+		assert.Equal(t, expectedCometConfig.Mempool.Type, cometConfig.Mempool.Type)
+
+		serverConfig, err := loadServerConfig(appConfigPath)
+		require.NoError(t, err)
+
+		expectedServerConfig := app.DefaultAppConfig()
+		assert.Equal(t, expectedServerConfig.MinGasPrices, serverConfig.MinGasPrices)
+	}
+}
diff --git a/test/docker-e2e/dockerchain/config.go b/test/docker-e2e/dockerchain/config.go
index d9438416..4884e73c 100644
--- a/test/docker-e2e/dockerchain/config.go
+++ b/test/docker-e2e/dockerchain/config.go
@@ -12,7 +12,7 @@ import (
 
 const (
 	multiplexerImage    = "ghcr.io/celestiaorg/celestia-app"
-	defaultCelestiaTag  = "v4.0.10-mocha"
+	defaultCelestiaTag  = "v5.0.1"
 	celestiaTagEnvVar   = "CELESTIA_TAG"
 	celestiaImageEnvVar = "CELESTIA_IMAGE"
 )
diff --git a/test/docker-e2e/dockerchain/testchain.go b/test/docker-e2e/dockerchain/testchain.go
index 4a7b5012..7c400104 100644
--- a/test/docker-e2e/dockerchain/testchain.go
+++ b/test/docker-e2e/dockerchain/testchain.go
@@ -10,6 +10,7 @@ import (
 	"github.com/celestiaorg/celestia-app/v6/app/encoding"
 	"github.com/celestiaorg/celestia-app/v6/pkg/user"
 	tastoradockertypes "github.com/celestiaorg/tastora/framework/docker"
+	tastoracontainertypes "github.com/celestiaorg/tastora/framework/docker/container"
 	"github.com/celestiaorg/tastora/framework/testutil/config"
 	"github.com/celestiaorg/tastora/framework/testutil/maps"
 	cometcfg "github.com/cometbft/cometbft/config"
@@ -51,15 +52,22 @@ func NewCelestiaChainBuilder(t *testing.T, cfg *Config) *tastoradockertypes.Chai
 			Build()
 	}
 
+	// register the first wallet as the faucet wallet
+	addr, err := records[0].GetAddress()
+	require.NoError(t, err, "failed to get address from keyring record")
+	// Create the wallet with all required fields
+	faucetWallet := tastoradockertypes.NewWallet(addr, addr.String(), "celestia", records[0].Name)
+
 	return tastoradockertypes.NewChainBuilder(t).
 		WithName("celestia"). // just influences home directory on the host.
 		WithChainID(cfg.Genesis.ChainID).
 		WithDockerClient(cfg.DockerClient).
 		WithDockerNetworkID(cfg.DockerNetworkID).
-		WithImage(tastoradockertypes.NewDockerImage(cfg.Image, cfg.Tag, "10001:10001")).
+		WithImage(tastoracontainertypes.NewImage(cfg.Image, cfg.Tag, "10001:10001")).
 		WithAdditionalStartArgs("--force-no-bbr").
 		WithEncodingConfig(&encodingConfig).
 		WithPostInit(getPostInitModifications("0.025utia")...).
+		WithFaucetWallet(faucetWallet).
 		WithNodes(vals...).
 		WithGenesis(genesisBz)
 }
@@ -142,7 +150,7 @@ func NodeConfigBuilders(cfg *Config) ([]*tastoradockertypes.ChainNodeConfigBuild
 		chainNodeBuilders[i] = tastoradockertypes.NewChainNodeConfigBuilder().
 			WithPrivValidatorKey(privKeyBz).
 			WithAccountName(record.Name).
-			WithImage(tastoradockertypes.NewDockerImage(cfg.Image, cfg.Tag, "10001:10001")).
+			WithImage(tastoracontainertypes.NewImage(cfg.Image, cfg.Tag, "10001:10001")).
 			WithKeyring(kr)
 	}
 
diff --git a/test/docker-e2e/e2e_block_sync_test.go b/test/docker-e2e/e2e_block_sync_test.go
index aef4e584..b2207558 100644
--- a/test/docker-e2e/e2e_block_sync_test.go
+++ b/test/docker-e2e/e2e_block_sync_test.go
@@ -70,6 +70,7 @@ func (s *CelestiaTestSuite) TestBlockSync() {
 
 	// build peer list for the new node to connect to existing validators
 	peerList, err := addressutil.BuildInternalPeerAddressList(ctx, celestia.GetNodes())
+	s.Require().NoError(err, "failed to build peer address list")
 
 	t.Logf("Latest height: %d", latestHeight)
 	t.Logf("Peers: %s", peerList)
@@ -109,7 +110,7 @@ func (s *CelestiaTestSuite) TestBlockSync() {
 
 	s.T().Logf("Checking validator liveness from height %d", initialHeight)
 	s.Require().NoError(
-		s.CheckLiveness(ctx, celestia, initialHeight),
+		s.CheckLiveness(ctx, celestia),
 		"validator liveness check failed",
 	)
 }
diff --git a/test/docker-e2e/e2e_full_stack_pfb_test.go b/test/docker-e2e/e2e_full_stack_pfb_test.go
index 1d7f85c3..341ac140 100644
--- a/test/docker-e2e/e2e_full_stack_pfb_test.go
+++ b/test/docker-e2e/e2e_full_stack_pfb_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+	tastoracontainertypes "github.com/celestiaorg/tastora/framework/docker/container"
 	"testing"
 	"time"
 
@@ -87,7 +88,7 @@ func (s *CelestiaTestSuite) TestE2EFullStackPFB() {
 
 	s.T().Logf("Checking validator liveness from height %d", startHeight)
 	s.Require().NoError(
-		s.CheckLiveness(ctx, celestia, startHeight),
+		s.CheckLiveness(ctx, celestia),
 		"validator liveness check failed",
 	)
 
@@ -300,7 +301,7 @@ func getDAConfig(logger *zap.Logger, client *dockerclient.Client, networkID stri
 			BridgeNodeCount: 1,
 			FullNodeCount:   1,
 			LightNodeCount:  1,
-			Image: tastoradockertypes.DockerImage{
+			Image: tastoracontainertypes.Image{
 				Repository: celestiaNodeRepository,
 				Version:    celestiaNodeVersion,
 			},
diff --git a/test/docker-e2e/e2e_ibc_test.go b/test/docker-e2e/e2e_ibc_test.go
new file mode 100644
index 00000000..f5e16cae
--- /dev/null
+++ b/test/docker-e2e/e2e_ibc_test.go
@@ -0,0 +1,347 @@
+package docker_e2e
+
+import (
+	"celestiaorg/celestia-app/test/docker-e2e/dockerchain"
+	"context"
+	"fmt"
+	"github.com/celestiaorg/celestia-app/v6/pkg/appconsts"
+	tastoradockertypes "github.com/celestiaorg/tastora/framework/docker"
+	tastoracontainertypes "github.com/celestiaorg/tastora/framework/docker/container"
+	"github.com/celestiaorg/tastora/framework/testutil/query"
+	"github.com/celestiaorg/tastora/framework/testutil/sdkacc"
+	"github.com/cosmos/cosmos-sdk/types/module/testutil"
+	"testing"
+	"time"
+
+	sdkmath "cosmossdk.io/math"
+	"github.com/celestiaorg/celestia-app/v6/app"
+	"github.com/celestiaorg/celestia-app/v6/app/encoding"
+	"github.com/celestiaorg/celestia-app/v6/pkg/user"
+	"github.com/celestiaorg/tastora/framework/docker"
+	"github.com/celestiaorg/tastora/framework/docker/ibc"
+	"github.com/celestiaorg/tastora/framework/docker/ibc/relayer"
+	"github.com/celestiaorg/tastora/framework/testutil/wait"
+	tastoratypes "github.com/celestiaorg/tastora/framework/types"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	ibctransfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
+	clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
+	"golang.org/x/sync/errgroup"
+)
+
+// TestIBC tests IBC functionality by
+// - deploying 2 chains
+// - deploying hermes
+// - creating an IBC connection and channel
+// - performs a token transfer
+// - upgrades from app version N-1 to N
+// - performs token transfer over the same channel
+// - creates a new connection and channel after upgrade
+// - performs token transfer over a new channel
+func (s *CelestiaTestSuite) TestIBC() {
+	if testing.Short() {
+		s.T().Skip("skipping IBC test in short mode")
+	}
+
+	ctx := context.Background()
+	t := s.T()
+
+	baseAppVersion := appconsts.Version - 1
+	targetAppVersion := appconsts.Version
+	tag, err := dockerchain.GetCelestiaTagStrict()
+	s.Require().NoError(err, "failed to get celestia tag")
+
+	var chainA, chainB tastoratypes.Chain
+	var hermes *relayer.Hermes
+	var channel ibc.Channel
+
+	t.Cleanup(func() {
+		if err := chainA.Stop(ctx); err != nil {
+			t.Logf("Error stopping chain A: %v", err)
+		}
+		if err := chainB.Stop(ctx); err != nil {
+			t.Logf("Error stopping chain B: %v", err)
+		}
+	})
+
+	t.Cleanup(func() {
+		if err := hermes.Stop(ctx); err != nil {
+			t.Logf("Error stopping hermes: %v", err)
+		}
+	})
+
+	t.Run("setup_chains", func(t *testing.T) {
+		t.Logf("Creating celestia and simapp chains")
+		chainA, chainB = s.setupIBCChains(ctx, tag, baseAppVersion)
+	})
+
+	t.Run("create_hermes", func(t *testing.T) {
+		t.Logf("Creating hermes relayer")
+		hermes = s.createHermesRelayer(ctx, chainA, chainB)
+	})
+
+	t.Run("create_clients", func(t *testing.T) {
+		t.Logf("Creating IBC clients")
+		err := hermes.CreateClients(ctx, chainA, chainB)
+		s.Require().NoError(err, "failed to create IBC clients")
+	})
+
+	t.Run("setup_connection_and_channel", func(t *testing.T) {
+		t.Logf("Creating IBC connection and channel")
+		_, channel = s.establishIBCConnection(ctx, chainA, chainB, hermes)
+	})
+
+	t.Run("start_relayer", func(t *testing.T) {
+		t.Logf("Starting hermes relayer")
+		err := hermes.Start(ctx)
+		s.Require().NoError(err, "failed to start hermes relayer")
+	})
+
+	t.Run("initial_transfers", func(t *testing.T) {
+		t.Logf("Performing initial transfers")
+		s.testTokenTransfers(ctx, chainA, chainB, channel)
+	})
+
+	t.Run("upgrade_app_version", func(t *testing.T) {
+		t.Logf("Upgrading app version from %d to %d", baseAppVersion, targetAppVersion)
+		s.upgradeChain(ctx, chainA, targetAppVersion)
+	})
+
+	t.Run("existing_channel_transfers", func(t *testing.T) {
+		s.testTokenTransfers(ctx, chainA, chainB, channel)
+	})
+
+	t.Run("new_connection_and_channel", func(t *testing.T) {
+		t.Logf("Creating new connection and channel after upgrade")
+		_, channel = s.establishIBCConnection(ctx, chainA, chainB, hermes)
+	})
+
+	t.Run("new_channel_transfers", func(t *testing.T) {
+		t.Logf("Performing transfers over new channel")
+		s.testTokenTransfers(ctx, chainA, chainB, channel)
+	})
+}
+
+// setupIBCChains creates and starts two chains: Celestia app (chain A) and IBC-Go simapp (chain B)
+func (s *CelestiaTestSuite) setupIBCChains(ctx context.Context, imageTag string, appVersion uint64) (tastoratypes.Chain, tastoratypes.Chain) {
+	t := s.T()
+
+	s.celestiaCfg = dockerchain.DefaultConfig(s.client, s.network).WithTag(imageTag)
+	s.celestiaCfg.Genesis = s.celestiaCfg.Genesis.WithAppVersion(appVersion)
+
+	var chainA, chainB tastoratypes.Chain
+	g, gCtx := errgroup.WithContext(ctx)
+
+	g.Go(func() error {
+		var err error
+		chainA, err = dockerchain.NewCelestiaChainBuilder(t, s.celestiaCfg).Build(gCtx)
+		if err != nil {
+			return fmt.Errorf("failed to build chain A: %w", err)
+		}
+		return chainA.Start(gCtx)
+	})
+
+	g.Go(func() error {
+		var err error
+		builder := newSimappChainBuilder(t, s.celestiaCfg)
+		chainB, err = builder.Build(gCtx)
+		if err != nil {
+			return fmt.Errorf("failed to build chain B: %w", err)
+		}
+		return chainB.Start(gCtx)
+	})
+
+	s.Require().NoError(g.Wait(), "failed to setup chains")
+
+	return chainA, chainB
+}
+
+// createHermesRelayer creates and initializes Hermes relayer without starting it
+func (s *CelestiaTestSuite) createHermesRelayer(ctx context.Context, chainA, chainB tastoratypes.Chain) *relayer.Hermes {
+	t := s.T()
+
+	hermes, err := relayer.NewHermes(ctx, s.client, t.Name(), s.network, 0, s.logger)
+	s.Require().NoError(err, "failed to create hermes")
+
+	err = hermes.Init(ctx, chainA, chainB)
+	s.Require().NoError(err, "failed to initialize hermes")
+
+	return hermes
+}
+
+// establishIBCConnection creates IBC clients, connection, and channel between the chains
+func (s *CelestiaTestSuite) establishIBCConnection(ctx context.Context, chainA, chainB tastoratypes.Chain, hermes *relayer.Hermes) (ibc.Connection, ibc.Channel) {
+	connection, err := hermes.CreateConnections(ctx, chainA, chainB)
+	s.Require().NoError(err, "failed to create IBC connection")
+
+	channelOpts := ibc.CreateChannelOptions{
+		SourcePortName: "transfer",
+		DestPortName:   "transfer",
+		Order:          ibc.OrderUnordered,
+		Version:        "ics20-1",
+	}
+
+	channel, err := hermes.CreateChannel(ctx, chainA, connection, channelOpts)
+	s.Require().NoError(err, "failed to create IBC channel")
+
+	return connection, channel
+}
+
+// testTokenTransfers tests token transfers from Celestia to simapp
+func (s *CelestiaTestSuite) testTokenTransfers(ctx context.Context, chainA, chainB tastoratypes.Chain, channel ibc.Channel) {
+	s.transferTokens(ctx, chainA, chainB, channel, "utia", 100000)
+}
+
+// transferTokens performs an IBC token transfer from Celestia to simapp
+func (s *CelestiaTestSuite) transferTokens(ctx context.Context, sourceChain, destChain tastoratypes.Chain, channel ibc.Channel, denom string, amount int64) {
+	sourceWallet := sourceChain.GetFaucetWallet()
+	destWallet := destChain.GetFaucetWallet()
+	ibcTransfer := s.createIBCTransferMsg(sourceWallet, destWallet, channel, denom, amount)
+
+	// Submit transaction and verify results - always use TxClient since we only transfer from Celestia
+	s.submitTransactionAndVerify(ctx, sourceChain, destChain, ibcTransfer, denom, amount)
+}
+
+// createIBCTransferMsg creates an IBC transfer message
+func (s *CelestiaTestSuite) createIBCTransferMsg(sourceWallet, destWallet tastoratypes.Wallet, channel ibc.Channel, denom string, amount int64) *ibctransfertypes.MsgTransfer {
+	destAddr, err := sdkacc.AddressFromWallet(destWallet)
+	s.Require().NoError(err, "failed to parse destination address")
+
+	transferAmount := sdkmath.NewInt(amount)
+	return ibctransfertypes.NewMsgTransfer(
+		channel.PortID,
+		channel.ChannelID,
+		sdk.NewCoin(denom, transferAmount),
+		sourceWallet.GetFormattedAddress(),
+		destAddr.String(),
+		clienttypes.ZeroHeight(),
+		uint64(time.Now().Add(time.Hour).UnixNano()),
+		"",
+	)
+}
+
+// submitTransactionAndVerify submits a transaction using the appropriate method and verifies results
+func (s *CelestiaTestSuite) submitTransactionAndVerify(ctx context.Context, sourceChain, destChain tastoratypes.Chain, msg *ibctransfertypes.MsgTransfer, denom string, amount int64) {
+	channel := ibc.Channel{PortID: msg.SourcePort, CounterpartyID: msg.SourceChannel}
+	ibcDenom := s.calculateIBCDenom(channel, denom)
+
+	destWallet := destChain.GetFaucetWallet()
+	destBalance := s.getBalance(ctx, destChain, destWallet.GetFormattedAddress(), ibcDenom)
+
+	txClient, err := s.setupTxClient(ctx, sourceChain)
+	s.Require().NoError(err, "failed to setup tx client for celestia chain")
+
+	resp, err := txClient.SubmitTx(ctx, []sdk.Msg{msg}, user.SetGasLimit(200000), user.SetFee(5000))
+	s.Require().NoError(err, "failed to submit IBC transfer via TxClient")
+
+	s.Require().Equal(uint32(0), resp.Code, "IBC transfer tx failed with code %d", resp.Code)
+
+	err = wait.ForBlocks(ctx, 5, sourceChain)
+	s.Require().NoError(err, "failed to wait for blocks")
+
+	transferAmount := sdkmath.NewInt(amount)
+	s.verifyTransferResults(ctx, destChain, destWallet, ibcDenom, destBalance, transferAmount)
+}
+
+// verifyTransferResults checks the final balances and verifies the transfer results
+func (s *CelestiaTestSuite) verifyTransferResults(ctx context.Context, destChain tastoratypes.Chain, destWallet tastoratypes.Wallet, ibcDenom string, destBalance, transferAmount sdkmath.Int) {
+	finalDestBalance := s.getBalance(ctx, destChain, destWallet.GetFormattedAddress(), ibcDenom)
+
+	expectedDestBalance := destBalance.Add(transferAmount)
+	s.Require().True(finalDestBalance.Equal(expectedDestBalance),
+		"destination balance mismatch: expected %s, got %s", expectedDestBalance.String(), finalDestBalance.String())
+}
+
+// upgradeChain upgrades the celestia chain from baseAppVersion to targetAppVersion
+// This reuses the existing upgrade logic from e2e_upgrade_test.go
+func (s *CelestiaTestSuite) upgradeChain(ctx context.Context, chain tastoratypes.Chain, targetAppVersion uint64) {
+	validatorNode := chain.GetNodes()[0]
+	cfg := s.celestiaCfg
+	kr := cfg.Genesis.Keyring()
+
+	records, err := kr.List()
+	s.Require().NoError(err, "failed to list keyring records")
+
+	upgradeHeight := s.signalAndGetUpgradeHeight(ctx, chain, validatorNode, cfg, records, targetAppVersion)
+
+	rpcClient, err := validatorNode.GetRPCClient()
+	s.Require().NoError(err, "failed to get RPC client")
+
+	status, err := rpcClient.Status(ctx)
+	s.Require().NoError(err, "failed to get node status")
+	currentHeight := status.SyncInfo.LatestBlockHeight
+
+	blocksToWait := int(upgradeHeight-currentHeight) + 2
+	s.Require().NoError(wait.ForBlocks(ctx, blocksToWait, chain))
+
+	abciInfo, err := rpcClient.ABCIInfo(ctx)
+	s.Require().NoError(err, "failed to fetch ABCI info")
+	s.Require().Equal(targetAppVersion, abciInfo.Response.GetAppVersion(), "app version mismatch after upgrade")
+}
+
+// setupTxClient sets up a tx client using the node's keyring
+func (s *CelestiaTestSuite) setupTxClient(ctx context.Context, chain tastoratypes.Chain) (*user.TxClient, error) {
+	node := chain.GetNodes()[0].(*docker.ChainNode)
+
+	keyring, err := node.GetKeyring()
+	if err != nil {
+		return nil, err
+	}
+
+	encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)
+
+	return user.SetupTxClient(
+		ctx,
+		keyring,
+		node.GrpcConn,
+		encCfg,
+	)
+}
+
+// getBalance gets the balance of a specific denom for an address
+func (s *CelestiaTestSuite) getBalance(ctx context.Context, chain tastoratypes.Chain, address, denom string) sdkmath.Int {
+	dockerChain, ok := chain.(*docker.Chain)
+	if !ok {
+		s.T().Logf("Chain is not a docker Chain, returning zero balance")
+		return sdkmath.ZeroInt()
+	}
+
+	node := dockerChain.GetNode()
+	if node.GrpcConn == nil {
+		s.T().Logf("GRPC connection is nil for chain %s, returning zero balance", chain.GetChainID())
+		return sdkmath.ZeroInt()
+	}
+
+	amount, err := query.Balance(ctx, node.GrpcConn, address, denom)
+	s.Require().NoError(err, "failed to query balance for %s %s", address, denom)
+	return amount
+}
+
+// calculateIBCDenom calculates the IBC denomination using ibc-go utilities
+func (s *CelestiaTestSuite) calculateIBCDenom(channel ibc.Channel, baseDenom string) string {
+	prefixedDenom := ibctransfertypes.GetPrefixedDenom(
+		channel.PortID,
+		channel.CounterpartyID,
+		baseDenom,
+	)
+	return ibctransfertypes.ParseDenomTrace(prefixedDenom).IBCDenom()
+}
+
+// newSimappChainBuilder builds a standard simapp chain without token filters
+func newSimappChainBuilder(t *testing.T, cfg *dockerchain.Config) *tastoradockertypes.ChainBuilder {
+	encodingConfig := testutil.MakeTestEncodingConfig(app.ModuleEncodingRegisters...)
+	return tastoradockertypes.NewChainBuilder(t).
+		WithEncodingConfig(&encodingConfig).
+		WithName("simapp").
+		WithChainID("chain-b").
+		// TODO: this is a custom built simapp that has the bech32prefix as "celestia" as a workaround for the global
+		// SDK config not being usable when 2 chains have a different beck32 preix (e.g. "celestia" and "cosmos" ) because it is sealed.
+		WithImage(tastoracontainertypes.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "1000:1000")).
+		WithBinaryName("simd").
+		WithBech32Prefix("celestia").
+		WithDenom("utia").
+		WithGasPrices("0.000001utia").
+		WithDockerNetworkID(cfg.DockerNetworkID).
+		WithDockerClient(cfg.DockerClient).
+		WithChainID("chain-b").
+		WithNode(tastoradockertypes.NewChainNodeConfigBuilder().Build())
+}
diff --git a/test/docker-e2e/e2e_simple_test.go b/test/docker-e2e/e2e_simple_test.go
index 794ce7dd..32118679 100644
--- a/test/docker-e2e/e2e_simple_test.go
+++ b/test/docker-e2e/e2e_simple_test.go
@@ -60,7 +60,7 @@ func (s *CelestiaTestSuite) TestE2ESimple() {
 
 	s.T().Logf("Checking validator liveness from height %d", startHeight)
 	s.Require().NoError(
-		s.CheckLiveness(ctx, celestia, startHeight),
+		s.CheckLiveness(ctx, celestia),
 		"validator liveness check failed",
 	)
 }
diff --git a/test/docker-e2e/e2e_state_sync_test.go b/test/docker-e2e/e2e_state_sync_test.go
index 402dbbe6..e7632451 100644
--- a/test/docker-e2e/e2e_state_sync_test.go
+++ b/test/docker-e2e/e2e_state_sync_test.go
@@ -128,7 +128,7 @@ func (s *CelestiaTestSuite) TestStateSync() {
 
 	s.T().Logf("Checking validator liveness from height %d", initialHeight)
 	s.Require().NoError(
-		s.CheckLiveness(ctx, celestia, initialHeight),
+		s.CheckLiveness(ctx, celestia),
 		"validator liveness check failed",
 	)
 }
diff --git a/test/docker-e2e/e2e_test.go b/test/docker-e2e/e2e_test.go
index f8ce3f2d..5e881b44 100644
--- a/test/docker-e2e/e2e_test.go
+++ b/test/docker-e2e/e2e_test.go
@@ -3,9 +3,11 @@ package docker_e2e
 import (
 	"context"
 	"fmt"
+	tastoracontainertypes "github.com/celestiaorg/tastora/framework/docker/container"
 	"testing"
 	"time"
 
+	"celestiaorg/celestia-app/test/docker-e2e/dockerchain"
 	"github.com/celestiaorg/go-square/v2/share"
 	tastoradockertypes "github.com/celestiaorg/tastora/framework/docker"
 	"github.com/celestiaorg/tastora/framework/testutil/wait"
@@ -40,9 +42,10 @@ func TestCelestiaTestSuite(t *testing.T) {
 
 type CelestiaTestSuite struct {
 	suite.Suite
-	logger  *zap.Logger
-	client  *client.Client
-	network string
+	logger     *zap.Logger
+	client     *client.Client
+	network    string
+	celestiaCfg *dockerchain.Config // Config used to build the celestia chain, needed for upgrades
 }
 
 func (s *CelestiaTestSuite) SetupSuite() {
@@ -59,9 +62,9 @@ func (s *CelestiaTestSuite) CreateTxSim(ctx context.Context, chain tastoratypes.
 
 	// Deploy txsim image
 	t.Log("Deploying txsim image")
-	txsimImage := tastoradockertypes.NewImage(s.logger, s.client, networkName, t.Name(), txsimImage, txSimTag)
+	txsimImage := tastoracontainertypes.NewJob(s.logger, s.client, networkName, t.Name(), txsimImage, txSimTag)
 
-	opts := tastoradockertypes.ContainerOptions{
+	opts := tastoracontainertypes.Options{
 		User: "0:0",
 		// Mount the Celestia home directory into the txsim container
 		// this ensures txsim has access to a keyring and is able to broadcast transactions.
@@ -186,28 +189,28 @@ func (s *CelestiaTestSuite) WaitForSync(ctx context.Context, statusClient rpccli
 //
 // Upgrade-agnostic: can be called before/after upgrades or spanning the entire period.
 // Call at the end of E2E tests to validate network health.
-func (s *CelestiaTestSuite) CheckLiveness(ctx context.Context, chain tastoratypes.Chain, startHeight int64) error {
+func (s *CelestiaTestSuite) CheckLiveness(ctx context.Context, chain tastoratypes.Chain) error {
 	rpcClient, err := chain.GetNodes()[0].GetRPCClient()
 	if err != nil {
 		return fmt.Errorf("failed to get RPC client: %w", err)
 	}
 
-	endHeight, err := s.ensureMinimumBlocks(ctx, chain, rpcClient, startHeight)
+	endHeight, err := s.ensureMinimumBlocks(ctx, chain, rpcClient, 1)
 	if err != nil {
 		return fmt.Errorf("failed to ensure minimum blocks: %w", err)
 	}
 
-	startValidators, endValidators, err := s.fetchValidatorSets(ctx, rpcClient, startHeight, endHeight)
+	proposers, err := s.fetchProposerAddresses(ctx, rpcClient, 1, endHeight)
 	if err != nil {
-		return fmt.Errorf("failed to fetch validator sets: %w", err)
+		return fmt.Errorf("failed to fetch proposer addresses: %w", err)
 	}
 
-	proposers, err := s.fetchProposerAddresses(ctx, rpcClient, startHeight, endHeight)
+	endValidators, err := s.fetchValidatorSets(ctx, rpcClient, endHeight)
 	if err != nil {
-		return fmt.Errorf("failed to fetch proposer addresses: %w", err)
+		return fmt.Errorf("failed to fetch validator sets: %w", err)
 	}
 
-	if err := s.validateAllValidatorsProposed(startValidators, endValidators, proposers, startHeight, endHeight); err != nil {
+	if err := s.validateAllValidatorsProposed(endValidators, proposers, endHeight); err != nil {
 		return err
 	}
 
@@ -257,25 +260,13 @@ func (s *CelestiaTestSuite) ensureMinimumBlocks(ctx context.Context, chain tasto
 }
 
 // fetchValidatorSets retrieves validator sets at both start and end heights
-func (s *CelestiaTestSuite) fetchValidatorSets(ctx context.Context, rpcClient rpcclient.Client, startHeight, endHeight int64) (*coretypes.ResultValidators, *coretypes.ResultValidators, error) {
-	if endHeight <= startHeight {
-		return nil, nil, fmt.Errorf("invalid height range %d to %d", startHeight, endHeight)
-	}
-
-	blocksProduced := endHeight - startHeight
-	s.T().Logf("Checking validator liveness from height %d to %d (%d blocks)", startHeight, endHeight, blocksProduced)
-
-	startValidators, err := rpcClient.Validators(ctx, &startHeight, nil, nil)
-	if err != nil {
-		return nil, nil, fmt.Errorf("validators query at start height %d: %w", startHeight, err)
-	}
-
+func (s *CelestiaTestSuite) fetchValidatorSets(ctx context.Context, rpcClient rpcclient.Client, endHeight int64) (*coretypes.ResultValidators, error) {
 	endValidators, err := rpcClient.Validators(ctx, &endHeight, nil, nil)
 	if err != nil {
-		return nil, nil, fmt.Errorf("validators query at end height %d: %w", endHeight, err)
+		return nil, fmt.Errorf("validators query at end height %d: %w", endHeight, err)
 	}
 
-	return startValidators, endValidators, nil
+	return endValidators, nil
 }
 
 // fetchProposerAddresses gathers proposer addresses from block headers using efficient batching
@@ -329,31 +320,21 @@ func (s *CelestiaTestSuite) fetchProposerAddresses(ctx context.Context, rpcClien
 }
 
 // validateAllValidatorsProposed ensures every validator proposed at least one block
-func (s *CelestiaTestSuite) validateAllValidatorsProposed(startValidators, endValidators *coretypes.ResultValidators, proposerAddresses []string, startHeight, endHeight int64) error {
-	// Convert slice to map for O(1) lookups
+func (s *CelestiaTestSuite) validateAllValidatorsProposed(endValidators *coretypes.ResultValidators, proposerAddresses []string, endHeight int64) error {
 	proposers := make(map[string]struct{}, len(proposerAddresses))
 	for _, addr := range proposerAddresses {
 		proposers[addr] = struct{}{}
 	}
 
-	// Create a combined map of all validators that should have proposed
 	allValidators := make(map[string]struct{})
 
-	// Add start validators
-	for _, val := range startValidators.Validators {
-		addr := val.Address.String()
-		allValidators[addr] = struct{}{}
-	}
-
-	// Add end validators (in case validator set changed)
 	for _, val := range endValidators.Validators {
 		addr := val.Address.String()
 		allValidators[addr] = struct{}{}
 	}
 
-	s.T().Logf("Checking %d total validators for proposer activity from height %d to %d (validators at start: %d, validators at end: %d)", len(allValidators), startHeight, endHeight, len(startValidators.Validators), len(endValidators.Validators))
+	s.T().Logf("Checking %d total validators for proposer activity end height %d", len(allValidators), endHeight)
 
-	// Verify every validator appears in proposers
 	var missingValidators []string
 	for validatorAddr := range allValidators {
 		if _, ok := proposers[validatorAddr]; !ok {
@@ -362,7 +343,7 @@ func (s *CelestiaTestSuite) validateAllValidatorsProposed(startValidators, endVa
 	}
 
 	if len(missingValidators) > 0 {
-		return fmt.Errorf("%d validator(s) never proposed blocks from height %d to %d: %v", len(missingValidators), startHeight, endHeight, missingValidators)
+		return fmt.Errorf("%d validator(s) never proposed blocks for %d heights", len(missingValidators), endHeight)
 	}
 
 	return nil
@@ -383,11 +364,14 @@ func (s *CelestiaTestSuite) validateNodesNotHalted(ctx context.Context, chain ta
 		if err != nil {
 			return fmt.Errorf("failed to get RPC client for node %d: %w", i, err)
 		}
+
 		status, err := nodeClient.Status(ctx)
 		if err != nil {
 			return fmt.Errorf("failed to get status for node %d: %w", i, err)
 		}
-		if status.SyncInfo.LatestBlockHeight < endHeight {
+
+		// the +10 is just to leave room for error
+		if (status.SyncInfo.LatestBlockHeight + 10) < endHeight {
 			haltedNodes = append(haltedNodes, fmt.Sprintf("node_%d (height_%d)", i, status.SyncInfo.LatestBlockHeight))
 		}
 	}
diff --git a/test/docker-e2e/e2e_upgrade_test.go b/test/docker-e2e/e2e_upgrade_test.go
index 85bad272..f058dd95 100644
--- a/test/docker-e2e/e2e_upgrade_test.go
+++ b/test/docker-e2e/e2e_upgrade_test.go
@@ -128,7 +128,7 @@ func (s *CelestiaTestSuite) runUpgradeTest(ImageTag string, baseAppVersion, targ
 
 	s.T().Logf("Checking validator liveness from height %d with minimum %d blocks per validator", startHeight, defaultBlocksPerValidator)
 	s.Require().NoError(
-		s.CheckLiveness(ctx, chain, startHeight),
+		s.CheckLiveness(ctx, chain),
 		"validator liveness check failed",
 	)
 }
diff --git a/test/docker-e2e/go.mod b/test/docker-e2e/go.mod
index 84740fbe..adf1974d 100644
--- a/test/docker-e2e/go.mod
+++ b/test/docker-e2e/go.mod
@@ -6,13 +6,15 @@ require (
 	cosmossdk.io/math v1.5.3
 	github.com/celestiaorg/celestia-app/v6 v6.0.0-alpha
 	github.com/celestiaorg/go-square/v2 v2.3.1
-	github.com/celestiaorg/tastora v0.1.2
+	github.com/celestiaorg/tastora v0.2.0
 	github.com/cometbft/cometbft v0.38.17
 	github.com/cosmos/cosmos-sdk v0.50.13
+	github.com/cosmos/ibc-go/v8 v8.7.0
 	github.com/docker/docker v28.3.3+incompatible
 	github.com/moby/moby v27.5.1+incompatible
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/zap v1.27.0
+	golang.org/x/sync v0.16.0
 )
 
 require (
@@ -50,24 +52,24 @@ require (
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/avast/retry-go/v4 v4.6.1 // indirect
 	github.com/aws/aws-sdk-go v1.55.5 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.37.2 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.18.3 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
-	github.com/aws/smithy-go v1.22.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect
+	github.com/aws/smithy-go v1.22.5 // indirect
 	github.com/bcp-innovations/hyperlane-cosmos v1.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
@@ -101,7 +103,6 @@ require (
 	github.com/cosmos/iavl v1.2.6 // indirect
 	github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v8 v8.2.0 // indirect
 	github.com/cosmos/ibc-go/modules/capability v1.0.1 // indirect
-	github.com/cosmos/ibc-go/v8 v8.7.0 // indirect
 	github.com/cosmos/ics23/go v0.11.0 // indirect
 	github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect
 	github.com/danieljoos/wincred v1.2.1 // indirect
@@ -249,7 +250,6 @@ require (
 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
-	golang.org/x/sync v0.16.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/term v0.33.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
@@ -274,7 +274,6 @@ replace (
 	github.com/cometbft/cometbft => github.com/celestiaorg/celestia-core v1.57.3-tm-v0.38.17
 	github.com/cosmos/cosmos-sdk => github.com/celestiaorg/cosmos-sdk v1.29.5-sdk-v0.50.14
 	github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2
-	// goleveldb: canonical version
 	github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
 	// celestia-core(v0.34.x): used for multiplexing abci v1 requests
 	github.com/tendermint/tendermint => github.com/celestiaorg/celestia-core v1.55.0-tm-v0.34.35
diff --git a/test/docker-e2e/go.sum b/test/docker-e2e/go.sum
index 266178d7..c0ec8284 100644
--- a/test/docker-e2e/go.sum
+++ b/test/docker-e2e/go.sum
@@ -282,42 +282,42 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX
 github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
 github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
-github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
-github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
+github.com/aws/aws-sdk-go-v2 v1.37.2 h1:xkW1iMYawzcmYFYEV0UCMxc8gSsjCGEhBXQkdQywVbo=
+github.com/aws/aws-sdk-go-v2 v1.37.2/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
 github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
 github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.3 h1:ptfyXmv+ooxzFwyuBth0yqABcjVIkjDL0iTYZBSbum8=
+github.com/aws/aws-sdk-go-v2/credentials v1.18.3/go.mod h1:Q43Nci++Wohb0qUh4m54sNln0dbxJw8PvQWkrwOkGOI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2 h1:nRniHAvjFJGUCl04F3WaAj7qp/rcz5Gi1OVoj5ErBkc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.2/go.mod h1:eJDFKAMHHUvv4a0Zfa7bQb//wFNUXGrbFpYRCHe2kD0=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 h1:sPiRHLVUIIQcoVZTNwqQcdtjkqkPopyYmIX0M5ElRf4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2/go.mod h1:ik86P3sgV+Bk7c1tBFCwI3VxMoSEwl4YkRB9xn1s340=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 h1:ZdzDAg075H6stMZtbD2o+PyB933M/f20e9WmCBC17wA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2/go.mod h1:eE1IIzXG9sdZCB0pNNpMpsYTLl4YdOQD3njiVN1e/E4=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2 h1:oxmDEO14NBZJbK/M8y3brhMFEIGN4j8a6Aq8eY0sqlo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.2/go.mod h1:4hH+8QCrk1uRWDPsVfsNDUup3taAjO8Dnx63au7smAU=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE=
 github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
-github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
-github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 h1:j7/jTOjWeJDolPwZ/J4yZ7dUsxsWZEsxNwH5O7F8eEA=
+github.com/aws/aws-sdk-go-v2/service/sso v1.27.0/go.mod h1:M0xdEPQtgpNT7kdAX4/vOAPkFj60hSQRb7TvW9B0iug=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 h1:ywQF2N4VjqX+Psw+jLjMmUL2g1RDHlvri3NxHA08MGI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
+github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
+github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
+github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
+github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
 github.com/bcp-innovations/hyperlane-cosmos v1.0.1 h1:gT8OqyJ866Q6AHOlIXKxSdLjd0p8crKG9XXERIWoh4c=
 github.com/bcp-innovations/hyperlane-cosmos v1.0.1/go.mod h1:3yfa0io5Ii6GmhHWsWl2LEAOEHsqWuMgw2R02+LPogw=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -358,8 +358,8 @@ github.com/celestiaorg/nmt v0.24.0 h1:23u/mneledCG/6xWl1cZeTPrzRLpDTAHxMFsqoOL+B
 github.com/celestiaorg/nmt v0.24.0/go.mod h1:kYfIjRq5rmA2mJnv41GLWkxn5KyLNPlma3v5Q68rHdI=
 github.com/celestiaorg/rsmt2d v0.14.0 h1:L7XJ3tRJDY8sQcvCjzHq0L7JmsmaSD+VItymIYFLqYc=
 github.com/celestiaorg/rsmt2d v0.14.0/go.mod h1:4kxqiTdFev49sGiKXTDjohbWYOG5GlcIfftTgaBJnpc=
-github.com/celestiaorg/tastora v0.1.2 h1:6USWom4y+APD00MFMtPI2tmuEy29d1ZeaVlGnpt3vNE=
-github.com/celestiaorg/tastora v0.1.2/go.mod h1:A/DWauemWmg353BvhWYPCd8XbED12UCUiQsL+1sz9BU=
+github.com/celestiaorg/tastora v0.2.0 h1:C3pfPCW25HQC73Hmq9xFoyFQ3qgAcCc4InZ0ITTWnY4=
+github.com/celestiaorg/tastora v0.2.0/go.mod h1:D0/syL4Jo2i2S6VCCNhDz8BGTzOxtopOoWYgcgHnkVc=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
diff --git a/test/docker-e2e/networks/networks.go b/test/docker-e2e/networks/networks.go
index 75aec2a4..6b589f54 100644
--- a/test/docker-e2e/networks/networks.go
+++ b/test/docker-e2e/networks/networks.go
@@ -10,6 +10,7 @@ import (
 	"github.com/celestiaorg/celestia-app/v6/app"
 	"github.com/celestiaorg/celestia-app/v6/test/util/testnode"
 	celestiadockertypes "github.com/celestiaorg/tastora/framework/docker"
+	tastoracontainertypes "github.com/celestiaorg/tastora/framework/docker/container"
 	rpchttp "github.com/cometbft/cometbft/rpc/client/http"
 	"github.com/cosmos/cosmos-sdk/types/module/testutil"
 	"github.com/moby/moby/client"
@@ -44,7 +45,7 @@ func NewChainBuilder(t *testing.T, chainConfig *Config, cfg *dockerchain.Config)
 		WithChainID(chainConfig.ChainID).
 		WithDockerClient(cfg.DockerClient).
 		WithDockerNetworkID(cfg.DockerNetworkID).
-		WithImage(celestiadockertypes.NewDockerImage(cfg.Image, cfg.Tag, "10001:10001")).
+		WithImage(tastoracontainertypes.NewImage(cfg.Image, cfg.Tag, "10001:10001")).
 		WithAdditionalStartArgs("--force-no-bbr").
 		WithEncodingConfig(&encodingConfig).
 		WithGenesis(genesisBz)
diff --git a/test/util/test_app.go b/test/util/test_app.go
index 20cd35fe..805e69e9 100644
--- a/test/util/test_app.go
+++ b/test/util/test_app.go
@@ -91,7 +91,6 @@ func initialiseTestApp(testApp *app.App, valSet *tmtypes.ValidatorSet) {
 
 // NewTestApp creates a new app instance with an empty memDB and a no-op logger.
 func NewTestApp() *app.App {
-	// var anteOpt = func(bapp *baseapp.BaseApp) { bapp.SetAnteHandler(nil) }
 	db := dbm.NewMemDB()
 
 	return app.New(