@@ -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+ }
0 commit comments