<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: marselzaripov1988</title>
    <description>The latest articles on DEV Community by marselzaripov1988 (@marselzaripov1988).</description>
    <link>https://dev.to/marselzaripov1988</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179592%2F735afcdb-2a5d-4fff-980e-c9c1a2e81899.png</url>
      <title>DEV Community: marselzaripov1988</title>
      <link>https://dev.to/marselzaripov1988</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marselzaripov1988"/>
    <language>en</language>
    <item>
      <title>Cosmos SDK 0.42 0.53 on a live chain: the bugs that fail silently</title>
      <dc:creator>marselzaripov1988</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:44:33 +0000</pubDate>
      <link>https://dev.to/marselzaripov1988/cosmos-sdk-042-053-on-a-live-chain-the-bugs-that-fail-silently-3b9o</link>
      <guid>https://dev.to/marselzaripov1988/cosmos-sdk-042-053-on-a-live-chain-the-bugs-that-fail-silently-3b9o</guid>
      <description>&lt;p&gt;If your Cosmos SDK chain is still on 0.45 or older, it has been running without security patches for a while. Cosmos Labs dropped the LTS model in favour of release families. Only two are supported right now: 2025.1 (SDK 0.53, CometBFT 0.38) and 2026.1 (SDK 0.55, CometBFT 0.40). Everything below 0.53 is end-of-life.&lt;/p&gt;

&lt;p&gt;I recently moved Artery Network from SDK v0.42.6 / Tendermint v0.34 / Go 1.15 to SDK v0.53.8 / CometBFT v0.38. It's a live chain with eight custom modules and roughly 383k accounts on mainnet. It runs its own bank module instead of x/bank and its own delegation model built on a referral tree.&lt;/p&gt;

&lt;p&gt;This post is not a rewrite of UPGRADING.md. It covers what the official guides don't: the places where the chain compiles, passes its tests, keeps producing blocks, and is still broken.&lt;/p&gt;

&lt;p&gt;All the code is public: github.com/marselzaripov1988/artr. Each hop has its own branch, and every fix commit explains the cause in detail.&lt;/p&gt;

&lt;p&gt;Strategy: one hop per branch&lt;br&gt;
It's tempting to bump go.mod straight to 0.53 and fix compile errors until it builds. Don't. You'll get thousands of errors and won't be able to tell a mechanical rename from a change in behaviour.&lt;/p&gt;

&lt;p&gt;I went one major version at a time:&lt;/p&gt;

