Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/oracle/bundle_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,28 @@ bool OracleBundleManager::AddOracleMessage(const COraclePriceMessage& message)
}
}

// Periodic cleanup of seen message hashes to prevent deadlock.
// When the oracle consensus round stalls (e.g., due to rapid block production
// or network partition), messages with the same Phase2 hash keep getting
// rejected as duplicates, preventing recovery. Clearing the set periodically
// allows fresh consensus rounds to form. The 300-second interval is shorter
// than any network's epoch length (testnet=750s, mainnet=1500s), ensuring at
// least one cleanup per epoch. The pending_messages map (keyed by oracle_id)
// provides the authoritative dedup — seen_message_hashes is best-effort P2P
// optimization only.
{
static int64_t last_seen_cleanup = 0;
int64_t now_cleanup = GetTime();
if (now_cleanup - last_seen_cleanup > 300) {
size_t old_size = seen_message_hashes.size();
seen_message_hashes.clear();
last_seen_cleanup = now_cleanup;
if (old_size > 0) {
LogPrint(BCLog::DIGIDOLLAR, "Oracle: Periodic cleanup cleared %zu seen message hashes\n", old_size);
}
}
}

// Calculate message hash for duplicate detection.
// Use Phase2 hash (oracle_id + price + timestamp) for Phase2-signed messages.
// GetSignatureHash() includes block_height+nonce which are NOT covered by
Expand Down
39 changes: 26 additions & 13 deletions src/oracle/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -360,23 +360,36 @@ void OracleNode::BroadcastCurrentPrice()
int64_t consensus_timestamp = 0;

if (bm.GetMinOracleCount() > 1 && bm.ComputeConsensusValues(consensus_price, consensus_timestamp)) {
// Consensus exists — create attestation over consensus values
COraclePriceMessage attestation = CreateConsensusAttestation(consensus_price, consensus_timestamp);
if (!attestation.schnorr_sig.empty()) {
// Submit as consensus attestation (for block construction)
bm.AddConsensusAttestation(attestation);

// Also broadcast via P2P so other nodes receive our attestation
if (BroadcastPriceMessage(attestation)) {
std::lock_guard<std::mutex> lock(mtx_price);
last_broadcast_price = price;
last_broadcast_timestamp = timestamp;
// DEADLOCK PREVENTION: If the consensus timestamp is stale (>5 minutes
// old), the consensus round is frozen. All oracles are creating
// attestations with the same (price, timestamp) tuple, producing
// identical Phase2 hashes that get rejected by the duplicate filter.
// Fall through to individual price broadcast with a fresh timestamp
// to break the cycle and allow a new consensus round to form.
int64_t consensus_age = timestamp - consensus_timestamp;
if (consensus_age > 300) {
LogPrintf("Oracle: Consensus timestamp is %lld seconds stale, broadcasting individual price to break deadlock\n", consensus_age);
// Clear stale state so fresh messages can form a new consensus
bm.ClearPendingMessages();
} else {
// Consensus exists and is fresh — create attestation over consensus values
COraclePriceMessage attestation = CreateConsensusAttestation(consensus_price, consensus_timestamp);
if (!attestation.schnorr_sig.empty()) {
// Submit as consensus attestation (for block construction)
bm.AddConsensusAttestation(attestation);

// Also broadcast via P2P so other nodes receive our attestation
if (BroadcastPriceMessage(attestation)) {
std::lock_guard<std::mutex> lock(mtx_price);
last_broadcast_price = price;
last_broadcast_timestamp = timestamp;
}
return;
}
return;
}
}

// No consensus yet or Phase 1 — broadcast individual price as before
// No consensus yet, Phase 1, or stale consensus — broadcast individual price
COraclePriceMessage message = CreatePriceMessage(price, timestamp);
if (BroadcastPriceMessage(message)) {
std::lock_guard<std::mutex> lock(mtx_price);
Expand Down
11 changes: 11 additions & 0 deletions src/rpc/digidollar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3668,6 +3668,17 @@ static RPCHelpMan stoporacle()
oracle->Stop();
success = !oracle->IsRunning();
status_message = success ? "Oracle stopped successfully" : "Failed to stop oracle";

// Clear stale oracle messaging state to break potential deadlocks.
// The seen_message_hashes, pending_messages, and pending_attestations
// can hold stale entries that prevent consensus recovery after restart.
// ClearPendingMessages() resets the duplicate filter, allowing fresh
// messages to be accepted when the oracle is restarted.
if (success) {
OracleBundleManager& bundleManager = OracleBundleManager::GetInstance();
bundleManager.ClearPendingMessages();
status_message += " (messaging state cleared)";
}
} else {
status_message = "Oracle not found in manager";
}
Expand Down
Loading