Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/v1alpha1/seinodetask_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ const (
// sidecar keyring. The sidecar refuses before broadcast when the account has
// no validator, the validator's self-delegation is missing or below its min
// self-delegation, or the validator is not jailed, still in its jail period,
// or tombstoned. NOT chain-idempotent: a second unjail of a released validator
// spends the fee and fails, so do not re-create a Complete task.
// or tombstoned. On a node with the tx index off, as on most validators, the
// sidecar cannot look up the tx, so it confirms the release from the jail
// state instead. NOT chain-idempotent: a second unjail of a released
// validator spends the fee and fails, so do not re-create a Complete task.
SeiNodeTaskKindUnjail SeiNodeTaskKind = "Unjail"
)

Expand Down
3 changes: 2 additions & 1 deletion sidecar/tasks/gov_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func classifyGovResult(taskType engine.TaskType, r *SignAndBroadcastResult) (*wi
case r.Unverifiable:
// Broadcast accepted but the node's tx index is off, so the outcome is
// unobservable. Terminal (retrying this node is futile) but NOT
// committed_failed — the operator must verify via an indexed RPC.
// committed_failed — the operator must verify via an indexed RPC. The
// Unjail handler overrides this when the jail state shows the release.
out.InclusionStatus = wire.InclusionUnverifiable
txBroadcastTotal.WithLabelValues(string(taskType), wire.InclusionUnverifiable).Inc()
return out, Terminal(fmt.Errorf("tx %s inclusion unverifiable: %w", r.TxHash, errTxIndexingDisabled))
Expand Down
49 changes: 47 additions & 2 deletions sidecar/tasks/unjail.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,22 @@ type Unjailer struct {
// and SignAndBroadcast.
readJail func(ctx context.Context, cfg engine.ExecutionConfig, chainID string, valAddr sdk.ValAddress) (jailState, error)
broadcast func(ctx context.Context, cfg engine.ExecutionConfig, in SignAndBroadcastInput) (*SignAndBroadcastResult, error)

// confirmWait bounds how long an unjail whose tx the node cannot look up
// waits for the validator to read released, reads included; confirmEvery
// is the poll interval.
confirmWait time.Duration
confirmEvery time.Duration
}

func NewUnjailer(cfg engine.ExecutionConfig) *Unjailer {
return &Unjailer{cfg: cfg, readJail: chainJailState, broadcast: SignAndBroadcast}
return &Unjailer{
cfg: cfg,
readJail: chainJailState,
broadcast: SignAndBroadcast,
confirmWait: 30 * time.Second,
confirmEvery: time.Second,
}
}

// Handler checks the validator's jail state, then delegates to
Expand Down Expand Up @@ -117,17 +129,50 @@ func (u *Unjailer) Handler() engine.TaskHandler {
return nil, err
}
out, cerr := classifyGovResult(engine.TaskUnjail, result)
releasedByState := result.Unverifiable && u.releasedAfter(ctx, params.ChainID, valAddr)
if releasedByState {
Comment thread
seidroid[bot] marked this conversation as resolved.
cerr = nil
}
unjailLog.Info("unjail broadcast",
"taskId", taskID,
"chainId", params.ChainID,
"validator", valAddr.String(),
"txHash", out.TxHash,
"height", out.Height,
"inclusionStatus", out.InclusionStatus)
"inclusionStatus", out.InclusionStatus,
"releasedByState", releasedByState)
return out, cerr
})
}

// releasedAfter settles an unjail whose tx the node cannot look up because its
// tx index is off, as on most validators. The unjail's effect shows in state
// instead: a validator that reads not jailed was released. An unjail from
// elsewhere that lands first reads the same, and the validator is released
// either way. It polls until confirmWait passes; the deadline also bounds each
// read, so a slow read cannot stretch the wait.
func (u *Unjailer) releasedAfter(ctx context.Context, chainID string, valAddr sdk.ValAddress) bool {
ctx, cancel := context.WithTimeout(ctx, u.confirmWait)
defer cancel()
var lastErr error
for {
st, err := u.readJail(ctx, u.cfg, chainID, valAddr)
if err == nil && !st.CatchingUp && !st.Jailed {
return true
}
if err != nil {
lastErr = err
}
select {
case <-ctx.Done():
unjailLog.Warn("unjail release not confirmed from the jail state",
"validator", valAddr.String(), "wait", u.confirmWait, "lastReadErr", lastErr)
return false
case <-time.After(u.confirmEvery):
}
}
}

