protected function verifyResponseWithPeers(Channel $channel, MerkleTree $originalTree, TreeUpdate ...$updates) : bool
{
$state = State::instance();
$nodes = $this->updatesToNodes($updates);
$tree = $originalTree->getExpandedTree(...$nodes);
$maxUpdateIndex = \count($updates) - 1;
$expectedRoot = $updates[$maxUpdateIndex]->getRoot();
if (!\hash_equals($tree->getRoot(), $expectedRoot)) {
// Calculated root did not match.
self::$continuumLogger->store(LogLevel::EMERGENCY, 'Calculated Merkle root did not match the update.', [$tree->getRoot(), $expectedRoot]);
throw new CouldNotUpdate(\__('Calculated Merkle root did not match the update.'));
}
if ($state->universal['auto-update']['ignore-peer-verification']) {
// The user has expressed no interest in verification
return true;
}
$peers = $channel->getPeerList();
$numPeers = \count($peers);
/**
* These numbers are negotiable in future versions.
*
* If P is the set of trusted peer notaries (where ||P|| is the number
* of trusted peer notaries):
*
* 1. At least 1 must return 'success'.
* 2. At least ln(||P||) must return 'success'.
* 3. At most e * ln(||P||) can timeout.
* 4. If any peer disagrees with what we see, our
* result is discarded as invalid.
*
* The most harm a malicious peer can do is DoS if they
* are selected.
*/
$minSuccess = $channel->getAppropriatePeerSize();
$maxFailure = (int) \min(\floor($minSuccess * M_E), $numPeers - 1);
if ($maxFailure < 1) {
$maxFailure = 1;
}
\Airship\secure_shuffle($peers);
$success = $networkError = 0;
/**
* If any peers give a different answer, we're under attack.
* If too many peers don't respond, assume they're being DDoS'd.
* If enough peers respond in absolute agreement, we're good.
*/
for ($i = 0; $i < $numPeers; ++$i) {
try {
if (!$this->checkWithPeer($peers[$i], $tree->getRoot())) {
// Merkle root mismatch? Abort.
return false;
}
++$success;
} catch (TransferException $ex) {
self::$continuumLogger->store(LogLevel::EMERGENCY, 'A transfer exception occurred', \Airship\throwableToArray($ex));
++$networkError;
}
if ($success >= $minSuccess) {
// We have enough good responses.
return true;
} elseif ($networkError >= $maxFailure) {
// We can't give a confident response here.
return false;
}
}
self::$continuumLogger->store(LogLevel::EMERGENCY, 'We ran out of peers.', [$numPeers, $minSuccess, $maxFailure]);
// Fail closed:
return false;
}