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()