PirlGuard is not implemented as outlined in the Pirl blog
TL;DR: PirlGuard description in the official Pirl blog is not matching its implementation and is largely a pseudo-code (that, however, does the job somewhat).
When Callisto Network (CLO) was recently affected by a 51% attack they have implemented PirlGuard with the help of the Pirl team but initially blamed my team (2Miners.com) for "lack of cooperation and professionalism" for publically telling them about the attack instead of doing it in a private manner.
Analysis of the PirlGuard code implementation by Callisto Team (actually, a copy-paste) has raised more questions. First we have thought they didn't copy-pasted the entire protection system (however it raises some questions how a team positioning itself as "security experts" in the land of smart contracts could not fully execute a semi-simple task), so we came to Pirl source code only to find that their implementation is, in fact, identical.
The Pirl blog describes the PirlGuard as:
…once this happens PirlGuard will drop the peer and penalize them by sentencing them to mine X amount of penalty blocks due to their un-peered mining. The amount of penalty blocks assigned depends on the amount of blocks that the malicious miner mined in private.
However, implementation in the Pirl source code is not doing what is advertised. Instead of the described process and after doing a number of penalty value calculations, it simply marks the starting block of the offending chain as bad without actually penalizing the offending node or using those calculations anywhere else.
Let's observe the code in detail:
core/pirlguard.go hosts the engine for detecting offending subchains (
First, on lines 50-52 it assigns penalty values based on the block heights. Then, on lines 56-62 it transforms the same map again into a newly introduced Pair structure, and then defines the culprit, the penalty variable to hold the penalty value (which is a simple addition of each offending block's penalty values).
Then on lines 68-69 the penalty value is additionally multiplied by a factor depending on the current block difficulty.
After that, on lines 81-88 the value of the penalty is compared against being greater than zero, and if yes, an error
ErrDelayTooHigh is set. If penalty is 0, the error is cleared. Then the error is returned into its parent calling function,
insertChain() in core/blockchain.go:1139. Note that at this point, the previously calculated penalty is no longer available, returned or used anywhere. All we have is an error (
On blockchain.go:1181 the error is being compared against, and if it is set, then the block (the first block in the offending subchain) is marked bad by calling
bc.reportBlock() function. This is, essentially, all that PirlGuard does, in terms of code. The consequences of marking the block as bad may lead to the peer being banned, but there is absolutely nothing in the code that penalizes the malicious peer with mining more blocks to be accepted onto the chain, contrary to what the above cited blog post says.
With the present implementation, there is also lots of redundant/unused code in the pirlguard.go file as all checks could boil down to one or two comparisons, instead of filling a map, then refilling it onto a new dataset, then transforming it into a single unused penalty value.
Are we missing anything or the PirlGuard implementation is really not what was announced on the the Pirl blog?