diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -4,4 +4,6 @@ -This is a maintenance release with no user-visible change. +This release includes the following features: + - Add 2 new RPCs, `invalidateavalancheproof` and `reconsideravalancheproof` to + manually change the acceptation status of an avalanche proof. diff --git a/src/avalanche/peermanager.h b/src/avalanche/peermanager.h --- a/src/avalanche/peermanager.h +++ b/src/avalanche/peermanager.h @@ -501,6 +501,7 @@ void setInvalid(const ProofId &proofid); bool isInvalid(const ProofId &proofid) const; + void clearAllInvalid(); const ProofRadixTree &getShareableProofsSnapshot() const { return shareableProofs; diff --git a/src/avalanche/peermanager.cpp b/src/avalanche/peermanager.cpp --- a/src/avalanche/peermanager.cpp +++ b/src/avalanche/peermanager.cpp @@ -624,10 +624,15 @@ void PeerManager::setInvalid(const ProofId &proofid) { invalidProofs.insert(proofid); } + bool PeerManager::isInvalid(const ProofId &proofid) const { return invalidProofs.contains(proofid); } +void PeerManager::clearAllInvalid() { + invalidProofs.reset(); +} + bool PeerManager::saveRemoteProof(const ProofId &proofid, const NodeId nodeid, const bool present) { // Get how many proofs this node has announced diff --git a/src/avalanche/processor.h b/src/avalanche/processor.h --- a/src/avalanche/processor.h +++ b/src/avalanche/processor.h @@ -303,7 +303,9 @@ bool reconcileOrFinalize(const ProofRef &proof); bool isAccepted(const AnyVoteItem &item) const; int getConfidence(const AnyVoteItem &item) const; - bool isRecentlyFinalized(const AnyVoteItem &item) const; + + bool isRecentlyFinalized(const uint256 &itemId) const; + void clearFinalizedItems(); // TODO: Refactor the API to remove the dependency on avalanche/protocol.h void sendResponse(CNode *pfrom, Response response) const; diff --git a/src/avalanche/processor.cpp b/src/avalanche/processor.cpp --- a/src/avalanche/processor.cpp +++ b/src/avalanche/processor.cpp @@ -391,7 +391,7 @@ return false; } - if (isRecentlyFinalized(proof)) { + if (isRecentlyFinalized(proof->getId())) { PeerId peerid; LOCK(cs_peerManager); if (peerManager->forPeer(proof->getId(), [&](const Peer &peer) { @@ -433,9 +433,13 @@ return it->second.getConfidence(); } -bool Processor::isRecentlyFinalized(const AnyVoteItem &item) const { - return WITH_LOCK(cs_finalizedItems, - return finalizedItems.contains(GetVoteItemId(item))); +bool Processor::isRecentlyFinalized(const uint256 &itemId) const { + return WITH_LOCK(cs_finalizedItems, return finalizedItems.contains(itemId)); +} + +void Processor::clearFinalizedItems() { + LOCK(cs_finalizedItems); + finalizedItems.reset(); } namespace { @@ -1177,7 +1181,7 @@ bool Processor::isWorthPolling(const AnyVoteItem &item) const { return std::visit(IsWorthPolling(*this), item) && - !isRecentlyFinalized(item); + !isRecentlyFinalized(GetVoteItemId(item)); } bool Processor::GetLocalAcceptance::operator()( 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 @@ -2500,7 +2500,7 @@ // Finalize AvalancheTest::addProofToRecentfinalized(*m_processor, proof->getId()); - BOOST_CHECK(m_processor->isRecentlyFinalized(proof)); + BOOST_CHECK(m_processor->isRecentlyFinalized(proof->getId())); BOOST_CHECK(m_processor->reconcileOrFinalize(proof)); m_processor->withPeerManager([&](avalanche::PeerManager &pm) { diff --git a/src/rpc/avalanche.cpp b/src/rpc/avalanche.cpp --- a/src/rpc/avalanche.cpp +++ b/src/rpc/avalanche.cpp @@ -1296,6 +1296,59 @@ }; } +static RPCHelpMan invalidateavalancheproof() { + return RPCHelpMan{ + "invalidateavalancheproof", + "Reject a known avalanche proof by id.\n", + { + {"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The hex encoded avalanche proof identifier."}, + }, + RPCResult{ + RPCResult::Type::BOOL, + "success", + "", + }, + RPCExamples{HelpExampleRpc("invalidateavalancheproof", "")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Avalanche is not initialized"); + } + + const avalanche::ProofId proofid = + avalanche::ProofId::fromHex(request.params[0].get_str()); + + g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + if (!pm.exists(proofid) && !pm.isDangling(proofid)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Proof not found"); + } + + if (!pm.rejectProof( + proofid, + avalanche::PeerManager::RejectionMode::INVALIDATE)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Failed to reject the proof"); + } + + pm.setInvalid(proofid); + }); + + if (g_avalanche->isRecentlyFinalized(proofid)) { + // If the proof was previously finalized, clear the status. + // Because there is no way to selectively delete an entry from a + // Bloom filter, we have to clear the whole filter which could + // cause extra voting rounds. + g_avalanche->clearFinalizedItems(); + } + + return true; + }, + }; +} + static RPCHelpMan isfinalblock() { return RPCHelpMan{ "isfinalblock", @@ -1432,6 +1485,63 @@ }; } +static RPCHelpMan reconsideravalancheproof() { + return RPCHelpMan{ + "reconsideravalancheproof", + "Reconsider a known avalanche proof.\n", + { + {"proofid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The hex encoded avalanche proof."}, + }, + RPCResult{ + RPCResult::Type::BOOL, + "success", + "Whether the proof has been successfully registered.", + }, + RPCExamples{HelpExampleRpc("reconsideravalancheproof", "")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + if (!g_avalanche) { + throw JSONRPCError(RPC_INTERNAL_ERROR, + "Avalanche is not initialized"); + } + + auto proof = RCUPtr::make(); + NodeContext &node = EnsureAnyNodeContext(request.context); + + // Verify the proof. Note that this is redundant with the + // verification done when adding the proof to the pool, but we get a + // chance to give a better error message. + verifyProofOrThrow(node, *proof, request.params[0].get_str()); + + // There is no way to selectively clear the invalidation status of + // a single proof, so we clear the whole Bloom filter. This could + // cause extra voting rounds. + g_avalanche->withPeerManager([&](avalanche::PeerManager &pm) { + if (pm.isInvalid(proof->getId())) { + pm.clearAllInvalid(); + } + }); + + // Add the proof to the pool if we don't have it already. Since the + // proof verification has already been done, a failure likely + // indicates that there already is a proof with conflicting utxos. + avalanche::ProofRegistrationState state; + if (!registerProofIfNeeded(proof, state)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("%s (%s)\n", + state.GetRejectReason(), + state.GetDebugMessage())); + } + + return g_avalanche->withPeerManager( + [&](const avalanche::PeerManager &pm) { + return pm.isBoundToPeer(proof->getId()); + }); + }, + }; +} + static RPCHelpMan sendavalancheproof() { return RPCHelpMan{ "sendavalancheproof", @@ -1551,8 +1661,10 @@ { "avalanche", setstakingreward, }, { "avalanche", getremoteproofs, }, { "avalanche", getrawavalancheproof, }, + { "avalanche", invalidateavalancheproof, }, { "avalanche", isfinalblock, }, { "avalanche", isfinaltransaction, }, + { "avalanche", reconsideravalancheproof, }, { "avalanche", sendavalancheproof, }, { "avalanche", verifyavalancheproof, }, { "avalanche", verifyavalanchedelegation, }, diff --git a/test/functional/abc_rpc_invalidateavalancheproof.py b/test/functional/abc_rpc_invalidateavalancheproof.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_rpc_invalidateavalancheproof.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# Copyright (c) 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 invalidateavalancheproof and reconsideravalancheproof RPCs.""" + +from test_framework.address import ADDRESS_ECREG_UNSPENDABLE +from test_framework.avatools import ( + AvaP2PInterface, + avalanche_proof_from_hex, + create_coinbase_stakes, + gen_proof, + wait_for_proof, +) +from test_framework.key import ECPubKey +from test_framework.messages import ( + MSG_AVA_PROOF, + AvalancheProofVoteResponse, + AvalancheVote, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error, uint256_hex +from test_framework.wallet_util import bytes_to_wif + + +class InvalidateAvalancheProofTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.extra_args = [ + [ + "-avalancheconflictingproofcooldown=0", + "-avaproofstakeutxoconfirmations=1", + "-avacooldown=0", + "-avaminquorumstake=250000000", + "-avaproofstakeutxodustthreshold=1000000", + "-avaminavaproofsnodecount=0", + "-whitelist=noban@127.0.0.1", + ] + ] + + def run_test(self): + node = self.nodes[0] + + node_key, node_proof = gen_proof(self, node) + self.restart_node( + 0, + extra_args=self.extra_args[0] + + [ + f"-avaproof={node_proof.serialize().hex()}", + f"-avamasterkey={bytes_to_wif(node_key.get_bytes())}", + ], + ) + + node_pubkey = ECPubKey() + node_pubkey.set(bytes.fromhex(node.getavalanchekey())) + + quorum = [ + node.add_p2p_connection(AvaP2PInterface(self, node)) for _ in range(10) + ] + self.wait_until(lambda: node.getavalancheinfo()["ready_to_poll"]) + + _, proof = gen_proof(self, node) + proofid_hex = uint256_hex(proof.proofid) + assert_raises_rpc_error( + -8, "Proof not found", node.invalidateavalancheproof, proofid_hex + ) + + # Register that valid proof + assert node.sendavalancheproof(proof.serialize().hex()) + wait_for_proof(node, proofid_hex) + + # Now it can be invalidated + node.invalidateavalancheproof(proofid_hex) + # It's no longer in the node known proofs + assert_raises_rpc_error( + -8, "Proof not found", node.getrawavalancheproof, proofid_hex + ) + + # Make sure the node votes against the proof if polled + poll_node = quorum[0] + poll_node.send_poll([proof.proofid], MSG_AVA_PROOF) + + def assert_response(expected): + response = poll_node.wait_for_avaresponse() + r = response.response + assert_equal(r.cooldown, 0) + + # Verify signature. + assert node_pubkey.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])) + + assert_response( + [AvalancheVote(AvalancheProofVoteResponse.REJECTED, proof.proofid)] + ) + + assert node.reconsideravalancheproof(proof.serialize().hex()) + wait_for_proof(node, proofid_hex) + + # Now the node vote yes for that proof + poll_node.send_poll([proof.proofid], MSG_AVA_PROOF) + assert_response( + [AvalancheVote(AvalancheProofVoteResponse.ACTIVE, proof.proofid)] + ) + + # Reconsidering an existing proof is a no-op + assert node.reconsideravalancheproof(proof.serialize().hex()) + + # Reconsidering an invalid proof always fail + no_stake = node.buildavalancheproof( + 1, 0, bytes_to_wif(node_key.get_bytes()), [] + ) + assert_raises_rpc_error(-8, "no-stake", node.reconsideravalancheproof, no_stake) + + # If the proof is known but not valid (conflicting or immature) reconsider returns false + self.generate(node, 1) + stakes = create_coinbase_stakes( + node, [node.getbestblockhash()], node.get_deterministic_priv_key().key + ) + conflicting_proof = node.buildavalancheproof( + 1, + 0, + bytes_to_wif(node_key.get_bytes()), + stakes, + ADDRESS_ECREG_UNSPENDABLE, + ) + better_proof = node.buildavalancheproof( + 2, + 0, + bytes_to_wif(node_key.get_bytes()), + stakes, + ADDRESS_ECREG_UNSPENDABLE, + ) + + # Will just register the proof + assert node.reconsideravalancheproof(better_proof) + wait_for_proof( + node, uint256_hex(avalanche_proof_from_hex(better_proof).proofid) + ) + + # Will raise a registration error + assert_raises_rpc_error( + -8, "conflicting-utxos", node.reconsideravalancheproof, conflicting_proof + ) + + +if __name__ == "__main__": + InvalidateAvalancheProofTest().main()