diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -89,6 +89,7 @@ uint32_t node_count = 0; ProofRef proof; + bool hasFinalized = false; // The network stack uses timestamp in seconds, so we oblige. std::chrono::seconds registration_time; @@ -293,6 +294,11 @@ bool updateNextPossibleConflictTime(PeerId peerid, const std::chrono::seconds &nextTime); + /** + * Latch on that this peer has a finalized proof. + */ + bool setFinalized(PeerId peerid); + /** * Registration mode * - DEFAULT: Default policy, register only if the proof is unknown and has diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -184,6 +184,18 @@ return it->nextPossibleConflictTime == nextTime; } +bool PeerManager::setFinalized(PeerId peerid) { + auto it = peers.find(peerid); + if (it == peers.end()) { + // No such peer + return false; + } + + peers.modify(it, [&](Peer &p) { p.hasFinalized = true; }); + + return true; +} + template void PeerManager::moveToConflictingPool(const ProofContainer &proofs) { auto &peersView = peers.get(); diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5153,6 +5153,10 @@ proofid, [&](const avalanche::Peer &peer) { pm.updateNextPossibleConflictTime( peer.peerid, nextCooldownTimePoint); + if (u.getStatus() == + avalanche::VoteStatus::Finalized) { + pm.setFinalized(peer.peerid); + } // Only fail if the peer was not // created return true; diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -669,6 +669,9 @@ {RPCResult::Type::NUM, "connected_proof_count", "The number of avalanche proofs with at least one node " "we are connected to."}, + {RPCResult::Type::NUM, "finalized_proof_count", + "The number of known avalanche proofs that have been " + "finalized by avalanche."}, {RPCResult::Type::NUM, "conflicting_proof_count", "The number of known avalanche proofs that conflict with " "valid proofs."}, @@ -731,6 +734,7 @@ uint64_t proofCount{0}; uint64_t connectedProofCount{0}; + uint64_t finalizedProofCount{0}; Amount totalStakes = Amount::zero(); Amount connectedStakes = Amount::zero(); @@ -747,6 +751,10 @@ ++proofCount; totalStakes += proofStake; + if (peer.hasFinalized) { + ++finalizedProofCount; + } + if (peer.node_count > 0) { ++connectedProofCount; connectedStakes += proofStake; @@ -755,6 +763,7 @@ network.pushKV("proof_count", proofCount); network.pushKV("connected_proof_count", connectedProofCount); + network.pushKV("finalized_proof_count", finalizedProofCount); network.pushKV("conflicting_proof_count", uint64_t(pm.getConflictingProofCount())); network.pushKV("orphan_proof_count", diff --git a/test/functional/abc_rpc_getavalancheinfo.py b/test/functional/abc_rpc_getavalancheinfo.py --- a/test/functional/abc_rpc_getavalancheinfo.py +++ b/test/functional/abc_rpc_getavalancheinfo.py @@ -8,13 +8,19 @@ from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.avatools import ( + AvaP2PInterface, avalanche_proof_from_hex, create_coinbase_stakes, gen_proof, get_ava_p2p_interface, + wait_for_proof, ) from test_framework.key import ECKey -from test_framework.messages import LegacyAvalancheProof +from test_framework.messages import ( + AvalancheProofVoteResponse, + AvalancheVote, + LegacyAvalancheProof, +) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet_util import bytes_to_wif @@ -29,8 +35,7 @@ '-enableavalancheproofreplacement=1', f'-avalancheconflictingproofcooldown={self.conflicting_proof_cooldown}', '-avaproofstakeutxoconfirmations=2', - '-avacooldown=', - '-avatimeout=100', + '-avacooldown=0', '-enableavalanchepeerdiscovery=1', '-avaminquorumstake=250000000', '-avaminquorumconnectedstakeratio=0.9', @@ -68,6 +73,7 @@ "network": { "proof_count": 0, "connected_proof_count": 0, + "finalized_proof_count": 0, "conflicting_proof_count": 0, "orphan_proof_count": 0, "total_stake_amount": Decimal('0.00'), @@ -97,6 +103,7 @@ "network": { "proof_count": 0, "connected_proof_count": 0, + "finalized_proof_count": 0, "conflicting_proof_count": 0, "orphan_proof_count": 0, "total_stake_amount": Decimal('0.00'), @@ -122,6 +129,7 @@ "network": { "proof_count": 0, "connected_proof_count": 0, + "finalized_proof_count": 0, "conflicting_proof_count": 0, "orphan_proof_count": 0, "total_stake_amount": Decimal('0.00'), @@ -138,27 +146,39 @@ mock_time = int(time.time()) node.setmocktime(mock_time) - N = 10 + privkeys = [] + proofs = [] + conflicting_proofs = [] + quorum = [] + N = 13 for _ in range(N): _privkey, _proof = gen_proof(node) + proofs.append(_proof) + privkeys.append(_privkey) # For each proof, also make a conflicting one stakes = create_coinbase_stakes( node, [node.getbestblockhash()], node.get_deterministic_priv_key().key) - conflicting_proof = node.buildavalancheproof( + conflicting_proof_hex = node.buildavalancheproof( 10, 9999, bytes_to_wif(_privkey.get_bytes()), stakes) + conflicting_proof = avalanche_proof_from_hex(conflicting_proof_hex) + conflicting_proofs.append(conflicting_proof) # Make the proof and its conflicting proof mature node.generate(1) - n = get_ava_p2p_interface(node) - success = node.addavalanchenode( - n.nodeid, _privkey.get_pubkey().get_bytes().hex(), _proof.serialize().hex()) - assert success is True + n = AvaP2PInterface() + n.proof = _proof + n.master_privkey = _privkey + node.add_p2p_connection(n) + quorum.append(n) + + n.send_avaproof(_proof) + wait_for_proof(node, f"{_proof.proofid:0{64}x}", timeout=10) mock_time += self.conflicting_proof_cooldown node.setmocktime(mock_time) - n.send_avaproof(avalanche_proof_from_hex(conflicting_proof)) + n.send_avaproof(conflicting_proof) # Generate an orphan (immature) proof _, orphan_proof = gen_proof(node) @@ -177,6 +197,7 @@ "network": { "proof_count": N, "connected_proof_count": N, + "finalized_proof_count": 0, "conflicting_proof_count": N, "orphan_proof_count": 1, "total_stake_amount": coinbase_amount * N, @@ -209,6 +230,7 @@ "network": { "proof_count": N, "connected_proof_count": N - D, + "finalized_proof_count": 0, "conflicting_proof_count": N, "orphan_proof_count": 1, "total_stake_amount": coinbase_amount * N, @@ -259,6 +281,7 @@ # Orphan became mature "proof_count": N + 1, "connected_proof_count": N - D, + "finalized_proof_count": 0, "conflicting_proof_count": N, "orphan_proof_count": 0, "total_stake_amount": coinbase_amount * (N + 1), @@ -269,6 +292,51 @@ } }) + self.log.info("Finalize the proofs for some peers") + + def vote_for_all_proofs(): + done_voting = True + for i, n in enumerate(quorum): + if not n.is_connected: + continue + + poll = n.get_avapoll_if_available() + + # That node has not received a poll + if poll is None: + continue + + # Respond yes to all polls except the conflicting proofs + votes = [] + for inv in poll.invs: + response = AvalancheProofVoteResponse.ACTIVE + if inv.hash in [p.proofid for p in conflicting_proofs]: + response = AvalancheProofVoteResponse.REJECTED + + # We need to finish voting on the conflicting proofs to + # ensure the count is stable and that no valid proof + # was replaced. + done_voting = False + + votes.append(AvalancheVote(response, inv.hash)) + + # If we voted on one of our proofs, we're probably not done + # voting. + if inv.hash in [p.proofid for p in proofs]: + done_voting = False + + n.send_avaresponse(poll.round, votes, privkeys[i]) + + return done_voting + + # Vote until proofs have finalized + expected_logs = [] + for p in proofs: + expected_logs.append( + f"Avalanche finalized proof {p.proofid:0{64}x}") + with node.assert_debug_log(expected_logs): + self.wait_until(lambda: vote_for_all_proofs()) + self.log.info("Disconnect all the nodes") for n in node.p2ps: @@ -287,7 +355,8 @@ "network": { "proof_count": N + 1, "connected_proof_count": 0, - "conflicting_proof_count": N, + "finalized_proof_count": N + 1, + "conflicting_proof_count": 0, "orphan_proof_count": 0, "total_stake_amount": coinbase_amount * (N + 1), "connected_stake_amount": 0,