Scalable Stake Slashing
This post is yet another addendum to the paper titled Scalable Reward Distribution on the Ethereum Blockchain by Batog et al. They define a pull-based algorithm for distributing rewards proportionally to a number of participants without expensive iteration (as in push-based distribution). This simplifies the complexity from O(n)
(with n
participants) to constant time O(1)
.
This algorithm may be applied to another use-case in nominated Proof-of-Stake (PoS). More specifically, if Alice and Bob nominate their stake to Charlie who is subsequently slashed we need to update all stakes to restrict future withdrawals. As there may be a large number of participants, it is not really feasible to iterate over each entry and re-calculate their stake. Fortunately, it is possible to reuse the algorithm described above with minimal changes. We must restrict the amount of stake a participant can withdraw based on their actual_stake
as defined:
$$ to\_slash_j = stake_{j,n} × slash\_per\_token_n − slash\_tally_{j,n} $$
$$ actual\_stake_j = stake_{j,n} - to\_slash_j $$
Following the addendum by Onur Solmaz which accounts for changing stake sizes, we can reformulate the pseudocode as follows:
class PullBasedSlashing:
"Constant Time Stake Slashing with Changing Stake Sizes"
def __init__(self):
self.total_stake = 0
self.slash_per_token = 0
self.stake = {}
self.slash_tally = {}
def deposit_stake(self, address, amount):
"Increase the stake of `address` by `amount`"
if address not in self.stake:
self.stake[address] = 0
self.slash_tally[address] = 0
self.stake[address] = self.stake[address] + amount
self.slash_tally[address] = self.slash_tally[address] + self.slash_per_token * amount
self.total_stake = self.total_stake + amount
def slash_stake(self, amount):
"Slash `amount` proportionally to active stakes"
if self.total_stake == 0:
raise Exception("Cannot slash zero total stake")
self.slash_per_token = self.slash_per_token + amount / self.total_stake
def compute_stake(self, address):
"Compute actual stake of `address`"
to_slash = self.stake[address] * self.slash_per_token - self.slash_tally[address]
return self.stake[address] - to_slash
def withdraw_stake(self, address, amount):
"Decrease the stake of `address` by `amount`"
actual_stake = self.compute_stake(address)
if amount > actual_stake:
raise Exception("Requested amount greater than staked amount")
self.stake[address] = self.stake[address] - amount
self.slash_tally[address] = self.slash_tally[address] - self.slash_per_token * amount
self.total_stake = self.total_stake - amount
addr1 = 0x1
addr2 = 0x2
contract = PullBasedSlashing()
contract.deposit_stake(addr1, 100)
contract.deposit_stake(addr2, 100)
contract.slash_stake(20)
print(contract.compute_stake(addr1))
print(contract.compute_stake(addr2))
contract.slash_stake(80)
contract.withdraw_stake(addr1, 50)
print(contract.compute_stake(addr1))