Software Upgrade Proposal: Terra Classic Core v4.1.1

TLDR

This proposal schedules a coordinated upgrade of the Terra Classic mainnet from
v4.0.2 to v4.1.1.

The main reason for the upgrade is a fix for stuck (“ghost”) transactions:
under certain conditions a transaction could be accepted by a node, never be
included in a block, and never be dropped again. The account that signed it was
then unable to send any further transaction. This happened on mainnet to a
validator, who could not unjail for an extended period
(#648).

v4.1.1 removes the cause of that problem and adds a safeguard that prevents
this class of problem in general. It also contains two small fixes that harden
the tax module against panics, and a fix that makes accounts with a vesting
schedule (LazyGradedVestingAccount) readable and usable again.

The upgrade contains no state migrations, no new modules and no parameter
changes
. One of the fixes changes which block proposals a validator accepts,
and another changes whether transactions from vesting accounts succeed, so all
validators have to switch at the same height. That is why a governance upgrade
is needed instead of a rolling update.

Mempool parity: what happened, in plain words

No technical background needed for this section. The technical details follow
in the next one.

How a transaction normally gets into a block. When you send a transaction,
it does not go into a block right away. It first waits in a queue on the
validators’ computers until a validator picks it up and puts it into the next
block. This queue is called the mempool.

Each validator actually keeps two lists of waiting transactions:

  • one list for passing transactions on to the other validators, and
  • one list for building the next block.

Both lists are supposed to always contain the same transactions.

What went wrong. In a rare case the two lists stopped matching. A
transaction was taken off the “build the next block” list, but stayed on the
“pass it on” list. From then on:

  • no validator ever put it into a block, because it was missing from the list
    blocks are built from, and
  • it never disappeared either, because validators kept passing it around to
    each other.

The transaction was stuck: never confirmed, never rejected.

Why that blocks a whole account. Every account’s transactions are numbered
and must be processed strictly in order: number 5 can only go through after
number 4. If number 4 is stuck forever, everything the account sends afterwards
waits behind it. The account is effectively frozen, although nothing is wrong
with the account itself and no funds are lost.

What this meant in practice. This happened to a validator on mainnet. Their
validator had been jailed, and the transaction to unjail it was queued behind a
stuck transaction. They were unable to unjail for an extended period, and
neither the validator nor their delegators could do anything about it with
normal means. It took a manual rescue with the help of another validator
running a patched node to resolve it.

Why the transaction was dropped from one list in the first place. Every
transaction comes with a budget for the computing work it may use (“gas”).
Because of a bug, the chain charged a small extra amount of work at a moment
when it should not have. A transaction with a budget that was just enough at
the entrance check was then found to be just short one step later, and was
dropped from one list but not the other.

What the upgrade changes.

  1. The extra charge at the wrong moment is removed, so the entrance check and
    the block-building step now agree with each other. This removes the cause.
  2. Validators now compare the two lists after every block. A transaction that
    is gone from one list is removed from the other one too. So even if a
    similar mismatch ever happens for a different reason, the transaction is
    simply dropped and the sender can send it again, instead of the account
    being frozen.

What it means for you. Nothing changes in how you use the chain: no changes
to fees, taxes, balances or staking. Funds were not at risk from this bug, and
they are not affected by the upgrade. The chain will pause briefly at the
upgrade height while validators switch to the new version.

What is fixed (technical details)

1. Mempool parity — stuck transactions (#689)

The problem. A node keeps pending transactions in two places: the CometBFT
mempool (used for gossip between nodes) and the application mempool (used to
build blocks). The two could drift apart:

  • The dynamic commission (dyncomm) post handler also ran while a block
    proposal was being prepared and verified, although no messages are
    executed in those phases. It consumed about 7,000 gas there.
  • A MsgEditValidator whose gas limit was enough for the admission check but
    not for those extra 7,000 gas was accepted into both mempools, then failed
    when the proposer tried to put it into a block. It was removed from the
    application mempool, but CometBFT kept it and kept gossiping it.
  • The result was a transaction that could never be included and never expired.
    It blocked the account’s sequence number, so every later transaction of that
    account was stuck behind it.
  • As a side effect, a proposer holding such a transaction could build a
    proposal that the other validators rejected, costing the network a consensus
    round.

The fix has two parts:

  • Root cause: the dyncomm post handler now runs only where messages are
    actually executed (block execution, and simulation so that --gas auto stays
    accurate). It no longer charges gas while proposals are prepared or verified.
    Gas usage and results of executed transactions are unchanged.
  • Safeguard: a new mempool parity check keeps both mempools in line. On every
    re-check after a block, a transaction that is no longer in the application
    mempool is evicted from the CometBFT mempool as well, and a transaction that
    fails admission is removed from the application mempool. A dropped
    transaction is therefore reported as dropped and can simply be re-sent.

2. Tax: panic when the reverse-charge flag is absent (#667)

IsReverseCharge assumed that a flag was always present in the transaction
context and panicked when it was not. A missing flag is now treated as “no
reverse charge”.

3. Tax: division by zero in the community-tax split (#670)

With a community tax of 1.0 and an oracle split rate of 0 — both values can
be set by governance — the tax split divided by zero, which would have made
every taxable transaction panic. The divisor is now guarded. For all other
parameter values, including the current mainnet parameters, the calculation and
its rounding are unchanged.

4. Vesting: accounts with a vesting schedule could not be read (#692)

The problem. With Cosmos SDK v0.53 the account store decodes every
account as sdk.AccountI. Terra’s LazyGradedVestingAccount was registered
only for the deprecated x/auth interface of the same name, which is a
different interface to the decoder. Reading such an account therefore failed:

collections: encoding error: value decode: no concrete type registered for type URL /terra.vesting.v1beta1.LazyGradedVestingAccount against interface *types.AccountI
  • Queries for these accounts (account, account info, spendable balances)
    return this error on mainnet today. Plain balance queries still work.
  • A transaction signed by such an account fails, because the signer cannot be
    loaded. The same applies to moving funds out of such an account on its
    behalf.
  • The affected accounts are the vesting accounts from genesis. Their funds are
    not lost; the accounts are just not usable.

The fix. LazyGradedVestingAccount and BaseVestingAccount are now
registered for sdk.AccountI. Nothing stored on chain changes and no migration
is needed: the accounts become readable as soon as the new binary runs.

Because transactions from these accounts fail on v4.0.2 and succeed on
v4.1.1, this fix also has to be activated by all validators at the same
height.

5. Documentation (#550)

Updated REST API demo endpoint in the README.

Impact

Who Impact
Users / dApps No action needed. No change to fees, taxes, balances or contract behaviour. Transactions can no longer get permanently stuck in the way described above. Holders of vesting accounts (LazyGradedVestingAccount) can query their account and send transactions again.
Validators / node operators Must switch to v4.1.1 at the upgrade height. See below.
Exchanges / infrastructure Expect a short halt at the upgrade height. Nodes must be upgraded to continue syncing. No API or state format changes.
  • State migrations: none (the handler only runs the standard module
    migrations, and no module version changes).
  • Store upgrades: none.
  • Dependencies: unchanged from v4.0.2 (Cosmos SDK v0.53.6, CometBFT
    v0.38.21, wasmvm v3.0.8, Go 1.24).

Testing

In addition to unit tests for all four fixes, the upgrade was rehearsed on an
in-place testnet created from real mainnet state (height 30679563), using a
binary built from the v4.1.0 commit (v4.1.1 is v4.1.0 plus the vesting
fix #692 only):

  • Before the upgrade (v4.0.2): the stuck-transaction scenario was reproduced
    — the transaction stayed in the CometBFT mempool, was never proposed, and the
    account was blocked.
  • The v14_4 upgrade proposal passed and the upgrade was applied at the
    scheduled height without errors.
  • After the upgrade (v4.1.0): the same scenario no longer produces a stuck
    transaction. The transaction is included (and consumes its sequence), dropped
    transactions are evicted from the CometBFT mempool, and follow-up
    transactions of the account go through.
  • Smoke tests after the upgrade passed: bank, staking (delegate, redelegate,
    unbond), and CosmWasm (store, instantiate, execute, and queries/executions
    against contracts deployed before the upgrade, including existing mainnet
    contracts).

The root-cause fix was also verified earlier against the original mainnet
transactions from issue #648 on a copy of mainnet state.

The vesting fix (#692) was added after the rehearsal. The decoding error was
reproduced against mainnet for vesting accounts from genesis, and with the
application’s own codec: decoding a LazyGradedVestingAccount fails on
v4.1.0 and succeeds on v4.1.1.

Instructions for validators and node operators

Keep running v4.0.2 until the upgrade height. Do not switch to v4.1.1
early: it accepts block proposals slightly differently from v4.0.2.

At height 31010600 the node halts with
UPGRADE "v14_4" NEEDED at height: 31010600.

Build the new binary:

git clone https://github.com/classic-terra/core.git && cd core
git fetch --tags
git checkout v4.1.1
make install
terrad version   # 4.1.1

With Cosmovisor — place the binary before the upgrade height:

mkdir -p $DAEMON_HOME/cosmovisor/upgrades/v14_4/bin
cp $(which terrad) $DAEMON_HOME/cosmovisor/upgrades/v14_4/bin/terrad

Without Cosmovisor — after the node has halted at the upgrade height, stop
the service, replace the terrad binary with v4.1.1, and start it again.

The chain resumes as soon as validators holding more than 2/3 of the voting
power are running the new binary.

Timeline

Step Date
Proposal submitted 2026-10-12 12:29 UTC
Voting period ends (7 days after submission) 2026-10-19 12:29 UTC
Upgrade height 31010600* ≈ 2026-10-26, 15:00 UTC

*The upgrade height is an estimate and subject to a correction when the proposal is put up for on-chain vote. Varying blocktimes and network load can shift the estimated block height significantly.

Voting

  • Yes — you agree to upgrade the mainnet to v4.1.1 at the height above.
  • No — you do not agree to the upgrade.
  • No with veto — you consider the proposal harmful or spam.
  • Abstain — you take part in the quorum without voting for or against.