diff --git a/src/avalanche/avalanche.h b/src/avalanche/avalanche.h --- a/src/avalanche/avalanche.h +++ b/src/avalanche/avalanche.h @@ -46,6 +46,19 @@ */ static constexpr size_t AVALANCHE_DEFAULT_COOLDOWN = 100; +/** + * Default minimum amount of stake available for polling that constitutes a + * usable quorum. + */ +static constexpr size_t AVALANCHE_DEFAULT_MIN_QUORUM_STAKE = 32400000; + +/** + * Default minimum proportion of stake which is active and responsive that + * constitutes a usable quorum. + */ +static constexpr double AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO = + 0.6; + /** * Global avalanche instance. */ diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -230,6 +230,19 @@ } } + /** + * Iterates over peers ordered by their score, descending, calling the given + * Callable for each. The Callable must return a boolean and iteration stops + * when false is returned so the caller may terminate iteration early. + */ + template void forEachPeerByScore(Callable &&func) { + auto &pview = peers.get(); + bool cont = true; + for (auto it = pview.begin(); cont && it != pview.end(); ++it) { + cont = func(*it); + } + } + /** * Proof and Peer related API. */ diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -202,6 +202,8 @@ bool startEventLoop(CScheduler &scheduler); bool stopEventLoop(); + bool isQuorumAvailable(); + private: void runEventLoop(); void clearTimedoutRequests(); diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include @@ -662,6 +663,11 @@ return; } + // Don't poll if quorum hasn't been established yet + if (!isQuorumAvailable()) { + return; + } + // First things first, check if we have requests that timed out and clear // them. clearTimedoutRequests(); @@ -724,4 +730,49 @@ } while (nodeid != NO_NODE); } +/* + * Returns a bool indicating whether we think we have a usable Avalanche quorum, + * enabling us to take decisions based on polls. + */ +bool Processor::isQuorumAvailable() { + uint64_t minQuorumStake = + gArgs.GetArg("-avaminquorumstake", AVALANCHE_DEFAULT_MIN_QUORUM_STAKE); + double minQuorumConnectedStakeRatio = + gArgs.GetArg("-avaminquorumconnectedstakeratio", + AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO); + + LOCK(cs_peerManager); + + // Approximate the amount of stake with the number of slots after + // a compaction. + peerManager->compact(); + uint64_t knownQuorumStake = peerManager->getSlotCount(); + if (knownQuorumStake < minQuorumStake) { + LogPrint(BCLog::AVALANCHE, + "Quorum unavailable: Stake is %d; need at least %d\n", + knownQuorumStake, minQuorumStake); + return false; + } + + // Sum the stake of connected peers and ensure it hits our threshold + uint64_t minConnectedQuorumStake = + std::round(double(knownQuorumStake) * minQuorumConnectedStakeRatio); + uint64_t connectedQuorumStake = 0; + peerManager->forEachPeerByScore( + [&connectedQuorumStake, &minConnectedQuorumStake](Peer peer) { + connectedQuorumStake += peer.getScore(); + return connectedQuorumStake < minConnectedQuorumStake; + }); + + if (connectedQuorumStake < minConnectedQuorumStake) { + LogPrint( + BCLog::AVALANCHE, + "Quorum unavailable: Stake connection is %d; need at least %d\n", + connectedQuorumStake, minConnectedQuorumStake); + return false; + } + + return true; +} + } // namespace avalanche diff --git a/src/avalanche/test/processor_tests.cpp b/src/avalanche/test/processor_tests.cpp --- a/src/avalanche/test/processor_tests.cpp +++ b/src/avalanche/test/processor_tests.cpp @@ -91,6 +91,8 @@ *m_node.scheduler, *m_node.chainman, *m_node.mempool, false); m_node.chain = interfaces::MakeChain(m_node, config.GetChainParams()); + gArgs.ForceSetArg("-avaminquorumstake", "0"); + // Get the processor ready. bilingual_str error; m_processor = Processor::MakeProcessor(*m_node.args, *m_node.chain, @@ -101,6 +103,7 @@ ~AvalancheTestingSetup() { m_connman->ClearNodes(); SyncWithValidationInterfaceQueue(); + gArgs.ClearForcedArg("-avaminquorumstake"); } CNode *ConnectNode(ServiceFlags nServices) { diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -1325,6 +1325,19 @@ strprintf("Enable avalanche proof replacement (default: %u)", AVALANCHE_DEFAULT_PROOF_REPLACEMENT_ENABLED), ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avaminquorumstake", + strprintf( + "Minimum amount of known stake for a usable quorum (default: %u)", + AVALANCHE_DEFAULT_MIN_QUORUM_STAKE), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avaminquorumconnectedstakeratio", + strprintf("Minimum proportion of responsive stake for a usable quorum " + "(default: %u)", + AVALANCHE_DEFAULT_MIN_QUORUM_CONNECTED_STAKE_RATIO), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); + argsman.AddArg( "-avacooldown", strprintf("Mandatory cooldown between two avapoll (default: %u)", diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5037,6 +5037,12 @@ // Default vote for unknown inv type uint32_t vote = -1; + // We don't vote definitively until we have an available quorum + if (!g_avalanche || !g_avalanche->isQuorumAvailable()) { + votes.emplace_back(vote, inv.hash); + continue; + } + // If inv's type is known, get a vote for its hash switch (inv.type) { case MSG_BLOCK: { diff --git a/test/functional/abc_p2p_avalanche_proof_voting.py b/test/functional/abc_p2p_avalanche_proof_voting.py --- a/test/functional/abc_p2p_avalanche_proof_voting.py +++ b/test/functional/abc_p2p_avalanche_proof_voting.py @@ -40,7 +40,9 @@ self.peer_replacement_cooldown = 2000 self.extra_args = [ ['-enableavalanche=1', '-enableavalancheproofreplacement=1', - f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', f'-avalanchepeerreplacementcooldown={self.peer_replacement_cooldown}', '-avacooldown=0'], + f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', + f'-avalanchepeerreplacementcooldown={self.peer_replacement_cooldown}', + '-avacooldown=0', '-avaminquorumstake=0'], ] self.supports_cli = False @@ -300,7 +302,8 @@ self.restart_node(0, extra_args=['-enableavalanche=1', '-avacooldown=0', '-avalancheconflictingproofcooldown=0', - '-whitelist=noban@127.0.0.1', ]) + '-whitelist=noban@127.0.0.1', + '-avaminquorumstake=0']) ava_node = get_ava_p2p_interface(node) diff --git a/test/functional/abc_p2p_avalanche_quorum.py b/test/functional/abc_p2p_avalanche_quorum.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_p2p_avalanche_quorum.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020-2022 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the quorum detection of avalanche.""" + +from test_framework.avatools import ( + create_coinbase_stakes, + get_ava_p2p_interface, +) +from test_framework.key import ECKey, ECPubKey +from test_framework.messages import AvalancheVote, AvalancheVoteError +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal +from test_framework.wallet_util import bytes_to_wif + + +class AvalancheQuorumTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [ + ['-enableavalanche=1', '-avacooldown=0', '-avaminquorumstake=5000'] + ] + self.supports_cli = False + + def run_test(self): + node = self.nodes[0] + poll_node = get_ava_p2p_interface(node) + + # Generate many block and poll for them. + addrkey0 = node.get_deterministic_priv_key() + blockhashes = node.generatetoaddress(1, addrkey0.address) + block = int(node.getbestblockhash(), 16) + + # Use the first coinbase to create a stake + stakes = create_coinbase_stakes(node, [blockhashes[0]], addrkey0.key) + + # Get the key so we can verify signatures. + avakey = ECPubKey() + avakey.set(bytes.fromhex(node.getavalanchekey())) + + # Create a proof with the stake + privkey = ECKey() + privkey.set(bytes.fromhex( + "12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747"), True) + + proof_sequence = 11 + proof_expiration = 12 + proof = node.buildavalancheproof(proof_sequence, proof_expiration, + bytes_to_wif(privkey.get_bytes()), stakes) + + def assert_response(expected): + response = poll_node.wait_for_avaresponse() + r = response.response + assert_equal(r.cooldown, 0) + + # Verify signature. + assert avakey.verify_schnorr(response.sig, r.get_hash()) + + votes = r.votes + assert_equal(len(votes), len(expected)) + for i in range(0, len(votes)): + assert_equal(repr(votes[i]), repr(expected[i])) + + # Node should respond UNKNOWN because there isn't a quorum + poll_node.send_poll([block]) + assert_response( + [AvalancheVote(AvalancheVoteError.UNKNOWN, block)]) + + # Add the first stakes and poll again; still not enough stake + assert node.addavalanchenode(poll_node.nodeid, + privkey.get_pubkey().get_bytes().hex(), proof) is True + + # There is now a quorum so the node should respond with a known vote + poll_node.send_poll([block]) + assert_response( + [AvalancheVote(AvalancheVoteError.ACCEPTED, block)]) + + +if __name__ == '__main__': + AvalancheQuorumTest().main() diff --git a/test/functional/abc_p2p_avalanche_voting.py b/test/functional/abc_p2p_avalanche_voting.py --- a/test/functional/abc_p2p_avalanche_voting.py +++ b/test/functional/abc_p2p_avalanche_voting.py @@ -23,7 +23,7 @@ self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [ - ['-enableavalanche=1', '-avacooldown=0'], + ['-enableavalanche=1', '-avacooldown=0', '-avaminquorumstake=0'], ['-enableavalanche=1', '-avacooldown=0', '-noparkdeepreorg', '-maxreorgdepth=-1']] self.supports_cli = False self.rpc_timeout = 120