Skip to content

Commit 70b7eb0

Browse files
hoank101TropicalDog17
authored andcommitted
fix: register slashing unjail via AutoCLI by suppressing only auto-discovered tx modules (#644)
1 parent f6ebb16 commit 70b7eb0

File tree

9 files changed

+196
-19
lines changed

9 files changed

+196
-19
lines changed

.github/workflows/e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ jobs:
9898
- TestAuthz
9999
- TestFeeTaxWasm
100100
- TestAPIRegression
101+
- TestOracleDelegateFeedConsent
101102

102103
steps:
103104
# 1. Checkout

app/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"path/filepath"
1111

12+
autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
1213
sdklog "cosmossdk.io/log"
1314
upgradetypes "cosmossdk.io/x/upgrade/types"
1415
"github.com/CosmWasm/wasmd/x/wasm"
@@ -59,6 +60,7 @@ import (
5960
"github.com/cosmos/cosmos-sdk/codec"
6061
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
6162
"github.com/cosmos/cosmos-sdk/runtime"
63+
runtimeservices "github.com/cosmos/cosmos-sdk/runtime/services"
6264
"github.com/cosmos/cosmos-sdk/server"
6365
"github.com/cosmos/cosmos-sdk/server/api"
6466
"github.com/cosmos/cosmos-sdk/server/config"
@@ -244,6 +246,9 @@ func NewTerraApp(
244246
if err != nil {
245247
panic(err)
246248
}
249+
250+
autocliv1.RegisterQueryServer(app.GRPCQueryRouter(), runtimeservices.NewAutoCLIQueryService(app.mm.Modules))
251+
247252
app.setupUpgradeHandlers()
248253
app.setupUpgradeStoreLoaders()
249254

cmd/terrad/root.go

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8-
"sort"
98

109
"cosmossdk.io/client/v2/autocli"
10+
"cosmossdk.io/core/appmodule"
1111
log "cosmossdk.io/log"
1212
sdklog "cosmossdk.io/log"
1313
store "cosmossdk.io/store"
@@ -33,7 +33,6 @@ import (
3333
"github.com/cosmos/cosmos-sdk/client/pruning"
3434
snapshot "github.com/cosmos/cosmos-sdk/client/snapshot"
3535
addresscodec "github.com/cosmos/cosmos-sdk/codec/address"
36-
"github.com/cosmos/cosmos-sdk/runtime/services"
3736
"github.com/cosmos/cosmos-sdk/server"
3837
servertypes "github.com/cosmos/cosmos-sdk/server/types"
3938
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
@@ -153,23 +152,18 @@ func NewRootCmd() (*cobra.Command, params.EncodingConfig) {
153152
// This adds missing upstream module commands (e.g., staking, distribution, gov) under query/tx.
154153
{
155154
sc := encodingConfig.InterfaceRegistry.SigningContext()
156-
modOpts := services.ExtractAutoCLIOptions(tempApp.Modules())
157-
// Only enhance Query via AutoCLI to avoid conflicting/duplicate TX flags and commands
158-
// Iterate deterministically to ensure consistent behavior across runs
159-
moduleNames := make([]string, 0, len(modOpts))
160-
for moduleName := range modOpts {
161-
moduleNames = append(moduleNames, moduleName)
162-
}
163-
sort.Strings(moduleNames)
155+
modules := make(map[string]appmodule.AppModule)
164156

165-
for _, moduleName := range moduleNames {
166-
opt := modOpts[moduleName]
167-
if opt != nil {
168-
opt.Tx = nil
157+
for _, m := range tempApp.Modules() {
158+
if moduleWithName, ok := m.(module.HasName); ok {
159+
moduleName := moduleWithName.Name()
160+
if appModule, ok := moduleWithName.(appmodule.AppModule); ok {
161+
modules[moduleName] = appModule
162+
}
169163
}
170164
}
171165
autoOpts := autocli.AppOptions{
172-
ModuleOptions: modOpts,
166+
Modules: modules,
173167
AddressCodec: sc.AddressCodec(),
174168
ValidatorAddressCodec: sc.ValidatorAddressCodec(),
175169
ConsensusAddressCodec: addresscodec.NewBech32Codec(sdk.GetConfig().GetBech32ConsensusAddrPrefix()),

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ toolchain go1.24.7
55
module github.com/classic-terra/core/v4
66

77
require (
8+
cosmossdk.io/api v0.9.2
89
cosmossdk.io/client/v2 v2.0.0-beta.8
910
cosmossdk.io/core v0.11.3
1011
cosmossdk.io/errors v1.0.2
@@ -45,7 +46,6 @@ require (
4546
cloud.google.com/go/auth v0.16.4 // indirect
4647
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
4748
cloud.google.com/go/monitoring v1.24.2 // indirect
48-
cosmossdk.io/api v0.9.2 // indirect
4949
cosmossdk.io/collections v1.3.1 // indirect
5050
cosmossdk.io/depinject v1.2.1 // indirect
5151
cosmossdk.io/schema v1.1.0 // indirect

tests/e2e/configurer/chain/commands.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
sdkmath "cosmossdk.io/math"
1313
app "github.com/classic-terra/core/v4/app"
14+
"github.com/classic-terra/core/v4/tests/e2e/containers"
1415
"github.com/classic-terra/core/v4/tests/e2e/initialization"
1516
"github.com/classic-terra/core/v4/types/assets"
1617
"github.com/cometbft/cometbft/libs/bytes"
@@ -544,6 +545,26 @@ func (n *NodeConfig) Status() (resultStatus, error) {
544545
return result, nil
545546
}
546547

548+
// Unjail broadcasts an unjail tx and returns any broadcast-level error.
549+
// It does NOT assert on-chain delivery; callers must verify via QuerySigningInfo.
550+
// Returning an error (rather than panicking) lets callers retry inside Eventually.
551+
func (n *NodeConfig) Unjail(walletName string) error {
552+
n.LogActionF("unjailing validator using wallet %s", walletName)
553+
cmd := []string{
554+
"terrad", "tx", "slashing", "unjail",
555+
fmt.Sprintf("--from=%s", walletName),
556+
fmt.Sprintf("--chain-id=%s", n.chainID),
557+
"--yes", "--keyring-backend=test", "--log_format=json",
558+
fmt.Sprintf("--gas=%d", containers.GasLimit), "--fees=0uluna",
559+
}
560+
_, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "txhash", false)
561+
if err != nil {
562+
return err
563+
}
564+
n.LogActionF("successfully broadcast unjail tx from wallet %s", walletName)
565+
return nil
566+
}
567+
547568
func (n *NodeConfig) DelegateFeedConsent(feederAddr string, walletName string) {
548569
n.LogActionF("delegating feed consent to %s from wallet %s", feederAddr, walletName)
549570
cmd := []string{"terrad", "tx", "oracle", "set-feeder", feederAddr, fmt.Sprintf("--from=%s", walletName)}

tests/e2e/configurer/chain/node.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type NodeConfig struct {
1919
initialization.Node
2020

2121
OperatorAddress string
22+
ConsensusAddress string // bech32 terravalcons... format
2223
SnapshotInterval uint64
2324
chainID string
2425
rpcClient *rpchttp.HTTP
@@ -75,6 +76,23 @@ func (n *NodeConfig) Run() error {
7576
"Terra node failed to produce blocks",
7677
)
7778

79+
// Wait for 2 more blocks to confirm p2p connections are established.
80+
// Without this, a just-restarted node may not yet have peers and any
81+
// tx broadcast to it would sit in the local mempool and never be committed.
82+
firstHeight, _ := n.QueryCurrentHeight()
83+
if firstHeight > 0 {
84+
require.Eventually(
85+
n.t,
86+
func() bool {
87+
h, err := n.QueryCurrentHeight()
88+
return err == nil && h >= firstHeight+2
89+
},
90+
initialization.TwoMin,
91+
time.Second,
92+
"Terra node failed to advance blocks after start",
93+
)
94+
}
95+
7896
return n.extractOperatorAddressIfValidator()
7997
}
8098

@@ -114,13 +132,26 @@ func (n *NodeConfig) extractOperatorAddressIfValidator() error {
114132

115133
cmd := []string{"terrad", "debug", "addr", n.PublicKey}
116134
n.t.Logf("extracting validator operator addresses for validator: %s", n.Name)
117-
_, errBuf, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "", false)
135+
outBuf, _, err := n.containerManager.ExecCmd(n.t, n.Name, cmd, "", false)
118136
if err != nil {
119137
return err
120138
}
121-
re := regexp.MustCompile("terravaloper(.{39})")
122-
operAddr := fmt.Sprintf("%s\n", re.FindString(errBuf.String()))
139+
out := outBuf.String()
140+
141+
reOper := regexp.MustCompile("terravaloper(.{39})")
142+
operAddr := fmt.Sprintf("%s\n", reOper.FindString(out))
123143
n.OperatorAddress = strings.TrimSuffix(operAddr, "\n")
144+
145+
// The consensus address is derived from the ed25519 consensus key, which is
146+
// different from the secp256k1 account/operator key fed to "debug addr".
147+
// Use "comet show-address" to read it directly from the node's local keyfiles.
148+
showAddrCmd := []string{"terrad", "comet", "show-address"}
149+
showAddrBuf, _, err := n.containerManager.ExecCmd(n.t, n.Name, showAddrCmd, "", false)
150+
if err != nil {
151+
return err
152+
}
153+
n.ConsensusAddress = strings.TrimSpace(showAddrBuf.String())
154+
124155
return nil
125156
}
126157

tests/e2e/configurer/chain/queries.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,26 @@ func (n *NodeConfig) QueryBurnTaxExemptionList() ([]string, error) {
146146
return taxRateResp.Addresses, nil
147147
}
148148

149+
// QuerySigningInfo returns the jailed_until timestamp string for the given
150+
// consensus address (terravalcons... format). When the validator is not jailed
151+
// the REST API returns the protobuf zero Timestamp as "1970-01-01T00:00:00Z".
152+
func (n *NodeConfig) QuerySigningInfo(consAddress string) (string, error) {
153+
path := fmt.Sprintf("cosmos/slashing/v1beta1/signing_infos/%s", consAddress)
154+
bz, err := n.QueryGRPCGateway(path)
155+
if err != nil {
156+
return "", err
157+
}
158+
var resp struct {
159+
ValSigningInfo struct {
160+
JailedUntil string `json:"jailed_until"`
161+
} `json:"val_signing_info"`
162+
}
163+
if err := json.Unmarshal(bz, &resp); err != nil {
164+
return "", err
165+
}
166+
return resp.ValSigningInfo.JailedUntil, nil
167+
}
168+
149169
func (n *NodeConfig) QueryFeederDelegation(validatorAddr string) (string, error) {
150170
path := fmt.Sprintf("terra/oracle/v1beta1/validators/%s/feeder", validatorAddr)
151171
bz, err := n.QueryGRPCGateway(path)

tests/e2e/e2e_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,83 @@ func (s *IntegrationTestSuite) TestOracleDelegateFeedConsent() {
298298
s.Require().NoError(err)
299299
s.Require().Equal(feederAddr, delegated)
300300
}
301+
302+
// TestSlashingUnjail verifies that a jailed validator can be unjailed via
303+
// "tx slashing unjail", which is only exposed through AutoCLI in SDK v0.53+.
304+
// The test stops a non-default validator to trigger downtime jailing, then
305+
// restarts it and submits the unjail transaction, confirming that
306+
// jailed_until is cleared.
307+
func (s *IntegrationTestSuite) TestSlashingUnjail() {
308+
chain := s.configurer.GetChainConfig(0)
309+
310+
// nodeToJail is the second validator; stopping it keeps chain consensus
311+
// alive (3 of 4 validators remain, well above the 2/3 threshold).
312+
nodeToJail := chain.NodeConfigs[1]
313+
// defaultNode stays running and is used for signing-info queries.
314+
defaultNode, err := chain.GetDefaultNode()
315+
s.Require().NoError(err)
316+
317+
s.Require().NotEmpty(nodeToJail.ConsensusAddress,
318+
"consensus address must be extracted at startup")
319+
320+
// --- jail phase ---
321+
s.T().Log("stopping validator to trigger downtime jailing")
322+
s.Require().NoError(nodeToJail.Stop())
323+
324+
// Wait until the signing info shows jailed_until in the future, meaning
325+
// the slashing module has processed the downtime and jailed the validator.
326+
// The REST API serialises the zero protobuf Timestamp as Unix epoch
327+
// ("1970-01-01T00:00:00Z"), not Go's zero time ("0001-01-01T00:00:00Z").
328+
const notJailed = "1970-01-01T00:00:00Z"
329+
s.Require().Eventually(func() bool {
330+
jailedUntil, err := defaultNode.QuerySigningInfo(nodeToJail.ConsensusAddress)
331+
if err != nil {
332+
return false
333+
}
334+
return jailedUntil != notJailed
335+
}, initialization.FiveMin, 5*time.Second,
336+
"validator was not jailed within the timeout")
337+
338+
// --- unjail phase ---
339+
s.T().Log("restarting validator")
340+
s.Require().NoError(nodeToJail.Run())
341+
342+
// Wait until the real clock has passed jailed_until by at least 5 seconds.
343+
// Submitting the unjail tx while jailed_until is still in the future causes
344+
// DeliverTx to fail even though CheckTx (mempool) accepts it, leaving the
345+
// validator permanently jailed. The 5-second buffer accounts for BFT clock
346+
// drift: the block's BFT timestamp can be a few seconds behind real time,
347+
// so waiting until time.Now() > jailed_until+5s ensures the next committed
348+
// block's BFT time is also definitively past jailed_until.
349+
s.Require().Eventually(func() bool {
350+
jailedUntil, err := defaultNode.QuerySigningInfo(nodeToJail.ConsensusAddress)
351+
if err != nil || jailedUntil == notJailed {
352+
return false
353+
}
354+
jailTime, err := time.Parse(time.RFC3339Nano, jailedUntil)
355+
if err != nil {
356+
jailTime, err = time.Parse(time.RFC3339, jailedUntil)
357+
if err != nil {
358+
return false
359+
}
360+
}
361+
return time.Now().UTC().After(jailTime.Add(5 * time.Second))
362+
}, initialization.TwoMin, time.Second, "jail period did not expire within timeout")
363+
364+
// Retry the unjail tx every poll interval until signing info confirms success.
365+
// A single broadcast is insufficient: if the committed block's BFT timestamp
366+
// is still before jailed_until (BFT clock can lag real time in CI), DeliverTx
367+
// returns a non-zero code and the validator stays jailed. Re-broadcasting every
368+
// 5 seconds lets BFT time catch up without manual timing tuning.
369+
s.T().Log("jail period expired, entering unjail retry loop")
370+
s.Require().Eventually(func() bool {
371+
jailedUntil, err := defaultNode.QuerySigningInfo(nodeToJail.ConsensusAddress)
372+
if err == nil && jailedUntil == notJailed {
373+
return true
374+
}
375+
// Ignore broadcast errors; on-chain delivery is checked via signing info.
376+
_ = nodeToJail.Unjail(initialization.ValidatorWalletName)
377+
return false
378+
}, initialization.FiveMin, 5*time.Second,
379+
"jailed_until should be cleared after unjail")
380+
}

tests/e2e/initialization/node.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
3333
"github.com/cosmos/cosmos-sdk/x/genutil"
3434
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
35+
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
3536
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
3637
"github.com/cosmos/go-bip39"
3738
"github.com/spf13/viper"
@@ -276,6 +277,30 @@ func (n *internalNode) init() error {
276277
return fmt.Errorf("failed to JSON encode app genesis state: %w", err)
277278
}
278279

280+
// Override slashing params so TestSlashingUnjail completes in reasonable time.
281+
// Defaults (signed_blocks_window=100, downtime_jail_duration=600s) would require
282+
// the test to wait >10 min before unjailing is possible.
283+
// 60s jail duration gives enough margin so BFT block time is well past
284+
// jailed_until by the time the unjail tx is submitted.
285+
{
286+
var rawState map[string]json.RawMessage
287+
if err = json.Unmarshal(appState, &rawState); err != nil {
288+
return fmt.Errorf("failed to unmarshal app state: %w", err)
289+
}
290+
var slashGenState slashingtypes.GenesisState
291+
if err = util.Cdc.UnmarshalJSON(rawState[slashingtypes.ModuleName], &slashGenState); err != nil {
292+
return fmt.Errorf("failed to unmarshal slashing genesis: %w", err)
293+
}
294+
slashGenState.Params.SignedBlocksWindow = 10
295+
slashGenState.Params.DowntimeJailDuration = 60 * time.Second
296+
if rawState[slashingtypes.ModuleName], err = util.Cdc.MarshalJSON(&slashGenState); err != nil {
297+
return fmt.Errorf("failed to marshal slashing genesis: %w", err)
298+
}
299+
if appState, err = json.MarshalIndent(rawState, "", " "); err != nil {
300+
return fmt.Errorf("failed to re-marshal app state: %w", err)
301+
}
302+
}
303+
279304
genDoc.ChainID = n.chain.chainMeta.ID
280305
genDoc.AppState = appState
281306

0 commit comments

Comments
 (0)