changed a year ago
Published Linked with GitHub

Corrupted Staking ledgers

walkthrough and recovery


  • Possible corruption states
  • Call::restore_ledger()
  • Status in Polkadot and Kusama
  • Tests with chopsticks

Possible corruption states

Note: per ledger (two ledgers may be affected)


  • Double bonded ledger (not corrupted)
  • Case 1: Corrupted Ledger
  • Case 2: Corrupted and killed
  • Case 3: Corrupted and killed (other)
  • Case 4: Corrupted with bond_extra

Refresher

The main issue is a combination of:

  • Controller can become a stash of another ledger;
  • Ledgers are keyed by controllers;
  • set_controller assumes the stash can become a controller (and not overlap with other ledgers);


    Thus, a ledger (and all data that is keyed based on the ledger state e.g. lock) may be overwritten.

Double bonded ledger

This is not a corrupted state (but latent until v1.1.3)

Bonded(A) = B
Bonded(B) = C

Ledger(B) = Ledger_Ab { stash = A, ctrl = B }
Ledger(C) = Ledger_Bc { stash = B, ctrl = C }

Ledger_Ab will get corrupted if stash B calls set_controller.


Case 1: Corrupted Ledger

// init state: double bonded
Bonded(A) = B
Bonded(B) = C
Ledger(B) = Ledger_Ab { stash = A, ctrl = B }
Ledger(C) = Ledger_Bc { stash = B, ctrl = C }

// calling `set_controller` corrupts one ledger
set_controller(C)

Bonded(A) = B
Bonded(B) = B

Ledger(B) = Ledger_Bb { stash = B, ctrl = B } ❌ Ledger_Ab overwritten

To fix in this case:

  1. Restore Ledger_Ab (fetch ledger.total from staking lock of A)
  2. Set controller of A to stash (Bonded(A) = A)

Case 2: Corrupted and killed

// starting with corrupted ledger
Bonded(A) = B
Bonded(B) = B
Ledger(B) = Ledger_Bb { stash = B, ctrl = B } ❌ Ledger_Ab overwritten

kill(A) // corrupted ledger calls `kill`

StakingLock(A) != 0 ❌
Bonded(A) = None

StakingLock(B) == 0 ❌
Bonded(B) = B
Ledger(B) = None  ❌

To fix in this case:

  1. Release lock StakingLock(A)
  2. Restore ledger Ledger_Bb with amount before kill
  3. Restore Ledger_Bb with same amount as the lock

Note that in this case, we need to provide explicitly the ledger.amount/ lock of the new restored ledger (that data is not on-chain anymore).


Case 3: Corrupted and killed (other)

// starting with corrupted ledger
Bonded(A) = B
Bonded(B) = B
Ledger(B) = Ledger_Bb { stash = B, ctrl = B } ❌ Ledger_Ab overwritten

kill(B) // ledger overlapping corrupted calls `kill`

StakingLock(A) = ledger.total
Bonded(A) = B
Ledger(A) = None  ❌

(everything with B has been cleared, OK)

To fix in this case:

  1. Restore Ledger(A) with ledger.total = StakingLock(A)

Case 4: Corrupted with bond_extra

// starting with corrupted ledger
Bonded(A) = B
Bonded(B) = B
Ledger(B) = Ledger_Bb { stash = B, ctrl = B } ❌ Ledger_Ab overwritten

bond_extra(A, amount)

StakingLock(A) = amount_before_bond_extra ❌
StakingLock(B) = ledger.total + amount ❌

To fix in this case:

  1. (Same as Case 1.)
  2. Ensure Ledger_A.total == StakingLock(A)
  3. Ensure Ledger_B.total == StakingLock(B)

Call::restore_ledger()

Called with a stash, maybe_total and maybe_controller as inputs.

  1. Identifies the (stash, ledger) tuple state. May be one of
    • Ok(LedgerIntegrityState::Ok)
    • Ok(LedgerIntegrityState::Corrupted)
    • Ok(LedgerIntegrityState::LockCorrupted)
    • Err(BadState)
  2. Restores ledger and lock based on the tuple's state

https://github.com/paritytech/polkadot-sdk/pull/3706


Status in Polkadot and Kusama

Reports https://hackmd.io/Dsa2tvhISNSs7zcqriTaxQ?view


Select a repo