--- type: slide --- # Corrupted Staking ledgers ### walkthrough and recovery <br> - Possible corruption states - `Call::restore_ledger()` - Status in Polkadot and Kusama - Tests with [chopsticks](https://github.com/AcalaNetwork/chopsticks) <style> .reveal { font-size: 30px; } section { text-align: left; } </style> --- ## Possible corruption states Note\: per ledger (two ledgers may be affected) <br> - 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); <br> 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 } ``` <br> `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 ---