&lt;p&gt;Branch  SDK Consensus   Go  What the hop required&lt;br&gt;
master  0.42.6  Tendermint 0.34.11  1.15    starting point&lt;br&gt;
migrate/sdk-0.45    0.45.16 Tendermint 0.34.27  1.19    codec renames, ConsensusVersion, SupplyI and sdk.AddrLen gone&lt;br&gt;
migrate/sdk-0.46    0.46.16 Tendermint 0.34.29  1.19    legacy REST removed, Tendermint → CometBFT in code&lt;br&gt;
migrate/sdk-0.47    0.47.17 CometBFT 0.37   1.19    params moved out of x/params into module stores&lt;br&gt;
migrate/sdk-0.50    0.50.15 CometBFT 0.38   1.22    ABCI 2.0, cosmossdk.io/*, appmodule&lt;br&gt;
migrate/sdk-0.53    0.53.8  CometBFT 0.38   1.23    version bump only&lt;br&gt;
Three rules:&lt;/p&gt;

&lt;p&gt;A hop is done when every module test suite is green, not just when it builds.&lt;br&gt;
Keep mechanical changes separate from decisions. Package renames go in one commit. Any place where I had to choose a behaviour gets its own commit that explains the choice.&lt;br&gt;
Replace before it's removed. If the SDK is about to delete something your state format depends on, re-implement it on the previous version, while the original still exists, and test the two against each other.&lt;br&gt;
Rule 3 saved the validator keys. types/bech32/legacybech32 is gone in 0.50, but Artery's validator consensus keys (artrvalconspub...) use it in genesis, in x/noding state and in messages. You can't change the state format of a running network. So on 0.47 I re-implemented the encoding and checked it against the original on 100 random keys. Only after that did I bump the SDK.&lt;/p&gt;

&lt;p&gt;The 0.50 → 0.53 hop, by the way, needed zero code changes. All the work was in 0.47 and 0.50.&lt;/p&gt;

&lt;p&gt;The three heavy lifts&lt;br&gt;
0.47: params leave x/params&lt;br&gt;
From 0.47 on, each module keeps its own params, and x/params is deprecated. All eight Artery modules read params through x/params subspaces.&lt;/p&gt;

&lt;p&gt;Moving them is mechanical. The interesting part came after. Once all eight modules had moved, the x/params store was still mounted, but empty. Since 0.46, CacheMultiStoreWithVersion requires a version from every mounted IAVL tree, and a completely empty tree doesn't have one.&lt;/p&gt;

&lt;p&gt;Result: every state query failed, whatever module it was for. The fix was to rip out x/params entirely (keeper, store, transient store, nine subspaces). Index stores that are legitimately empty now get a marker entry in InitGenesis.&lt;/p&gt;

&lt;p&gt;0.50: ABCI 2.0, and a judgement call&lt;br&gt;
BeginBlock and EndBlock are folded into FinalizeBlock, and handlers no longer receive the request. Votes and evidence now come through the context: ctx.VoteInfos() and ctx.CometInfo().&lt;/p&gt;

&lt;p&gt;That's mostly mechanical, with one exception. Validator liveness used the boolean SignedLastBlock. CometBFT 0.38 replaced it with a three-state BlockIdFlag: committed, voted nil, absent.&lt;/p&gt;

&lt;p&gt;// before (Tendermint 0.34)&lt;br&gt;
if vote.SignedLastBlock { ... }&lt;/p&gt;

&lt;p&gt;// after (CometBFT 0.38)&lt;br&gt;
if vote.BlockIdFlag != cmtproto.BlockIDFlagAbsent { ... }&lt;br&gt;
This is not a rename. It's a decision: does a nil vote count as a missed block? I count only absent. That matches what the SDK itself does in x/slashing, and it's closer to the old meaning, since in Tendermint 0.34 a nil vote still carried a signature.&lt;/p&gt;

&lt;p&gt;0.50: the message signer moves into proto&lt;br&gt;
This one is covered in detail below, because it belongs to the silent category.&lt;/p&gt;

&lt;p&gt;Six bugs that left nothing in the logs&lt;br&gt;
This is the useful part. Each of these bugs survived compilation and at least some of the tests. Several survived running devnets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The upgrade is scheduled, the chain never halts
A Cosmos software upgrade works like this: a governance proposal calls ScheduleUpgrade, every node halts at the same height, and operators swap the binary. For a mainnet migration this is critical, because halting at one height gives every node an identical state export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In 0.50, x/upgrade checks the plan in a new PreBlock phase, not in BeginBlock. The app never called PreBlock. upgrade was still listed first in BeginBlockers, but it no longer has a BeginBlock.&lt;/p&gt;

&lt;p&gt;From the outside: the vote passes, the plan is stored, the halt height arrives, and the chain just keeps going. No error, no warning.&lt;/p&gt;

&lt;p&gt;A test didn't catch this. A full rehearsal on a test stand did: proposal, vote, plan at height 275, and the nodes went straight past it.&lt;/p&gt;

&lt;p&gt;app.mm.SetOrderPreBlockers(&lt;br&gt;
    upgradetypes.ModuleName,&lt;br&gt;
    authtypes.ModuleName, // x/auth has PreBlock too; the manager panics if any module is missing&lt;br&gt;
)&lt;br&gt;
app.SetPreBlocker(app.PreBlocker)&lt;/p&gt;

&lt;p&gt;func (app *ArteryApp) PreBlocker(ctx sdk.Context, _ *abci.RequestFinalizeBlock) (*sdk.ResponsePreBlock, error) {&lt;br&gt;
    return app.mm.PreBlock(ctx)&lt;br&gt;
}&lt;br&gt;
After the fix, both stand nodes halted exactly at the planned height.&lt;/p&gt;

&lt;p&gt;Takeaway: verify the upgrade path with a rehearsal. A plan sitting in state proves nothing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Time-based upgrade plans the SDK now rejects
The second bug was in the same path. Artery's governance module scheduled upgrades by time: at some point the chain had deprecated height in favour of time. Since 0.47 the SDK goes the other way: Plan.ValidateBasic rejects a non-empty Time and requires a positive height.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The worst part is what the failure would have looked like. The proposal handler logs the error and moves on. The proposal stays in history as passed, everyone believes the halt is scheduled, and the chain doesn't stop.&lt;/p&gt;

&lt;p&gt;The fix is to schedule by height. That's also more correct: every node has its own clock, but height is the same for all of them. The subtle part is history. The mainnet export contains old time-based proposals, and you can't declare half the chain's history invalid. So historical validation checks structure only.&lt;/p&gt;

&lt;p&gt;The test asserts that "a plan is scheduled", not "no error", because the empty plan was the bug. A mutation check confirmed that the old code fails it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No transaction can be sent
After the 0.53 hop, every artrd tx ... command failed before broadcasting:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;no cosmos.msg.v1.signer option found&lt;br&gt;
Since 0.50, x/tx finds the signer through a proto option instead of the GetSigners() method. Artery's protos predate that requirement. Module tests didn't catch it, because they call keepers directly and skip signing.&lt;/p&gt;

&lt;p&gt;I annotated all 28 message types across seven modules. Each signer field was taken from the existing GetSigners(), not guessed: the profile module has sender/address pairs where the signer isn't the account the message is about.&lt;/p&gt;

&lt;p&gt;message MsgDelegate {&lt;br&gt;
  option (cosmos.msg.v1.signer) = "address";&lt;br&gt;
  ...&lt;br&gt;
}&lt;br&gt;
Verified on testnet: a delegation went through with code 0 at height 91,985. It was the first transaction in 91,985 blocks. The chain had been alive the whole time, and accepting nothing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Writes and reads on different key prefixes
When the params moved, module data went under prefixes, but some keeper paths still used the raw store. Writes went to one place and reads came from another.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In x/noding the validator set came back empty and the node crashed on start. In x/referral tree lookups returned empty records. Neither raised an error, because an empty result isn't one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Genesis export read past the end of a slice
SDK 0.43 removed sdk.AddrLen, since addresses became variable-length. I replaced it with address.Len. But that's 32 bytes, the ADR-028 SHA-256 length, and Artery addresses are 20.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// before: bug&lt;br&gt;
addr := key[:address.Len] // 32&lt;/p&gt;

&lt;p&gt;// after&lt;br&gt;
const addrLen = 20&lt;br&gt;
addr := key[:addrLen]&lt;br&gt;
Genesis export split keys at the wrong boundary. The bug went unnoticed through three hops and every devnet run, because devnets never export state. The first export/import test caught it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The module is wired in but never initialised
This one came up while adding IBC. The module manager runs InitGenesis only for modules that have a section in genesis. Neither the test genesis nor the mainnet export has ibc or transfer sections, so why would they.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The chain starts and runs normally. Then the first state export fails with params are not set in store. Exactly the same trap came back later in a CosmWasm trial, this time with wasm.v1.Params.&lt;/p&gt;

&lt;p&gt;Why 0.53 and not 0.55&lt;br&gt;
0.55 is newer, but it comes with CometBFT 0.40, which is another consensus-layer change. For a chain that has just been through ABCI 2.0, that's risk you don't need. 0.53 is supported and keeps CometBFT 0.38 as it is. Moving to 0.55 makes sense once 0.53 is proven on mainnet.&lt;/p&gt;

&lt;p&gt;Checklist for your own migration&lt;br&gt;
not done&lt;br&gt;
One hop per branch. A hop is done only when every test suite is green.&lt;br&gt;
not done&lt;br&gt;
Rehearse the full upgrade cycle on a stand: proposal → vote → plan → halt. A plan in state proves nothing.&lt;br&gt;
not done&lt;br&gt;
Send a real transaction through the CLI after every hop from 0.50 on. Keeper tests skip signing.&lt;br&gt;
not done&lt;br&gt;
Run a genesis export → import test. Devnets never do it, and you can't move mainnet without it.&lt;br&gt;
not done&lt;br&gt;
Give every new module a genesis section, or it stays uninitialised.&lt;br&gt;
not done&lt;br&gt;
Replace removed constants by meaning, not by name. address.Len is not "the length of your addresses".&lt;br&gt;
not done&lt;br&gt;
An empty store is not a harmless store. A mounted, empty tree breaks queries for every module.&lt;br&gt;
not done&lt;br&gt;
Set up CI before you start. Artery had none, and two breakages were found on the server instead of in checks. Add a check that generated protobuf code matches the .proto files; that's how the signer annotations nearly got lost.&lt;br&gt;
Wrapping up&lt;br&gt;
Five hops, 56 commits, about 150 files changed in app and module code. On top of that: IBC (ibc-go v10, ICS-20), slashing for double-signing, and a CosmWasm trial. The 0.53 build runs on a test stand, and the mainnet switch-over is the next step.&lt;/p&gt;

&lt;p&gt;If your chain is stuck on an old SDK version, I take on migrations like this one. Reach me on Telegram (@mars_developer), or see the code on GitHub.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>web3</category>
      <category>crypto</category>
    </item>
  </channel>
</rss>