// checkJailed refuses an unjail that the chain would reject after taking the
// fee. CheckTx does not run the message handler, so these cases otherwise
// surface only as a committed-but-failed tx. After the catching-up check, the
Expand Down
50 changes: 50 additions & 0 deletions sidecar/tasks/unjail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,56 @@ func TestUnjailCommittedFailureIsTerminalAndKeepsTxHash(t *testing.T) {
}
}

// PLT-1392 harbor e2e: validators often run with the tx index off, so the
// node cannot look up the unjail tx. The task then confirms the release from
// the jail state: Complete once the validator reads released.
func TestUnjailUnverifiableTxConfirmedByJailState(t *testing.T) {
kr, _ := testKeyring(t)
states := []jailState{releasable(), releasable(), with(func(s *jailState) { s.Jailed = false })}
reads := 0
u := &Unjailer{
cfg: engine.ExecutionConfig{Keyring: kr, Checkpointer: newFakeCheckpointer(nil)},
readJail: func(context.Context, engine.ExecutionConfig, string, sdk.ValAddress) (jailState, error) {
st := states[min(reads, len(states)-1)]
reads++
return st, nil
},
broadcast: func(context.Context, engine.ExecutionConfig, SignAndBroadcastInput) (*SignAndBroadcastResult, error) {
return &SignAndBroadcastResult{TxHash: "ABCD", Unverifiable: true}, nil
},
confirmWait: time.Second,
confirmEvery: time.Millisecond,
}
out, err := runUnjail(t, u, "node_admin")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if out == nil || out.TxHash != "ABCD" || out.InclusionStatus != wire.InclusionUnverifiable {
t.Errorf("result = %+v; want the tx hash, still marked unverifiable", out)
}
if reads != 3 {
t.Errorf("read jail state %d times, want 3 (pre-check, then two polls)", reads)
}
}

// A validator that still reads jailed when the wait ends keeps the
// unverifiable failure: the operator must check the tx through an indexed RPC.
func TestUnjailUnverifiableTxStillJailedStaysUnverifiable(t *testing.T) {
h, _ := newUnjailHarness(t, releasable(), nil, &SignAndBroadcastResult{TxHash: "ABCD", Unverifiable: true})
h.u.confirmWait = 20 * time.Millisecond
h.u.confirmEvery = 5 * time.Millisecond
out, err := runUnjail(t, h.u, "node_admin")
if !IsTerminal(err) || !strings.Contains(err.Error(), "inclusion unverifiable") {
t.Fatalf("want terminal inclusion-unverifiable error, got %v", err)
}
if out == nil || out.TxHash != "ABCD" {
t.Errorf("result = %+v", out)
}
if h.reads < 2 {
t.Errorf("read jail state %d times, want the pre-check plus at least one poll", h.reads)
}
}

// A rehydrated run must adopt the first run's tx, not re-check the jail: the
// first unjail may already have released the validator.
func TestUnjailWithTxMarkerSkipsJailCheck(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion sidecarapi/wire/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ const (
// its on-chain outcome cannot be observed from the target node (its tx
// index is disabled), so the sidecar can confirm neither success nor
// failure. Terminal — retrying the same node is futile — but distinct from
// committed_failed: the operator must verify via an indexed RPC.
// committed_failed: the operator must verify via an indexed RPC. One
// exception: an unjail confirms its effect from the validator's jail state,
// so an Unjail task can complete with this status once the validator reads
// released. Key completion on the task phase, not on this value alone.
InclusionUnverifiable = "unverifiable"
)

Expand Down
Loading