33813 - [BC - Insight] Double slashing of validators
Submitted on Jul 30th 2024 at 03:08:19 UTC by @Lastc0de for Boost | Shardeum: Core
Report ID: #33813
Report type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/shardeum/shardus-core/tree/dev
Impacts:
Direct loss of funds
Description
Brief/Intro
Slashing mechanism for POS blockchains is a critical feature. Its implementation must have a strong test suite.
Shardeum has the concept of a penalty, to slash nodes if they have malicious behaviour. For example if a node goes offline while it is in active mode, and stops processing transactions, then other nodes confirm it is a lost node and apply a configurable penalty to that nodes staked tokens. But there is a bug in the shardeum repository which causes the network to apply a penalty 2x of configured value. So when penalty of leaving network early is configured to be 20% of staked value, if a validator leaves it loses 40% of its stake.
I will explain the cause of the bug here and provide a POC after.
Vulnerability Details
One way of getting a penalty is to leave network early, in the shardeum/shardus-core repository if a node removed because it lefts network early, a node-left-early event would be triggered
src/p2p/NodeList.ts
this event is handled in src/shardus/index.ts file of same repository
src/shardus/index.ts
and it calls eventNotify function of app which is a method that defined in another repository shardeum/shardeum
src/index.ts
which calls the injectPenaltyTX function. injectPenaltyTX function creates an internal penalty transactions and puts into shardus by calling shardus.put function
src/tx/penalty/transaction.ts
shardus.put is defined in the shardeum/shardus-core repository and it validates and sends transactions to queue for execution. After the queue, transactions are passed to the apply function in the shardeum/shardeum repository for execution. apply function would pass the created internal transaction to the applyInternalTx function.
src/index.ts
and applyInternalTx function would pass it to applyPenaltyTx function
src/index.ts
this applyPenaltyTx function calculated amount of penalty, and calls applyPenalty function to apply penaltyAmount to validator account.
src/tx/penalty/transaction.ts
applyPenalty function does some calculations and some modifications
src/tx/penalty/penaltyFunctions.ts
root cause of this bug is because penalty is reduced from stake of validator account here
so if other parts of application wants to do some calculation based on original stake, it is so easy to mistake this stake field by original stake of validator, which is happened in unstake transaction.
Again in apply function of shardeum/shardeum repository, we have below implementation for unstake transaction
src/index.ts
It sets stake variable to current stake value, penalty variable to current penalty value, and when it calculates newBalance it reduces penalty from stake, AGAIN.
Impact Details
This bug could affect direct loss for any validator
References
Add any relevant links to documentation or code
Proof of Concept
To see how the validator penalty applies twice we have to run a network. Almost all of what we do below is from README of different repositories of shardeum. If you have a running network just make sure step 3 and 4 is configured and skip to step 16.
clone repositories
install metamask on your browser and create two accounts validator01, validator02. Copy their address and open
shardeum/src/config/genesis.jsonand append them to the list of genesis accounts.open
shardeum/src/config/index.tsand change below values
open
shardeum/src/shardeum/shardeumFlags.tsand change below values
go to the
shardeumfolder and executenpm install && npm prepareinstall shardus command line tool with
npm install -g shardus && npm update @shardus/archiverstart a local network by
shardus start 10. it spins up a network with 10 nodes and an archiver. runshardus pm2 logsand wait until all 10 nodes are in active mode.Now we have to run our validator and stake some SHM. go to
validator01folder and change docker-compose.yaml to
create a hashed password with
echo password | openssl dgst -sha256 -r(which is what installer.sh does)add a
.envfile with below content (replace 192.168.1.100 with your computer ip, and replace DASHPASS value with previous step output)
replace entrypoint.sh with below text
replace Dockerfile content with below text
apply step 8 to 12 in
validator02folder, only indocker-compose.ymlfile instead of validator01 use validator02Now we have to run a json-rpc-server. To do so go to the
rpcfolder and openarchiverConfig.jsonand change ip to192.168.1.100(or your computer ip address) and rundocker compose up. It should run and connect to the archive node without problem.Now go to the
validator01folder and start validator bydocker compose up. After a while you would see done output and can open the dashboard in your browser athttps://localhost:9030. Then login with your password and go to the Maintenance tab.In the validator01 dashboard, click on Start button to start validator node. After a few seconds in the terminal that you executed
shardus pm2 logsthere must be a standby node which is this validator01 that we started.Open Metamask and add a new network
Network Name
Shardeum Atomium
New RPC URL
Chain ID
8082
Currency Symbol
SHM
Block Explorer URL (optional)
none or http://localhost:6001/
Now change your metamask network to Shardeum Atomium and you must be able to see values that we set for validator01 and validator02 accounts in the
genesis.jsonas balances of these accounts.In the Maintenance tab of validator01 dashboard, click add stake. It opens Metamask, connects validator01 account then set for example 100 as stake value, click on add stake again and confirm transaction in Metamask, when the transaction confirmed node goes to sync mode and after a while, it goes to active!. You can see in Metamask that the account now has a send transaction of 100 and its balance is reduced by ~101 tokens, that 1 is because of the fee. In
shardus pm2 logswe can see after a few seconds, the network kicks an old node and resizes its active size to be 10 which is cool. And the removed node would go to standby mode.stop validator01 docker container to simulate an early left node. After a few seconds in
shardus pm2 logsyou should see active nodes become 9 again.Now go to the
validator02folder and start it like validator01, go to its dashboard, stake some token and wait for the node to go to active mode. We add this seconds validator so network can process our unstake transaction. network size must be at least 10 to process application transactions. Although there is a standby node which started byshardus start 10but it does not go to active mode again, i think because it does not stake anything. Anyway, now that we have 10 active node again, we can send an unstake transaction to the network.go to
validator01folder and execute this command
it asks for a private key, go to Metamask and open validator01 account and copy it's private key, and paste it here.
After transaction confirmed you can see in Metamask that instead of 80, 60 tokens are returned to your account. Which is 40% penalty.
Last updated
Was this helpful?